Visual Novel generativa: Ren'Py conversa com um backend FastAPI que orquestra um LLM local (Qwen 2.5 via LM Studio) e uma memória semântica em PostgreSQL + pgvector (embeddings via Nomic).
MnemonicVN/
├── api/ # backend FastAPI + SQLAlchemy + pgvector
│ ├── main.py # rotas /interact /world-tick /get-npc-status
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ ├── llm_service.py # dois clientes OpenAI: Chat (Qwen) × Embed (Nomic)
│ ├── memory_service.py # save_memory / retrieve_relevant_context
│ ├── world_engine.py # advance_world: hora de jogo + movimento por rotina
│ ├── seed.py # popula 3 NPCs + locais + rotinas + memórias
│ ├── manage.py # CLI: run / init-db / reset-db / seed
│ ├── config.py
│ ├── requirements.txt
│ └── .env.example
└── renpy/
├── source_assets/ # backup dos PNGs brutos (backgrounds + sprite packs Sutemo) — não é lido pelo jogo
└── game/
├── script.rpy # demo das três cenas iniciais
├── api_client.rpy # cliente HTTP async (AsyncRequest + restart_interaction)
├── screens_loading.rpy # indicador "[NPC] está pensando..."
├── characters.rpy # 3 layeredimage + ConditionSwitch por humor
├── character_bible.md # personalidade detalhada dos 3 personagens
├── options.rpy
└── images/
├── backgrounds/ # 88 PNGs (copiados de source_assets/Backgrounds)
└── sprites/
├── bodies/ # aria_body.png, mei_body.png, sayuri_body.png
├── hair/ # um por personagem
├── heads/ # <personagem>_head_<humor>.png (4 humores cada)
└── outfits/ # <personagem>_outfit_<estilo>.png (3 cada)
| ID | Nome | Arquétipo | Local inicial | Outfits |
|---|---|---|---|---|
| 1 | Aria Tanaka | A faísca extrovertida | Sala de Aula | seifuku, casual, pajama |
| 2 | Mei Kobayashi | A observadora quieta | Quarto de Mei | casual, seifuku, pajama |
| 3 | Sayuri Hoshino | A mentora calma | Sala de Aula | office, dress, casual |
Personalidade completa, voz e gancho narrativo de cada um em
renpy/game/character_bible.md. Os mesmos textos (resumidos) vão
para a coluna personalidade no POST /npcs via o seed.py.
- Python 3.11+
- PostgreSQL 14+ com extensão
vector(pgvector) - LM Studio em
localhost:1234comqwen2.5-coder-7b-instructenomic-embed-text-v1.5.f32carregados - Ren'Py SDK 8+ (apontar o launcher para
renpy/)
Tenha um PostgreSQL 14+ rodando localmente com a extensão pgvector
disponível (no Windows, instaladores oficiais já trazem; em Linux, instale
o pacote postgresql-XX-pgvector). Não precisa criar o banco manualmente
— o manage.py faz isso. Só ajuste DATABASE_URL em api/.env (copie
de .env.example) com usuário e senha que tenham permissão de
CREATE DATABASE.
Abra o LM Studio, carregue os dois modelos e habilite o servidor local
(Developer → Start Server). Ele expõe /v1/chat/completions e
/v1/embeddings no mesmo endpoint.
cd api
pip install -r requirements.txt
python manage.py init-db # cria banco vividnexus + extensão vector + tabelas
python manage.py seed # popula 3 NPCs, 9 locais, 13 rotinas, 9 memórias
python manage.py run # sobe em http://localhost:8000Para limpar tudo e começar do zero:
python manage.py reset-db # pede confirmação
python manage.py reset-db --yes # sem confirmaçãomanage.py é o painel de controle do backend — todos os comandos abaixo
são equivalentes a passos manuais que você não precisa mais executar:
| Comando | O que faz |
|---|---|
run |
uvicorn main:app com reload=False (não duplica scheduler) |
init-db |
CREATE DATABASE + CREATE EXTENSION vector + create_all |
seed |
popula NPCs, Locais, Rotinas, Memórias iniciais (idempotente) |
reset-db |
dropa o banco inteiro, recria, e roda o seed |
- Abra o Ren'Py SDK launcher.
- Aponte
Projects directoryparaMnemonicVN/. - Selecione
renpye clique em Launch Project.
O jogo abre num loop sandbox (label main_loop):
- Sincroniza humores do banco (
GET /get-npc-statuspara cada NPC). - Carrega a lista de locais (
GET /locais). - Põe o jogador em "Sala de Aula" como ponto de partida.
- Em cada iteração: busca quem está no local atual (
GET /locais/{id}/npcs), mostra os sprites posicionados (esquerda/centro/direita) e oferece um menu: falar com NPC presente, observar, mudar de local (avança um tick viaPOST /world-tick) ou encerrar o dia.
Como o scheduler do mundo move NPCs entre rotinas em background, voltar a um local depois de alguns minutos pode te dar uma cena diferente.
Ao subir o backend, um BackgroundScheduler (APScheduler) dispara
world_engine.advance_world() periodicamente. Cada tick:
- Avança
EstadoMundo.tick_atual. - Calcula a hora de jogo:
WORLD_START_TIME + tick * WORLD_MINUTES_PER_TICK. - Para cada NPC, encontra a
Rotinacuja janela contém a hora de jogo (suporta janelas que cruzam meia-noite) e atualizalocal_atual_idse for diferente. - Cada movimentação é logada no stdout do uvicorn:
[t=12 | 09:00] Aria Tanaka: Estação de Trem -> Sala de Aula (Aulas matinais...)
Configurável em .env:
WORLD_TICK_INTERVAL_SECONDS— período do scheduler em segundos reais (0 desliga).WORLD_MINUTES_PER_TICK— quantos minutos de jogo cada tick avança.WORLD_START_TIME— hora de jogo no tick 0 (HH:MM).
POST /world-tick continua disponível para avanços manuais — é o mesmo
caminho de código (advance_world).
api_client.rpy expõe interact_async() que devolve um AsyncRequest.
A label _talk_to no script.rpy:
- Dispara a thread.
- Mostra a screen
api_thinkingcom pontinhos animados e contador de ms. - Faz polling com
renpy.pause(0.2, hard=True)— a thread chamarenpy.restart_interaction()no fim, então o pause acorda imediatamente em vez de esperar o intervalo cheio. - Lê
result/errorviaparse_interact_response()e aplica o humor.
A UI do Ren'Py continua respondendo (menu/save/quit) mesmo enquanto o Qwen demora vários segundos para responder.
llm_service.py mantém dois clientes OpenAI separados apontando para
o mesmo LM Studio (localhost:1234/v1):
- Chat — modelo
qwen2.5-coder-7b-instruct,response_format=json_object(com fallback de parse se o modelo não respeitar). System prompt é montado emmain.py:/interactcompersonalidade + humor_atual + memórias_relevantes + contexto_extra. - Embedding — modelo
nomic-embed-text-v1.5.f32, valida 768 dimensões. Toda memória passa por aqui antes de virarVector(768)no Postgres. Busca semântica usaembedding.cosine_distance(vector)emmemory_service.retrieve_relevant_context.
- Adicionar índice
IVFFlatouHNSWna colunamemorias.embeddingquando a base ultrapassar ~10k vetores. - Mostrar o relógio do mundo (tick + hora de jogo) no HUD do Ren'Py.
- Adicionar mais humores (ex.:
pensativo,tímido) — copiar o asset, adicionarattribute X:nolayeredimage, expandir oConditionSwitche a tuplaHUMORES_VALIDOS. - Permitir trocar
<personagem>_outfitpor evento de jogo (ex.: noite acionapajama, fim de semana acionacasual). A camada já existe. - Adicionar o jogador como NPC no banco (com personalidade própria) e permitir que NPCs interajam entre si quando estiverem no mesmo local.
Arte original por Sutemo (Stereo-Mono).
- Links de Apoio: Ko-fi | DeviantArt
- Pacotes Utilizados:
- Licença (Sutemo): Os sprites podem ser usados em projetos pessoais ou comerciais (jogos, vídeos, etc.). É proibida a revenda dos sprites por si só, incluindo a criação de criadores de personagens (character creators).
Arte original por Noraneko Games (Yumebackground).
- Link do Projeto: Yumebackground Pack
- Regras de Uso (Noraneko Games):
- O crédito a "Noraneko Games" é obrigatório e deve ser dado nos créditos do jogo, em um arquivo de texto acompanhando o jogo, ou na descrição de onde o jogo é baixado.
- Os assets não podem ser revendidos sozinhos, dentro ou fora do jogo.
- Não utilizar para glorificar pedofilia, homofobia, racismo ou ameaças a pessoas reais.
- Uso comercial é permitido para jogos (desde que o usuário não tenha que comprar o background especificamente para vê-lo).
- É permitida a modificação da imagem (mudar cores, desenhar por cima à mão, adicionar personagens), desde que as regras originais continuem sendo seguidas.
- RESTRIÇÃO ESTRITA DE IA: Nenhum lançamento da Noraneko Games pode ser usado com IA de forma alguma (incluindo, mas não se limitando a treinamento, img2img, retoques, ou emparelhado com). (Nota do desenvolvedor: Os cenários neste projeto são exibidos como arte final 2D estática e não interagem, alimentam ou são modificados pela IA generativa textual do projeto).