funifier-mcp 0.2.25 → 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.
Files changed (211) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +5 -2
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,42 +1,578 @@
1
- # Story (História)
1
+ # `story`
2
2
 
3
- **Acesso Studio:** `/studio/story`
4
- **API Endpoint:** `/v3/story`
3
+ **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)_
4
+ **API Endpoint:** `/v3/story/*` (apenas endpoints especiais de IA + cascade delete) **+** CRUD genérico via `/v3/database/{story|story_character|story_scene}`
5
+ **Coleções MongoDB:** `story`, `story_character`, `story_scene` (+ gravação derivada na coleção `upload`)
5
6
 
6
- ## O que é
7
+ > **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.
7
8
 
8
- Criação de histórias interativas com múltiplos caminhos e finais. Permite criar narrativas gamificadas nas quais o jogador faz escolhas que afetam o desenrolar da trama. Cenários, personagens, imagens e vídeos podem ser utilizados para enriquecer a experiência.
9
+ ---
9
10
 
10
- ## Quando usar
11
+ ## 1. Visão Geral
11
12
 
12
- - Para treinamentos baseados em storytelling
13
- - Para campanhas de marketing com desfechos alternativos
14
- - Para jogos educacionais interativos
15
- - Para experiências de role-playing
13
+ 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.
16
14
 
17
- ## Checklist de Configuração no Studio
15
+ Papel arquitetural **atípico** em relação aos demais módulos da plataforma:
18
16
 
19
- - [ ] Definir título e descrição da história
20
- - [ ] Criar cenários/cenas com textos e mídias
21
- - [ ] Definir ramificações e escolhas
22
- - [ ] Configurar finais alternativos
17
+ - **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`.
18
+ - **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`.
19
+ - `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).
20
+ - 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`.
23
21
 
24
- ## API Endpoints
22
+ Problemas que resolve:
25
23
 
26
- ### Listar Stories
27
- **Método:** GET
28
- **Endpoint:** `/v3/story`
24
+ - Persistir e versionar uma história ramificada (cenas, decisões, finais) sem precisar de schema rígido.
25
+ - 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.
26
+ - Gerar automaticamente a estrutura narrativa (cenas + decisões + finais) a partir de um objetivo, via function-calling do GPT.
29
27
 
30
- ### Criar Story
31
- **Método:** POST
32
- **Endpoint:** `/v3/story`
28
+ Relação com outros componentes:
33
29
 
34
- ### Deletar Story
35
- **Método:** DELETE
36
- **Endpoint:** `/v3/story/:id`
30
+ - `AIService` (`com.funifier.engine.system.ai.AIService`) — token e helpers OpenAI (`generateImage`, `chatCompletionWithFunction`, constante `CHATGPT_TOKEN`, `MODEL_GPT_IMAGE_1`).
31
+ - `S3Service` / `AzureService` / `GoogleService` — destino do upload conforme `Configuration.getCurrentConfiguration().getCloud()`.
32
+ - Coleção `upload` (`Entity.UPLOAD.collection`) — `saveImageUpload` grava um registro de upload **apenas para imagens** (não para áudio nem vídeo).
33
+ - `DatabaseRest` (`/v3/database`) — caminho real de CRUD das três coleções.
34
+ - `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`.
35
+ - **Não dispara triggers nem webhooks.** Nenhuma chamada a `Trigger`, `before_*`/`after_*` ou notificação foi encontrada em `StoryRest`.
37
36
 
38
- ## Validações e Testes
37
+ ---
39
38
 
