@oh-story/dsh 0.1.7 → 0.1.9

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 (272) hide show
  1. package/README.md +30 -15
  2. package/lib/client.js +93 -16
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +165 -69
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -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 +18 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
  11. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  12. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  21. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  22. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
  23. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
  24. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
  25. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
  26. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
  27. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
  29. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  30. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  31. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  32. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  33. package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
  34. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
  35. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
  36. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  37. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  38. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  39. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
  40. package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
  41. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
  42. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
  43. package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
  44. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  45. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
  46. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  47. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
  50. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
  51. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
  53. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
  54. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
  55. package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
  56. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  57. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  58. package/lib/index.js +4 -2
  59. package/lib/index.js.map +2 -2
  60. package/lib/novel-to-game/VERSION +1 -1
  61. package/lib/novel-to-game/manifest.json +28 -28
  62. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
  64. package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
  65. package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
  66. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
  67. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
  68. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
  69. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
  70. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
  71. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
  72. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
  73. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
  74. package/lib/oh-story/manifest.json +292 -172
  75. package/lib/oh-story/roles/character-designer.md +1 -1
  76. package/lib/oh-story/roles/narrative-writer.md +56 -242
  77. package/lib/oh-story/roles/story-architect.md +1 -1
  78. package/lib/oh-story/skills/story/SKILL.md +1 -1
  79. package/lib/oh-story/skills/story/VERSION +1 -1
  80. package/lib/oh-story/skills/story/references/author-memory.md +10 -8
  81. package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
  82. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
  83. package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
  84. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  85. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  86. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  87. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  88. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  89. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  90. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  91. package/lib/oh-story/skills/story-import/SKILL.md +2 -2
  92. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  93. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  94. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
  95. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
  96. package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
  97. package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
  98. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
  99. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
  100. package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
  101. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  102. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  103. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
  104. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
  105. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  106. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  107. package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
  108. package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
  109. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  110. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  111. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  112. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  113. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  114. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
  115. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
  116. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
  117. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
  118. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  119. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
  120. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
  121. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  122. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  123. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
  124. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  125. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  126. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
  127. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  128. package/lib/oh-story/skills/story-review/SKILL.md +13 -3
  129. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
  130. package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
  131. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  132. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
  133. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
  134. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  135. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  136. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
  137. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  138. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  139. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  140. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  141. package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
  142. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
  143. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  144. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  145. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
  146. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  147. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  148. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  149. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  150. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  151. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
  152. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  153. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  154. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  155. package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
  156. package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
  157. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  158. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  159. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
  160. 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
  161. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  162. 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
  163. 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
  164. 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
  165. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  166. 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
  167. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  168. 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
  169. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  170. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  171. package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
  172. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  173. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  174. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  175. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  176. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  177. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
  178. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  179. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  180. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  181. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  182. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  183. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  184. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  185. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  186. package/lib/types/client/index.d.ts.map +1 -1
  187. package/lib/types/client/production-prompts.d.ts.map +1 -1
  188. package/lib/types/client/production-runtime.d.ts +17 -0
  189. package/lib/types/client/production-runtime.d.ts.map +1 -1
  190. package/lib/types/skill-provider.d.ts.map +1 -1
  191. package/lib/video-recap/CHANGELOG.md +31 -1
  192. package/lib/video-recap/manifest.json +162 -162
  193. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  194. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  195. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  196. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  197. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  198. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  199. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  200. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  201. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  202. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  203. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  204. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  205. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  206. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  207. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  208. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  209. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  210. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  211. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  212. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  213. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  214. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  215. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  216. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  217. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  218. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  219. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  220. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  221. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  222. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  223. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  224. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  225. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  226. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  227. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  228. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  229. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  230. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  231. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  232. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  233. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  234. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  235. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  236. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  237. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  238. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  239. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  240. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  241. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  242. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  243. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  244. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  245. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  246. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  247. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  248. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  249. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  250. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  251. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  252. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  253. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  254. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  255. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  256. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  257. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  258. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  259. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  260. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  262. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  263. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  264. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  265. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  266. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  267. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  268. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  269. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  270. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  271. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  272. package/package.json +41 -41
