@oh-story/dsh 0.1.8 → 0.1.10

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 (360) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +237 -84
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +183 -77
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +140 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +28 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +8 -2
  10. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  11. package/lib/drama/skills/short-drama-edit/SKILL.md +146 -0
  12. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  21. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  22. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  23. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +38 -0
  24. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +29 -0
  25. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +54 -0
  26. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
  27. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +51 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
  29. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +520 -0
  30. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  31. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  32. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  33. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  34. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  35. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  36. package/lib/drama/skills/short-drama-produce/SKILL.md +29 -4
  37. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +22 -0
  38. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +13 -12
  39. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  40. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  41. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  42. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  43. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +199 -3
  44. package/lib/drama/skills/short-drama-review/SKILL.md +2 -0
  45. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +7 -0
  46. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +10 -0
  47. package/lib/drama/skills/short-drama-storyboard/SKILL.md +12 -1
  48. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  49. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  50. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +78 -2
  51. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +12 -5
  53. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
  54. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +76 -7
  55. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +5 -2
  56. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +21 -0
  57. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -17
  58. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +12 -3
  59. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +61 -5
  60. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -3
  61. package/lib/drama/skills/short-drama-write/SKILL.md +11 -2
  62. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  63. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  64. package/lib/index.js +290 -90
  65. package/lib/index.js.map +4 -4
  66. package/lib/novel-to-game/README.md +299 -59
  67. package/lib/novel-to-game/README_ZH.md +299 -56
  68. package/lib/novel-to-game/VERSION +1 -1
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  78. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  79. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  80. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  81. package/lib/novel-to-game/manifest.json +77 -87
  82. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +9 -35
  83. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  84. package/lib/novel-to-game/skills/game-build/SKILL.md +17 -35
  85. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  86. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  87. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  88. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  89. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  90. package/lib/novel-to-game/skills/game-concept/SKILL.md +16 -34
  91. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  92. package/lib/novel-to-game/skills/game-qa/SKILL.md +9 -20
  93. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +10 -6
  94. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -32
  95. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  96. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  98. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +9 -18
  99. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  100. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +10 -27
  101. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -6
  102. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +6 -11
  103. package/lib/oh-story/manifest.json +419 -254
  104. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  105. package/lib/oh-story/roles/character-designer.md +1 -1
  106. package/lib/oh-story/roles/consistency-checker.md +4 -4
  107. package/lib/oh-story/roles/narrative-writer.md +56 -243
  108. package/lib/oh-story/roles/story-architect.md +6 -6
  109. package/lib/oh-story/roles/story-explorer.md +11 -11
  110. package/lib/oh-story/skills/story/SKILL.md +13 -7
  111. package/lib/oh-story/skills/story/VERSION +1 -1
  112. package/lib/oh-story/skills/story/references/author-memory.md +61 -29
  113. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  114. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  115. package/lib/oh-story/skills/story-deslop/SKILL.md +54 -229
  116. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +29 -56
  117. package/lib/oh-story/skills/story-deslop/references/author-memory.md +61 -29
  118. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  119. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  120. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  121. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  122. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  123. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  124. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  125. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  126. package/lib/oh-story/skills/story-import/SKILL.md +78 -82
  127. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  128. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  129. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  130. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
  131. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  132. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +9 -34
  133. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +197 -15
  134. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
  135. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
  136. package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
  137. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
  139. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
  141. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
  142. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
  143. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  144. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
  145. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
  146. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  147. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  148. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  149. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
  150. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  151. package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
  152. package/lib/oh-story/skills/story-long-write/SKILL.md +52 -216
  153. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +29 -56
  154. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +72 -12
  155. package/lib/oh-story/skills/story-long-write/references/author-memory.md +61 -29
  156. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  157. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  158. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  159. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +10 -4
  160. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  161. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +6 -15
  162. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  163. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  164. package/lib/oh-story/skills/story-long-write/references/project-files.md +110 -0
  165. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
  166. package/lib/oh-story/skills/story-long-write/references/reference-index.md +86 -0
  167. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  168. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  169. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  170. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  171. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  172. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +9 -34
  173. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +88 -118
  174. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -119
  175. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
  176. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +73 -54
  177. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  178. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +63 -4
  179. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  180. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +531 -0
  181. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  182. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  183. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +197 -15
  184. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  185. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  186. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +285 -0
  187. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
  188. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  189. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
  190. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
  191. package/lib/oh-story/skills/story-review/SKILL.md +80 -87
  192. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +29 -56
  193. package/lib/oh-story/skills/story-review/references/author-memory.md +61 -29
  194. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  195. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +10 -4
  196. package/lib/oh-story/skills/story-review/references/review-quality.md +6 -12
  197. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  198. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  199. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +9 -34
  200. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  201. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  202. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  203. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  204. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  205. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
  206. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
  207. package/lib/oh-story/skills/story-setup/SKILL.md +58 -43
  208. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +29 -56
  209. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  210. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  211. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +10 -4
  212. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  213. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  214. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
  215. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  216. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  217. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  218. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  219. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +63 -4
  220. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  221. package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
  222. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  223. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
  224. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  225. package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
  226. package/lib/oh-story/skills/story-short-write/SKILL.md +15 -324
  227. package/lib/oh-story/skills/story-short-write/references/author-memory.md +61 -29
  228. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  229. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  230. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +10 -4
  231. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  232. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  233. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  234. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  235. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  236. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  237. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  238. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  239. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  240. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  241. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  242. package/lib/oh-story/skills/story-short-write/references/reference-index.md +55 -0
  243. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  244. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  245. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  246. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  247. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  248. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +78 -0
  249. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
  250. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
  251. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  252. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  253. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  254. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  255. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  256. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  257. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  258. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  259. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  260. package/lib/types/client/file-activity.d.ts +19 -1
  261. package/lib/types/client/file-activity.d.ts.map +1 -1
  262. package/lib/types/client/index.d.ts.map +1 -1
  263. package/lib/types/client/production-prompts.d.ts +7 -0
  264. package/lib/types/client/production-prompts.d.ts.map +1 -1
  265. package/lib/types/client/production-runtime.d.ts +33 -0
  266. package/lib/types/client/production-runtime.d.ts.map +1 -1
  267. package/lib/types/drama-adapters.d.ts +5 -3
  268. package/lib/types/drama-adapters.d.ts.map +1 -1
  269. package/lib/types/native-hooks.d.ts +52 -0
  270. package/lib/types/native-hooks.d.ts.map +1 -1
  271. package/lib/types/role-identity.d.ts +17 -0
  272. package/lib/types/role-identity.d.ts.map +1 -0
  273. package/lib/types/role-tool.d.ts.map +1 -1
  274. package/lib/types/skill-provider.d.ts.map +1 -1
  275. package/lib/video-recap/CHANGELOG.md +31 -1
  276. package/lib/video-recap/manifest.json +162 -162
  277. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  278. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  279. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  280. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  281. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  282. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  283. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  284. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  285. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  286. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  287. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  288. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  289. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  290. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  291. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  292. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  293. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  294. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  295. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  296. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  297. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  298. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  299. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  300. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  301. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  302. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  303. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  304. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  305. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  306. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  307. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  308. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  309. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  310. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  311. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  312. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  313. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  314. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  315. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  316. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  317. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  318. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  319. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  320. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  321. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  322. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  323. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  324. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  325. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  326. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  327. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  328. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  329. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  330. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  331. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  332. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  333. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  334. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  335. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  336. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  337. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  338. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  339. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  340. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  341. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  342. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  343. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  344. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  345. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  346. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  347. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  348. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  349. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  350. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  351. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  352. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  353. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  354. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  355. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  356. package/package.json +45 -45
  357. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -59
  358. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  359. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -42
  360. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