40
- - [ ] História aparece na lista
41
- - [ ] Navegação entre cenários funciona
42
- - [ ] Escolhas do jogador são registradas
39
+ ## 2. Arquitetura e Fluxos
40
+
41
+ ### 2.1 Classes envolvidas
42
+
43
+ | Classe / arquivo | Papel |
44
+ |---|---|
45
+ | `com.funifier.rest.v3.rest.StoryRest` | **Único** controller do módulo (`@Path("v3/story")`). Cascade delete + 10 endpoints de IA/diagnóstico. |
46
+ | `com.funifier.rest.v3.rest.DatabaseRest` (`@Path("v3/database")`) | CRUD genérico real das coleções `story`, `story_character`, `story_scene`. |
47
+ | `com.funifier.engine.system.ai.AIService` | Integração OpenAI (token, geração de imagem, chat com function-calling). |
48
+ | `S3Service` / `AzureService` / `GoogleService` | Persistência de binários (imagem/áudio/vídeo) por tenant. |
49
+ | `com.funifier.engine.guid.Guid` | Geração de `_id`/nome de arquivo (`newShortGuid`). |
50
+ | `com.funifier.rest.v3.rest.AIRest` (`@Path` em `/v3/ai`) | Endpoint legado `buildStory` (`/build/story`) — formato distinto. |
51
+
52
+ > **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: #}"`).
53
+
54
+ ### 2.2 Modelo de persistência
55
+
56
+ ```mermaid
57
+ erDiagram
58
+ story ||--o{ story_character : "story_id"
59
+ story ||--o{ story_scene : "story_id"
60
+ story_scene }o--|| upload : "imagens geram registro em upload"
61
+ story {
62
+ string _id
63
+ string title
64
+ string genre
65
+ string synopsis
66
+ string aspect_ratio
67
+ bool subtitle_karaoke
68
+ }
69
+ story_character {
70
+ string _id
71
+ string story_id
72
+ string name
73
+ string description
74
+ string voice_profile
75
+ }
76
+ ```
77
+
78
+ - 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.
79
+ - `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.
80
+
81
+ ### 2.3 Pipeline — geração de estrutura de cenas (`generateSceneStructure`)
82
+
83
+ `POST /v3/story/{id}/scene/structure`. Síncrono. Usa GPT com function-calling.
84
+
85
+ ```
86
+ [1] Lê story (title, synopsis, genre) e todos os story_character do story_id.
87
+ [2] Lê parâmetros: mode (full|continue), objective, num_scenes (def 6),
88
+ num_decisions (def 2), num_endings (def 1*), continuation_prompt.
89
+ (* o JavaDoc diz "num_endings: 2", mas o código usa default 1 — linha 571)
90
+ [3] buildStructureSystemPrompt(...) monta o prompt-sistema com as REGRAS de ref/type.
91
+ [4] buildStructureUserPrompt(...) monta o pedido conforme o mode.
92
+ [5] Declara a Function 'generate_story_structure' com UM único parâmetro
93
+ 'scenes_json' do tipo STRING (não array — ver "Comportamento real" abaixo).
94
+ [6] Aumenta o socket timeout do Unirest para 8 min (480_000ms) só nesta chamada,
95
+ restaurando 180_000ms no finally.
96
+ [7] AIService.chatCompletionWithFunction(system, user, function) → argsJson.
97
+ [8] Faz parse de argsJson; extrai scenes_json. Se vier String → reparseia como List;
98
+ senão usa como List direto (tolera ambas as formas).
99
+ [9] Retorna { "scenes": [...] }. NÃO PERSISTE nada — apenas devolve a estrutura.
100
+ ```
101
+
102
+ **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.
103
+
104
+ Forma de cada cena retornada (definida no `description` da property — linha 586):
105
+
106
+ - `ref` (string, ex.: `C0`, `C1`, `C2A`) — identificador único da cena
107
+ - `title` (string)
108
+ - `type`: `scene` | `decision` | `end`
109
+ - `description` (string)
110
+ - `next_scene_ref` (string) — **apenas** `type=scene`
111
+ - `end_label` (string), `end_score` (number) — **apenas** `type=end`
112
+ - `decision` (object `{ prompt, options[] }`) — **apenas** `type=decision`; cada `option` = `{ label, score (number), next_scene_ref }`
113
+
114
+ ### 2.4 Pipeline — geração de vídeo (assíncrono, 2 etapas)
115
+
116
+ Etapa 1 — disparo (`generateSceneVideo`, `POST .../generate-video`):
117
+
118
+ ```mermaid
119
+ flowchart LR
120
+ A[POST generate-video] --> B{Resolve image_url}
121
+ B -->|1. request.image_url| C[image_url ok]
122
+ B -->|2. scene.image_url| C
123
+ B -->|3. scene.media.url se media.type=image| C
124
+ B -->|nenhum| E[400 image_url is required]
125
+ C --> F{model == sora-2 ?}
126
+ F -->|nao e FREEPIK_ENABLED=false| G[400 requer Freepik]
127
+ F -->|sora-2| H[dispatchSoraVideo]
128
+ F -->|outro e Freepik ON| I[dispatchFreepikVideo]
129
+ H --> J[salva scene.video = pending]
130
+ I --> J
131
+ J --> K[retorna job_id + status pending]
132
+ ```
133
+
134
+ ```
135
+ [1] Carrega story_scene por sceneId (404 se ausente).
136
+ [2] Resolve image_url por prioridade: request.image_url → scene.image_url
137
+ → scene.media.url (somente se scene.media.type == "image").
138
+ Se nenhum → 400 "image_url is required. Generate or upload a scene image first."
139
+ [3] Lê story.aspect_ratio (def "16:9").
140
+ [4] Lê model (def "veo-3-fast"), quality (def "high"), prompt (def ""),
141
+ end_image_url, duration_sec (def 6), generate_audio (def true*).
142
+ (* generate_audio só é false se vier explicitamente Boolean.FALSE)
143
+ [5] SE model != "sora-2" E FREEPIK_ENABLED == false → 400 (Freepik desabilitado).
144
+ -> Na prática, com o default "veo-3-fast", a chamada SEM informar model FALHA.
145
+ [6] sora-2 → dispatchSoraVideo(...); senão → dispatchFreepikVideo(...).
146
+ [7] Grava em scene.video = { job_id, status:"pending", model, quality, has_native_audio }
147
+ e salva o documento.
148
+ [8] Retorna { job_id, status:"pending", model } imediatamente (não bloqueia).
149
+ ```
150
+
151
+ Etapa 2 — polling (`checkSceneVideoJob`, `GET .../video-job`):
152
+
153
+ ```mermaid
154
+ stateDiagram-v2
155
+ [*] --> none: scene.video ausente
156
+ none --> pending: generate-video dispara job
157
+ pending --> pending: API ainda processando
158
+ pending --> completed: API devolve completed
159
+ pending --> failed: API devolve failed/errored
160
+ completed --> completed: estado em cache (nao reconsulta)
161
+ failed --> failed: estado em cache (nao reconsulta)
162
+ ```
163
+
164
+ ```
165
+ [1] Carrega scene (404 se ausente).
166
+ [2] Se scene.video ausente → { status:"none" }.
167
+ [3] Se status já é completed/failed OU job_id == null → retorna estado em cache
168
+ (não reconsulta a API externa).
169
+ [4] Senão consulta a API: sora-2 → checkSoraVideo(jobId); outro → checkFreepikVideo.
170
+ [5] SE completed:
171
+ videoUrl = checkResult.video_url
172
+ SE videoUrl vazio (caso Sora) → downloadAndSaveSoraVideo(jobId) baixa de
173
+ /v1/videos/{id}/content e salva no CDN (URL da OpenAI expira em ~1h).
174
+ Atualiza scene.video.url/status/duration_sec,
175
+ scene.media_mode = "video",
176
+ scene.media = { type:"video", url, duration } e SALVA.
177
+ SE failed: grava scene.video.status = "failed" e salva.
178
+ SENÃO: { status:"pending" } (sem persistir).
179
+ ```
180
+
181
+ Pseudocódigo da cadeia de fallback de URL do Sora (`checkSoraVideo`, linhas 1127-1147) — a resposta da OpenAI varia entre versões:
182
+
183
+ ```
184
+ videoUrl = json.url
185
+ se vazio: videoUrl = json.data[0].url
186
+ se vazio: videoUrl = json.result.url
187
+ se vazio: videoUrl = json.output[0].url
188
+ se vazio: videoUrl = json.video.url
189
+ se ainda vazio: (etapa 2) baixa o binário de /v1/videos/{id}/content e salva no CDN
190
+ ```
191
+
192
+ ### 2.5 Pipeline — TTS de diálogo + Whisper karaokê (`generateDialogueTts`)
193
+
194
+ `POST /v3/story/{storyId}/scene/{sceneId}/dialogue/{dialogueIndex}/tts`. Síncrono.
195
+
196
+ ```
197
+ [1] Lê text (obrigatório → 400 se vazio) e voice (def "alloy").
198
+ [2] POST OpenAI /v1/audio/speech { model:"tts-1", input:text, voice, response_format:"mp3" }.
199
+ (atenção: o comentário do método diz "tts-1-hd", mas o código envia "tts-1")
200
+ [3] Se HTTP != 200 → RuntimeException com corpo do erro.
201
+ [4] Lê os bytes do MP3.
202
+ [5] SE story.subtitle_karaoke == true → callWhisperWordTimestamps(mp3Bytes)
203
+ (best-effort: qualquer exceção é capturada e logada em stderr; NÃO falha o request).
204
+ [6] Salva o MP3 no storage do tenant (session "story_audio") via S3/Azure/Google.
205
+ [7] SE houve word_timestamps → updateDialogueWordTimestamps grava
206
+ scene.dialogues[dialogueIndex].word_timestamps (re-lê a cena, muta e salva).
207
+ [8] Retorna { url, word_timestamps? }.
208
+ ```
209
+
210
+ `callWhisperWordTimestamps`: `POST /v1/audio/transcriptions` com `model=whisper-1`, `response_format=verbose_json`, `timestamp_granularities[]=word`, `language=pt`. Retorna lista de `{ word, start, end }`.
211
+
212
+ ### 2.6 Pipeline — geração de imagem (cena / personagem / capa)
213
+
214
+ Três endpoints (`generateSceneImage`, `generateCharacterImage`, `generateStoryCover`) seguem o mesmo padrão, variando apenas o prompt e o `aspect_ratio`:
215
+
216
+ ```
217
+ [1] Lê documentos de contexto (story e/ou story_character).
218
+ [2] Monta prompt em PT-BR concatenando título/gênero/sinopse/personagens + prompt do usuário.
219
+ [3] generateImageBytes(prompt, reference_image_urls, aspectRatio):
220
+ SE não há reference_image_urls → AIService.generateImage(prompt, sizeGenerations)
221
+ (OpenAI /v1/images/generations, modelo padrão do AIService)
222
+ SENÃO → baixa cada imagem de referência (fetchImageBytes) e chama
223
+ /v1/images/edits (multipart, model = gpt-image-1, n=1) usando-as como referência.
224
+ Se o download de TODAS as refs falhar → cai no caminho text-only.
225
+ [4] saveImageUpload(bytes, prefix, session): salva o PNG no storage E cria um
226
+ registro na coleção `upload` (_id, filename, content_type, size, time + campos do cloud).
227
+ [5] Retorna { url, upload }.
228
+ ```
229
+
230
+ | Endpoint | `aspect_ratio` usado | session de storage | prefixo do arquivo |
231
+ |---|---|---|---|
232
+ | `generateSceneImage` | `story.aspect_ratio` (def 16:9) | `story_scenes` | `story-scene-` |
233
+ | `generateCharacterImage` | **fixo `1:1`** | `story_characters` | `story-char-` |
234
+ | `generateStoryCover` | `story.aspect_ratio` (def 16:9) | `story_covers` | `story-cover-` |
235
+
236
+ Tabelas de tamanho (helpers em `StoryRest`):
237
+
238
+ | aspect_ratio | `/images/generations` e `/edits` | Sora `high` | Sora `light` | Freepik enum |
239
+ |---|---|---|---|---|
240
+ | `16:9` | `1536x1024` | `1920x1080` | `1280x720` | `widescreen_16_9` |
241
+ | `9:16` | `1024x1536` | `1080x1920` | `720x1280` | `social_story_9_16` |
242
+ | `1:1` | `1024x1024` | `1080x1080` | `720x720` | `square_1_1` |
243
+
244
+ ### 2.7 Cascade delete (`deleteStory`)
245
+
246
+ `DELETE /v3/story/{id}` executa, **em ordem**, três `remove` Jongo parametrizados:
247
+
248
+ ```
249
+ db.story_character.remove({ story_id: id })
250
+ db.story_scene.remove({ story_id: id })
251
+ db.story.remove({ _id: id })
252
+ → 204 No Content
253
+ ```
254
+
255
+ Sem transação (Mongo standalone): se falhar no meio, pode deixar órfãos. As subcoleções são apagadas **antes** do documento raiz.
256
+
257
+ ### 2.8 Diagnóstico OpenAI
258
+
259
+ - `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.
260
+ - `listOpenAiModels` (`GET /v3/story/openai/models`): lista modelos da conta filtrando por `sora`/`video`/`dall`; retorna `sora_accessible` e contagem total.
261
+
262
+ Ambos servem para diagnosticar acesso da API key da OpenAI — não tocam nas coleções.
263
+
264
+ ---
265
+
266
+ ## 3. Estrutura dos Objetos
267
+
268
+ > **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.
269
+
270
+ ### 3.1 `story` — documento raiz
271
+
272
+ | Campo | Tipo | Padrão (no read) | Acesso backend | Descrição |
273
+ |---|---|---|---|---|
274
+ | `_id` | String | gerado pelo cliente / `/v3/database` | R | Id da história. Referenciado por `story_id` nas subcoleções. |
275
+ | `title` | String | `""` | R | Título; entra nos prompts de imagem e de estrutura. |
276
+ | `genre` | String | `""` | R | Gênero; entra nos prompts. |
277
+ | `synopsis` | String | `""` | R | Sinopse; entra no prompt de capa e de estrutura. |
278
+ | `aspect_ratio` | String | `"16:9"` | R | `16:9` \| `9:16` \| `1:1`. Define o tamanho de imagem/vídeo de cena e capa. |
279
+ | `subtitle_karaoke` | Boolean | `false` | R | Se `true`, o TTS de diálogo chama o Whisper para gerar `word_timestamps`. |
280
+
281
+ Demais campos (capa, descrição, etc.) que o frontend persista **não são lidos** pelo backend.
282
+
283
+ ### 3.2 `story_character`
284
+
285
+ | Campo | Tipo | Padrão (no read) | Acesso backend | Descrição |
286
+ |---|---|---|---|---|
287
+ | `_id` | String | cliente | R | Id do personagem. |
288
+ | `story_id` | String | — | R | FK lógica para `story._id` (usada no cascade delete e na geração de estrutura). |
289
+ | `name` | String | `""` / `"Personagem"` | R | Nome; entra no prompt de retrato e na prévia de voz. |
290
+ | `description` | String | `""` | R | Descrição visual/narrativa; entra nos prompts. Na prévia de voz é truncada em 120 chars. |
291
+ | `voice_profile` | String | `"alloy"` | R | Voz OpenAI padrão do personagem (fallback da prévia de voz). |
292
+
293
+ ### 3.3 `story_scene`
294
+
295
+ | Campo | Tipo | Padrão | Acesso backend | Descrição |
296
+ |---|---|---|---|---|
297
+ | `_id` | String | cliente | R | Id da cena. |
298
+ | `story_id` | String | — | R | FK lógica para `story._id` (cascade delete). |
299
+ | `image_url` | String | — | R | URL da imagem da cena; pré-requisito para gerar vídeo. |
300
+ | `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. |
301
+ | `media_mode` | String | — | **W** | Definido como `"video"` quando o vídeo conclui. |
302
+ | `video` | Object | — | R/**W** | Estado do job de vídeo (ver 3.4). **Escrito** no disparo e no polling. |
303
+ | `dialogues` | Array | — | R/**W** | Lista de diálogos da cena. O backend só escreve `dialogues[i].word_timestamps`. |
304
+
305
+ 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.
306
+
307
+ ### 3.4 Subobjeto `story_scene.video`
308
+
309
+ | Campo | Tipo | Origem | Descrição |
310
+ |---|---|---|---|
311
+ | `job_id` | String | dispatch | Id do job na API externa (Sora `id`, Freepik `task_id`). |
312
+ | `status` | String | dispatch/polling | `pending` \| `completed` \| `failed`. (`none` é retornado quando o subobjeto não existe.) |
313
+ | `model` | String | request | `sora-2` (único operacional) \| `veo-3-fast` \| `kling-3.0`. |
314
+ | `quality` | String | request | `high` \| `light` (afeta o tamanho do vídeo Sora). |
315
+ | `has_native_audio` | Boolean | dispatch | Se o modelo gera áudio nativo. |
316
+ | `url` | String | polling | URL final (CDN, após download do Sora). |
317
+ | `duration_sec` | Number | polling | Duração efetiva, quando informada. |
318
+
319
+ ### 3.5 Subobjeto `story_scene.media`
320
+
321
+ `{ "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.
322
+
323
+ ### 3.6 `story_scene.dialogues[i].word_timestamps`
324
+
325
+ 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.
326
+
327
+ > **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.
328
+
329
+ ---
330
+
331
+ ## 4. Endpoints
332
+
333
+ > 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).
334
+
335
+ ### 4.0 CRUD (não é `StoryRest`)
336
+
337
+ Criar/ler/atualizar `story`, `story_character`, `story_scene` usa o endpoint **genérico**:
338
+
339
+ ```
340
+ GET /v3/database/{collection}/{id}
341
+ GET /v3/database/{collection}?... (query/aggregate)
342
+ POST /v3/database/{collection} (insert/save)
343
+ PUT /v3/database/{collection}/{id}
344
+ DELETE /v3/database/{collection}/{id}
345
+ ```
346
+
347
+ `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).
348
+
349
+ ### 4.1 `DELETE /v3/story/{id}` — cascade delete
350
+
351
+ | Aspecto | Detalhe |
352
+ |---|---|
353
+ | Finalidade | Apagar a história e tudo que a referencia (`story_character`, `story_scene`). |
354
+ | Autenticação | Bearer token (resolve o tenant). **Sem checagem de scope/role.** |
355
+ | Tipo | Deleção em cascata, 3 `remove` sequenciais, sem transação. |
356
+ | Resposta | `204 No Content`. |
357
+
358
+ ### 4.2 `POST /v3/story/{id}/scene/generate` — imagem de cena
359
+
360
+ Body: `{ "scene_id": "...", "prompt": "...", "characters": [{name, description}], "reference_image_urls": ["url"] }`
361
+ Resposta: `{ "url": "...", "upload": { ... } }`. Usa `story.aspect_ratio`. Com `reference_image_urls` → `/v1/images/edits`; sem → `/v1/images/generations`.
362
+
363
+ ### 4.3 `POST /v3/story/{storyId}/character/{charId}/generate-image` — retrato
364
+
365
+ Body: `{ "prompt": "...", "reference_image_urls": ["url"] }`. **Aspect ratio fixo `1:1`.** 404 se o personagem não existe. Resposta `{ url, upload }`.
366
+
367
+ ### 4.4 `POST /v3/story/{id}/cover/generate` — capa
368
+
369
+ Body: `{ "prompt": "...", "reference_image_urls": ["url"] }`. Usa `story.aspect_ratio`. Resposta `{ url, upload }`.
370
+
371
+ ### 4.5 `POST /v3/story/{storyId}/scene/{sceneId}/dialogue/{dialogueIndex}/tts` — áudio do diálogo
372
+
373
+ | Param | Tipo | Descrição |
374
+ |---|---|---|
375
+ | `storyId` | path | Usado só para checar `subtitle_karaoke`. |
376
+ | `sceneId` | path | Cena cujo diálogo recebe `word_timestamps`. |
377
+ | `dialogueIndex` | path (int) | Índice em `scene.dialogues`. |
378
+
379
+ 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).
380
+
381
+ ### 4.6 `POST /v3/story/{storyId}/character/{charId}/voice-preview` — prévia de voz
382
+
383
+ 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.
384
+
385
+ ### 4.7 `POST /v3/story/{id}/scene/structure` — estrutura por GPT
386
+
387
+ Body:
388
+ ```json
389
+ {
390
+ "mode": "full",
391
+ "objective": "...",
392
+ "num_scenes": 6,
393
+ "num_decisions": 2,
394
+ "num_endings": 1,
395
+ "continuation_prompt": "...",
396
+ "from_scene_id": "..."
397
+ }
398
+ ```
399
+ 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).
400
+
401
+ ### 4.8 `POST /v3/story/{storyId}/scene/{sceneId}/generate-video` — disparo de vídeo
402
+
403
+ 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).
404
+
405
+ ### 4.9 `GET /v3/story/{storyId}/scene/{sceneId}/video-job` — polling
406
+
407
+ 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).
408
+
409
+ ### 4.10 Diagnóstico
410
+
411
+ - `POST /v3/story/openai/video-probe` → `{ probes: [{url, http_status, response_body}] }`.
412
+ - `GET /v3/story/openai/models` → `{ models: [...], total_models_in_account, sora_accessible }`.
413
+
414
+ ### 4.11 Legado — `POST /v3/ai/build/story` (em `AIRest`, **não** em `StoryRest`)
415
+
416
+ 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.
417
+
418
+ ---
419
+
420
+ ## 5. Regras de Negócio
421
+
422
+ Regras que existem **no código** e não em nenhum schema:
423
+
424
+ - **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).
425
+ - **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**.
426
+ - **Á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).
427
+ - **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.
428
+ - **Karaokê condicional:** Whisper só roda se `story.subtitle_karaoke==true`; falha de Whisper **não** quebra o TTS (best-effort, capturada).
429
+ - **Voz default `alloy`** em TTS e prévia de voz quando não informada / `voice_profile` ausente.
430
+ - **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.
431
+ - **`num_endings` default = 1** no código (apesar do JavaDoc dizer 2).
432
+ - **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"`.
433
+ - **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`.
434
+
435
+ ---
436
+
437
+ ## 6. Comportamentos Automáticos
438
+
439
+ | Comportamento | Trigger | Impacto | Persistência |
440
+ |---|---|---|---|
441
+ | 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` |
442
+ | 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`) |
443
+ | Word timestamps de karaokê | TTS com `story.subtitle_karaoke==true` | Escreve `scene.dialogues[i].word_timestamps` | `story_scene` |
444
+ | Registro de upload de imagem | Qualquer geração de imagem (cena/personagem/capa) | Cria documento na coleção `upload` | `upload` |
445
+ | 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 |
446
+ | Cascade delete | `DELETE /v3/story/{id}` | Remove `story_character` + `story_scene` + `story` | 3 coleções |
447
+
448
+ > **Não há triggers (`before_*`/`after_*`) nem webhooks** disparados por `StoryRest`. Nenhum evento de gamificação é emitido por este módulo.
449
+
450
+ ---
451
+
452
+ ## 7. Suportado vs NÃO Suportado
453
+
454
+ ### ✅ Suportado
455
+
456
+ - CRUD das três coleções via `/v3/database/*` (schema-less).
457
+ - Cascade delete via `DELETE /v3/story/{id}`.
458
+ - Geração de imagem (cena, personagem, capa) com ou sem imagens de referência.
459
+ - TTS de diálogo (`tts-1`) e prévia de voz de personagem.
460
+ - Karaokê (word timestamps Whisper) quando `subtitle_karaoke=true`.
461
+ - Geração de estrutura narrativa por GPT (function-calling).
462
+ - Geração de vídeo **apenas via Sora 2** (disparo + polling + download para CDN).
463
+ - Diagnóstico de acesso OpenAI (`/openai/video-probe`, `/openai/models`).
464
+
465
+ ### ❌ NÃO Suportado / armadilhas
466
+
467
+ - **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`.
468
+ - **`tts-1-hd`:** citado no comentário, **não** usado — o código envia `tts-1`.
469
+ - **`from_scene_id`** (estrutura, `mode=continue`): documentado no JavaDoc, **não** é lido pelo código.
470
+ - **CRUD dedicado em `/v3/story`:** não existe (`POST`/`GET /v3/story` não estão implementados).
471
+ - **Triggers/webhooks/notificações:** não disparados por este módulo.
472
+ - **`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.
473
+ - **Transação no cascade delete:** inexistente — falha parcial deixa órfãos.
474
+ - **Validação de schema dos documentos:** inexistente — qualquer campo extra é aceito e ignorado.
475
+
476
+ ---
477
+
478
+ ## 8. Segurança e Permissões
479
+
480
+ - **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.
481
+ - **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.
482
+ - **Isolamento multi-tenant:** garantido pela conexão Jongo por tenant (`FrontController.getInstance(apiKey)`); não há campo de tenant nos documentos.
483
+ - **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.
484
+ - **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).
485
+ - **Injeção MongoDB:** **não há** — todas as queries usam placeholders Jongo parametrizados (`"{_id: #}"`, `"{story_id: #}"`). Não há concatenação de query crua.
486
+ - **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.
487
+
488
+ ---
489
+
490
+ ## 9. Observabilidade e Troubleshooting
491
+
492
+ **Diagnóstico rápido**
493
+
494
+ - Verificar acesso à OpenAI/Sora: `GET /v3/story/openai/models` (cheque `sora_accessible:true`) e `POST /v3/story/openai/video-probe`.
495
+ - 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: ...`.
496
+
497
+ **Queries úteis (via `/v3/database`)**
498
+
499
+ ```
500
+ GET /v3/database/story/<storyId>
501
+ GET /v3/database/story_character/<charId>
502
+ GET /v3/database/story_scene/<sceneId>
503
+ # todas as cenas de uma história (aggregate/find por story_id):
504
+ POST /v3/database/story_scene/aggregate
505
+ [ { "$match": { "story_id": "<storyId>" } } ]
506
+ ```
507
+
508
+ **Erros comuns e causas**
509
+
510
+ | Sintoma | Causa provável |
511
+ |---|---|
512
+ | 400 `image_url is required` no `generate-video` | Cena sem imagem (gere/anexe a imagem antes). |
513
+ | 400 `Modelo '...' requer integração Freepik` | `model != sora-2` com Freepik desabilitado — use `sora-2`. |
514
+ | `video-job` preso em `pending` | Job ainda processando, ou Sora não devolveu URL (cai no download via `/content`). |
515
+ | Vídeo "some" após ~1h | URL temporária da OpenAI expirou — o download para CDN só ocorre no `video-job completed`. |
516
+ | TTS funciona mas sem karaokê | `story.subtitle_karaoke != true`, ou Whisper falhou (cheque stderr). |
517
+ | 404 `Character/Scene not found` | `_id` inexistente no tenant atual. |
518
+
519
+ **O que verificar quando algo não funciona**
520
+
521
+ 1. Token Bearer válido e tenant correto (a coleção é por tenant).
522
+ 2. `story.aspect_ratio` válido (`16:9|9:16|1:1`) — valores fora disso caem em 16:9.
523
+ 3. Para vídeo: `model:"sora-2"` explícito e imagem de início presente.
524
+ 4. Acesso da conta OpenAI ao Sora (`/openai/models`).
525
+
526
+ ---
527
+
528
+ ## 10. Exemplos Práticos
529
+
530
+ ### 10.1 Mínimo funcional — gerar estrutura e capa
531
+
532
+ ```http
533
+ POST /v3/story/STORY123/scene/structure
534
+ Authorization: Bearer <token>
535
+ Content-Type: application/json
536
+
537
+ { "mode": "full", "objective": "Treinamento de segurança no trabalho", "num_scenes": 5, "num_decisions": 2, "num_endings": 2 }
538
+ ```
539
+ ```http
540
+ POST /v3/story/STORY123/cover/generate
541
+ { "prompt": "estilo cartoon corporativo" }
542
+ ```
543
+
544
+ ### 10.2 Avançado — vídeo de cena via Sora com áudio
545
+
546
+ ```http
547
+ POST /v3/story/STORY123/scene/SCENE9/generate-video
548
+ { "model": "sora-2", "quality": "high", "duration_sec": 8, "prompt": "câmera lenta aproximando do personagem", "generate_audio": true }
549
+ → { "job_id": "video_abc", "status": "pending", "model": "sora-2" }
550
+ ```
551
+ Polling até concluir:
552
+ ```http
553
+ GET /v3/story/STORY123/scene/SCENE9/video-job
554
+ → { "status": "completed", "video_url": "https://cdn.../story-video-xyz.mp4", "duration_sec": 8 }
555
+ ```
556
+
557
+ ### 10.3 Anti-pattern (o que NÃO fazer)
558
+
559
+ - **Disparar vídeo sem informar `model`** → usa `veo-3-fast` por default → **400** (Freepik desabilitado). Sempre passe `"model":"sora-2"`.
560
+ - **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.
561
+ - **Confiar na URL retornada direto pela OpenAI** → ela expira em ~1h; só persiste no CDN após `video-job` retornar `completed`.
562
+ - **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.
563
+ - **Misturar `/v3/ai/build/story` com o editor novo** → formatos de história incompatíveis (objeto-mapa vs. array de cenas com `ref`).
564
+
565
+ ---
566
+
567
+ ## Checklist de Configuração
568
+
569
+ - [ ] Token Bearer válido do tenant em todas as chamadas.
570
+ - [ ] Conta OpenAI com acesso ao **Sora 2** (`GET /v3/story/openai/models` → `sora_accessible:true`).
571
+ - [ ] `story.aspect_ratio` definido (`16:9|9:16|1:1`) antes de gerar imagem/vídeo.
572
+ - [ ] **Imagem de cena gerada/anexada antes** de `generate-video` (pré-requisito).
573
+ - [ ] `"model":"sora-2"` **explícito** no `generate-video` (default falha).
574
+ - [ ] `story.subtitle_karaoke=true` se quiser word timestamps no TTS.
575
+ - [ ] `voice_profile` definido no personagem para prévia de voz consistente.
576
+ - [ ] Armadilha: `duration_sec` é **reescrito** (snap 4/8/12) no Sora.
577
+ - [ ] Armadilha: omitir `generate_audio` ⇒ `true`; `false` apenas injeta texto "silent" no prompt.
578
+ - [ ] Lembrar: cascade `DELETE` é **irreversível e sem autorização por role**.