@oh-story/dsh 0.1.10 → 0.1.12

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 (444) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +7 -3
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +212 -142
  5. package/lib/drama/skills/short-drama/SKILL.md +14 -2
  6. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
  8. package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  11. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  13. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  14. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  15. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  16. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  17. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  18. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  19. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  20. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
  21. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  22. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  23. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
  24. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  25. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  26. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  27. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  28. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  29. package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
  30. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  31. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  32. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  33. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  46. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
  47. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
  48. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
  49. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
  50. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
  51. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
  53. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
  54. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  55. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  56. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  57. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  58. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  59. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  60. package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
  61. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  62. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  63. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  64. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
  65. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  66. package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
  67. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  68. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
  70. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
  71. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
  72. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
  73. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  74. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  75. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
  77. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
  78. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
  79. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  80. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  81. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  82. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  83. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  84. package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
  85. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  86. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  87. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  88. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  89. package/lib/index.js +71 -21
  90. package/lib/index.js.map +3 -3
  91. package/lib/oh-story/manifest.json +545 -310
  92. package/lib/oh-story/roles/chapter-extractor.md +11 -11
  93. package/lib/oh-story/roles/character-designer.md +1 -2
  94. package/lib/oh-story/roles/consistency-checker.md +11 -9
  95. package/lib/oh-story/roles/narrative-writer.md +24 -25
  96. package/lib/oh-story/roles/story-architect.md +9 -61
  97. package/lib/oh-story/roles/story-explorer.md +20 -125
  98. package/lib/oh-story/roles/story-researcher.md +0 -1
  99. package/lib/oh-story/skills/story/SKILL.md +6 -6
  100. package/lib/oh-story/skills/story/VERSION +1 -1
  101. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  102. package/lib/oh-story/skills/story/references/author-memory.md +40 -118
  103. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
  104. package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
  105. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  106. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
  107. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  108. package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
  109. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  110. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
  111. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  112. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
  113. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  114. package/lib/oh-story/skills/story-import/SKILL.md +63 -579
  115. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  116. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  117. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  118. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  119. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  120. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  121. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  122. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  123. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
  124. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  125. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  126. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
  127. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  128. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
  129. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
  130. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
  131. package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
  132. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
  133. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
  134. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  135. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  136. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
  137. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  139. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  141. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  142. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
  143. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
  144. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  145. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
  146. package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
  147. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  148. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  149. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  150. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  151. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  152. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  153. package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
  154. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  155. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
  156. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
  157. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  158. package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
  159. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  160. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
  161. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  162. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  163. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  164. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  165. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  166. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
  167. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  168. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  169. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  170. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  171. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  172. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  173. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  174. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  175. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  176. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  177. package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
  178. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
  179. package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
  180. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  181. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  182. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  183. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  184. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  185. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
  186. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
  187. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
  188. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  189. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
  190. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
  191. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  192. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
  193. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
  194. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
  195. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  196. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
  197. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  198. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
  199. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  200. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
  201. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
  202. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
  203. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
  204. package/lib/oh-story/skills/story-review/SKILL.md +19 -273
  205. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  206. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
  207. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  208. package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
  209. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  210. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  211. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  212. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
  213. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  214. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  215. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  216. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  217. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
  218. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
  219. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  220. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
  221. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
  222. package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
  223. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  224. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
  225. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  226. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  227. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  228. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
  229. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  230. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
  234. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  236. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  237. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  238. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  239. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  240. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  241. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  242. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  243. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  244. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  245. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  246. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
  247. package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
  248. package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
  249. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  250. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  251. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  252. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  253. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  254. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  255. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
  257. package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
  258. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  259. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  260. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  261. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  262. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
  263. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  264. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  265. package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
  266. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  267. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  268. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  270. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  271. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
  272. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  273. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  274. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  276. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  277. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  278. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  279. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  280. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
  281. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
  282. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
  283. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  284. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
  285. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  286. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  287. package/lib/types/client/video-studio.d.ts.map +1 -1
  288. package/lib/types/native-hooks.d.ts +2 -2
  289. package/lib/types/native-hooks.d.ts.map +1 -1
  290. package/lib/types/role-tool.d.ts.map +1 -1
  291. package/lib/types/skill-provider.d.ts.map +1 -1
  292. package/lib/types/video-project.d.ts.map +1 -1
  293. package/lib/types/workspace-route.d.ts.map +1 -1
  294. package/lib/video-recap/CHANGELOG.md +60 -1
  295. package/lib/video-recap/README.en.md +348 -137
  296. package/lib/video-recap/README.md +340 -136
  297. package/lib/video-recap/manifest.json +406 -251
  298. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  299. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  300. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  301. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  302. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  303. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  304. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  305. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  306. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  307. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  308. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  309. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  310. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  311. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  312. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  313. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  314. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  315. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  316. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  317. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  318. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  319. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  320. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  321. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  322. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  323. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  324. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  325. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  326. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  327. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  328. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  329. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  330. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  331. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  332. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  333. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  334. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  335. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  336. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  337. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  338. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  339. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  340. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  341. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  342. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  343. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  344. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  345. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  346. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  347. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  348. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  349. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  350. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  351. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  352. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  353. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  354. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  355. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  356. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  357. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  358. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  359. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  360. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  361. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  362. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  363. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  364. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  365. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  366. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  367. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  368. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  369. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  370. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  371. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  372. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  373. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  374. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  375. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  376. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  377. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  378. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  379. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  380. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  381. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  382. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  383. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  384. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  385. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  386. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  387. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  388. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  389. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  390. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  391. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  392. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  393. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  394. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  395. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  396. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  397. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  398. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  399. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  400. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  401. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  402. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  403. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  404. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  405. package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
  406. package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
  407. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  408. package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
  409. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  410. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  411. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  412. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  413. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  414. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  415. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  416. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  417. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  418. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  419. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  420. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  421. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  422. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  423. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  424. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  425. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  426. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  427. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  428. package/package.json +41 -41
  429. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
  430. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
  431. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  432. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  433. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  435. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  436. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  437. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
  438. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
  439. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  440. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
  441. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  442. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  443. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  444. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -3,33 +3,40 @@ import base64
