From 6189a5fb7441a05ff26de87a0942a91820da0280 Mon Sep 17 00:00:00 2001 From: grigo Date: Thu, 4 Jun 2026 08:05:06 +0300 Subject: [PATCH] Fixed SD RPG --- RPG_FLOW.md | 369 +++++++++ database/db.py | 17 + main.py | 10 +- ...st-a-friendly-hug-3ccb4b5342bb_spec_v2.png | Bin 0 -> 257298 bytes ...-obsessive-girlfriend-af26ead7_spec_v2.png | Bin 0 -> 400679 bytes main_delta-125aa7a6_spec_v2.png | Bin 0 -> 348525 bytes main_violet-merino-d2e9f62b5d77_spec_v2.png | Bin 0 -> 1586970 bytes main_vulpisfoglia-e0a6befda921_spec_v2.png | Bin 0 -> 1374355 bytes ...perheroine-friend-c65bf1fe881c_spec_v2.png | Bin 0 -> 2755898 bytes models/schemas.py | 31 + routers/characters.py | 1 + routers/chat.py | 720 ++++++++++++------ routers/debug.py | 248 ++++++ routers/personas.py | 1 + routers/sessions.py | 197 ++++- services/character_card.py | 28 +- services/chat_prompt.py | 30 + services/comfy_models.py | 40 + services/context_budget.py | 30 + services/llm.py | 125 ++- services/memory.py | 334 +++++++- services/opening.py | 173 +++++ services/outfit_tags.py | 94 +++ services/personas.py | 28 +- services/rp_sanitize.py | 77 ++ services/rpg_context.py | 53 ++ services/rpg_facts.py | 386 ++++++++-- services/rpg_narrator.py | 78 +- services/rpg_plot.py | 410 +++++++++- services/rpg_state.py | 321 ++++++++ services/sd_images.py | 48 ++ services/sd_prompt.py | 717 +++++++++++++++-- services/sdbackend.py | 347 ++++++++- services/session_identity.py | 31 + services/system_message_migration.py | 21 + .../avatars/card_carrie_0578318a_dbee17bc.png | Bin 0 -> 257298 bytes .../avatars/card_carrie_926629e6_09f02497.png | Bin 0 -> 257298 bytes .../avatars/card_carrie_926629e6_8b9b8891.png | Bin 0 -> 257298 bytes .../avatars/card_carrie_926629e6_e01546a5.png | Bin 0 -> 257298 bytes static/css/app.css | 273 ++++++- static/css/debug.css | 209 +++++ static/debug.html | 134 ++++ static/index.html | 85 ++- static/js/app.js | 5 +- static/js/chat.js | 435 +++++++++-- static/js/chatSettings.js | 139 +++- static/js/contextEditor.js | 105 +++ static/js/debug.js | 217 ++++++ static/js/newChatWizard.js | 123 +-- static/js/personas.js | 32 +- static/js/rpFormat.js | 209 +++++ static/js/sessions.js | 39 +- static/js/state.js | 21 +- static/js/utils.js | 22 +- tests/test_arc_stuck_recovery.py | 87 +++ tests/test_dice_llm_payload.py | 54 ++ tests/test_outfit_tags.py | 18 + tests/test_plot_choices.py | 29 + tests/test_rp_sanitize.py | 18 + tests/test_rpg_facts.py | 42 + tests/test_rpg_state_prompts.py | 17 + tests/test_sd_prompt.py | 243 ++++++ 62 files changed, 6969 insertions(+), 552 deletions(-) create mode 100644 RPG_FLOW.md create mode 100644 main_carrie-just-a-friendly-hug-3ccb4b5342bb_spec_v2.png create mode 100644 main_clingy-obsessive-girlfriend-af26ead7_spec_v2.png create mode 100644 main_delta-125aa7a6_spec_v2.png create mode 100644 main_violet-merino-d2e9f62b5d77_spec_v2.png create mode 100644 main_vulpisfoglia-e0a6befda921_spec_v2.png create mode 100644 main_your-scumbag-superheroine-friend-c65bf1fe881c_spec_v2.png create mode 100644 routers/debug.py create mode 100644 services/chat_prompt.py create mode 100644 services/comfy_models.py create mode 100644 services/context_budget.py create mode 100644 services/opening.py create mode 100644 services/outfit_tags.py create mode 100644 services/rp_sanitize.py create mode 100644 services/rpg_context.py create mode 100644 services/rpg_state.py create mode 100644 services/sd_images.py create mode 100644 services/session_identity.py create mode 100644 services/system_message_migration.py create mode 100644 static/avatars/card_carrie_0578318a_dbee17bc.png create mode 100644 static/avatars/card_carrie_926629e6_09f02497.png create mode 100644 static/avatars/card_carrie_926629e6_8b9b8891.png create mode 100644 static/avatars/card_carrie_926629e6_e01546a5.png create mode 100644 static/css/debug.css create mode 100644 static/debug.html create mode 100644 static/js/contextEditor.js create mode 100644 static/js/debug.js create mode 100644 static/js/rpFormat.js create mode 100644 tests/test_arc_stuck_recovery.py create mode 100644 tests/test_dice_llm_payload.py create mode 100644 tests/test_outfit_tags.py create mode 100644 tests/test_plot_choices.py create mode 100644 tests/test_rp_sanitize.py create mode 100644 tests/test_rpg_facts.py create mode 100644 tests/test_rpg_state_prompts.py create mode 100644 tests/test_sd_prompt.py diff --git a/RPG_FLOW.md b/RPG_FLOW.md new file mode 100644 index 0000000..c552495 --- /dev/null +++ b/RPG_FLOW.md @@ -0,0 +1,369 @@ +# RPG-режим: полный алгоритм запросов (шпаргалка) + +Документ описывает, **какой HTTP-запрос что получает и что делает**, когда RPG включён полностью (`rpg_enabled = 1`) и в настройках активны все опции мастера: + +```json +{ + "dice": true, + "narrator": true, + "quests": true, + "affinity": true, + "choices": true, + "stats": false +} +``` + +> **Шкалы** (`lust` / `stamina` / `tension`): по умолчанию `stats: false`. Если включить чекбокс «Шкалы», после каждого хода добавляется ветка `stats_delta` (см. § «Вариант: stats включены»). + +Пример проходит **от создания чата до 3-го сообщения игрока**, с перекрытием основных веток (диалог / d20 / кнопка выбора / сдвиг сюжета / факты / SD). + +--- + +## Модели и роли + +| Роль | Env / модель по умолчанию | Где вызывается | +|------|---------------------------|----------------| +| **Персонаж (чат)** | `CHAT_MODEL` → `mistralai/mistral-nemo` | `POST /chat/stream` → `stream_message()` | +| **Нарратор pre/post** | `RPG_NARRATOR_MODEL` → `deepseek/deepseek-chat-v3` | Перед/после ответа персонажа | +| **Сюжетная арка** | `RPG_PLOT_MODEL` → DeepSeek v3 | Opening, fallback если арки нет | +| **Факты** | `RPG_FACTS_MODEL` → DeepSeek v3 | После каждого ответа персонажа | +| **SD-сцена** | `SD_PROMPT_MODEL` или `SYSTEM_MODEL` | Opening + конец каждого stream | +| **Появление (prose)** | `SYSTEM_MODEL` | Только при сохранении тегов персоны (не в ходе RPG) | + +Персонаж **не пишет** affinity/stats в БД — только читает их из `runtime_suffix`. Пишет **только narrator post** (`affinity_delta`, `stats_delta`, `scene_update`, …). + +--- + +## Слои промпта (что видит CHAT_MODEL) + +``` +llm_system = static_prompt + runtime_suffix [+ context_warning] +``` + +| Слой | Источник | Когда меняется | +|------|----------|----------------| +| **static_prompt** | `get_system_prompt()` — persona + lorebook (последние 5 реплик + текущий user) + `ROLEPLAY_GUARDRAILS` | Каждый stream; в `messages` хранится **только** static (без RPG) | +| **facts_block** | До 20 фактов из `facts_json` | После `extract_facts` на прошлом ходу | +| **PlotArc** | `title`, `phase`, `next_beat_hint` | При генерации/сдвиге арки | +| **Status quo** | `sessions.status_quo` | narrator pre/post, opening | +| **Scene** | `sessions.scene_json` | narrator pre/post, opening | +| **Relationship** | `affinity` + тон | narrator post (`affinity_delta`) | +| **Character state** | lust/stamina/tension | Только если `stats: true` | +| **Narrator directives** | narrator **pre** (текущий ход) | Эфемерно, не в БД | +| **Mechanics** | d20 + outcome (текущий ход) | Только если была проверка | +| **Context warning** | `~N% of CHAT_CONTEXT_MAX` | Если оценка > 85% | + +История в LLM: **все** `user` / `assistant` из БД + один объединённый `system`. + +--- + +## Фаза 0: создание RPG-чата (UI → API) + +### 0.1 `PATCH /sessions/{id}` + +**Тело (пример):** +```json +{ + "persona_id": "card_abc", + "rpg_enabled": true, + "genre": "fantasy", + "rpg_settings_json": "{\"dice\":true,\"narrator\":true,...}" +} +``` + +**Действия:** запись в `sessions` — флаги RPG, жанр, настройки. Сообщений ещё нет. + +### 0.2 `POST /chat/init` + +**Тело:** `{ "session_id", "persona_id", "first_mes_override"? }` + +**Действия:** +1. `get_system_prompt(persona, [], "")` → static (персона, lorebook, guardrails). +2. `upsert_static_system_message` — одна строка `role=system` в `messages`. +3. Если истории нет — `add_message(assistant, first_mes)` из карты / override. + +**LLM:** не вызывается. + +**БД после:** `messages = [system, assistant(first_mes)]`. + +### 0.3 `POST /chat/opening/process` + +**Тело:** `{ "session_id", "persona_id", "rpg": true }` + +Последовательность **только на opening** (без `narrator_pre`, без stream персонажа): + +```mermaid +sequenceDiagram + participant UI + participant API as opening/process + participant Plot as RPG_PLOT_MODEL + participant Nar as RPG_NARRATOR_MODEL + participant SD as SD_PROMPT_MODEL + + UI->>API: POST opening/process + API->>Plot: generate_plot_arc(first_mes, persona, genre) + Plot-->>API: plot_arc_json + API->>API: seed rpg_quests from beats + API->>Nar: narrator_post OPENING + Nar-->>API: status_quo, scene, affinity_delta, outfit, choices, quests + API->>API: apply_narrator_post → sessions + API->>SD: generate_sd_prompt + run_sd_for_message + API-->>UI: plot_arc, quests, affinity, choices, image_* +``` + +| Шаг | Модель | Вход | Выход в БД / UI | +|-----|--------|------|-----------------| +| 1 | `generate_plot_arc` | имя, description, scenario, **first_mes**, facts, genre | `plot_arc_json`; квесты из `beats[].title` | +| 2 | `narrator_post(is_opening=true)` | контекст: `assistant: {first_mes}`; arc; facts | `status_quo`, `scene_json`, `outfit_json`, `affinity += delta`, квесты, **choices** (в ответ API, не в messages) | +| 3 | `generate_sd_prompt` | история, outfit, **scene_json** | картинка на last assistant id | + +**Особенности opening для narrator post:** +- Просит заполнить `scene_update` из greeting + scenario. +- Просит **non-zero `affinity_delta`**, если в first_mes явный тёплый/враждебный тон. +- `outfit_update` — начальная одежда из greeting (danbooru-теги). + +**UI после:** `reloadChatFromServer`, панель квестов, 💖 affinity, кнопки `choices`, картинка SD. + +--- + +## Состояние сессии перед 1-м сообщением игрока + +| Поле | Пример | +|------|--------| +| `messages` | system, assistant(first_mes) | +| `plot_arc_json` | сгенерированная арка | +| `status_quo`, `scene_json`, `outfit_json` | из narrator opening | +| `affinity` | 0 или ±1..2 после opening | +| `facts_json` | `[]` | +| `narrative_stats_json` | `{"lust":0,"stamina":10,"tension":0}` (если stats выкл. — всё равно дефолт в БД) | + +--- + +## Сообщение игрока №1 — обычная реплика (без d20) + +**Пример:** игрок пишет: «Привет, как тебя зовут?» + +**UI:** `POST /chat/stream` +```json +{ + "session_id": "...", + "persona_id": "...", + "message": "Привет, как тебя зовут?", + "is_narrator_choice": false +} +``` + +### Этап A — подготовка (до stream) + +1. **static_prompt** = `get_system_prompt(persona, history, message)` — lorebook по последним 5 репликам + guardrails. +2. **RPG pre (narrator):** + - **Вход user:** persona, user action, Global plot (= full arc JSON), Facts, Recent (до 8 реплик). + - **Модель:** `RPG_NARRATOR_MODEL`. + - **Ожидание:** `needs_check: false` для чистого диалога. + - **Эффекты:** + - `directives` → попадут в `narrator_extra`. + - `status_quo_update` → `UPDATE sessions.status_quo`. + - `scene_update` (partial) → merge в `scene_json`. + - **Нет** d20, **нет** bubble «Рассказчик». +3. **runtime_suffix** = `build_rpg_runtime_suffix(session)` + `narrator_extra` (directives только). +4. `upsert_static_system_message(static)` — в БД system без RPG-блоков. +5. `add_message(user, message)`. +6. **context_usage** — если > 85%, в конец system добавляется `[Context: ~N% …]`. +7. **llm_messages** = system(static+runtime) + вся история user/assistant. + +### Этап B — SSE stream + +| Событие SSE | Когда | +|-------------|--------| +| `{ "chunk": "..." }` | Поток `CHAT_MODEL` — ответ персонажа | +| `{ "done": true, ... }` | После сохранения assistant в БД | + +**Персонаж:** один запрос stream, читает affinity/scene/status/arc/facts/directives, **не меняет** счётчики. + +### Этап C — post-process (после ответа персонажа, тот же HTTP stream) + +Порядок внутри `generate()`: + +| # | Действие | Модель | Условие | +|---|----------|--------|---------| +| C1 | `generate_plot_arc` | PLOT | Только если `plot_arc_json` пуст (редко после opening) | +| C2 | `should_advance_arc(user_message)` | код | Ключевые слова: отдых → `event_driven:rest`, путь → `travel`, помощь → `help_request` | +| C3 | `pop_matching_beats` + injection | — | Если trig совпал с beat; choices из beat | +| C4 | `advance_phase` | — | Если beats пусты — фаза opening→hook→… | +| C5 | `extract_facts` | FACTS | Последние 10 реплик; merge до 80, в промпт 20 | +| C6 | **`narrator_post`** | NARRATOR | Контекст: последние 8 реплик **включая новый ответ** | +| C7 | **`apply_narrator_post`** | — | status_quo, affinity_delta, scene, outfit, quests, stats_delta* | +| C8 | `generate_sd_prompt` + Comfy | SD | outfit + scene_json + последние 6 реплик | +| C9 | SSE `done` | — | choices, affinity, quests, image_*, debug | + +\* `stats_delta` только при `rpg_settings.stats === true`. + +**Для реплики №1 (диалог):** narrator pre → no check; post → facts, возможно choices, affinity ±, scene/status. + +--- + +## Сообщение игрока №2 — действие с проверкой d20 + +**Пример:** «Пытаюсь перепрыгнуть через пропасть» + +Тот же `POST /chat/stream`. Отличия в **этапе A**: + +```mermaid +flowchart TD + pre1[narrator_pre без броска] + need{needs_check AND dice?} + roll[d20 1-20] + pre2[narrator_pre с roll+outcome] + res[resolution_text + action_resolutions] + chat[CHAT_MODEL stream] + post[narrator_post + facts + SD] + + pre1 --> need + need -->|да| roll --> pre2 --> res + need -->|нет| chat + res --> chat + chat --> post +``` + +| Шаг | Что происходит | +|-----|----------------| +| pre (фаза 1) | `needs_check: true` | +| Бросок | `random 1..20` → outcome: crit fail / fail / success / crit success | +| pre2 | Тот же user + `Roll d20=N`, `Outcome=...` → **`resolution_text` обязателен** | +| UI | SSE **`{ "narrator": { roll, outcome, text } }`** **до** chunks — bubble «📖 Рассказчик» | +| runtime | + `--- Mechanics ---` (d20, outcome, «не противоречь») | +| БД | `action_resolutions` — intent, roll, outcome, resolution_text | +| Персонаж | Должен согласовать ответ с resolution + mechanics | + +Post-process (C) — **тот же**, что на ходу 1. Facts могут записать «игрок не перепрыгнул», quest может обновиться. + +--- + +## Сообщение игрока №3 — кнопка выбора + сдвиг арки + +**Пример:** игрок жмёт choice «Отправиться в лес» (label уходит как user message) +или пишет: «Идём дальше в лес» + +**UI:** `sendMessage(label, true)` → `is_narrator_choice: true` + +| Отличие | Поведение | +|---------|-----------| +| Текст в БД | `"[Player chose: Отправиться в лес]"` вместо сырого label | +| pre/post | Как обычный ход; narrator видит метку выбора | +| **should_advance_arc** | Если в тексте есть «идем дальше», «в путь», … → `event_driven:travel` | +| beats | `pop_matching_beats(arc, trig)` — до 1 beat; **injection** в debug; **choices** из beat + post | +| phase | Если beats опустели → `advance_phase` (например hook → complication) | + +Остальной pipeline = stream №1 или №2 (в зависимости от needs_check). + +--- + +## Сводная таблица: кто что обновляет (ход 1–3) + +| Данные | Opening | narrator pre | CHAT_MODEL | narrator post | extract_facts | arc engine | +|--------|---------|--------------|------------|---------------|---------------|------------| +| messages | first_mes | — | user + assistant | — | — | — | +| plot_arc_json | create | — | read | read | — | advance/beats | +| status_quo | post | pre/post | read | post | — | — | +| scene_json | post | pre/post | read | post | — | — | +| outfit_json | post | — | read | post | — | — | +| affinity | post | — | read | delta | — | — | +| facts_json | — | read | read | post.facts* | merge | — | +| rpg_quests | seed | — | — | post | — | — | +| action_resolutions | — | d20 path | — | — | — | — | +| narrative_stats | default | — | read** | stats_delta** | — | — | + +\* facts из post в JSON схеме narrator — сейчас основной путь facts = `extract_facts`. +\** только при `stats: true`. + +--- + +## Варианты настроек (перекрытие) + +### `narrator: false` + +- Нет narrator pre/post, нет directives/mechanics bubble. +- `runtime_suffix` = только `build_rpg_runtime_suffix` (facts, arc, status, scene, affinity). +- Post-process: нет C6–C7; facts/arc/SD могут остаться (arc/SD/facts всё ещё в коде stream). + +### `dice: false` + +- `needs_check` от pre **игнорируется** — всегда ветка без броска (как диалог). +- pre2 и mechanics не вызываются. + +### `affinity: false` + +- Нет блока Relationship в runtime; `affinity_delta` не применяется. + +### `quests: false` + +- Opening не сидирует квесты; post не вызывает `upsert_quest`. + +### `choices: false` + +- UI не рисует кнопки; post/beat choices не добавляются в SSE. + +### `stats: true` + +- В runtime: блок lust/stamina/tension (0–10). +- post: `stats_delta` каждый ключ −2..+2, clamp 0–10. +- Дефолт stamina = 10. + +### `is_narrator_choice: true` + +- User content = `[Player chose: …]`. + +### Контекст > 85% + +- В system добавляется одна строка-предупреждение (не в БД). + +--- + +## Вспомогательные запросы (не в примере 1–3, но в RPG) + +| Запрос | Когда | +|--------|-------| +| `GET /chat/system/{id}` | Панель system blob: static, runtime-превью, scene, stats, **context_usage** | +| `GET /chat/history/{id}` | Перезагрузка чата | +| `POST /chat/rpg/bootstrap` | RPG на **старом** чате: arc + **narrator_post opening** + apply (как opening, без нового first_mes) | +| `POST /chat/` (не stream) | Упрощённый ход: static + runtime, один ответ, без narrator pre/post | + +--- + +## Пример хронологии (3 сообщения, все опции ON) + +| # | HTTP | LLM-вызовы | Заметки | +|---|------|------------|---------| +| 0 | PATCH session | — | rpg_enabled | +| 0 | POST init | — | system + first_mes | +| 0 | POST opening/process | Plot, Narrator post, SD | arc, scene, affinity, quests, choices | +| 1 | POST stream | Pre (no check), **Chat**, Facts, Post, SD | Диалог; +facts; choices в done | +| 2 | POST stream | Pre (check), **Pre2+d20**, **Chat**, Facts, Post, SD | Bubble рассказчика; action_resolutions | +| 3 | POST stream | Pre, Chat, Facts, Post, SD; arc travel? | `[Player chose:…]` или текст; beat injection | + +**Порядок SSE на ходу 2:** +1. `narrator` (d20) +2. `chunk` × N +3. `image_generating` (если SD) +4. `done` (choices, affinity, quests, debug) + +--- + +## Файлы для углубления + +| Тема | Файл | +|------|------| +| Stream + RPG | `routers/chat.py` → `chat_stream` | +| Opening | `services/opening.py` | +| Narrator JSON | `services/rpg_narrator.py` | +| Runtime blocks | `routers/chat.py` → `build_rpg_runtime_suffix`, `services/rpg_state.py` | +| Persist post | `services/rpg_state.py` → `apply_narrator_post` | +| Arc / beats | `services/rpg_plot.py` | +| Facts | `services/rpg_facts.py` | +| UI stream | `static/js/chat.js` → `sendMessage`, `consumeStream` | +| New chat | `static/js/newChatWizard.js` | +| Context % | `services/context_budget.py` | + +--- + +*Обновлено под текущую реализацию RP UX (anti-OOC, scene_json, context blob, rpFormat, bootstrap narrator).* diff --git a/database/db.py b/database/db.py index 38f7a49..d753ebb 100644 --- a/database/db.py +++ b/database/db.py @@ -86,6 +86,12 @@ async def _migrate_messages_columns(db): await db.execute("ALTER TABLE messages ADD COLUMN image_prompt TEXT") if "image_path" not in cols: await db.execute("ALTER TABLE messages ADD COLUMN image_path TEXT") + if "image_prompt_alt" not in cols: + await db.execute("ALTER TABLE messages ADD COLUMN image_prompt_alt TEXT") + if "image_path_alt" not in cols: + await db.execute("ALTER TABLE messages ADD COLUMN image_path_alt TEXT") + if "choices_json" not in cols: + await db.execute("ALTER TABLE messages ADD COLUMN choices_json TEXT") async def _migrate_personas_columns(db): @@ -105,6 +111,8 @@ async def _migrate_personas_columns(db): await db.execute("ALTER TABLE personas ADD COLUMN avatar_path TEXT DEFAULT ''") if "alternate_greetings_json" not in cols: await db.execute("ALTER TABLE personas ADD COLUMN alternate_greetings_json TEXT DEFAULT '[]'") + if "appearance_prose" not in cols: + await db.execute("ALTER TABLE personas ADD COLUMN appearance_prose TEXT DEFAULT ''") async def _migrate_sessions_columns(db): @@ -130,6 +138,13 @@ async def _migrate_sessions_columns(db): await db.execute("ALTER TABLE sessions ADD COLUMN rpg_settings_json TEXT DEFAULT '{}'") if "outfit_json" not in cols: await db.execute("ALTER TABLE sessions ADD COLUMN outfit_json TEXT DEFAULT '[]'") + if "scene_json" not in cols: + await db.execute("ALTER TABLE sessions ADD COLUMN scene_json TEXT DEFAULT '{}'") + if "narrative_stats_json" not in cols: + await db.execute( + "ALTER TABLE sessions ADD COLUMN narrative_stats_json TEXT DEFAULT " + "'{\"lust\":0,\"stamina\":10,\"tension\":0}'" + ) async def _migrate_rpg_quests(db): await db.executescript(""" @@ -170,3 +185,5 @@ async def _migrate_characters_columns(db): await db.execute("ALTER TABLE characters ADD COLUMN avatar_path TEXT DEFAULT ''") if "alternate_greetings_json" not in cols: await db.execute("ALTER TABLE characters ADD COLUMN alternate_greetings_json TEXT DEFAULT '[]'") + if "appearance_prose" not in cols: + await db.execute("ALTER TABLE characters ADD COLUMN appearance_prose TEXT DEFAULT ''") diff --git a/main.py b/main.py index 802a95a..869840c 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,10 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse -from routers import chat, personas, sessions, characters, images, translate +from routers import chat, personas, sessions, characters, images, translate, debug from database.db import init_db from services.persona_seed import seed_default_personas +from services.system_message_migration import migrate_static_system_messages logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") @@ -14,6 +15,7 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(messag async def lifespan(app: FastAPI): await init_db() await seed_default_personas() + await migrate_static_system_messages() yield @@ -25,6 +27,7 @@ app.include_router(sessions.router) app.include_router(characters.router) app.include_router(images.router) app.include_router(translate.router) +app.include_router(debug.router) app.mount("/static", StaticFiles(directory="static"), name="static") @@ -34,6 +37,11 @@ async def root(): return FileResponse("static/index.html") +@app.get("/debug") +async def debug_page(): + return FileResponse("static/debug.html") + + @app.get("/health") async def health(): return {"status": "ok"} diff --git a/main_carrie-just-a-friendly-hug-3ccb4b5342bb_spec_v2.png b/main_carrie-just-a-friendly-hug-3ccb4b5342bb_spec_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..8ef6eb05234e01e6d970cf4a10409bcad07edd7e GIT binary patch literal 257298 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