funifier-mcp 0.2.26 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# funifier-create-story
|
|
2
|
+
|
|
3
|
+
Create a Funifier interactive story — branching narratives with player choices, scenes, and media for training or campaigns; use when building choose-your-own-adventure experiences, not when a linear quiz or folder trail is sufficient
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Documentação do módulo (já incluída — não precisa buscar)
|
|
8
|
+
|
|
9
|
+
**Acesso Studio:** `/studio/story` _(convenção de frontend — **não há nenhuma referência a `/studio/story` no `funifier-service`**; a tela é servida por outro repositório)_
|
|
10
|
+
**API Endpoint:** `/v3/story/*` (apenas endpoints especiais de IA + cascade delete) **+** CRUD genérico via `/v3/database/{story|story_character|story_scene}`
|
|
11
|
+
**Coleções MongoDB:** `story`, `story_character`, `story_scene` (+ gravação derivada na coleção `upload`)
|
|
12
|
+
|
|
13
|
+
> **Origem desta documentação:** engenharia reversa de `com.funifier.rest.v3.rest.StoryRest` (`src/main/java/com/funifier/rest/v3/rest/StoryRest.java`, 1380 linhas) e da referência cruzada em `AIRest.buildStory`. Todo comportamento descrito foi confirmado lendo o código-fonte real. Quando algo **não foi encontrado**, isso está dito explicitamente.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Visão Geral
|
|
18
|
+
|
|
19
|
+
O módulo `story` é uma **camada fina de aumento por IA** sobre três coleções MongoDB **sem schema no servidor**: `story`, `story_character` e `story_scene`. Ele implementa o backend de um editor de **histórias visuais interativas** (visual novel / branching story): cenas encadeadas, cenas de decisão com pontuação, finais alternativos, personagens com voz, e mídia (imagem/vídeo/áudio) gerada por IA.
|
|
20
|
+
|
|
21
|
+
Papel arquitetural — **atípico** em relação aos demais módulos da plataforma:
|
|
22
|
+
|
|
23
|
+
- **Não existe entidade, manager, service ou repository dedicados.** Não há classe `Story`, `StoryManager`, `StoryService` nem `StoryDao`/`StoryRepository` no projeto. Confirmado por busca (`grep -r 'class Story|StoryManager|StoryService' src/`): o único arquivo com `Story` no nome é `StoryRest.java`.
|
|
24
|
+
- **O CRUD (criar/ler/atualizar) dos documentos é feito pelo endpoint genérico** `/v3/database/{collection}` (`com.funifier.rest.v3.rest.DatabaseRest`), exatamente como qualquer coleção custom da plataforma. `StoryRest` **não** expõe `POST /v3/story` nem `GET /v3/story`.
|
|
25
|
+
- `StoryRest` adiciona **apenas** operações que não cabem no CRUD genérico: cascade delete e geração de conteúdo por IA (imagem de cena, retrato de personagem, capa, áudio TTS de diálogo, prévia de voz, estrutura de cenas via GPT e vídeo via Sora/Freepik).
|
|
26
|
+
- As coleções são **schema-less**: os documentos são gravados pelo frontend via `/v3/database/*` com a forma que o frontend quiser. O backend só **lê campos específicos** (ver seção 3) e, em pontos pontuais, **escreve** apenas os campos `video`, `media`, `media_mode` e `dialogues[i].word_timestamps` em `story_scene`.
|
|
27
|
+
|
|
28
|
+
Problemas que resolve:
|
|
29
|
+
|
|
30
|
+
- Persistir e versionar uma história ramificada (cenas, decisões, finais) sem precisar de schema rígido.
|
|
31
|
+
- Orquestrar chamadas a APIs externas de IA (OpenAI Images, OpenAI TTS, OpenAI Whisper, OpenAI Sora 2 e — desabilitado — Freepik) e **persistir o resultado** no storage do tenant (S3/Azure/Google) e nos documentos de cena.
|
|
32
|
+
- Gerar automaticamente a estrutura narrativa (cenas + decisões + finais) a partir de um objetivo, via function-calling do GPT.
|
|
33
|
+
|
|
34
|
+
Relação com outros componentes:
|
|
35
|
+
|
|
36
|
+
- `AIService` (`com.funifier.engine.system.ai.AIService`) — token e helpers OpenAI (`generateImage`, `chatCompletionWithFunction`, constante `CHATGPT_TOKEN`, `MODEL_GPT_IMAGE_1`).
|
|
37
|
+
- `S3Service` / `AzureService` / `GoogleService` — destino do upload conforme `Configuration.getCurrentConfiguration().getCloud()`.
|
|
38
|
+
- Coleção `upload` (`Entity.UPLOAD.collection`) — `saveImageUpload` grava um registro de upload **apenas para imagens** (não para áudio nem vídeo).
|
|
39
|
+
- `DatabaseRest` (`/v3/database`) — caminho real de CRUD das três coleções.
|
|
40
|
+
- `AIRest.buildStory` (`/v3/ai/build/story`) — helper **legado** que gera uma história em **formato de objeto totalmente diferente** (ver seções 4 e 7); **não** compartilha schema com `StoryRest`.
|
|
41
|
+
- **Não dispara triggers nem webhooks.** Nenhuma chamada a `Trigger`, `before_*`/`after_*` ou notificação foi encontrada em `StoryRest`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Arquitetura e Fluxos
|
|
46
|
+
|
|
47
|
+
### 2.1 Classes envolvidas
|
|
48
|
+
|
|
49
|
+
| Classe / arquivo | Papel |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `com.funifier.rest.v3.rest.StoryRest` | **Único** controller do módulo (`@Path("v3/story")`). Cascade delete + 10 endpoints de IA/diagnóstico. |
|
|
52
|
+
| `com.funifier.rest.v3.rest.DatabaseRest` (`@Path("v3/database")`) | CRUD genérico real das coleções `story`, `story_character`, `story_scene`. |
|
|
53
|
+
| `com.funifier.engine.system.ai.AIService` | Integração OpenAI (token, geração de imagem, chat com function-calling). |
|
|
54
|
+
| `S3Service` / `AzureService` / `GoogleService` | Persistência de binários (imagem/áudio/vídeo) por tenant. |
|
|
55
|
+
| `com.funifier.engine.guid.Guid` | Geração de `_id`/nome de arquivo (`newShortGuid`). |
|
|
56
|
+
| `com.funifier.rest.v3.rest.AIRest` (`@Path` em `/v3/ai`) | Endpoint legado `buildStory` (`/build/story`) — formato distinto. |
|
|
57
|
+
|
|
58
|
+
> **Ausências confirmadas (não fitar no template à força):** não há `Story` (entidade/POJO), `StoryManager`, `StoryService`, `StoryRepository`/`StoryDao`, scheduler/job dedicado, enum `Entity.STORY`, nem pipeline MongoDB pré-cadastrado. A manipulação Mongo é feita **inline via Jongo** dentro de `StoryRest`, sempre com queries parametrizadas (`"{_id: #}"`, `"{story_id: #}"`).
|
|
59
|
+
|
|
60
|
+
### 2.2 Modelo de persistência
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
erDiagram
|
|
64
|
+
story ||--o{ story_character : "story_id"
|
|
65
|
+
story ||--o{ story_scene : "story_id"
|
|
66
|
+
story_scene }o--|| upload : "imagens geram registro em upload"
|
|
67
|
+
story {
|
|
68
|
+
string _id
|
|
69
|
+
string title
|
|
70
|
+
string genre
|
|
71
|
+
string synopsis
|
|
72
|
+
string aspect_ratio
|
|
73
|
+
bool subtitle_karaoke
|
|
74
|
+
}
|
|
75
|
+
story_character {
|
|
76
|
+
string _id
|
|
77
|
+
string story_id
|
|
78
|
+
string name
|
|
79
|
+
string description
|
|
80
|
+
string voice_profile
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Conexão é resolvida por tenant: `FrontController.getInstance(authBean.getApiKey()).getManagerFactory().getJongoConnection()`. **O isolamento multi-tenant é pela API key → conexão Jongo do tenant** (cada tenant tem seu próprio banco). Não há campo de tenant dentro do documento.
|
|
85
|
+
- `story_character.story_id` e `story_scene.story_id` são **strings que referenciam `story._id`**. A integridade é mantida apenas pelo cascade delete (seção 2.7) — não há foreign key real.
|
|
86
|
+
|
|
87
|
+
### 2.3 Pipeline — geração de estrutura de cenas (`generateSceneStructure`)
|
|
88
|
+
|
|
89
|
+
`POST /v3/story/{id}/scene/structure`. Síncrono. Usa GPT com function-calling.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
[1] Lê story (title, synopsis, genre) e todos os story_character do story_id.
|
|
93
|
+
[2] Lê parâmetros: mode (full|continue), objective, num_scenes (def 6),
|
|
94
|
+
num_decisions (def 2), num_endings (def 1*), continuation_prompt.
|
|
95
|
+
(* o JavaDoc diz "num_endings: 2", mas o código usa default 1 — linha 571)
|
|
96
|
+
[3] buildStructureSystemPrompt(...) monta o prompt-sistema com as REGRAS de ref/type.
|
|
97
|
+
[4] buildStructureUserPrompt(...) monta o pedido conforme o mode.
|
|
98
|
+
[5] Declara a Function 'generate_story_structure' com UM único parâmetro
|
|
99
|
+
'scenes_json' do tipo STRING (não array — ver "Comportamento real" abaixo).
|
|
100
|
+
[6] Aumenta o socket timeout do Unirest para 8 min (480_000ms) só nesta chamada,
|
|
101
|
+
restaurando 180_000ms no finally.
|
|
102
|
+
[7] AIService.chatCompletionWithFunction(system, user, function) → argsJson.
|
|
103
|
+
[8] Faz parse de argsJson; extrai scenes_json. Se vier String → reparseia como List;
|
|
104
|
+
senão usa como List direto (tolera ambas as formas).
|
|
105
|
+
[9] Retorna { "scenes": [...] }. NÃO PERSISTE nada — apenas devolve a estrutura.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Comportamento real:** o parâmetro da function é declarado como `type("string")` de propósito — comentário no código (linha 577-578): _"type:array is not properly serialized by ChatCompletionRequest.Parameter builder"_. Por isso o GPT devolve um JSON-string que precisa ser reparseado.
|
|
109
|
+
|
|
110
|
+
Forma de cada cena retornada (definida no `description` da property — linha 586):
|
|
111
|
+
|
|
112
|
+
- `ref` (string, ex.: `C0`, `C1`, `C2A`) — identificador único da cena
|
|
113
|
+
- `title` (string)
|
|
114
|
+
- `type`: `scene` | `decision` | `end`
|
|
115
|
+
- `description` (string)
|
|
116
|
+
- `next_scene_ref` (string) — **apenas** `type=scene`
|
|
117
|
+
- `end_label` (string), `end_score` (number) — **apenas** `type=end`
|
|
118
|
+
- `decision` (object `{ prompt, options[] }`) — **apenas** `type=decision`; cada `option` = `{ label, score (number), next_scene_ref }`
|
|
119
|
+
|
|
120
|
+
### 2.4 Pipeline — geração de vídeo (assíncrono, 2 etapas)
|
|
121
|
+
|
|
122
|
+
Etapa 1 — disparo (`generateSceneVideo`, `POST .../generate-video`):
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
flowchart LR
|
|
126
|
+
A[POST generate-video] --> B{Resolve image_url}
|
|
127
|
+
B -->|1. request.image_url| C[image_url ok]
|
|
128
|
+
B -->|2. scene.image_url| C
|
|
129
|
+
B -->|3. scene.media.url se media.type=image| C
|
|
130
|
+
B -->|nenhum| E[400 image_url is required]
|
|
131
|
+
C --> F{model == sora-2 ?}
|
|
132
|
+
F -->|nao e FREEPIK_ENABLED=false| G[400 requer Freepik]
|
|
133
|
+
F -->|sora-2| H[dispatchSoraVideo]
|
|
134
|
+
F -->|outro e Freepik ON| I[dispatchFreepikVideo]
|
|
135
|
+
H --> J[salva scene.video = pending]
|
|
136
|
+
I --> J
|
|
137
|
+
J --> K[retorna job_id + status pending]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
[1] Carrega story_scene por sceneId (404 se ausente).
|
|
142
|
+
[2] Resolve image_url por prioridade: request.image_url → scene.image_url
|
|
143
|
+
→ scene.media.url (somente se scene.media.type == "image").
|
|
144
|
+
Se nenhum → 400 "image_url is required. Generate or upload a scene image first."
|
|
145
|
+
[3] Lê story.aspect_ratio (def "16:9").
|
|
146
|
+
[4] Lê model (def "veo-3-fast"), quality (def "high"), prompt (def ""),
|
|
147
|
+
end_image_url, duration_sec (def 6), generate_audio (def true*).
|
|
148
|
+
(* generate_audio só é false se vier explicitamente Boolean.FALSE)
|
|
149
|
+
[5] SE model != "sora-2" E FREEPIK_ENABLED == false → 400 (Freepik desabilitado).
|
|
150
|
+
-> Na prática, com o default "veo-3-fast", a chamada SEM informar model FALHA.
|
|
151
|
+
[6] sora-2 → dispatchSoraVideo(...); senão → dispatchFreepikVideo(...).
|
|
152
|
+
[7] Grava em scene.video = { job_id, status:"pending", model, quality, has_native_audio }
|
|
153
|
+
e salva o documento.
|
|
154
|
+
[8] Retorna { job_id, status:"pending", model } imediatamente (não bloqueia).
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Etapa 2 — polling (`checkSceneVideoJob`, `GET .../video-job`):
|
|
158
|
+
|
|
159
|
+
```mermaid
|
|
160
|
+
stateDiagram-v2
|
|
161
|
+
[*] --> none: scene.video ausente
|
|
162
|
+
none --> pending: generate-video dispara job
|
|
163
|
+
pending --> pending: API ainda processando
|
|
164
|
+
pending --> completed: API devolve completed
|
|
165
|
+
pending --> failed: API devolve failed/errored
|
|
166
|
+
completed --> completed: estado em cache (nao reconsulta)
|
|
167
|
+
failed --> failed: estado em cache (nao reconsulta)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
[1] Carrega scene (404 se ausente).
|
|
172
|
+
[2] Se scene.video ausente → { status:"none" }.
|
|
173
|
+
[3] Se status já é completed/failed OU job_id == null → retorna estado em cache
|
|
174
|
+
(não reconsulta a API externa).
|
|
175
|
+
[4] Senão consulta a API: sora-2 → checkSoraVideo(jobId); outro → checkFreepikVideo.
|
|
176
|
+
[5] SE completed:
|
|
177
|
+
videoUrl = checkResult.video_url
|
|
178
|
+
SE videoUrl vazio (caso Sora) → downloadAndSaveSoraVideo(jobId) baixa de
|
|
179
|
+
/v1/videos/{id}/content e salva no CDN (URL da OpenAI expira em ~1h).
|
|
180
|
+
Atualiza scene.video.url/status/duration_sec,
|
|
181
|
+
scene.media_mode = "video",
|
|
182
|
+
scene.media = { type:"video", url, duration } e SALVA.
|
|
183
|
+
SE failed: grava scene.video.status = "failed" e salva.
|
|
184
|
+
SENÃO: { status:"pending" } (sem persistir).
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Pseudocódigo da cadeia de fallback de URL do Sora (`checkSoraVideo`, linhas 1127-1147) — a resposta da OpenAI varia entre versões:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
videoUrl = json.url
|
|
191
|
+
se vazio: videoUrl = json.data[0].url
|
|
192
|
+
se vazio: videoUrl = json.result.url
|
|
193
|
+
se vazio: videoUrl = json.output[0].url
|
|
194
|
+
se vazio: videoUrl = json.video.url
|
|
195
|
+
se ainda vazio: (etapa 2) baixa o binário de /v1/videos/{id}/content e salva no CDN
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 2.5 Pipeline — TTS de diálogo + Whisper karaokê (`generateDialogueTts`)
|
|
199
|
+
|
|
200
|
+
`POST /v3/story/{storyId}/scene/{sceneId}/dialogue/{dialogueIndex}/tts`. Síncrono.
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
[1] Lê text (obrigatório → 400 se vazio) e voice (def "alloy").
|
|
204
|
+
[2] POST OpenAI /v1/audio/speech { model:"tts-1", input:text, voice, response_format:"mp3" }.
|
|
205
|
+
(atenção: o comentário do método diz "tts-1-hd", mas o código envia "tts-1")
|
|
206
|
+
[3] Se HTTP != 200 → RuntimeException com corpo do erro.
|
|
207
|
+
[4] Lê os bytes do MP3.
|
|
208
|
+
[5] SE story.subtitle_karaoke == true → callWhisperWordTimestamps(mp3Bytes)
|
|
209
|
+
(best-effort: qualquer exceção é capturada e logada em stderr; NÃO falha o request).
|
|
210
|
+
[6] Salva o MP3 no storage do tenant (session "story_audio") via S3/Azure/Google.
|
|
211
|
+
[7] SE houve word_timestamps → updateDialogueWordTimestamps grava
|
|
212
|
+
scene.dialogues[dialogueIndex].word_timestamps (re-lê a cena, muta e salva).
|
|
213
|
+
[8] Retorna { url, word_timestamps? }.
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`callWhisperWordTimestamps`: `POST /v1/audio/transcriptions` com `model=whisper-1`, `response_format=verbose_json`, `timestamp_granularities[]=word`, `language=pt`. Retorna lista de `{ word, start, end }`.
|
|
217
|
+
|
|
218
|
+
### 2.6 Pipeline — geração de imagem (cena / personagem / capa)
|
|
219
|
+
|
|
220
|
+
Três endpoints (`generateSceneImage`, `generateCharacterImage`, `generateStoryCover`) seguem o mesmo padrão, variando apenas o prompt e o `aspect_ratio`:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
[1] Lê documentos de contexto (story e/ou story_character).
|
|
224
|
+
[2] Monta prompt em PT-BR concatenando título/gênero/sinopse/personagens + prompt do usuário.
|
|
225
|
+
[3] generateImageBytes(prompt, reference_image_urls, aspectRatio):
|
|
226
|
+
SE não há reference_image_urls → AIService.generateImage(prompt, sizeGenerations)
|
|
227
|
+
(OpenAI /v1/images/generations, modelo padrão do AIService)
|
|
228
|
+
SENÃO → baixa cada imagem de referência (fetchImageBytes) e chama
|
|
229
|
+
/v1/images/edits (multipart, model = gpt-image-1, n=1) usando-as como referência.
|
|
230
|
+
Se o download de TODAS as refs falhar → cai no caminho text-only.
|
|
231
|
+
[4] saveImageUpload(bytes, prefix, session): salva o PNG no storage E cria um
|
|
232
|
+
registro na coleção `upload` (_id, filename, content_type, size, time + campos do cloud).
|
|
233
|
+
[5] Retorna { url, upload }.
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
| Endpoint | `aspect_ratio` usado | session de storage | prefixo do arquivo |
|
|
237
|
+
|---|---|---|---|
|
|
238
|
+
| `generateSceneImage` | `story.aspect_ratio` (def 16:9) | `story_scenes` | `story-scene-` |
|
|
239
|
+
| `generateCharacterImage` | **fixo `1:1`** | `story_characters` | `story-char-` |
|
|
240
|
+
| `generateStoryCover` | `story.aspect_ratio` (def 16:9) | `story_covers` | `story-cover-` |
|
|
241
|
+
|
|
242
|
+
Tabelas de tamanho (helpers em `StoryRest`):
|
|
243
|
+
|
|
244
|
+
| aspect_ratio | `/images/generations` e `/edits` | Sora `high` | Sora `light` | Freepik enum |
|
|
245
|
+
|---|---|---|---|---|
|
|
246
|
+
| `16:9` | `1536x1024` | `1920x1080` | `1280x720` | `widescreen_16_9` |
|
|
247
|
+
| `9:16` | `1024x1536` | `1080x1920` | `720x1280` | `social_story_9_16` |
|
|
248
|
+
| `1:1` | `1024x1024` | `1080x1080` | `720x720` | `square_1_1` |
|
|
249
|
+
|
|
250
|
+
### 2.7 Cascade delete (`deleteStory`)
|
|
251
|
+
|
|
252
|
+
`DELETE /v3/story/{id}` executa, **em ordem**, três `remove` Jongo parametrizados:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
db.story_character.remove({ story_id: id })
|
|
256
|
+
db.story_scene.remove({ story_id: id })
|
|
257
|
+
db.story.remove({ _id: id })
|
|
258
|
+
→ 204 No Content
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Sem transação (Mongo standalone): se falhar no meio, pode deixar órfãos. As subcoleções são apagadas **antes** do documento raiz.
|
|
262
|
+
|
|
263
|
+
### 2.8 Diagnóstico OpenAI
|
|
264
|
+
|
|
265
|
+
- `probeOpenAiVideo` (`POST /v3/story/openai/video-probe`): tenta `POST` em `https://api.openai.com/v1/videos` e `/v1/video/generations` com um corpo de teste; para no primeiro retorno != 404; devolve status+body bruto de cada tentativa.
|
|
266
|
+
- `listOpenAiModels` (`GET /v3/story/openai/models`): lista modelos da conta filtrando por `sora`/`video`/`dall`; retorna `sora_accessible` e contagem total.
|
|
267
|
+
|
|
268
|
+
Ambos servem para diagnosticar acesso da API key da OpenAI — não tocam nas coleções.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 3. Estrutura dos Objetos
|
|
273
|
+
|
|
274
|
+
> **Aviso crítico:** **não existe schema no servidor.** As tabelas abaixo são **inferidas dos campos que o código realmente lê/escreve** em runtime. Qualquer outro campo gravado pelo frontend via `/v3/database/*` é persistido tal qual e **ignorado pelo backend**. A coluna **Acesso backend** indica se `StoryRest` apenas lê (R) ou também escreve (W) o campo.
|
|
275
|
+
|
|
276
|
+
### 3.1 `story` — documento raiz
|
|
277
|
+
|
|
278
|
+
| Campo | Tipo | Padrão (no read) | Acesso backend | Descrição |
|
|
279
|
+
|---|---|---|---|---|
|
|
280
|
+
| `_id` | String | gerado pelo cliente / `/v3/database` | R | Id da história. Referenciado por `story_id` nas subcoleções. |
|
|
281
|
+
| `title` | String | `""` | R | Título; entra nos prompts de imagem e de estrutura. |
|
|
282
|
+
| `genre` | String | `""` | R | Gênero; entra nos prompts. |
|
|
283
|
+
| `synopsis` | String | `""` | R | Sinopse; entra no prompt de capa e de estrutura. |
|
|
284
|
+
| `aspect_ratio` | String | `"16:9"` | R | `16:9` \| `9:16` \| `1:1`. Define o tamanho de imagem/vídeo de cena e capa. |
|
|
285
|
+
| `subtitle_karaoke` | Boolean | `false` | R | Se `true`, o TTS de diálogo chama o Whisper para gerar `word_timestamps`. |
|
|
286
|
+
|
|
287
|
+
Demais campos (capa, descrição, etc.) que o frontend persista **não são lidos** pelo backend.
|
|
288
|
+
|
|
289
|
+
### 3.2 `story_character`
|
|
290
|
+
|
|
291
|
+
| Campo | Tipo | Padrão (no read) | Acesso backend | Descrição |
|
|
292
|
+
|---|---|---|---|---|
|
|
293
|
+
| `_id` | String | cliente | R | Id do personagem. |
|
|
294
|
+
| `story_id` | String | — | R | FK lógica para `story._id` (usada no cascade delete e na geração de estrutura). |
|
|
295
|
+
| `name` | String | `""` / `"Personagem"` | R | Nome; entra no prompt de retrato e na prévia de voz. |
|
|
296
|
+
| `description` | String | `""` | R | Descrição visual/narrativa; entra nos prompts. Na prévia de voz é truncada em 120 chars. |
|
|
297
|
+
| `voice_profile` | String | `"alloy"` | R | Voz OpenAI padrão do personagem (fallback da prévia de voz). |
|
|
298
|
+
|
|
299
|
+
### 3.3 `story_scene`
|
|
300
|
+
|
|
301
|
+
| Campo | Tipo | Padrão | Acesso backend | Descrição |
|
|
302
|
+
|---|---|---|---|---|
|
|
303
|
+
| `_id` | String | cliente | R | Id da cena. |
|
|
304
|
+
| `story_id` | String | — | R | FK lógica para `story._id` (cascade delete). |
|
|
305
|
+
| `image_url` | String | — | R | URL da imagem da cena; pré-requisito para gerar vídeo. |
|
|
306
|
+
| `media` | Object | — | R/**W** | `{ type, url, duration }`. **Escrito** quando o vídeo conclui (`type:"video"`). Também lido (`type:"image"`) para resolver `image_url` no vídeo. |
|
|
307
|
+
| `media_mode` | String | — | **W** | Definido como `"video"` quando o vídeo conclui. |
|
|
308
|
+
| `video` | Object | — | R/**W** | Estado do job de vídeo (ver 3.4). **Escrito** no disparo e no polling. |
|
|
309
|
+
| `dialogues` | Array | — | R/**W** | Lista de diálogos da cena. O backend só escreve `dialogues[i].word_timestamps`. |
|
|
310
|
+
|
|
311
|
+
Campos da **estrutura narrativa** (`ref`, `type`, `next_scene_ref`, `decision`, `end_label`, `end_score`, `title`, `description`) são **gravados pelo frontend** a partir do retorno de `generateSceneStructure` (seção 2.3) — o backend **não os lê nem valida** nos fluxos de mídia. Ver formato em 2.3.
|
|
312
|
+
|
|
313
|
+
### 3.4 Subobjeto `story_scene.video`
|
|
314
|
+
|
|
315
|
+
| Campo | Tipo | Origem | Descrição |
|
|
316
|
+
|---|---|---|---|
|
|
317
|
+
| `job_id` | String | dispatch | Id do job na API externa (Sora `id`, Freepik `task_id`). |
|
|
318
|
+
| `status` | String | dispatch/polling | `pending` \| `completed` \| `failed`. (`none` é retornado quando o subobjeto não existe.) |
|
|
319
|
+
| `model` | String | request | `sora-2` (único operacional) \| `veo-3-fast` \| `kling-3.0`. |
|
|
320
|
+
| `quality` | String | request | `high` \| `light` (afeta o tamanho do vídeo Sora). |
|
|
321
|
+
| `has_native_audio` | Boolean | dispatch | Se o modelo gera áudio nativo. |
|
|
322
|
+
| `url` | String | polling | URL final (CDN, após download do Sora). |
|
|
323
|
+
| `duration_sec` | Number | polling | Duração efetiva, quando informada. |
|
|
324
|
+
|
|
325
|
+
### 3.5 Subobjeto `story_scene.media`
|
|
326
|
+
|
|
327
|
+
`{ "type": "image"|"video", "url": "...", "duration": <number opcional> }`. O backend escreve este objeto na conclusão do vídeo; lê quando precisa achar a imagem de início do vídeo.
|
|
328
|
+
|
|
329
|
+
### 3.6 `story_scene.dialogues[i].word_timestamps`
|
|
330
|
+
|
|
331
|
+
Lista de `{ "word": String, "start": Number, "end": Number }`, em segundos, produzida pelo Whisper. Gravada **somente** quando `story.subtitle_karaoke == true` e o Whisper respondeu com sucesso.
|
|
332
|
+
|
|
333
|
+
> **Não há enums numéricos nem "técnicas de jogo (GT)" neste módulo** — diferentemente de `achievement`/`challenge`. Os únicos conjuntos fechados de valores são strings: `type` da cena (`scene|decision|end`), `aspect_ratio` (`16:9|9:16|1:1`), `status` do vídeo (`pending|completed|failed`), vozes TTS (`alloy|echo|fable|onyx|nova|shimmer`) e `model` de vídeo.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 4. Endpoints
|
|
338
|
+
|
|
339
|
+
> Todos os endpoints exigem `Authorization: Bearer <token>` (via `@BeanParam AuthBean`). **Nenhum endpoint de `StoryRest` verifica scope ou role** — ver seção 8. Erros são sempre devolvidos como `{ "errorMessage": "..." }` (o handler nunca propaga exceção).
|
|
340
|
+
|
|
341
|
+
### 4.0 CRUD (não é `StoryRest`)
|
|
342
|
+
|
|
343
|
+
Criar/ler/atualizar `story`, `story_character`, `story_scene` usa o endpoint **genérico**:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
GET /v3/database/{collection}/{id}
|
|
347
|
+
GET /v3/database/{collection}?... (query/aggregate)
|
|
348
|
+
POST /v3/database/{collection} (insert/save)
|
|
349
|
+
PUT /v3/database/{collection}/{id}
|
|
350
|
+
DELETE /v3/database/{collection}/{id}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`DatabaseRest.find` exige o scope `SCOPE_DATABASE` e `collection.length() > 3` (todas as três coleções qualificam). É **full replace** no padrão do Funifier (o `save` substitui o documento).
|
|
354
|
+
|
|
355
|
+
### 4.1 `DELETE /v3/story/{id}` — cascade delete
|
|
356
|
+
|
|
357
|
+
| Aspecto | Detalhe |
|
|
358
|
+
|---|---|
|
|
359
|
+
| Finalidade | Apagar a história e tudo que a referencia (`story_character`, `story_scene`). |
|
|
360
|
+
| Autenticação | Bearer token (resolve o tenant). **Sem checagem de scope/role.** |
|
|
361
|
+
| Tipo | Deleção em cascata, 3 `remove` sequenciais, sem transação. |
|
|
362
|
+
| Resposta | `204 No Content`. |
|
|
363
|
+
|
|
364
|
+
### 4.2 `POST /v3/story/{id}/scene/generate` — imagem de cena
|
|
365
|
+
|
|
366
|
+
Body: `{ "scene_id": "...", "prompt": "...", "characters": [{name, description}], "reference_image_urls": ["url"] }`
|
|
367
|
+
Resposta: `{ "url": "...", "upload": { ... } }`. Usa `story.aspect_ratio`. Com `reference_image_urls` → `/v1/images/edits`; sem → `/v1/images/generations`.
|
|
368
|
+
|
|
369
|
+
### 4.3 `POST /v3/story/{storyId}/character/{charId}/generate-image` — retrato
|
|
370
|
+
|
|
371
|
+
Body: `{ "prompt": "...", "reference_image_urls": ["url"] }`. **Aspect ratio fixo `1:1`.** 404 se o personagem não existe. Resposta `{ url, upload }`.
|
|
372
|
+
|
|
373
|
+
### 4.4 `POST /v3/story/{id}/cover/generate` — capa
|
|
374
|
+
|
|
375
|
+
Body: `{ "prompt": "...", "reference_image_urls": ["url"] }`. Usa `story.aspect_ratio`. Resposta `{ url, upload }`.
|
|
376
|
+
|
|
377
|
+
### 4.5 `POST /v3/story/{storyId}/scene/{sceneId}/dialogue/{dialogueIndex}/tts` — áudio do diálogo
|
|
378
|
+
|
|
379
|
+
| Param | Tipo | Descrição |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| `storyId` | path | Usado só para checar `subtitle_karaoke`. |
|
|
382
|
+
| `sceneId` | path | Cena cujo diálogo recebe `word_timestamps`. |
|
|
383
|
+
| `dialogueIndex` | path (int) | Índice em `scene.dialogues`. |
|
|
384
|
+
|
|
385
|
+
Body: `{ "text": "...", "voice": "alloy|echo|fable|onyx|nova|shimmer" }`. `text` obrigatório (400 se vazio). Resposta: `{ "url": "...", "word_timestamps"?: [...] }`. **Comportamento real:** envia `model:"tts-1"` (o comentário do método cita `tts-1-hd`, mas não é o que roda).
|
|
386
|
+
|
|
387
|
+
### 4.6 `POST /v3/story/{storyId}/character/{charId}/voice-preview` — prévia de voz
|
|
388
|
+
|
|
389
|
+
Body: `{ "voice": "nova" }` (opcional; cai em `character.voice_profile`, depois `"alloy"`). Gera uma autoapresentação curta (`"Olá! Meu nome é {name}. {description[:120]}"`) e devolve `{ url }`. 404 se o personagem não existe.
|
|
390
|
+
|
|
391
|
+
### 4.7 `POST /v3/story/{id}/scene/structure` — estrutura por GPT
|
|
392
|
+
|
|
393
|
+
Body:
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"mode": "full",
|
|
397
|
+
"objective": "...",
|
|
398
|
+
"num_scenes": 6,
|
|
399
|
+
"num_decisions": 2,
|
|
400
|
+
"num_endings": 1,
|
|
401
|
+
"continuation_prompt": "...",
|
|
402
|
+
"from_scene_id": "..."
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
Resposta: `{ "scenes": [ { "ref":"C0", "type":"scene|decision|end", ... } ] }`. **Não persiste** — o frontend é quem grava as cenas. Timeout estendido para 8 min nesta chamada. **Atenção:** `from_scene_id` aparece no JavaDoc mas **não é lido** no código (ignorado silenciosamente).
|
|
406
|
+
|
|
407
|
+
### 4.8 `POST /v3/story/{storyId}/scene/{sceneId}/generate-video` — disparo de vídeo
|
|
408
|
+
|
|
409
|
+
Body: `{ "model":"sora-2", "quality":"high|light", "duration_sec":6, "prompt":"...", "image_url":"...", "end_image_url":"...", "generate_audio":true }`. Async: devolve `{ job_id, status:"pending", model }`. **Requer uma imagem de início** (ver prioridade em 2.4). **Trap:** o default de `model` é `veo-3-fast`, que com `FREEPIK_ENABLED=false` retorna **400** — só `sora-2` funciona (informe-o explicitamente).
|
|
410
|
+
|
|
411
|
+
### 4.9 `GET /v3/story/{storyId}/scene/{sceneId}/video-job` — polling
|
|
412
|
+
|
|
413
|
+
Resposta: `{ status: "none|pending|completed|failed", video_url?, duration_sec?, error? }`. Em `completed`, persiste `scene.video.url`, `scene.media`, `scene.media_mode="video"`. Estados `completed`/`failed` ficam em cache (não reconsultam a API).
|
|
414
|
+
|
|
415
|
+
### 4.10 Diagnóstico
|
|
416
|
+
|
|
417
|
+
- `POST /v3/story/openai/video-probe` → `{ probes: [{url, http_status, response_body}] }`.
|
|
418
|
+
- `GET /v3/story/openai/models` → `{ models: [...], total_models_in_account, sora_accessible }`.
|
|
419
|
+
|
|
420
|
+
### 4.11 Legado — `POST /v3/ai/build/story` (em `AIRest`, **não** em `StoryRest`)
|
|
421
|
+
|
|
422
|
+
Body: `{ "content": "<pedido em texto>", "current": <objeto opcional> }`. Usa GPT-3.5 + function `build_story` e devolve um JSON em **formato totalmente diferente** do `StoryRest` (objeto-mapa de cenas com `next_scenes[]`, personagens como objeto-mapa). **Não persiste nada.** Ver seção 7.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 5. Regras de Negócio
|
|
427
|
+
|
|
428
|
+
Regras que existem **no código** e não em nenhum schema:
|
|
429
|
+
|
|
430
|
+
- **Pré-requisito de imagem para vídeo:** sem `image_url` resolvível → 400. A imagem do Sora é **redimensionada para o tamanho exato** do vídeo e enviada como `input_reference.image_url` (data URL base64).
|
|
431
|
+
- **Snap de duração do Sora:** `seconds = min(duration_sec, 20)`, depois `snapSoraSeconds`: `<=6 → "4"`, `<=10 → "8"`, senão `"12"`. O `duration_sec` do cliente é **reescrito silenciosamente**.
|
|
432
|
+
- **Áudio "silencioso":** se `generate_audio=false`, o backend **anexa ao prompt** o texto `" Silent video, no audio track."` (não desliga o áudio via parâmetro de API).
|
|
433
|
+
- **Freepik desabilitado:** `FREEPIK_ENABLED=false` (constante). Qualquer `model != sora-2` → 400. Os caminhos `veo-3-1-fast`/`kling-v2-6-pro` são **código morto** no estado atual.
|
|
434
|
+
- **Karaokê condicional:** Whisper só roda se `story.subtitle_karaoke==true`; falha de Whisper **não** quebra o TTS (best-effort, capturada).
|
|
435
|
+
- **Voz default `alloy`** em TTS e prévia de voz quando não informada / `voice_profile` ausente.
|
|
436
|
+
- **Multi-tenant por API key:** todo acesso resolve a conexão Jongo do tenant via `FrontController.getInstance(apiKey)`. Não há `_id` global cruzando tenants.
|
|
437
|
+
- **`num_endings` default = 1** no código (apesar do JavaDoc dizer 2).
|
|
438
|
+
- **Consistência eventual de mídia:** o vídeo só "existe" no documento após um `GET .../video-job` retornar `completed`; antes disso, `scene.video.status="pending"`.
|
|
439
|
+
- **Imagens geram registro em `upload`; áudio e vídeo não.** Áudio (`story_audio`) e vídeo (`story_videos`) são salvos no storage mas **sem** documento na coleção `upload`.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## 6. Comportamentos Automáticos
|
|
444
|
+
|
|
445
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
446
|
+
|---|---|---|---|
|
|
447
|
+
| Conclusão de vídeo atualiza a cena | `GET .../video-job` retorna `completed` | Escreve `scene.video.{url,status,duration_sec}`, `scene.media_mode="video"`, `scene.media={type:video,url,duration}` | `story_scene` |
|
|
448
|
+
| Download do vídeo Sora para o CDN | `video-job completed` e URL ausente na resposta | Baixa `/v1/videos/{id}/content` e salva em `story_videos`; usa URL permanente do CDN | storage (sem doc `upload`) |
|
|
449
|
+
| Word timestamps de karaokê | TTS com `story.subtitle_karaoke==true` | Escreve `scene.dialogues[i].word_timestamps` | `story_scene` |
|
|
450
|
+
| Registro de upload de imagem | Qualquer geração de imagem (cena/personagem/capa) | Cria documento na coleção `upload` | `upload` |
|
|
451
|
+
| Auto-cadastro do prompt legado | 1ª chamada a `/v3/ai/build/story` sem `intro_build_story` | Insere o `Prompt` `intro_build_story` no sistema | coleção de prompts |
|
|
452
|
+
| Cascade delete | `DELETE /v3/story/{id}` | Remove `story_character` + `story_scene` + `story` | 3 coleções |
|
|
453
|
+
|
|
454
|
+
> **Não há triggers (`before_*`/`after_*`) nem webhooks** disparados por `StoryRest`. Nenhum evento de gamificação é emitido por este módulo.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 7. Suportado vs NÃO Suportado
|
|
459
|
+
|
|
460
|
+
### ✅ Suportado
|
|
461
|
+
|
|
462
|
+
- CRUD das três coleções via `/v3/database/*` (schema-less).
|
|
463
|
+
- Cascade delete via `DELETE /v3/story/{id}`.
|
|
464
|
+
- Geração de imagem (cena, personagem, capa) com ou sem imagens de referência.
|
|
465
|
+
- TTS de diálogo (`tts-1`) e prévia de voz de personagem.
|
|
466
|
+
- Karaokê (word timestamps Whisper) quando `subtitle_karaoke=true`.
|
|
467
|
+
- Geração de estrutura narrativa por GPT (function-calling).
|
|
468
|
+
- Geração de vídeo **apenas via Sora 2** (disparo + polling + download para CDN).
|
|
469
|
+
- Diagnóstico de acesso OpenAI (`/openai/video-probe`, `/openai/models`).
|
|
470
|
+
|
|
471
|
+
### ❌ NÃO Suportado / armadilhas
|
|
472
|
+
|
|
473
|
+
- **Modelos de vídeo Freepik (`veo-3-fast`, `kling-3.0`):** desabilitados (`FREEPIK_ENABLED=false`) — código morto; chamada retorna 400. **O default de `model` (`veo-3-fast`) falha** — é obrigatório passar `sora-2`.
|
|
474
|
+
- **`tts-1-hd`:** citado no comentário, **não** usado — o código envia `tts-1`.
|
|
475
|
+
- **`from_scene_id`** (estrutura, `mode=continue`): documentado no JavaDoc, **não** é lido pelo código.
|
|
476
|
+
- **CRUD dedicado em `/v3/story`:** não existe (`POST`/`GET /v3/story` não estão implementados).
|
|
477
|
+
- **Triggers/webhooks/notificações:** não disparados por este módulo.
|
|
478
|
+
- **`AIRest.buildStory` (`/v3/ai/build/story`) — formato legado/incompatível:** gera história em **objeto-mapa** (`scenes:{ "start": {..., next_scenes:[{scene,text}]} }`, `characters:{...}`) com GPT-3.5, **sem persistir**. **Não** é o schema usado por `StoryRest` (que trabalha com cenas em array, `ref`/`type`/`next_scene_ref`, e personagens/cenas como documentos separados). Tratar como recurso legado.
|
|
479
|
+
- **Transação no cascade delete:** inexistente — falha parcial deixa órfãos.
|
|
480
|
+
- **Validação de schema dos documentos:** inexistente — qualquer campo extra é aceito e ignorado.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## 8. Segurança e Permissões
|
|
485
|
+
|
|
486
|
+
- **Autenticação:** todo endpoint recebe `@BeanParam AuthBean` e usa `authBean.getApiKey()` para resolver a conexão do tenant. Um token Bearer válido é necessário para resolver o tenant.
|
|
487
|
+
- **Sem autorização por endpoint:** **nenhum** método de `StoryRest` chama `getScope()`, `SCOPE_*`, `getRole()` ou similar (confirmado por grep — 0 ocorrências). Isso contrasta com `DatabaseRest.find`, que exige `SCOPE_DATABASE`. **Consequência:** qualquer token válido do tenant pode chamar qualquer endpoint de `StoryRest`, **inclusive o `DELETE` em cascata** — que apaga história, personagens e cenas sem checagem de papel/scope.
|
|
488
|
+
- **Isolamento multi-tenant:** garantido pela conexão Jongo por tenant (`FrontController.getInstance(apiKey)`); não há campo de tenant nos documentos.
|
|
489
|
+
- **Segredo hardcoded:** `FREEPIK_API_KEY = "FPSX69b6861d3b45e4591f8e8d354ebf3ec0"` está **literalmente no código-fonte** (linha 630). Mesmo com `FREEPIK_ENABLED=false`, é um vazamento de credencial versionado — deve ser rotacionado e movido para configuração.
|
|
490
|
+
- **Superfície de SSRF:** `fetchImageBytes(url)` faz `GET` em **URLs arbitrárias** vindas do request (`reference_image_urls`, `image_url`, `end_image_url`) sem allowlist. Um chamador pode induzir o servidor a buscar URLs internas. Mitigado parcialmente apenas por reencode de URL (não por validação de destino).
|
|
491
|
+
- **Injeção MongoDB:** **não há** — todas as queries usam placeholders Jongo parametrizados (`"{_id: #}"`, `"{story_id: #}"`). Não há concatenação de query crua.
|
|
492
|
+
- **Token OpenAI compartilhado:** `AIService.CHATGPT_TOKEN` é global do servidor (não por tenant) — custo/uso de IA não é isolado por tenant no nível da credencial.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## 9. Observabilidade e Troubleshooting
|
|
497
|
+
|
|
498
|
+
**Diagnóstico rápido**
|
|
499
|
+
|
|
500
|
+
- Verificar acesso à OpenAI/Sora: `GET /v3/story/openai/models` (cheque `sora_accessible:true`) e `POST /v3/story/openai/video-probe`.
|
|
501
|
+
- Logs vão para **stderr** com prefixos `StoryRest...` — ex.: `StoryRest | Whisper word timestamps failed (non-fatal): ...`, `StoryRest.generateImageBytes | failed to fetch ref image: ...`, `StoryRest.updateDialogueWordTimestamps | error: ...`.
|
|
502
|
+
|
|
503
|
+
**Queries úteis (via `/v3/database`)**
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
GET /v3/database/story/<storyId>
|
|
507
|
+
GET /v3/database/story_character/<charId>
|
|
508
|
+
GET /v3/database/story_scene/<sceneId>
|
|
509
|
+
# todas as cenas de uma história (aggregate/find por story_id):
|
|
510
|
+
POST /v3/database/story_scene/aggregate
|
|
511
|
+
[ { "$match": { "story_id": "<storyId>" } } ]
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Erros comuns e causas**
|
|
515
|
+
|
|
516
|
+
| Sintoma | Causa provável |
|
|
517
|
+
|---|---|
|
|
518
|
+
| 400 `image_url is required` no `generate-video` | Cena sem imagem (gere/anexe a imagem antes). |
|
|
519
|
+
| 400 `Modelo '...' requer integração Freepik` | `model != sora-2` com Freepik desabilitado — use `sora-2`. |
|
|
520
|
+
| `video-job` preso em `pending` | Job ainda processando, ou Sora não devolveu URL (cai no download via `/content`). |
|
|
521
|
+
| Vídeo "some" após ~1h | URL temporária da OpenAI expirou — o download para CDN só ocorre no `video-job completed`. |
|
|
522
|
+
| TTS funciona mas sem karaokê | `story.subtitle_karaoke != true`, ou Whisper falhou (cheque stderr). |
|
|
523
|
+
| 404 `Character/Scene not found` | `_id` inexistente no tenant atual. |
|
|
524
|
+
|
|
525
|
+
**O que verificar quando algo não funciona**
|
|
526
|
+
|
|
527
|
+
1. Token Bearer válido e tenant correto (a coleção é por tenant).
|
|
528
|
+
2. `story.aspect_ratio` válido (`16:9|9:16|1:1`) — valores fora disso caem em 16:9.
|
|
529
|
+
3. Para vídeo: `model:"sora-2"` explícito e imagem de início presente.
|
|
530
|
+
4. Acesso da conta OpenAI ao Sora (`/openai/models`).
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 10. Exemplos Práticos
|
|
535
|
+
|
|
536
|
+
### 10.1 Mínimo funcional — gerar estrutura e capa
|
|
537
|
+
|
|
538
|
+
```http
|
|
539
|
+
POST /v3/story/STORY123/scene/structure
|
|
540
|
+
Authorization: Bearer <token>
|
|
541
|
+
Content-Type: application/json
|
|
542
|
+
|
|
543
|
+
{ "mode": "full", "objective": "Treinamento de segurança no trabalho", "num_scenes": 5, "num_decisions": 2, "num_endings": 2 }
|
|
544
|
+
```
|
|
545
|
+
```http
|
|
546
|
+
POST /v3/story/STORY123/cover/generate
|
|
547
|
+
{ "prompt": "estilo cartoon corporativo" }
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### 10.2 Avançado — vídeo de cena via Sora com áudio
|
|
551
|
+
|
|
552
|
+
```http
|
|
553
|
+
POST /v3/story/STORY123/scene/SCENE9/generate-video
|
|
554
|
+
{ "model": "sora-2", "quality": "high", "duration_sec": 8, "prompt": "câmera lenta aproximando do personagem", "generate_audio": true }
|
|
555
|
+
→ { "job_id": "video_abc", "status": "pending", "model": "sora-2" }
|
|
556
|
+
```
|
|
557
|
+
Polling até concluir:
|
|
558
|
+
```http
|
|
559
|
+
GET /v3/story/STORY123/scene/SCENE9/video-job
|
|
560
|
+
→ { "status": "completed", "video_url": "https://cdn.../story-video-xyz.mp4", "duration_sec": 8 }
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### 10.3 Anti-pattern (o que NÃO fazer)
|
|
564
|
+
|
|
565
|
+
- **Disparar vídeo sem informar `model`** → usa `veo-3-fast` por default → **400** (Freepik desabilitado). Sempre passe `"model":"sora-2"`.
|
|
566
|
+
- **Pedir `duration_sec: 6` esperando 6s** → o Sora **snap** para `"4"`. Use a tabela do snap (`<=6→4`, `<=10→8`, senão 12) para escolher conscientemente.
|
|
567
|
+
- **Confiar na URL retornada direto pela OpenAI** → ela expira em ~1h; só persiste no CDN após `video-job` retornar `completed`.
|
|
568
|
+
- **Chamar o `DELETE /v3/story/{id}` sem cuidado** → não há checagem de scope/role e não há transação: apaga história + personagens + cenas de forma irreversível.
|
|
569
|
+
- **Misturar `/v3/ai/build/story` com o editor novo** → formatos de história incompatíveis (objeto-mapa vs. array de cenas com `ref`).
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Checklist de Configuração
|
|
574
|
+
|
|
575
|
+
- [ ] Token Bearer válido do tenant em todas as chamadas.
|
|
576
|
+
- [ ] Conta OpenAI com acesso ao **Sora 2** (`GET /v3/story/openai/models` → `sora_accessible:true`).
|
|
577
|
+
- [ ] `story.aspect_ratio` definido (`16:9|9:16|1:1`) antes de gerar imagem/vídeo.
|
|
578
|
+
- [ ] **Imagem de cena gerada/anexada antes** de `generate-video` (pré-requisito).
|
|
579
|
+
- [ ] `"model":"sora-2"` **explícito** no `generate-video` (default falha).
|
|
580
|
+
- [ ] `story.subtitle_karaoke=true` se quiser word timestamps no TTS.
|
|
581
|
+
- [ ] `voice_profile` definido no personagem para prévia de voz consistente.
|
|
582
|
+
- [ ] Armadilha: `duration_sec` é **reescrito** (snap 4/8/12) no Sora.
|
|
583
|
+
- [ ] Armadilha: omitir `generate_audio` ⇒ `true`; `false` apenas injeta texto "silent" no prompt.
|
|
584
|
+
- [ ] Lembrar: cascade `DELETE` é **irreversível e sem autorização por role**.
|
|
585
|
+
|
|
586
|
+
## Steps
|
|
587
|
+
|
|
588
|
+
### Sobre Interactive Stories
|
|
589
|
+
|
|
590
|
+
Histórias interativas têm: **título** → **cenas** → **escolhas** → **desfechos alternativos**.
|
|
591
|
+
O Studio oferece um editor visual para montar a narrativa. Via API, cada scene e choice é um documento.
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Steps
|
|
596
|
+
|
|
597
|
+
### 1. Verificar se já existe
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
funifier_list type=story search=<titulo>
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 2. Ler a documentação do módulo
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
datasource-funifier-docs/knowledge/modules/story.md
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### 3. Criar a história
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{
|
|
613
|
+
"title": "Sales Training Story",
|
|
614
|
+
"description": "A branching scenario for sales training.",
|
|
615
|
+
"active": true
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
funifier_save type=story payload=<json>
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 4. Criar cenas e escolhas via Studio
|
|
624
|
+
|
|
625
|
+
O editor visual do Studio (`/studio/story`) é a forma recomendada para montar a narrativa com ramificações, imagens e vídeos.
|
|
626
|
+
|
|
627
|
+
Para criação via API, consulte `GET /v3/story/:id` para entender a estrutura dos documentos existentes e replicar o padrão.
|
|
628
|
+
|
|
629
|
+
### 5. Validar
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
funifier_list type=story search=<titulo>
|
|
633
|
+
GET /v3/story/<_id>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Teste a navegação entre cenas no Studio antes de publicar.
|