3
3
  import json
4
4
  import os
5
5
  import re
6
- import shutil
7
6
  import tempfile
8
7
  import time
9
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
10
9
  from pathlib import Path
11
10
  from threading import Lock
12
11
 
13
- from fish_audio import synthesize_fish_audio
12
+ from approved_text_policy import (
13
+ PRESERVE_APPROVED_TEXT_POLICY,
14
+ ApprovedTextDurationError,
15
+ archive_current_meta,
16
+ enforce_duration,
17
+ policy_name,
18
+ validate_required_texts,
19
+ write_json_atomically as _write_tts_meta_atomically,
20
+ )
21
+ from providers.fish_audio import synthesize_fish_audio
22
+ import providers.index_tts as index_provider
14
23
  from lib import (
15
24
  CONFIG,
16
25
  _text_char_count,
17
26
  _truncate_at_sentence,
18
- file_fingerprint,
27
+ file_identity,
19
28
  get_video_duration,
20
29
  log,
21
30
  mimo_tts_api_call,
22
31
  narration_tempo_budget,
23
32
  run_cmd,
24
- stable_hash,
25
33
  )
26
34
 
27
35
  # Re-exported: tests and callers reach these through voiceover, the module owns the flow.
28
36
  from tts_audio import _maybe_normalize_tts_wav, _normalize_tts_wav_rms # noqa: F401
29
37
 
30
- SUPPORTED_TTS_ENGINES = {"mimo-tts", "fish-audio"}
38
+ SUPPORTED_TTS_ENGINES = {"mimo-tts", "fish-audio", "index-tts"}
31
39
  SEGMENT_AUDIO_SCHEMA_VERSION = 1
32
- TTS_CACHE_VERSION = 2
33
40
  VOICE_REFERENCE_PREP_VERSION = 1
34
41
  _VOICE_REFERENCE_LOCK = Lock()
35
42
  # Process-wide voice-reference state; prepared bytes are invocation-scoped.
@@ -37,12 +44,24 @@ _VOICE_REFERENCE_STATE_KEYS = (
37
44
  "voice_ref_b64",
38
45
  "voice_ref_snapshot_path",
39
46
  "voice_ref_snapshot_signature",
40
- "voice_ref_source_signature",
41
- "voice_ref_fingerprint",
42
47
  "voice_ref_snapshot_locked",
43
48
  )
44
49
 
45
50
 
51
+ def authored_text_policy():
52
+ """Return the explicit cacheable policy governing creative text mutation."""
53
+ return policy_name(CONFIG.get("preserve_approved_text", False))
54
+
55
+
56
+ def enforce_approved_text_policy(index, seg, authored_text, spoken_text, audio_duration,
57
+ available_duration, max_raw_duration):
58
+ """Fail closed when immutable approved text cannot fit its authored window."""
59
+ enforce_duration(
60
+ index, seg, authored_text, spoken_text, audio_duration, available_duration,
61
+ max_raw_duration, CONFIG.get("preserve_approved_text", False), CONFIG["breath_ms"],
62
+ )
63
+
64
+
46
65
  def _parse_rate_offset(rate_str):
47
66
  """'+5%' -> 0.05, '-3%' -> -0.03, '+0%' -> 0.0"""
48
67
  return float(rate_str.rstrip("%")) / 100.0
@@ -103,17 +122,23 @@ def _clean_narration_text(text):
103
122
  return re.sub(r'\s+', ' ', text).strip()
104
123
 
105
124
 