@@ -1,61 +1,29 @@
1
1
  """Load and format research, consolidation, and substrate context."""
2
2
 
3
3
  import hashlib
4
-
5
- import importlib.util
6
-
7
-
8
4
  import json
9
-
10
-
11
5
  import re
12
-
13
6
  from pathlib import Path
14
7
 
15
8
  from lib import CONFIG
16
9
 
17
- from lib import log
18
-
19
- try:
20
- from deslop_qc import analyze_deslop_qc
21
- except ModuleNotFoundError:
22
- _deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
23
- _deslop_qc_spec = importlib.util.spec_from_file_location(
24
- "deslop_qc", _deslop_qc_path
25
- )
26
- if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
27
- raise
28
- _deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
29
- _deslop_qc_spec.loader.exec_module(_deslop_qc_module)
30
- analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
31
-
32
10
 
33
11
  def _load_background_research(work_dir):
34
- """Load the agent-authored background_research.json if present.
35
-
36
- This file is the single highest-leverage quality input (character names,
37
- relationships, plot context). It used to be documented but never read by
38
- any code, so researched story knowledge never reached the writing brief.
39
- """
12
+ """Load the agent-authored background_research.json ({} when absent)."""
40
13
  path = Path(work_dir) / "background_research.json"
41
14
  if not path.exists():