@@ -1,73 +1,51 @@
1
1
  """Remap cut evidence and format output-timeline brief directives."""
2
2
 
3
- import importlib.util
4
-
5
-
6
3
  import json
7
-
8
4
  import math
9
-
10
5
  import re
11
-
12
6
  from pathlib import Path
13
7
 
14
8
  from lib import CONFIG, stable_hash
15
9
 
16
10
 
17
- try:
18
- from deslop_qc import analyze_deslop_qc
19
- except ModuleNotFoundError:
20
- _deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
21
- _deslop_qc_spec = importlib.util.spec_from_file_location(
22
- "deslop_qc", _deslop_qc_path
23
- )
24
- if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
25
- raise
26
- _deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
27
- _deslop_qc_spec.loader.exec_module(_deslop_qc_module)
28
- analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
29
-
30
-
31
11
  def _parse_target_seconds(value):
32
12
  """Parse a cut-mode target duration ("30m" / "600" / "1h5m" / "00:30:00") to seconds.
33
13
 
34
- Mirrors the local clip-plan contract closely enough to size the brief; returns None
35
- on unparseable input so the brief simply falls back to the source duration.
14
+ None or blank means the knob is unset (None). Any other value that does not parse to
15
+ a positive duration is a typo in user input and raises ValueError.
36
16
  """
37
17
  if value is None:
38
18
  return None
39
19
  if isinstance(value, (int, float)):
40
- return float(value) if value > 0 else None
41
- text = str(value).strip().lower()
42
- if not text:
43
- return None
44
- try:
20
+ seconds = float(value)
21
+ else:
22
+ text = str(value).strip().lower()
23
+ if not text:
24
+ return None
45
25
  if ":" in text:
46
26
  parts = [float(p) for p in text.split(":")]
47
- if any(p < 0 for p in parts):
48
- return None
49
27
  if len(parts) == 2:
50
28
  seconds = parts[0] * 60 + parts[1]
51
29
  elif len(parts) == 3:
52
30
  seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]
53
31
  else:
54
- return None
55
- return seconds if seconds > 0 else None
56
- factors = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
57
- seconds = 0.0
58
- matched = False
59
- pos = 0
60
- for m in re.finditer(r"([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?", text):
61
- if m.start() != pos:
62
- break
63
- pos = m.end()
64
- matched = True
65
- seconds += float(m.group(1)) * factors[m.group(2) or "s"]
66
- if not matched or pos != len(text):
67
- return None
68
- return seconds if seconds > 0 else None
69
- except (ValueError, TypeError):
70
- return None
32
+ raise ValueError(f"unparseable target duration: {value!r}")
33
+ if any(p < 0 for p in parts):
34
+ raise ValueError(f"unparseable target duration: {value!r}")
35
+ else:
36
+ factors = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
37
+ seconds = 0.0
38
+ pos = 0
39
+ for m in re.finditer(r"([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?", text):
40
+ if m.start() != pos:
41
+ break
42
+ pos = m.end()
43
+ seconds += float(m.group(1)) * factors[m.group(2) or "s"]
44
+ if pos == 0 or pos != len(text):
45
+ raise ValueError(f"unparseable target duration: {value!r}")
46
+ if not seconds > 0:
47
+ raise ValueError(f"target duration must be positive: {value!r}")
48
+ return seconds
71
49
 
72
50
 
73
51
  def _format_research_directive(work_dir, substrate):