106
- def _synthesize_segment(i, seg, narration, tts_dir, engine):
107
- """合成单个 TTS 段(线程安全),支持 resume 跳过已有文件"""
108
- prepared = _prepare_tts_segment(i, seg, narration, tts_dir, engine)
109
- if prepared is None:
110
- return None
111
- text, output_wav, rate, pitch, cache_key = prepared
112
- cached = _reuse_tts_segment_cache(i, seg, output_wav, cache_key)
113
- if cached:
114
- return cached
125
+ def _synthesize_segment(i, seg, narration, tts_dir, engine, prepared=None):
126
+ """合成单个 TTS 段(线程安全),支持 resume 跳过已有文件。
115
127
 
116
- _run_tts_engine(engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion"))
128
+ `prepared` is the `_prepare_tts_segment` tuple of a segment synthesize_tts already probed
129
+ as a cache miss; passing it skips a second sidecar read + settings resolution."""
130
+ if prepared is None:
131
+ prepared = _prepare_tts_segment(i, seg, narration, tts_dir, engine)
132
+ if prepared is None:
133
+ return None
134
+ cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4], engine)
135
+ if cached:
136
+ return cached
137
+ text, output_wav, rate, pitch, cache_inputs = prepared
138
+
139
+ provider_receipt = _run_tts_engine(
140
+ engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion")
141
+ )
117
142
 
118
143
  dur = get_video_duration(output_wav)
119
144
  seg_slot = seg["end"] - seg["start"]
@@ -124,6 +149,11 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
124
149
  raw_budget = available * budget["max_raw_duration_factor"]
125
150
  truncated = False
126
151
  truncate_reason = "none"
152
+ try:
153
+ enforce_approved_text_policy(i, seg, seg["narration"], text, dur, available, raw_budget)
154
+ except ApprovedTextDurationError:
155
+ _cleanup_partial_tts_outputs(output_wav)
156
+ raise
127
157
  if dur > raw_budget and len(text) > 5:
128
158
  chars_per_sec = _text_char_count(text) / dur
129
159
  target_chars = max(5, int(raw_budget * chars_per_sec) - 1)
@@ -133,20 +163,24 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
133
163
  text = shortened
134
164
  truncated = True
135
165
  truncate_reason = "sentence_boundary"
136
- _run_tts_engine(engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion"))
166
+ provider_receipt = _run_tts_engine(
167
+ engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion")
168
+ )
137
169
  dur = get_video_duration(output_wav)
138
170
 
139
171
  norm_meta = _maybe_normalize_tts_wav(output_wav)
140
172
  if norm_meta:
141
173
  dur = get_video_duration(output_wav)
142
- _write_tts_segment_cache(output_wav, cache_key, text, dur, rate_offset,
143
- truncated, truncate_reason, norm_meta)
174
+ _write_tts_segment_cache(output_wav, cache_inputs, text, dur, rate_offset,
175
+ truncated, truncate_reason, norm_meta, provider_receipt)
144
176
  return _build_tts_segment_result(
145
- i, seg, text, output_wav, dur, rate_offset, truncated, truncate_reason, norm_meta)
177
+ i, seg, text, output_wav, dur, rate_offset, truncated, truncate_reason, norm_meta,
178
+ provider_receipt)
146
179
 
147
180
 
148
181
  def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offset,
149
- truncated=False, truncate_reason="none", norm_meta=None):
182
+ truncated=False, truncate_reason="none", norm_meta=None,
183
+ provider_receipt=None):
150
184
  budget = narration_tempo_budget(rate_offset)
151
185
  authored_text = _clean_narration_text(seg["narration"])
152
186
  resolved_truncated = truncated or text != authored_text
@@ -156,6 +190,7 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
156
190
  "start": seg["start"],
157
191
  "end": seg["end"],
158
192
  "narration": authored_text,
193
+ "authored_text": seg["narration"],
159
194
  "spoken_text": text,
160
195
  "truncated": resolved_truncated,
161
196
  "truncate_reason": (truncate_reason if truncate_reason != "none" else "sentence_boundary") if resolved_truncated else "none",
@@ -174,6 +209,8 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
174
209
  "tts_rate_offset": rate_offset,
175
210
  "pause_after_ms": seg.get("pause_after_ms", CONFIG["breath_ms"]),
176
211
  "overlaps_speech": seg.get("overlaps_speech", True),
212
+ "authored_text_policy": authored_text_policy(),
213
+ "provider_receipt": provider_receipt,
177
214
  }
178
215
  for optional_key in ("source_start", "source_end", "source_clip_id", "emotion"):
179
216
  if optional_key in seg:
@@ -183,13 +220,33 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
183
220
 
184
221
  def _tts_failure_record(index, seg, error):
185
222
  """Build a user-visible failure record for partial TTS output."""