42
15
  return {}
43
- try:
44
- data = json.loads(path.read_text(encoding="utf-8"))
45
- except (json.JSONDecodeError, OSError) as exc:
46
- log(f"警告: background_research.json 读取失败,忽略: {exc}")
47
- return {}
48
- return data if isinstance(data, dict) else {}
16
+ return json.loads(path.read_text(encoding="utf-8"))
49
17
 
50
18
 
51
19
  def _clip_text(text, limit):
52
- value = re.sub(r"\s+", " ", str(text or "")).strip()
53
- return value[:limit]
20
+ """Collapse whitespace and clip. Research fields are optional, so None clips to ""."""
21
+ return re.sub(r"\s+", " ", str(text or "")).strip()[:limit]
54
22
 
55
23
 
56
24
  def _format_background_research(research, limit=1800):
57
25
  """Render background_research.json into a bounded Story-context brief section."""
58
- if not isinstance(research, dict) or not research:
26
+ if not research:
59
27
  return []
60
28
  lines = [
61
29
  "## Story context (from background_research.json)",
@@ -72,50 +40,33 @@ def _format_background_research(research, limit=1800):
72
40
  if value:
73
41
  lines.append(f"- {label}: {value}")
74
42
 
75
- characters = research.get("characters")
76
- if isinstance(characters, dict) and characters:
43
+ characters = research.get("characters", {})
44
+ if characters:
77
45
  lines.append("- Characters:")
78
46
  for name, desc in list(characters.items())[:12]:
79
- clean_name = _clip_text(name, 60)
80
- clean_desc = _clip_text(desc, 160)
81
- if clean_name:
82
- lines.append(f" - {clean_name}: {clean_desc}")
47
+ lines.append(f" - {_clip_text(name, 60)}: {_clip_text(desc, 160)}")
83
48
 
84
- details = research.get("character_details")
85
- if isinstance(details, dict) and details:
49
+ details = research.get("character_details", {})
50
+ if details:
86
51
  lines.append("- Character details:")
87
52
  for name, info in list(details.items())[:8]:
88
- if not isinstance(info, dict):
89
- continue
90
53
  bits = []
91
- aliases = info.get("aliases")
92
- if isinstance(aliases, list) and aliases:
93
- clean_aliases = [_clip_text(alias, 40) for alias in aliases[:4]]
94
- clean_aliases = [alias for alias in clean_aliases if alias]
95
- if clean_aliases:
96
- bits.append("别名 " + "/".join(clean_aliases))
54
+ aliases = [_clip_text(alias, 40) for alias in info.get("aliases", [])[:4]]
55
+ if aliases:
56
+ bits.append("别名 " + "/".join(aliases))
97
57
  role = _clip_text(info.get("role"), 80)
98
58
  if role:
99
59
  bits.append(role)
100
- rels = info.get("relationships")
101
- if isinstance(rels, list) and rels:
102
- clean_rels = [_clip_text(rel, 80) for rel in rels[:4]]
103
- clean_rels = [rel for rel in clean_rels if rel]
104
- if clean_rels:
105
- bits.append(";".join(clean_rels))
106
- clean_name = _clip_text(name, 60)
107
- if clean_name and bits:
108
- lines.append(f" - {clean_name}: {'; '.join(bits)}")
109
-
110
- arcs = research.get("plot_arcs")
111
- if isinstance(arcs, list) and arcs:
60
+ rels = [_clip_text(rel, 80) for rel in info.get("relationships", [])[:4]]
61
+ if rels:
62
+ bits.append(";".join(rels))
63
+ if bits:
64
+ lines.append(f" - {_clip_text(name, 60)}: {'; '.join(bits)}")
65
+
66
+ arcs = research.get("plot_arcs", [])
67
+ if arcs:
112
68
  lines.append("- Plot arcs:")
113
69
  for arc in arcs[:8]:
114
- if not isinstance(arc, dict):
115
- value = _clip_text(arc, 180)
116
- if value:
117
- lines.append(f" - {value}")
118
- continue
119
70
  name = _clip_text(arc.get("name"), 80)
120
71
  desc = _clip_text(arc.get("description"), 180)
121
72
  status = _clip_text(arc.get("status"), 40)
@@ -123,15 +74,10 @@ def _format_background_research(research, limit=1800):
123
74
  tail = f" [{status}]" if status else ""
124
75
  lines.append(f" - {name}: {desc}{tail}".rstrip())
125
76
 
126
- notes = research.get("cultural_notes")
127
- if isinstance(notes, list) and notes:
77
+ notes = research.get("cultural_notes", [])
78
+ if notes:
128
79
  lines.append("- Cultural notes:")
129
80
  for note in notes[:6]:
130
- if not isinstance(note, dict):
131
- value = _clip_text(note, 160)
132
- if value:
133
- lines.append(f" - {value}")
134
- continue
135
81
  item = _clip_text(note.get("item"), 80)
136
82
  expl = _clip_text(note.get("explanation"), 160)
137
83
  if item and expl:
@@ -150,7 +96,8 @@ def _format_background_research(research, limit=1800):
150
96
  if len(text) <= limit:
151
97
  return lines
152
98
  clipped = text[:limit].rsplit("\n", 1)[0].rstrip()
153
- return clipped.splitlines() + [
99
+ return [
100
+ *clipped.splitlines(),
154
101
  "",
155
102
  "[Story context clipped to keep ASR/visual evidence in context]",
156
103
  "",
@@ -169,16 +116,9 @@ def assess_understanding_substrate(
169
116
  so it must not grade as "rich"; otherwise the sparse-substrate warning, the research
170
117
  directive, and the density relief never fire for exactly that cold-narration case.
171
118
  """
172
- scenes = scenes_analysis or []
173
- asr_chars = sum(len(str(seg.get("text", "")).strip()) for seg in (asr_result or []))
174
- scenes_with_facts = sum(
175
- 1 for s in scenes if isinstance(s, dict) and s.get("frame_facts")
176
- )
177
- desc_lens = [
178
- len(str(s.get("description", "")).strip())
179
- for s in scenes
180
- if isinstance(s, dict)
181
- ]
119
+ asr_chars = sum(len(seg["text"]) for seg in asr_result)
120
+ scenes_with_facts = sum(1 for s in scenes_analysis if s.get("frame_facts"))
121
+ desc_lens = [len(s["description"]) for s in scenes_analysis]
182
122
  avg_desc = sum(desc_lens) // len(desc_lens) if desc_lens else 0
183
123
 
184
124
  has_asr = asr_chars >= 20
@@ -193,7 +133,7 @@ def assess_understanding_substrate(
193
133
  return {
194
134
  "level": level,
195
135
  "asr_chars": asr_chars,
196
- "scene_count": len(scenes),
136
+ "scene_count": len(scenes_analysis),
197
137
  "scenes_with_frame_facts": scenes_with_facts,
198
138
  "avg_description_len": avg_desc,
199
139
  "has_story_context": bool(has_story_context),
@@ -202,7 +142,7 @@ def assess_understanding_substrate(
202
142
 
203
143
  def _format_substrate_warning(assessment):
204
144
  """Render a loud brief banner when the understanding substrate is weak."""
205
- if not assessment or assessment.get("level") == "rich":
145
+ if assessment["level"] == "rich":
206
146
  return []
207
147
  if assessment["level"] == "empty":
208
148
  head = "⚠️ UNDERSTANDING SUBSTRATE IS EMPTY — narration will be generic guesswork unless you fix this first."
@@ -236,8 +176,8 @@ def _format_asr_chunks_for_brief(chunks, max_chunks=24):
236
176
  "",
237
177
  ]
238
178
  for chunk in chunks[:max_chunks]:
239
- scene_ids = ",".join(str(sid) for sid in chunk.get("scene_ids", [])) or "n/a"
240
- text = str(chunk.get("text", "")).strip()
179
+ scene_ids = ",".join(str(sid) for sid in chunk["scene_ids"]) or "n/a"
180
+ text = chunk["text"]
241
181
  if len(text) > 900:
242
182
  text = text[:897] + "..."
243
183
  lines.extend(
@@ -266,29 +206,27 @@ def _format_timeline_fusion_for_brief(fusion, max_items=40):
266
206
  "",
267
207
  ]
268
208
  for item in fusion[:max_items]:
269
- start, end = item.get("time_range", [0, 0])
270
- slots = item.get("narration_slots") or []
209
+ start, end = item["time_range"]
271
210
  slot_text = (
272
211
  ", ".join(
273
212
  f"{slot['start']:.1f}-{slot['end']:.1f}s/{slot['char_budget']}字"
274
- for slot in slots[:4]
213
+ for slot in item["narration_slots"][:4]
275
214
  )
276
215
  or "none"
277
216
  )
278
- dialogue = item.get("dialogue_segments") or []
279
217
  dialogue_text = (
280
218
  "; ".join(
281
- f"{seg['start']:.1f}-{seg['end']:.1f}s {seg.get('text', '')[:80]}"
282
- for seg in dialogue[:3]
283
- if seg.get("text")
219
+ f"{seg['start']:.1f}-{seg['end']:.1f}s {seg['text'][:80]}"
220
+ for seg in item["dialogue_segments"][:3]
221
+ if seg["text"]
284
222
  )
285
223
  or "none"
286
224
  )
287
225
  lines.extend(
288
226
  [
289
- f"### Fusion scene {item.get('scene_id')}: {start:.1f}-{end:.1f}s ({item.get('recommended_mode')})",
290
- f"- Visual: {item.get('visual_description', '')}",
291
- f"- Dialogue overlap: {item.get('dialogue_overlap_seconds', 0):.1f}s | {dialogue_text}",
227
+ f"### Fusion scene {item['scene_id']}: {start:.1f}-{end:.1f}s ({item['recommended_mode']})",
228
+ f"- Visual: {item['visual_description']}",
229
+ f"- Dialogue overlap: {item['dialogue_overlap_seconds']:.1f}s | {dialogue_text}",
292
230
  f"- Narration slots: {slot_text}",
293
231
  "",
294
232
  ]
@@ -302,7 +240,7 @@ def _format_timeline_fusion_for_brief(fusion, max_items=40):
302
240
 
303
241
 
304
242
  def _consolidation_model():
305
- return CONFIG.get("vlm_model", "")
243
+ return CONFIG["vlm_model"]
306
244
 
307
245
 
308
246
  def _clean_asr_prompt_fingerprint():
@@ -332,70 +270,77 @@ def _index_prompt_fingerprint():
332
270
  return hashlib.md5(prompt.encode("utf-8")).hexdigest()
333
271
 
334
272
 
335
- def _load_consolidation(work_dir, scenes_analysis=None):
336
- """Load consolidate.py's understanding_index.json only when provenance matches VLM input."""
273
+ def _load_consolidation(work_dir, scenes_analysis):
274
+ """Load consolidate.py's understanding_index.json only when its provenance matches
275
+ the current vlm_analysis.json, model and index prompt ({} otherwise)."""
337
276
  work_dir = Path(work_dir)
338
277
  path = work_dir / "understanding_index.json"
339
278
  meta_path = work_dir / "understanding_index.json.meta.json"
340
279
  if not path.exists() or not meta_path.exists():
341
280
  return {}
342
281
  try:
343
- data = json.loads(path.read_text(encoding="utf-8"))
344
282
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
345
- except (json.JSONDecodeError, OSError):
283
+ source_md5 = hashlib.md5(
284
+ (work_dir / "vlm_analysis.json").read_bytes()
285
+ ).hexdigest()
286
+ except (OSError, json.JSONDecodeError):
346
287
  return {}
347
- if not isinstance(data, dict) or not isinstance(meta, dict):
288
+ if not isinstance(meta, dict):
348
289
  return {}
349
- src_path = work_dir / "vlm_analysis.json"
350
- try:
351
- source_md5 = hashlib.md5(src_path.read_bytes()).hexdigest()
352
- except OSError:
290
+ expected = {
291
+ "source_md5": source_md5,
292
+ "model": _consolidation_model(),
293
+ "prompt_md5": _index_prompt_fingerprint(),
294
+ }
295
+ if scenes_analysis:
296
+ expected["scene_count"] = len(scenes_analysis)
297
+ if not meta.items() >= expected.items():
353
298
  return {}
354
- if meta.get("source_md5") != source_md5:
299
+ try:
300
+ index = json.loads(path.read_text(encoding="utf-8"))
301
+ except (OSError, json.JSONDecodeError):
355
302
  return {}
356
- expected_count = len([s for s in (scenes_analysis or []) if isinstance(s, dict)])
357
- if expected_count and meta.get("scene_count") != expected_count:
303
+ if not isinstance(index, dict) or not all(
304
+ isinstance(index.get(key), list)
305
+ for key in ("characters", "relationships", "plot_points", "entities")
306
+ ):
358
307
  return {}
359
- if meta.get("model") != _consolidation_model():
308
+ if not all(isinstance(item, dict) for item in index["characters"]):
360
309
  return {}
361
- if meta.get("prompt_md5") != _index_prompt_fingerprint():
310
+ if not all(isinstance(item, dict) for item in index["relationships"]):
362
311
  return {}
363
- return data
312
+ return index
364
313
 
365
314
 
366
315
  def _format_consolidation(index):
367
- """Render consolidate.py's understanding_index.json into a compact brief section."""
316
+ """Render consolidate.py's understanding_index.json into a compact brief section.
317
+
318
+ plot_points/entities items are model output and may be bare strings or objects."""
368
319
  if not index:
369
320
  return []
370
321
  lines = ["## Understanding index (from consolidate.py)", ""]
371
- chars = index.get("characters") or []
322
+ chars = index["characters"]
372
323
  if chars:
373
324
  lines.append("- Characters:")
374
325
  for c in chars:
375
- if isinstance(c, dict):
376
- lines.append(
377
- f" - {c.get('name', '?')}: {str(c.get('description', '')).strip()}"
378
- )
379
- else:
380
- lines.append(f" - {c}")
381
- rels = index.get("relationships") or []
326
+ lines.append(
327
+ f" - {c.get('name', '?')}: {str(c.get('description', '')).strip()}"
328
+ )
329
+ rels = index["relationships"]
382
330
  if rels:
383
331
  lines.append("- Relationships:")
384
332
  for r in rels:
385
- if isinstance(r, dict):
386
- lines.append(
387
- f" - {r.get('a', '?')} — {r.get('relation', '?')} — {r.get('b', '?')}"
388
- )
389
- else:
390
- lines.append(f" - {r}")
391
- plot = index.get("plot_points") or []
333
+ lines.append(
334
+ f" - {r.get('a', '?')} — {r.get('relation', '?')} — {r.get('b', '?')}"
335
+ )
336
+ plot = index["plot_points"]
392
337
  if plot:
393
338
  lines.append("- Plot spine:")
394
339
  lines.extend(
395
340
  f" {i + 1}. {p.get('text', p) if isinstance(p, dict) else p}"
396
341
  for i, p in enumerate(plot)
397
342
  )
398
- ents = index.get("entities") or []
343
+ ents = index["entities"]
399
344
  if ents:
400
345
  ent_names = [str(e.get("name", e) if isinstance(e, dict) else e) for e in ents]
401
346
  lines.append(f"- Entities: {', '.join(ent_names)}")