@@ -81,9 +59,9 @@ def _format_research_directive(work_dir, substrate):
81
59
  """
82
60
  if (Path(work_dir) / "background_research.json").exists():
83
61
  return [] # already researched; _format_background_research surfaces it
84
- if not (substrate and substrate.get("level") in ("thin", "empty")):
62
+ if substrate["level"] not in ("thin", "empty"):
85
63
  return [] # rich enough to write from dialogue/spine; do not nag
86
- context = str(CONFIG.get("context_info") or "").strip()
64
+ context = CONFIG["context_info"].strip()
87
65
  return [
88
66
  "## ⚑ Research the story FIRST (do this before writing narration)",
89
67
  "",
@@ -114,7 +92,7 @@ def _load_cut_output_spans_for_brief(work_dir, *, required=False):
114
92
  if required:
115
93
  raise SystemExit(
116
94
  "cut pass2 brief requires fresh clip_plan_validated.json with explicit "
117
- f"finite source/output spans ({reason})"
95
+ f"source/output spans ({reason})"
118
96
  )
119
97
  return None
120
98
 
@@ -124,59 +102,31 @@ def _load_cut_output_spans_for_brief(work_dir, *, required=False):
124
102
  validated_path = work_dir / "clip_plan_validated.json"
125
103
  if not validated_path.exists():
126
104
  return fail("missing clip_plan_validated.json")
127
- try:
128
- plan = json.loads(validated_path.read_text(encoding="utf-8"))
129
- except (json.JSONDecodeError, OSError):
130
- return fail("invalid clip_plan_validated.json")
131
- if not isinstance(plan, dict):
132
- return fail("clip_plan_validated.json is not an object")
105
+ plan = json.loads(validated_path.read_text(encoding="utf-8"))
133
106
  raw_path = work_dir / "clip_plan.json"
134
- if raw_path.exists():
135
- try:
136
- raw_plan = json.loads(raw_path.read_text(encoding="utf-8"))
137
- except (json.JSONDecodeError, OSError):
138
- return fail("invalid clip_plan.json")
139
- if plan.get("raw_plan_fingerprint") != stable_hash(raw_plan):
140
- return fail("stale clip_plan_validated.json")
141
- clips = plan.get("clips")
142
- if not isinstance(clips, list):
143
- return fail("clips is not a list")
107
+ if raw_path.exists() and plan["raw_plan_fingerprint"] != stable_hash(
108
+ json.loads(raw_path.read_text(encoding="utf-8"))
109
+ ):
110
+ return fail("stale clip_plan_validated.json")
144
111
  spans = []
145
- for clip in clips:
146
- if not isinstance(clip, dict):
147
- continue
148
- if not all(
149
- key in clip
112
+ for clip in plan["clips"]:
113
+ span = {
114
+ key: clip[key]
150
115
  for key in ("source_start", "source_end", "output_start", "output_end")
151
- ):
152
- return fail("clip missing source/output span fields")
153
- try:
154
- ss = float(clip["source_start"])
155
- se = float(clip["source_end"])
156
- os_ = float(clip["output_start"])
157
- oe = float(clip["output_end"])
158
- except (TypeError, ValueError):
159
- return fail("non-numeric clip span")
160
- if not all(math.isfinite(x) for x in (ss, se, os_, oe)):
116
+ }
117
+ if not all(math.isfinite(value) for value in span.values()):
161
118
  return fail("non-finite clip span")
162
- if se <= ss or oe <= os_:
119
+ if span["source_end"] <= span["source_start"] or span["output_end"] <= span["output_start"]:
163
120
  return fail("non-positive clip span")
164
- spans.append(
165
- {
166
- "source_start": ss,
167
- "source_end": se,
168
- "output_start": os_,
169
- "output_end": oe,
170
- }
171
- )
172
- return spans or fail("no valid clips")
121
+ spans.append(span)
122
+ return spans or fail("no clips")
173
123
 
174
124
 
175
125
  def _source_output_overlaps_for_brief(start, end, spans):
176
126
  overlaps = []
177
- for span in spans or []:
178
- source_start = max(float(start), span["source_start"])
179
- source_end = min(float(end), span["source_end"])
127
+ for span in spans:
128
+ source_start = max(start, span["source_start"])
129
+ source_end = min(end, span["source_end"])
180
130
  if source_end <= source_start:
181
131
  continue
182
132
  output_start = span["output_start"] + (source_start - span["source_start"])
@@ -193,14 +143,9 @@ def _source_output_overlaps_for_brief(start, end, spans):
193
143
 
194
144
 
195
145
  def _remap_frame_facts_for_brief(frame_facts, overlap):
196
- if not isinstance(frame_facts, dict):
197
- return frame_facts
198
146
  out = {}
199
147
  for raw_ts, vals in frame_facts.items():
200
- try:
201
- ts = float(raw_ts)
202
- except (TypeError, ValueError):
203
- continue
148
+ ts = float(raw_ts)
204
149
  if not (overlap["source_start"] <= ts <= overlap["source_end"]):
205
150
  continue
206
151
  out_ts = overlap["output_start"] + (ts - overlap["source_start"])
@@ -209,54 +154,34 @@ def _remap_frame_facts_for_brief(frame_facts, overlap):
209
154
 
210
155
 
211
156
  def _remap_scenes_to_output_for_brief(scenes, spans):
212
- if not spans:
213
- return scenes or []
214
157
  out = []
215
- for scene in scenes or []:
216
- if not isinstance(scene, dict):
217
- continue
218
- try:
219
- start = float(scene.get("start", 0))
220
- end = float(scene.get("end", start))
221
- except (TypeError, ValueError):
222
- continue
223
- overlaps = _source_output_overlaps_for_brief(start, end, spans)
158
+ for scene in scenes:
159
+ overlaps = _source_output_overlaps_for_brief(scene["start"], scene["end"], spans)
224
160
  for part_idx, overlap in enumerate(overlaps):
225
161
  item = dict(scene)
226
162
  item["start"] = round(overlap["output_start"], 3)
227
163
  item["end"] = round(overlap["output_end"], 3)
228
164
  item["frame_facts"] = _remap_frame_facts_for_brief(
229
- scene.get("frame_facts"), overlap
165
+ scene.get("frame_facts", {}), overlap
230
166
  )
231
167
  if len(overlaps) > 1:
232
- item["scene_id"] = f"{scene.get('scene_id', '?')}.{part_idx}"
168
+ item["scene_id"] = f"{scene['scene_id']}.{part_idx}"
233
169
  out.append(item)
234
- out.sort(key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0))))
170
+ out.sort(key=lambda x: (x["start"], x["end"]))
235
171
  return out
236
172
 
237
173
 
238
174
  def _remap_segments_to_output_for_brief(segments, spans):
239
- if not spans:
240
- return segments or []
241
175
  out = []
242
- for seg in segments or []:
243
- if not isinstance(seg, dict):
244
- continue
245
- try:
246
- start = float(seg.get("start", 0))
247
- end = float(seg.get("end", start))
248
- except (TypeError, ValueError):
249
- continue
250
- if end <= start:
251
- continue
252
- for overlap in _source_output_overlaps_for_brief(start, end, spans):
176
+ for seg in segments:
177
+ for overlap in _source_output_overlaps_for_brief(seg["start"], seg["end"], spans):
253
178
  item = dict(seg)
254
179
  item["start"] = round(overlap["output_start"], 3)
255
180
  item["end"] = round(overlap["output_end"], 3)
256
181
  if "duration" in item:
257
182
  item["duration"] = round(item["end"] - item["start"], 3)
258
183
  out.append(item)
259
- out.sort(key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0))))
184
+ out.sort(key=lambda x: (x["start"], x["end"]))
260
185
  return out
261
186
 
262
187
 
@@ -273,26 +198,27 @@ def _remap_brief_evidence_to_output_timeline(
273
198
  )
274
199
 
275
200
 
201
+ def _read_json(path, default):
202
+ """JSON artifact, or `default` when the optional file was never written."""
203
+ return json.loads(path.read_text(encoding="utf-8")) if path.exists() else default
204
+
205
+
276
206
  def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
277
207
  """Load sentence anchors and remap them to cut OUTPUT time when needed."""
278
208
  work_dir = Path(work_dir)
279
- source_path = work_dir / "speech_boundary_anchors.json"
280
- try:
281
- payload = json.loads(source_path.read_text(encoding="utf-8"))
282
- except (OSError, json.JSONDecodeError):
283
- payload = {}
284
- anchors = payload.get("sentence_anchors", []) if isinstance(payload, dict) else []
285
- anchors = [dict(item) for item in anchors if isinstance(item, dict)]
209
+ anchors = [
210
+ dict(item)
211
+ for item in _read_json(
212
+ work_dir / "speech_boundary_anchors.json", {"sentence_anchors": []}
213
+ )["sentence_anchors"]
214
+ ]
286
215
  if edit_mode != "cut" or not (work_dir / "edited_source.mp4").exists():
287
216
  return anchors
288
217
 
289
218
  spans = _load_cut_output_spans_for_brief(work_dir, required=True)
290
219
  remapped = []
291
220
  for anchor in anchors:
292
- try:
293
- source_time = float(anchor.get("time"))
294
- except (TypeError, ValueError):
295
- continue
221
+ source_time = anchor["time"]
296
222
  for span in spans:
297
223
  if span["source_start"] - 0.05 <= source_time <= span["source_end"] + 0.05:
298
224
  item = dict(anchor)
@@ -300,16 +226,10 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
300
226
  item["time"] = round(
301
227
  span["output_start"] + source_time - span["source_start"], 3
302
228
  )
303
- try:
304
- source_pause_start = float(
305
- anchor.get("pause_start", source_time - 0.12)
306
- )
307
- except (TypeError, ValueError):
308
- source_pause_start = source_time - 0.12
309
- # Preserve the measured safe pause in OUTPUT time too. Leaving pause_start
310
- # in SOURCE time made cut-mode lint compare two different clocks.
229
+ # Preserve the measured safe pause in OUTPUT time too, so cut-mode lint
230
+ # compares one clock.
311
231
  source_pause_start = max(
312
- span["source_start"], min(source_pause_start, source_time)
232
+ span["source_start"], min(anchor.get("pause_start", source_time - 0.12), source_time)
313
233
  )
314
234
  item["source_pause_start"] = round(source_pause_start, 3)
315
235
  item["pause_start"] = round(
@@ -317,34 +237,14 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
317
237
  )
318
238
  remapped.append(item)
319
239
 
320
- speech_rows = []
321
- for name in ("asr_result.json", "asr_clean.json"):
322
- try:
323
- raw = json.loads((work_dir / name).read_text(encoding="utf-8"))
324
- except (OSError, json.JSONDecodeError):
325
- continue
326
- if isinstance(raw, dict):
327
- raw = raw.get("segments", [])
328
- if isinstance(raw, list):
329
- candidate = [
330
- item
331
- for item in raw
332
- if isinstance(item, dict) and str(item.get("text") or "").strip()
333
- ]
334
- if candidate:
335
- speech_rows = candidate
336
- break
337
- try:
338
- raw_quiet = json.loads(
339
- (work_dir / "silence_periods.json").read_text(encoding="utf-8")
340
- )
341
- except (OSError, json.JSONDecodeError):
342
- raw_quiet = []
240
+ speech_rows = [
241
+ row for row in _read_json(work_dir / "asr_result.json", []) if row["text"]
242
+ ]
343
243
  quiet_rows = [
344
- item
345
- for item in raw_quiet
346
- if isinstance(item, dict) and not bool(item.get("has_speech", False))
347
- ] if isinstance(raw_quiet, list) else []
244
+ row
245
+ for row in _read_json(work_dir / "silence_periods.json", [])
246
+ if not row["has_speech"]
247
+ ]
348
248
  out_payload = {
349
249
  "schema_version": 2,
350
250
  "artifact": "speech_boundary_anchors_output.json",
@@ -353,7 +253,7 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
353
253
  "clip_plan_fingerprint": stable_hash(
354
254
  json.loads((work_dir / "clip_plan_validated.json").read_text(encoding="utf-8"))
355
255
  ),
356
- "sentence_anchors": sorted(remapped, key=lambda item: float(item["time"])),
256
+ "sentence_anchors": sorted(remapped, key=lambda item: item["time"]),
357
257
  "speech_spans": _remap_segments_to_output_for_brief(speech_rows, spans),
358
258
  "quiet_windows": _remap_segments_to_output_for_brief(quiet_rows, spans),
359
259
  }
@@ -364,15 +264,11 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
364
264
 
365
265
 
366
266
  def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
367
- anchors = []
368
- for anchor in _sentence_entry_anchors_for_brief(work_dir, edit_mode):
369
- if anchor.get("confidence") not in {"high", "medium"}:
370
- continue
371
- try:
372
- when = float(anchor.get("time"))
373
- except (TypeError, ValueError):
374
- continue
375
- anchors.append((when, anchor))
267
+ anchors = [
268
+ anchor
269
+ for anchor in _sentence_entry_anchors_for_brief(work_dir, edit_mode)
270
+ if anchor["confidence"] in {"high", "medium"}
271
+ ]
376
272
  if not anchors:
377
273
  return []
378
274
  lines = [
@@ -384,13 +280,14 @@ def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
384
280
  '- 原声句子完整性是硬约束:切入块写 `"source_entry_policy": "sentence_boundary"`;'
385
281
  "不存在 `intentional_interrupt` 绕过方式。没有后续可靠句末锚点时,移动、缩短或删除旁白块。",
386
282
  ]
387
- for when, anchor in anchors:
388
- tail = str(anchor.get("text_tail") or "").strip()
389
- confidence = anchor.get("confidence", "unknown")
390
- source_suffix = ""
391
- if "source_time" in anchor:
392
- source_suffix = f" (SOURCE {float(anchor['source_time']):.2f}s)"
393
- lines.append(f"- {when:.2f}s [{confidence}]{source_suffix} {tail}")
283
+ for anchor in anchors:
284
+ source_suffix = (
285
+ f" (SOURCE {anchor['source_time']:.2f}s)" if "source_time" in anchor else ""
286
+ )
287
+ text_tail = str(anchor.get("text_tail", "")).strip()
288
+ lines.append(
289
+ f"- {anchor['time']:.2f}s [{anchor['confidence']}]{source_suffix} {text_tail}".rstrip()
290
+ )
394
291
  lines.append("")
395
292
  return lines
396
293
 
@@ -401,25 +298,13 @@ def _format_output_clip_list(work_dir):
401
298
  path = Path(work_dir) / "clip_plan_validated.json"
402
299
  if not path.exists():
403
300
  return []
404
- try:
405
- plan = json.loads(path.read_text(encoding="utf-8"))
406
- except (ValueError, OSError):
407
- return []
408
- clips = (plan.get("clips") if isinstance(plan, dict) else plan) or []
301
+ plan = json.loads(path.read_text(encoding="utf-8"))
409
302
  out = ["## Kept clips on the OUTPUT timeline", ""]
410
- for c in clips:
411
- if not isinstance(c, dict):
412
- continue
413
- try:
414
- os_, oe = float(c.get("output_start")), float(c.get("output_end"))
415
- ss, se = float(c.get("source_start")), float(c.get("source_end"))
416
- except (TypeError, ValueError):
417
- continue
418
- reason = str(c.get("reason", "")).strip()
419
- source_id = c.get("source_id", c.get("source", "0"))
420
- clip_id = c.get("id", c.get("clip_id", "?"))
303
+ for c in plan["clips"]:
304
+ reason = c.get("reason", "")
421
305
  out.append(
422
- f"- OUTPUT {os_:.1f}–{oe:.1f}s ← SOURCE[{source_id}] {ss:.1f}–{se:.1f}s (clip_id={clip_id})"
306
+ f"- OUTPUT {c['output_start']:.1f}–{c['output_end']:.1f}s ← SOURCE[{c.get('source_id', '0')}] "
307
+ f"{c['source_start']:.1f}–{c['source_end']:.1f}s (clip_id={c.get('clip_id', '?')})"
423
308
  + (f" — {reason}" if reason else "")
424
309
  )
425
310
  out.append("")
@@ -37,10 +37,11 @@ CONNECTIVE_MARKERS = ["但", "却", "而", "于是", "随后", "直到", "结果
37
37
 
38
38
 
39
39
  def _sentence_pieces(text: str) -> list[str]:
40
- text = re.sub(r"\s+", " ", str(text or "")).strip()
40
+ """Split text into sentence-like pieces while keeping terminal punctuation."""
41
+ text = re.sub(r"\s+", " ", text).strip()
41
42
  if not text:
42
43
  return []
43
- parts = re.split(r"([。!?!?;;\.])", text)
44
+ parts = re.split(r"([。!?!?;;.])", text)
44
45
  out: list[str] = []
45
46
  for idx in range(0, len(parts), 2):
46
47
  body = parts[idx].strip()
@@ -51,76 +52,50 @@ def _sentence_pieces(text: str) -> list[str]:
51
52
 
52
53
 
53
54
  def _text_units(text: str) -> int:
54
- text = str(text or "")
55
+ """Length unit for prose: CJK characters, otherwise words."""
55
56
  if re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text):
56
57
  return len(re.sub(r"\s+", "", text))
57
58
  return len(re.findall(r"\b\w+\b", text))
58
59
 
59
60
 
60
- def _normalise_segments(payload: Any, *, source: str) -> list[dict[str, Any]]:
61
- if isinstance(payload, str):
62
- return [{"source": source, "index": None, "text": payload}]
63
- if not isinstance(payload, list):
64
- return []
61
+ def _normalise_segments(payload: list[dict[str, Any]], *, source: str) -> list[dict[str, Any]]:
62
+ key = "narration" if source == "narration" else "text"
65
63
  segments: list[dict[str, Any]] = []
66
64
  for idx, item in enumerate(payload):
67
- if isinstance(item, dict):
68
- key = "narration" if source == "narration" else "text"
69
- text = str(item.get(key, item.get("text", item.get("narration", ""))) or "").strip()
70
- else:
71
- text = str(item or "").strip()
65
+ text = str(item.get(key, "")).strip()
72
66
  if text:
73
67
  segments.append({"source": source, "index": idx, "text": text})
74
68
  return segments
75
69
 
76
70
 
77
- def _load_json(path: Path) -> tuple[Any, str | None]:
78
- try:
79
- return json.loads(path.read_text(encoding="utf-8")), None
80
- except Exception as exc: # intentionally broad: malformed artifacts are QC findings
81
- return None, str(exc)
82
-
83
-
84
71
  def _load_original_subtitles(work_dir: Path | None) -> list[dict[str, Any]]:
85
72
  if work_dir is None:
86
73
  return []
87
74
  path = work_dir / "original_subtitles.json"
88
75
  if not path.exists():
89
76
  return []
90
- data, err = _load_json(path)
91
- if err:
92
- return []
93
- return _normalise_segments(data, source="original_subtitles")
77
+ return _normalise_segments(json.loads(path.read_text(encoding="utf-8")), source="original_subtitles")
94
78
 
95
79
 
96
80
  def _style_card_requirement(work_dir: Path | None) -> tuple[bool, str]:
97
- """Read the explicit stable requirements contract.
98
-
99
- Legacy/migration workspaces that do not have a valid requirements file are
100
- advisory-only: they must not be hard-gated by prompt text in
101
- agent_narration_brief.md.
102
- """
81
+ """Read the explicit requirements contract; workspaces without one are advisory-only."""
103
82
  if work_dir is None:
104
83
  return False, "legacy_default"
105
84
  path = work_dir / "deslop_qc_requirements.json"
106
85
  if not path.exists():
107
86
  return False, "legacy_default"
108
- data, err = _load_json(path)
109
- if err is not None or not isinstance(data, dict):
110
- return False, "legacy_default"
111
- required = data.get("style_card_required")
112
- if not isinstance(required, bool):
113
- return False, "legacy_default"
114
- return required, "deslop_qc_requirements.json"
87
+ data = json.loads(path.read_text(encoding="utf-8"))
88
+ return data["style_card_required"], "deslop_qc_requirements.json"
115
89
 
116
90
 
117
91
  def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] | None:
118
92
  if work_dir is None:
119
93
  return None
94
+ severity = "blocker" if required else "advisory"
120
95
  path = work_dir / "style_card.json"
121
96
  if not path.exists():
122
97
  return {
123
- "severity": "blocker" if required else "advisory",
98
+ "severity": severity,
124
99
  "code": "missing_style_card",
125
100
  "source": "style_card",
126
101
  "index": None,
@@ -130,19 +105,18 @@ def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] |
130
105
  "style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it"
131
106
  ),
132
107
  }
133
- data, err = _load_json(path)
134
- if err is not None or not isinstance(data, dict) or not data:
108
+ data = json.loads(path.read_text(encoding="utf-8"))
109
+ if not isinstance(data, dict) or not data:
135
110
  return {
136
- "severity": "blocker" if required else "advisory",
111
+ "severity": severity,
137
112
  "code": "malformed_style_card",
138
113
  "source": "style_card",
139
114
  "index": None,
140
115
  "message": (
141
- "style_card.json is required but missing, malformed, empty, or not a JSON object"
116
+ "style_card.json is required but empty or not a JSON object"
142
117
  if required else
143
- "style_card.json is present but malformed/empty; treat as migration warning unless the run requires it"
118
+ "style_card.json is present but empty or not a JSON object; treat as migration warning unless the run requires it"
144
119
  ),
145
- "detail": err,
146
120
  }
147
121
  return None
148
122
 
@@ -153,7 +127,7 @@ def _add_issue(bucket: list[dict[str, Any]], severity: str, code: str, source: s
153
127
  bucket.append(issue)
154
128
 
155
129
 
156
- def analyze_deslop_qc(narration: Any, *, work_dir: str | Path | None = None, original_subtitles: Any = None) -> dict[str, Any]:
130
+ def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path | None = None, original_subtitles: list[dict[str, Any]] | None = None) -> dict[str, Any]:
157
131
  """Return a structured report; never mutates or rewrites input text."""
158
132
  work_path = Path(work_dir) if work_dir is not None else None
159
133
  segments = _normalise_segments(narration, source="narration")
@@ -206,7 +180,7 @@ def analyze_deslop_qc(narration: Any, *, work_dir: str | Path | None = None, ori
206
180
 
207
181
  long_segments = [seg for seg in segments if seg["source"] == "narration" and _text_units(seg["text"]) > 180]
208
182
  if long_segments:
209
- _add_issue(advisories, "advisory", "overlong_narration_block", "narration", long_segments[0].get("index"), "单个解说块过长,建议拆成更可听的段落", max_units=max(_text_units(seg["text"]) for seg in long_segments))
183
+ _add_issue(advisories, "advisory", "overlong_narration_block", "narration", long_segments[0]["index"], "单个解说块过长,建议拆成更可听的段落", max_units=max(_text_units(seg["text"]) for seg in long_segments))
210
184
  long_sentences = [s for s in sentences if _text_units(s) > 90]
211
185
  if long_sentences:
212
186
  _add_issue(advisories, "advisory", "long_paragraph", "narration", None, "长句/长段落偏多,字幕阅读压力较大", max_units=max(_text_units(s) for s in long_sentences))