186
- return {
223
+ record = {
187
224
  "index": index,
188
225
  "start": seg["start"],
189
226
  "end": seg["end"],
190
227
  "text": _clean_narration_text(seg["narration"]),
191
228
  "error": str(error),
192
229
  }
230
+ if authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
231
+ record.update({"required": True, "policy": PRESERVE_APPROVED_TEXT_POLICY})
232
+ if isinstance(error, ApprovedTextDurationError):
233
+ record.update({"required": True, **error.evidence})
234
+ return record
235
+
236
+
237
+ def _voice_record(engine):
238
+ """Which voice this run actually used: provider, model, voice id or reference audio."""
239
+ settings = tts_settings_payload(engine)
240
+ if engine == "fish-audio":
241
+ return {"provider": engine, "model": settings["fish_tts_model"],
242
+ "voice_id": settings["fish_tts_reference_id"], "reference": None}
243
+ if engine == "index-tts":
244
+ return {"provider": engine, "model": None,
245
+ "voice_id": settings["index_tts_voice"], "reference": None}
246
+ reference = settings.get("voice_ref_identity")
247
+ return {"provider": engine, "model": settings["mimo_tts_model"],
248
+ "voice_id": None if reference else settings["mimo_tts_voice"],
249
+ "reference": reference}
193
250
 
194
251
 
195
252
  def _build_tts_meta(segments, engine, narration_name, failures):
@@ -197,6 +254,7 @@ def _build_tts_meta(segments, engine, narration_name, failures):
197
254
  return {
198
255
  "segments": segments,
199
256
  "engine": engine,
257
+ "voice": _voice_record(engine),
200
258
  "narration": narration_name,
201
259
  "partial": bool(failures),
202
260
  "failures": failures,
@@ -218,32 +276,34 @@ def synthesize_tts(narration, work_dir):
218
276
 
219
277
  if not narration:
220
278
  raise RuntimeError("narration.json 没有可配音的解说段,已中止以避免生成无解说视频")
279
+ validate_required_texts(
280
+ narration, _clean_narration_text, CONFIG.get("preserve_approved_text", False)
281
+ )
221
282
 
222
283
  cache_engine = _configured_tts_engine_for_cache()
223
- if cache_engine == "fish-audio" and voice_ref:
224
- raise RuntimeError(
225
- "Fish Audio 不接受本地 VOICE_REF/--voice-ref;请改用 FISH_TTS_REFERENCE_ID"
226
- )
227
- # A fully cached narration needs no credential: probe every segment's sidecar first.
228
- cached_segments = []
229
- needs_fresh = False
230
- prepared_count = 0
284
+ if cache_engine in {"fish-audio", "index-tts"} and voice_ref:
285
+ if cache_engine == "fish-audio":
286
+ raise RuntimeError("Fish Audio 不接受本地 VOICE_REF/--voice-ref;请改用 FISH_TTS_REFERENCE_ID")
287
+ raise RuntimeError("index-tts 不支持本地 VOICE_REF/--voice-ref 克隆")
288
+ # A fully cached narration needs no credential: probe every segment's sidecar once here;
289
+ # hits are final and only misses reach the workers (with their prepared inputs).
290
+ segments = []
291
+ misses = []
231
292
  for i, seg in enumerate(narration):
232
293
  prepared = _prepare_tts_segment(i, seg, narration, tts_dir, cache_engine)
233
294
  if prepared is None:
234
295
  continue
235
- prepared_count += 1
236
- cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4])
237
- if not cached:
238
- needs_fresh = True
239
- break
240
- cached_segments.append(cached)
241
- if prepared_count == 0:
296
+ cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4], cache_engine)
297
+ if cached:
298
+ segments.append(cached)
299
+ else:
300
+ misses.append((i, seg, prepared))
301
+ if not segments and not misses:
242
302
  raise RuntimeError("narration.json 没有可配音的有效文本,已中止以避免生成无解说视频")
243
- if not needs_fresh:
244
- cached_segments.sort(key=lambda x: x["index"])
303
+ if not misses:
304
+ segments.sort(key=lambda x: x["index"])
245
305
  log(f"TTS 引擎: {cache_engine} (cache)")
246
- return cached_segments, cache_engine, []
306
+ return segments, cache_engine, []
247
307
 
248
308
  engine = resolve_tts_engine()
249
309
 
@@ -253,15 +313,14 @@ def synthesize_tts(narration, work_dir):
253
313
 
254
314
  log(f"TTS 引擎: {engine}")
255
315
 
256
- segments = []
257
316
  failures = []
258
- max_workers = min(len(narration), CONFIG["tts_workers"])
317
+ max_workers = min(len(misses), CONFIG["tts_workers"])
259
318
 
260
319
  try:
261
320
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
262
321
  futures = {
263
- executor.submit(_synthesize_segment, i, seg, narration, tts_dir, engine): i
264
- for i, seg in enumerate(narration)
322
+ executor.submit(_synthesize_segment, i, seg, narration, tts_dir, engine, prepared): i
323
+ for i, seg, prepared in misses
265
324
  }
266
325
  for future in as_completed(futures):
267
326
  try:
@@ -278,6 +337,25 @@ def synthesize_tts(narration, work_dir):
278
337
  CONFIG.pop("voice_ref_snapshot_locked", None)
279
338
 
280
339
  segments.sort(key=lambda x: x["index"])
340
+ approved_text_failures = [
341
+ failure for failure in failures
342
+ if failure.get("failure_kind") == "approved_text_duration_conflict"
343
+ ]
344
+ if approved_text_failures:
345
+ first = approved_text_failures[0]
346
+ evidence = {
347
+ key: value for key, value in first.items()
348
+ if key not in {"text", "error", "required"}
349
+ }
350
+ if len(approved_text_failures) > 1:
351
+ evidence["conflict_count"] = len(approved_text_failures)
352
+ raise ApprovedTextDurationError(evidence)
353
+ if failures and authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
354
+ sample = json.dumps(failures[:3], ensure_ascii=False, sort_keys=True)
355
+ raise RuntimeError(
356
+ f"批准稿严格模式有 {len(failures)}/{len(narration)} 个必需 TTS 段失败,"
357
+ f"不能按部分成功交付: {sample}"
358
+ )
281
359
  if failures and not CONFIG["allow_partial_tts"]:
282
360
  sample = "; ".join(f"段 {f['index']+1}: {f['error']}" for f in failures[:3])
283
361
  raise RuntimeError(
@@ -297,22 +375,26 @@ def synthesize_tts(narration, work_dir):
297
375
 
298
376
 
299
377
  def _run_tts_engine(engine, text, output_wav, rate="+0%", pitch="+0Hz", emotion=None):
300
- """Run one TTS engine with retry and remove partial files after failures."""
301
- if engine not in SUPPORTED_TTS_ENGINES:
302
- raise RuntimeError(f"不支持的 TTS 引擎: {engine}。当前支持 mimo-tts、fish-audio。")
378
+ """Run one TTS engine with retry and remove partial files after failures.
379
+
380
+ `engine` comes from resolve_tts_engine; index-tts controls were already forced to the
381
+ provider defaults by _prepare_tts_segment."""
303
382
  retries = CONFIG["tts_retries"]
304
383
  last_error = None
305
384
 
306
385
  for attempt in range(1, retries + 1):
307
386
  try:
308
387
  _cleanup_partial_tts_outputs(output_wav)
388
+ receipt = None
309
389
  if engine == "mimo-tts":
310
390
  _tts_mimo(text, output_wav, rate=rate, pitch=pitch, emotion=emotion)
311
- else:
391
+ elif engine == "fish-audio":
312
392
  synthesize_fish_audio(text, output_wav, rate=rate)
393
+ else:
394
+ receipt = index_provider.synthesize_configured(text, output_wav, CONFIG)
313
395
  if get_video_duration(output_wav) <= 0:
314
396
  raise RuntimeError(f"{engine} 输出音频时长无效")
315
- return
397
+ return receipt
316
398
  except Exception as exc:
317
399
  last_error = exc
318
400
  _cleanup_partial_tts_outputs(output_wav)
@@ -345,19 +427,23 @@ def _prepare_tts_segment(index, seg, narration, tts_dir, engine):
345
427
  if not text:
346
428
  return None
347
429
  output_wav = tts_dir / f"narr_{index:03d}.wav"
348
- if CONFIG["tts_dynamic_params"]:
430
+ if engine == "index-tts":
431
+ rate, pitch = index_provider.default_controls(seg)
432
+ elif CONFIG["tts_dynamic_params"]:
349
433
  rate, pitch = _compute_tts_params(text, narration, index)
350
434
  else:
351
435
  rate, pitch = "+0%", "+0Hz"
352
- cache_key = _tts_segment_cache_key(engine, index, seg, text, rate, pitch)
353
- return text, output_wav, rate, pitch, cache_key
436
+ cache_inputs = _tts_segment_cache_inputs(engine, index, seg, text, rate, pitch)
437
+ return text, output_wav, rate, pitch, cache_inputs
354
438
 
355
439
 
356
- def _reuse_tts_segment_cache(index, seg, output_wav, cache_key):
357
- cached = _load_tts_segment_cache(output_wav, cache_key)
440
+ def _reuse_tts_segment_cache(index, seg, output_wav, cache_inputs, engine):
441
+ cached = _load_tts_segment_cache(output_wav, cache_inputs)
358
442
  if cached is None:
359
443
  return None
360
- # The sidecar's audio_fingerprint already proved these exact bytes are what produced
444
+ if engine == "index-tts" and not index_provider.valid_cached_receipt(cached, CONFIG):
445
+ return None
446
+ # The sidecar's audio identity (size, mtime_ns) still matches the WAV that produced
361
447
  # `audio_duration`; re-probing would be one ffprobe process per segment on every rerun.
362
448
  log(f" 段 {index+1}: 复用已有 ({cached['audio_duration']:.1f}s)")
363
449
  return _build_tts_segment_result(
@@ -370,13 +456,13 @@ def _reuse_tts_segment_cache(index, seg, output_wav, cache_key):
370
456
  cached["truncated"],
371
457
  cached["truncate_reason"],
372
458
  cached["normalization"],
459
+ cached.get("provider_receipt"),
373
460
  )
374
461
 
375
462
 
376
- def _tts_segment_cache_key(engine, index, seg, source_text, rate, pitch):
377
- """Fingerprint the exact inputs that make a cached segment safe to reuse."""
463
+ def _tts_segment_cache_inputs(engine, index, seg, source_text, rate, pitch):
464
+ """The exact inputs that make a cached segment safe to reuse (compared by equality)."""
378
465
  payload = {
379
- "version": TTS_CACHE_VERSION,
380
466
  "engine": engine,
381
467
  "source_text": source_text,
382
468
  "segment_index": index,
@@ -386,49 +472,50 @@ def _tts_segment_cache_key(engine, index, seg, source_text, rate, pitch):
386
472
  "rate": rate,
387
473
  "pitch": pitch,
388
474
  "emotion": seg.get("emotion", ""),
389
- "settings": tts_settings_fingerprint(engine),
475
+ "settings": tts_settings_payload(engine),
390
476
  }
391
- return stable_hash(payload)
477
+ if authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
478
+ payload.update({
479
+ "authored_text_policy": PRESERVE_APPROVED_TEXT_POLICY,
480
+ "authored_text": seg["narration"],
481
+ "provider_text_cleanup": "clean-narration-text-v1",
482
+ })
483
+ return payload
392
484
 
393
485
 
394
- def _load_tts_segment_cache(output_wav, cache_key):
395
- """Return cache metadata only when the sidecar proves the WAV matches narration."""
486
+ def _load_tts_segment_cache(output_wav, cache_inputs):
487
+ """Return cache metadata only when the sidecar's inputs and WAV identity still match."""
396
488
  cache_path = _tts_segment_cache_path(output_wav)
397
- if not output_wav.exists() or not cache_path.exists():
489
+ if not output_wav.exists() or output_wav.stat().st_size == 0 or not cache_path.exists():
398
490
  return None
491
+ # This skill wrote the sidecar (_write_tts_segment_cache): one it cannot parse is a bug
492
+ # to surface. One from the earlier content-hash schema (cache_key/audio_fingerprint,
493
+ # no settings/audio) is a plain miss: the segment is re-synthesized once.
399
494
  try:
400
495
  data = json.loads(cache_path.read_text(encoding="utf-8"))
401
- except (OSError, ValueError):
496
+ except ValueError as exc:
497
+ raise RuntimeError(f"TTS 缓存 sidecar 损坏: {cache_path}") from exc
498
+ if not isinstance(data, dict) or "settings" not in data or "audio" not in data:
402
499
  return None
403
- required = {
404
- "cache_key", "audio_fingerprint", "spoken_text", "audio_duration",
405
- "tts_rate_offset", "truncated", "truncate_reason", "normalization",
406
- }
407
- if not isinstance(data, dict) or not required <= data.keys():
408
- return None
409
- try:
410
- audio_fingerprint = file_fingerprint(output_wav)
411
- except OSError:
412
- return None
413
- if data["cache_key"] != cache_key or data["audio_fingerprint"] != audio_fingerprint:
414
- return None
415
- return data
500
+ stale = data["settings"] != cache_inputs or data["audio"] != file_identity(output_wav)
501
+ return None if stale else data
416
502
 
417
503
 
418
- def _write_tts_segment_cache(output_wav, cache_key, spoken_text, duration, rate_offset,
419
- truncated=False, truncate_reason="none", norm_meta=None):
420
- """Persist non-secret provenance for safe per-segment TTS reuse."""
504
+ def _write_tts_segment_cache(output_wav, cache_inputs, spoken_text, duration, rate_offset,
505
+ truncated=False, truncate_reason="none", norm_meta=None,
506
+ provider_receipt=None):
507
+ """Persist non-secret inputs and the WAV identity for safe per-segment TTS reuse."""
421
508
  _tts_segment_cache_path(output_wav).write_text(
422
509
  json.dumps({
423
- "version": TTS_CACHE_VERSION,
424
- "cache_key": cache_key,
425
- "audio_fingerprint": file_fingerprint(output_wav),
510
+ "settings": cache_inputs,
511
+ "audio": file_identity(output_wav),
426
512
  "spoken_text": spoken_text,
427
513
  "audio_duration": duration,
428
514
  "tts_rate_offset": rate_offset,
429
515
  "truncated": truncated,
430
516
  "truncate_reason": truncate_reason if truncated else "none",
431
517
  "normalization": norm_meta or None,
518
+ "provider_receipt": provider_receipt,
432
519
  }, ensure_ascii=False, indent=2),
433
520
  encoding="utf-8",
434
521
  )
@@ -443,7 +530,7 @@ def _configured_tts_engine_for_cache():
443
530
  return "fish-audio"
444
531
  if provider not in SUPPORTED_TTS_ENGINES:
445
532
  raise RuntimeError(
446
- "TTS_PROVIDER/--tts-provider 必须是 auto、mimo-tts 或 fish-audio"
533
+ "TTS_PROVIDER/--tts-provider 必须是 auto、mimo-tts、fish-audio 或 index-tts"
447
534
  )
448
535
  return provider
449
536
 
@@ -451,6 +538,8 @@ def _configured_tts_engine_for_cache():
451
538
  def resolve_tts_engine():
452
539
  """Resolve the selected MiMo or Fish Audio TTS engine and require its credential."""
453
540
  engine = _configured_tts_engine_for_cache()
541
+ if engine == "index-tts":
542
+ return engine
454
543
  if engine == "fish-audio":
455
544
  if CONFIG["fish_api_key"]:
456
545
  return engine
@@ -458,11 +547,11 @@ def resolve_tts_engine():
458
547
  if CONFIG["mimo_tts_api_key"]:
459
548
  return engine
460
549
  raise RuntimeError(
461
- f"没有可用的 TTS 引擎:请设置 {CONFIG['mimo_tts_api_key_source']}(MiMo TTS 需要)。"
550
+ f"没有可用的 TTS 引擎:请设置 {CONFIG['mimo_tts_env_var']}(MiMo TTS 需要)。"
462
551
  )
463
552
 
464
553
 
465
- def tts_settings_fingerprint(engine):
554
+ def tts_settings_payload(engine):
466
555
  """Return non-secret TTS settings that materially affect generated audio."""
467
556
  settings = {
468
557
  "engine": engine,
@@ -475,7 +564,9 @@ def tts_settings_fingerprint(engine):
475
564
  "tts_segment_target_rms_dbfs": CONFIG["tts_segment_target_rms_dbfs"],
476
565
  "tts_segment_peak_limit": CONFIG["tts_segment_peak_limit"],
477
566
  }
478
- if engine == "fish-audio":
567
+ if engine == "index-tts":
568
+ settings.update(index_provider.cache_settings(CONFIG))
569
+ elif engine == "fish-audio":
479
570
  settings.update(
480
571
  {
481
572
  "fish_tts_api_url": CONFIG["fish_tts_api_url"],
@@ -496,7 +587,7 @@ def tts_settings_fingerprint(engine):
496
587
  if voice_ref and engine == "mimo-tts":
497
588
  ref_path = Path(voice_ref).expanduser()
498
589
  settings.pop("mimo_tts_voice", None) # ignored by the voiceclone API
499
- settings["voice_ref_fingerprint"] = _voice_reference_fingerprint(ref_path)
590
+ settings["voice_ref_identity"] = _voice_reference_identity(ref_path)
500
591
  settings["voice_ref_preparation"] = (
501
592
  f"pcm_s16le:24000hz:mono:30s:v{VOICE_REFERENCE_PREP_VERSION}"
502
593
  )
@@ -538,32 +629,22 @@ def _prepare_voice_reference(ref_path):
538
629
 
539
630
 
540
631
  def _voice_reference_signature(ref_path):
541
- """Cheaply identify the prepared source so a reused process cannot serve stale audio."""
632
+ """Identity of the reference source: resolved path plus size and mtime_ns."""
542
633
  ref = Path(ref_path).expanduser().resolve()
543
- stat = ref.stat()
544
- return f"{ref}:{stat.st_size}:{stat.st_mtime_ns}"
634
+ return {"path": str(ref), **file_identity(ref)}
545
635
 
546
636
 
547
- def _voice_reference_fingerprint(ref_path):
548
- ref = Path(ref_path).expanduser()
549
- resolved = str(ref.resolve())
637
+ def _voice_reference_identity(ref_path):
638
+ """Identity for cache settings: the locked snapshot's while a run is in flight, else live."""
639
+ resolved = str(Path(ref_path).expanduser().resolve())
550
640
  if (
551
641
  CONFIG.get("voice_ref_snapshot_locked")
552
642
  and CONFIG.get("voice_ref_b64")
553
643
  and CONFIG.get("voice_ref_snapshot_path") == resolved
554
- and CONFIG.get("voice_ref_fingerprint")
555
- ):
556
- return CONFIG["voice_ref_fingerprint"]
557
- signature = _voice_reference_signature(ref)
558
- if (
559
- CONFIG.get("voice_ref_source_signature") == signature
560
- and CONFIG.get("voice_ref_fingerprint")
644
+ and CONFIG.get("voice_ref_snapshot_signature")
561
645
  ):
562
- return CONFIG["voice_ref_fingerprint"]
563
- fingerprint = file_fingerprint(ref)
564
- CONFIG["voice_ref_source_signature"] = signature
565
- CONFIG["voice_ref_fingerprint"] = fingerprint
566
- return fingerprint
646
+ return CONFIG["voice_ref_snapshot_signature"]
647
+ return _voice_reference_signature(ref_path)
567
648
 
568
649
 
569
650
  def _cache_prepared_voice_reference(ref_path):
@@ -576,30 +657,19 @@ def _cache_prepared_voice_reference(ref_path):
576
657
  and CONFIG.get("voice_ref_snapshot_path") == resolved
577
658
  ):
578
659
  return CONFIG["voice_ref_b64"]
660
+ if not ref.is_file():
661
+ raise FileNotFoundError(f"参考音频不存在或不是文件: {ref}")
579
662
  signature = _voice_reference_signature(ref)
580
- snapshot_path = CONFIG.get("voice_ref_snapshot_path")
581
- snapshot_signature = CONFIG.get("voice_ref_snapshot_signature")
582
663
  if (
583
664
  CONFIG.get("voice_ref_b64")
584
- and snapshot_path == resolved
585
- and snapshot_signature == signature
665
+ and CONFIG.get("voice_ref_snapshot_path") == resolved
666
+ and CONFIG.get("voice_ref_snapshot_signature") == signature
586
667
  ):
587
668
  return CONFIG["voice_ref_b64"]
588
- if not ref.is_file():
589
- raise FileNotFoundError(f"参考音频不存在或不是文件: {ref}")
590
- # ffmpeg and the cache fingerprint must consume the same immutable bytes. Copy first,
591
- # then derive both the normalized WAV and identity from that snapshot rather than reading
592
- # a caller-mutable source twice.
593
- with tempfile.TemporaryDirectory(prefix="video-recap-voice-ref-snapshot-") as temp_dir:
594
- snapshot = Path(temp_dir) / f"source{ref.suffix or '.audio'}"
595
- shutil.copyfile(ref, snapshot)
596
- fingerprint = file_fingerprint(snapshot)
597
- encoded = _prepare_voice_reference(snapshot)
669
+ encoded = _prepare_voice_reference(ref)
598
670
  CONFIG["voice_ref_b64"] = encoded
599
- CONFIG["voice_ref_snapshot_path"] = str(ref)
671
+ CONFIG["voice_ref_snapshot_path"] = resolved
600
672
  CONFIG["voice_ref_snapshot_signature"] = signature
601
- CONFIG["voice_ref_source_signature"] = signature
602
- CONFIG["voice_ref_fingerprint"] = fingerprint
603
673
  return encoded
604
674
 
605
675
 
@@ -640,21 +710,28 @@ def main():
640
710
  description="video-voiceover: synthesize narration audio segments from narration.json.")
641
711
  ap.add_argument("--work-dir", required=True)
642
712
  ap.add_argument("--narration", default=None,
643
- help="narration json (default: <work-dir>/narration.json; pass narration_mapped.json explicitly for legacy cut runs)")
713
+ help="narration json (default: <work-dir>/narration.json)")
644
714
  ap.add_argument("--mimo-voice", default=None, help="MiMo TTS voice name")
645
715
  ap.add_argument(
646
716
  "--tts-provider",
647
717
  default=os.environ.get("TTS_PROVIDER", "auto"),
648
- choices=["auto", "mimo-tts", "fish-audio"],
718
+ choices=["auto", "mimo-tts", "fish-audio", "index-tts"],
649
719
  help="TTS provider (default: MiMo when configured, otherwise Fish Audio)",
650
720
  )
651
721
  ap.add_argument("--voice-ref", default=None,
652
722
  help="reference audio (wav/mp3/etc.) for mimo-v2.5-tts-voiceclone")
653
723
  ap.add_argument("--allow-partial-tts", action="store_true",
654
724
  help="allow output when some narration segments fail TTS")
725
+ ap.add_argument(
726
+ "--preserve-approved-text",
727
+ action="store_true",
728
+ help="fail closed when approved narration exceeds its window; never auto-truncate/resynthesize",
729
+ )
655
730
  args = ap.parse_args()
656
731
  work_dir = Path(args.work_dir)
657
732
  CONFIG["tts_provider"] = args.tts_provider
733
+ index_provider.load_private_config(CONFIG, os.environ)
734
+ CONFIG["preserve_approved_text"] = args.preserve_approved_text
658
735
  CONFIG["voice_ref"] = (
659
736
  args.voice_ref if args.voice_ref is not None else os.environ.get("VOICE_REF", "").strip()
660
737
  )
@@ -662,8 +739,9 @@ def main():
662
739
  CONFIG["mimo_tts_voice"] = args.mimo_voice
663
740
  if args.mimo_voice and CONFIG["voice_ref"]:
664
741
  ap.error("--mimo-voice and --voice-ref are mutually exclusive")
665
- if args.tts_provider == "fish-audio" and (args.mimo_voice or CONFIG["voice_ref"]):
666
- ap.error("--mimo-voice/--voice-ref are only supported by the MiMo TTS provider")
742
+ if args.mimo_voice and args.tts_provider in {"fish-audio", "index-tts"}:
743
+ ap.error("--mimo-voice is only supported by the MiMo TTS provider")
744
+ # A local --voice-ref with fish-audio/index-tts is rejected by synthesize_tts.
667
745
  # Voice-reference normalization is intentionally lazy: a fully cached rerun should not
668
746
  # invoke ffmpeg. _tts_mimo uses a process-wide lock so a fresh parallel run still converts
669
747
  # the reference exactly once.
@@ -672,15 +750,15 @@ def main():
672
750
  if args.narration:
673
751
  narration_path = Path(args.narration)
674
752
  else:
675
- # Canonical cut mode is cut-first/narrate-second: narration.json is already on the
676
- # output timeline. A stale legacy narration_mapped.json in the same work_dir must not
677
- # silently override it; legacy direct-cut callers can still pass --narration explicitly.
753
+ # Cut mode is cut-first/narrate-second: narration.json is already on the output
754
+ # timeline, so the default needs no remapping.
678
755
  narration_path = work_dir / "narration.json"
756
+ if CONFIG["preserve_approved_text"]:
757
+ archive_current_meta(work_dir / "tts_meta.json")
679
758
  narration = json.loads(narration_path.read_text(encoding="utf-8"))
680
759
  tts_segments, engine_used, failures = synthesize_tts(narration, work_dir)
681
760
  meta = _build_tts_meta(tts_segments, engine_used, narration_path.name, failures)
682
- (work_dir / "tts_meta.json").write_text(
683
- json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
761
+ _write_tts_meta_atomically(work_dir / "tts_meta.json", meta)
684
762
  if failures:
685
763
  log(f"配音完成但缺 {len(failures)} 段:成片可预览但不建议直接发布")
686
764
  log(f"配音完成: {len(tts_segments)} 段, 引擎 {engine_used}")