@oh-story/dsh 0.1.8 → 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 +11 -11
  2. package/lib/client.js +35 -14
  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,4 +1,5 @@
1
1
  """Self-contained utilities for the video-cut skill (no cross-skill imports)."""
2
+ import math
2
3
  import os
3
4
  import subprocess
4
5
 
@@ -12,20 +13,27 @@ def env_bool(name, default):
12
13
  val = os.environ.get(name)
13
14
  if val is None:
14
15
  return default
15
- return val.strip().lower() in ("1", "true", "yes")
16
+ text = val.strip().lower()
17
+ if text in ("1", "true", "yes"):
18
+ return True
19
+ if text in ("0", "false", "no"):
20
+ return False
21
+ raise ValueError(f"{name} must be 1/true/yes or 0/false/no, got {val!r}")
16
22
 
17
23
 
18
24
  def env_float(name, default, min_val=None):
19
- """Read an env var as a float, with an optional minimum clamp."""
25
+ """Read an env var as a float, rejecting malformed or below-minimum values."""
20
26
  val = os.environ.get(name)
21
27
  if val is None:
22
28
  return default
23
29
  try:
24
- result = float(val.strip())
25
- except (ValueError, AttributeError):
26
- return default
27
- if min_val is not None:
28
- result = max(min_val, result)
30
+ result = float(val)
31
+ except ValueError as exc:
32
+ raise ValueError(f"{name} must be a number, got {val!r}") from exc
33
+ if not math.isfinite(result):
34
+ raise ValueError(f"{name} must be finite, got {val!r}")
35
+ if min_val is not None and result < min_val:
36
+ raise ValueError(f"{name} must be >= {min_val}, got {val!r}")
29
37
  return result
30
38
 
31
39
 
@@ -49,29 +57,19 @@ CONFIG = {
49
57
 
50
58
 
51
59
  def run_cmd(cmd, **kwargs):
52
- """Run a command and return the CompletedProcess (stdout/stderr captured)."""
53
- if isinstance(cmd, list):
54
- display_parts = []
55
- for part in cmd:
56
- text = str(part)
57
- display_parts.append(text if len(text) <= 240 else text[:237] + "...")
58
- display = " ".join(display_parts)
59
- else:
60
- display = str(cmd)
61
- if len(display) > 2000:
62
- display = display[:1997] + "..."
60
+ """Run a command list and return the CompletedProcess (stdout/stderr captured)."""
61
+ display = " ".join(
62
+ str(part) if len(str(part)) <= 240 else str(part)[:237] + "..." for part in cmd
63
+ )
63
64
  log(f"运行: {display}")
64
65
  return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
65
66
 
66
67
 
67
68
  def get_video_duration(video_path):
68
- """Return media duration in seconds via ffprobe, or 0.0 on failure."""
69
- cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
69
+ """Return media duration in seconds via ffprobe."""
70
+ cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration",
70
71
  "-of", "csv=p=0", str(video_path)]
71
72
  result = run_cmd(cmd)
72
73
  if result.returncode != 0:
73
- return 0.0
74
- try:
75
- return float(result.stdout.strip())
76
- except (TypeError, ValueError):
77
- return 0.0
74
+ raise RuntimeError(f"ffprobe 无法读取时长: {video_path}: {result.stderr.strip()}")
75
+ return float(result.stdout.strip())
@@ -2,7 +2,6 @@
2
2
 
3
3
  import json
4
4
 
5
-
6
5
  from lib import run_cmd
7
6
 
8
7
 
@@ -24,71 +23,42 @@ def _has_audio_stream(video_path):
24
23
 
25
24
 
26
25
  class VideoGeometry(tuple):
27
- """Tuple-compatible geometry with probe facts attached for QC callers."""
26
+ """Tuple-compatible (width, height, fps) with probe facts attached for QC callers."""
28
27
 
29
- def __new__(cls, width, height, fps, facts=None):
28
+ def __new__(cls, width, height, fps, facts):
30
29
  obj = super().__new__(cls, (width, height, fps))
31
- obj.facts = facts or {}
30
+ obj.facts = facts
32
31
  return obj
33
32
 
34
33
 
35
34
  def _parse_ratio(value):
35
+ """ffprobe aspect ratios are 'N:D' (or 'N/D'); '0:1' and 'N/A' mean unknown."""
36
36
  if value in (None, "", "0:1", "0/1", "N/A"):
37
37
  return None
38
- text = str(value)
39
- sep = ":" if ":" in text else "/" if "/" in text else None
40
- if not sep:
41
- try:
42
- ratio = float(text)
43
- return ratio if ratio > 0 else None
44
- except ValueError:
45
- return None
46
- left, _, right = text.partition(sep)
47
- try:
48
- num, den = float(left), float(right)
49
- except ValueError:
50
- return None
38
+ left, _, right = str(value).replace("/", ":").partition(":")
39
+ num, den = float(left), float(right)
51
40
  return num / den if den > 0 and num > 0 else None
52
41
 
53
42
 
54
43
  def _stream_rotation(stream):
55
- candidates = []
56
- tags = stream.get("tags") if isinstance(stream.get("tags"), dict) else {}
44
+ """Rotation from the legacy `rotate` tag or the display-matrix side data, else 0."""
45
+ tags = stream.get("tags", {})
57
46
  if "rotate" in tags:
58
- candidates.append(tags.get("rotate"))
59
- for side_data in stream.get("side_data_list") or []:
60
- if isinstance(side_data, dict):
61
- candidates.append(side_data.get("rotation"))
62
- for value in candidates:
63
- try:
64
- return int(round(float(value))) % 360
65
- except (TypeError, ValueError):
66
- continue
47
+ return int(round(float(tags["rotate"]))) % 360
48
+ for side_data in stream.get("side_data_list", []):
49
+ if "rotation" in side_data:
50
+ return int(round(float(side_data["rotation"]))) % 360
67
51
  return 0
68
52
 
69
53
 
70
54
  def _fps_from_rate(rate):
71
- if not rate or "/" not in str(rate):
72
- return 0.0
73
- num, _, den = str(rate).partition("/")
74
- try:
75
- num_f, den_f = float(num), float(den)
76
- return num_f / den_f if den_f > 0 else 0.0
77
- except ValueError:
78
- return 0.0
79
-
55
+ """ffprobe frame rates are 'N/D' fractions; '0/0' means unknown."""
56
+ num, _, den = rate.partition("/")
57
+ return float(num) / float(den) if float(den) > 0 else 0.0
80
58
 
81
- def _geometry_from_stream(stream, *, fallback=False):
82
- coded_width = coded_height = 0
83
- try:
84
- coded_width = int(float(stream.get("width") or 0))
85
- coded_height = int(float(stream.get("height") or 0))
86
- except (TypeError, ValueError):
87
- coded_width = coded_height = 0
88
- if coded_width <= 0 or coded_height <= 0:
89
- coded_width, coded_height = 1280, 720
90
- fallback = True
91
59
 
60
+ def _geometry_from_stream(stream):
61
+ coded_width, coded_height = stream["width"], stream["height"]
92
62
  parsed_sar = _parse_ratio(stream.get("sample_aspect_ratio"))
93
63
  dar = _parse_ratio(stream.get("display_aspect_ratio"))
94
64
  rotation = _stream_rotation(stream)
@@ -110,8 +80,8 @@ def _geometry_from_stream(stream, *, fallback=False):
110
80
  display_width, display_height = display_height, display_width
111
81
 
112
82
  width, height = _clamp_even_geometry(round(display_width), round(display_height))
113
- fps = _fps_from_rate(stream.get("r_frame_rate")) or _fps_from_rate(
114
- stream.get("avg_frame_rate")
83
+ fps = _fps_from_rate(stream["r_frame_rate"]) or _fps_from_rate(
84
+ stream.get("avg_frame_rate", "0/0")
115
85
  )
116
86
  if not 0 < fps <= 120:
117
87
  fps = 30.0
@@ -121,27 +91,25 @@ def _geometry_from_stream(stream, *, fallback=False):
121
91
  "width": width,
122
92
  "height": height,
123
93
  "fps": round(fps, 3),
124
- "sample_aspect_ratio": stream.get("sample_aspect_ratio") or "1:1",
125
- "sample_aspect_ratio_float": round(float(sar), 6),
94
+ "sample_aspect_ratio": stream.get("sample_aspect_ratio", "1:1"),
95
+ "sample_aspect_ratio_float": round(sar, 6),
126
96
  "display_aspect_ratio": stream.get("display_aspect_ratio"),
127
- "display_aspect_ratio_float": round(float(dar or 0.0), 6),
97
+ "display_aspect_ratio_float": round(dar or 0.0, 6),
128
98
  "display_aspect_source": aspect_source,
129
99
  "display_width": width,
130
100
  "display_height": height,
131
101
  "rotation": rotation,
132
102
  "rotation_swaps_axes": rotation_swaps_axes,
133
- "fallback": bool(fallback),
134
103
  }
135
104
  return VideoGeometry(width, height, round(fps, 3), facts)
136
105
 
137
106
 
138
107
  def _probe_video_geometry(video_path):
139
- """Best-effort iterable (width, height, fps), rotation/SAR/DAR-aware.
108
+ """Display geometry (width, height, fps) of the first video stream, rotation/SAR/DAR-aware.
140
109
 
141
- Returned value unpacks like the historical 3-tuple while exposing `.facts`
142
- for QC. Used to normalize heterogeneous multi-source segments to one
143
- square-pixel geometry before concat (ffmpeg's concat filter rejects
144
- mismatched width/height/SAR/pixel-format/fps).
110
+ Unpacks like a 3-tuple while exposing `.facts` for QC. Used to normalize heterogeneous
111
+ multi-source segments to one square-pixel geometry before concat (ffmpeg's concat filter
112
+ rejects mismatched width/height/SAR/pixel-format/fps).
145
113
  """
146
114
  cmd = [
147
115
  "ffprobe",
@@ -155,38 +123,13 @@ def _probe_video_geometry(video_path):
155
123
  "json",
156
124
  str(video_path),
157
125
  ]
158
- try:
159
- result = run_cmd(cmd)
160
- except Exception: # noqa: BLE001 - probing is best-effort; fall back to defaults
161
- result = None
162
- if (
163
- result is not None
164
- and getattr(result, "returncode", 1) == 0
165
- and (result.stdout or "").strip()
166
- ):
167
- try:
168
- payload = json.loads(result.stdout)
169
- streams = payload.get("streams") or []
170
- if streams:
171
- return _geometry_from_stream(streams[0])
172
- except (AttributeError, TypeError, ValueError):
173
- pass
174
-
175
- # Backward-compatible fallback for tests/mocks that still return CSV output.
176
- stream = {}
177
- if result is not None and (result.stdout or "").strip():
178
- parts = result.stdout.strip().splitlines()[0].split(",")
179
- if len(parts) >= 2:
180
- stream["width"], stream["height"] = parts[0], parts[1]
181
- if len(parts) >= 3:
182
- stream["r_frame_rate"] = parts[2]
183
- if len(parts) >= 4 and parts[3] not in ("", "N/A"):
184
- stream["tags"] = {"rotate": parts[3]}
185
- if len(parts) >= 5 and parts[4] not in ("", "N/A"):
186
- stream["sample_aspect_ratio"] = parts[4]
187
- if len(parts) >= 6 and parts[5] not in ("", "N/A"):
188
- stream["display_aspect_ratio"] = parts[5]
189
- return _geometry_from_stream(stream, fallback=True)
126
+ result = run_cmd(cmd)
127
+ if result.returncode != 0:
128
+ raise RuntimeError(f"ffprobe 无法读取视频几何信息: {video_path}: {result.stderr.strip()}")
129
+ streams = json.loads(result.stdout).get("streams", [])
130
+ if not streams:
131
+ raise RuntimeError(f"没有视频流: {video_path}")
132
+ return _geometry_from_stream(streams[0])
190
133
 
191
134
 
192
135
  def _orientation(width, height):
@@ -198,91 +141,53 @@ def _orientation(width, height):
198
141
 
199
142
 
200
143
  def _fps_bucket(fps):
201
- if not fps or fps <= 0:
202
- return 30.0
203
144
  common = [23.976, 24.0, 25.0, 29.97, 30.0, 50.0, 59.94, 60.0]
204
- nearest = min(common, key=lambda x: abs(float(fps) - x))
205
- return nearest if abs(float(fps) - nearest) <= 0.15 else round(float(fps))
145
+ nearest = min(common, key=lambda x: abs(fps - x))
146
+ bucket = nearest if abs(fps - nearest) <= 0.15 else round(fps)
147
+ return max(1.0, min(60.0, bucket))
206
148
 
207
149
 
208
- def _clamp_even_geometry(width, height, max_height=None):
209
- width = max(2, int(width) - int(width) % 2)
210
- height = max(2, int(height) - int(height) % 2)
211
- max_height = int(max_height or 0)
212
- if max_height > 0 and height > max_height:
213
- scale = max_height / height
214
- height = max_height - max_height % 2
215
- width = max(2, int(width * scale))
216
- width -= width % 2
217
- return width, height
150
+ def _clamp_even_geometry(width, height):
151
+ return max(2, width - width % 2), max(2, height - height % 2)
218
152
 
219
153
 
220
- def _select_output_geometry(source_paths, clips, max_height=None):
154
+ def _select_output_geometry(source_paths, clips):
221
155
  """Deterministically select canvas/fps from all used sources, not just the first."""
222
156
  used = {}
223
- for clip in clips or []:
224
- path = str(clip.get("source_path") or "")
225
- if not path:
226
- continue
227
- used[path] = used.get(path, 0.0) + max(0.0, float(clip.get("duration") or 0.0))
228
- if not used:
229
- for path in source_paths or []:
230
- used[str(path)] = 0.0
157
+ for clip in clips:
158
+ if "source_path" in clip:
159
+ used[clip["source_path"]] = used.get(clip["source_path"], 0.0) + clip["duration"]
160
+ if not used: # single-source plans carry no per-clip source_path
161
+ used = {str(path): 0.0 for path in source_paths}
231
162
  rows = []
232
163
  for path in sorted(used):
233
164
  probed = _probe_video_geometry(path)
234
165
  width, height, fps = probed
235
- facts = dict(getattr(probed, "facts", {}) or {})
236
- facts.setdefault("width", width)
237
- facts.setdefault("height", height)
238
- facts.setdefault("fps", fps)
239
- facts.setdefault("rotation", 0)
240
- facts.setdefault("sample_aspect_ratio", "1:1")
166
+ facts = probed.facts
241
167
  rows.append(
242
168
  {
243
169
  "path": path,
244
170
  "source_id": next(
245
- (
246
- str(c.get("source_id"))
247
- for c in clips or []
248
- if str(c.get("source_path") or "") == path
249
- and c.get("source_id") is not None
250
- ),
251
- None,
171
+ (c["source_id"] for c in clips if c.get("source_path") == path), None
252
172
  ),
253
173
  "used_duration": round(used[path], 3),
254
174
  "width": width,
255
175
  "height": height,
256
- "coded_width": facts.get("coded_width", width),
257
- "coded_height": facts.get("coded_height", height),
258
- "display_width": facts.get("display_width", width),
259
- "display_height": facts.get("display_height", height),
176
+ "coded_width": facts["coded_width"],
177
+ "coded_height": facts["coded_height"],
178
+ "display_width": facts["display_width"],
179
+ "display_height": facts["display_height"],
260
180
  "area": width * height,
261
181
  "fps": fps,
262
- "fps_bucket": min(60.0, _fps_bucket(fps)),
182
+ "fps_bucket": _fps_bucket(fps),
263
183
  "orientation": _orientation(width, height),
264
- "rotation": facts.get("rotation", 0),
265
- "sample_aspect_ratio": facts.get("sample_aspect_ratio", "1:1"),
266
- "sample_aspect_ratio_float": facts.get(
267
- "sample_aspect_ratio_float", 1.0
268
- ),
269
- "display_aspect_ratio": facts.get("display_aspect_ratio"),
270
- "rotation_swaps_axes": bool(facts.get("rotation_swaps_axes", False)),
184
+ "rotation": facts["rotation"],
185
+ "sample_aspect_ratio": facts["sample_aspect_ratio"],
186
+ "sample_aspect_ratio_float": facts["sample_aspect_ratio_float"],
187
+ "display_aspect_ratio": facts["display_aspect_ratio"],
188
+ "rotation_swaps_axes": facts["rotation_swaps_axes"],
271
189
  }
272
190
  )
273
- if not rows:
274
- return (
275
- 1280,
276
- 720,
277
- 30.0,
278
- {
279
- "width": 1280,
280
- "height": 720,
281
- "fps": 30.0,
282
- "reason": "fallback_no_sources",
283
- "source_id": None,
284
- },
285
- )
286
191
 
287
192
  orientation_duration = {}
288
193
  for row in rows:
@@ -298,10 +203,9 @@ def _select_output_geometry(source_paths, clips, max_height=None):
298
203
  ),
299
204
  reverse=True,
300
205
  )[0][0]
301
- eligible = [r for r in rows if r["orientation"] == chosen_orientation] or rows
206
+ eligible = [r for r in rows if r["orientation"] == chosen_orientation]
302
207
  selected = sorted(
303
- eligible,
304
- key=lambda r: (-r["area"], str(r.get("source_id") or ""), r["path"]),
208
+ eligible, key=lambda r: (-r["area"], r["source_id"] or "", r["path"])
305
209
  )[0]
306
210
 
307
211
  fps_duration = {}
@@ -309,33 +213,26 @@ def _select_output_geometry(source_paths, clips, max_height=None):
309
213
  fps_duration[row["fps_bucket"]] = (
310
214
  fps_duration.get(row["fps_bucket"], 0.0) + row["used_duration"]
311
215
  )
312
- fps = sorted(fps_duration.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)[0][
313
- 0
314
- ]
315
- fps = max(1.0, min(60.0, float(fps or 30.0)))
216
+ fps = sorted(fps_duration.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)[0][0]
316
217
 
317
- width, height = _clamp_even_geometry(
318
- selected["width"], selected["height"], max_height=max_height
319
- )
218
+ width, height = selected["width"], selected["height"]
320
219
  reason = {
321
220
  "width": width,
322
221
  "height": height,
323
222
  "fps": round(fps, 3),
324
223
  "reason": "weighted_orientation_area_fps",
325
- "source_id": selected.get("source_id"),
224
+ "source_id": selected["source_id"],
326
225
  "source_path": selected["path"],
327
226
  "orientation": chosen_orientation,
328
- "orientation_used_duration": round(
329
- orientation_duration.get(chosen_orientation, 0.0), 3
330
- ),
331
- "fps_bucket_used_duration": round(fps_duration.get(fps, 0.0), 3),
332
- "rotation": selected.get("rotation", 0),
333
- "sample_aspect_ratio": selected.get("sample_aspect_ratio", "1:1"),
334
- "display_aspect_ratio": selected.get("display_aspect_ratio"),
335
- "coded_width": selected.get("coded_width"),
336
- "coded_height": selected.get("coded_height"),
337
- "display_width": selected.get("display_width"),
338
- "display_height": selected.get("display_height"),
227
+ "orientation_used_duration": round(orientation_duration[chosen_orientation], 3),
228
+ "fps_bucket_used_duration": round(fps_duration[fps], 3),
229
+ "rotation": selected["rotation"],
230
+ "sample_aspect_ratio": selected["sample_aspect_ratio"],
231
+ "display_aspect_ratio": selected["display_aspect_ratio"],
232
+ "coded_width": selected["coded_width"],
233
+ "coded_height": selected["coded_height"],
234
+ "display_width": selected["display_width"],
235
+ "display_height": selected["display_height"],
339
236
  "sources": rows,
340
237
  }
341
238
  return width, height, round(fps, 3), reason
@@ -4,51 +4,33 @@ from lib import log
4
4
 
5
5
 
6
6
  def source_time_to_output_time(source_time, clips):
7
- """Map a source timestamp into the post-concat output timeline."""
8
- ts = float(source_time)
7
+ """Map a source timestamp into the post-concat output timeline (None when cut away)."""
9
8
  for clip in clips:
10
9
  start = clip["source_start"]
11
10
  end = clip["source_end"]
12
- if start <= ts <= end:
13
- mapped = clip["output_start"] + (ts - start)
11
+ if start <= source_time <= end:
12
+ mapped = clip["output_start"] + (source_time - start)
14
13
  return round(max(clip["output_start"], min(mapped, clip["output_end"])), 3)
15
14
  return None
16
15
 
17
16
 
18
17
  def _clips_for_midpoint(start, end, clips):
19
- mid = (float(start) + float(end)) / 2
18
+ mid = (start + end) / 2
20
19
  return [clip for clip in clips if clip["source_start"] <= mid <= clip["source_end"]]
21
20
 
22
21
 
23
22
  def map_narration_to_clips(narration, validated_plan, min_duration=0.3):
24
23
  """Convert source-time narration segments to edited-output timeline segments."""
25
- clips = (
26
- validated_plan["clips"] if isinstance(validated_plan, dict) else validated_plan
27
- )
24
+ clips = validated_plan["clips"]
28
25
  mapped = []
29
- for raw in narration or []:
30
- if not isinstance(raw, dict):
31
- continue
32
- try:
33
- source_start = float(raw.get("start"))
34
- source_end = float(raw.get("end"))
35
- except (TypeError, ValueError):
36
- continue
37
- text = str(raw.get("narration", "")).strip()
38
- if source_end <= source_start or not text:
39
- continue
26
+ for raw in narration:
27
+ source_start = raw["start"]
28
+ source_end = raw["end"]
40
29
  if raw.get("source_clip_id") is not None:
41
- try:
42
- requested_clip_id = int(raw.get("source_clip_id"))
43
- except (TypeError, ValueError):
44
- requested_clip_id = None
45
- clip = next(
46
- (c for c in clips if c.get("clip_id") == requested_clip_id), None
47
- )
30
+ requested_clip_id = int(raw["source_clip_id"])
31
+ clip = next((c for c in clips if c["clip_id"] == requested_clip_id), None)
48
32
  if clip and not (
49
- clip["source_start"]
50
- <= ((source_start + source_end) / 2)
51
- <= clip["source_end"]
33
+ clip["source_start"] <= (source_start + source_end) / 2 <= clip["source_end"]
52
34
  ):
53
35
  clip = None
54
36
  else:
@@ -67,16 +49,12 @@ def map_narration_to_clips(narration, validated_plan, min_duration=0.3):
67
49
  if clipped_source_end - clipped_source_start < min_duration:
68
50
  log(f" 丢弃过短映射解说: {source_start:.1f}-{source_end:.1f}s")
69
51
  continue
70
- output_start = source_time_to_output_time(clipped_source_start, [clip])
71
- output_end = source_time_to_output_time(clipped_source_end, [clip])
72
- if output_start is None or output_end is None or output_end <= output_start:
73
- continue
74
52
  item = dict(raw)
75
53
  item["source_start"] = round(clipped_source_start, 3)
76
54
  item["source_end"] = round(clipped_source_end, 3)
77
55
  item["source_clip_id"] = clip["clip_id"]
78
- item["start"] = output_start
79
- item["end"] = output_end
56
+ item["start"] = source_time_to_output_time(clipped_source_start, [clip])
57
+ item["end"] = source_time_to_output_time(clipped_source_end, [clip])
80
58
  # Tag beats trimmed to a clip edge: their TEXT was written for a longer span and may
81
59
  # now describe footage that was cut away (a stale-text desync the lint surfaces).
82
60
  item["clamped"] = bool(
@@ -108,17 +86,15 @@ def lint_mapped_narration(
108
86
  enforces unless --allow-sparse-cut. Clamped beats are always blocking because their TTS
109
87
  sentence would be cut at a clip edge; sparse-cut intent never authorizes speech truncation.
110
88
  """
111
- mapped = sorted(mapped or [], key=lambda s: float(s.get("start", 0.0)))
89
+ mapped = sorted(mapped, key=lambda s: s["start"])
112
90
  mapped_count = len(mapped)
113
- original_count = int(original_count or 0)
114
91
  dropped = max(0, original_count - mapped_count)
115
92
  drop_ratio = dropped / original_count if original_count else 0.0
116
- out_dur = float(output_duration or 0.0)
117
- spm = mapped_count / (out_dur / 60) if out_dur > 0 else 0.0
118
- gaps = [float(b["start"]) - float(a["end"]) for a, b in zip(mapped, mapped[1:])]
93
+ spm = mapped_count / (output_duration / 60) if output_duration > 0 else 0.0
94
+ gaps = [b["start"] - a["end"] for a, b in zip(mapped, mapped[1:])]
119
95
  max_gap = max(gaps) if gaps else 0.0
120
- covered = sum(max(0.0, float(b["end"]) - float(b["start"])) for b in mapped)
121
- coverage = covered / out_dur if out_dur > 0 else 0.0
96
+ covered = sum(max(0.0, b["end"] - b["start"]) for b in mapped)
97
+ coverage = covered / output_duration if output_duration > 0 else 0.0
122
98
 
123
99
  warnings = []
124
100
  if drop_ratio >= drop_ratio_limit:
@@ -149,7 +125,7 @@ def lint_mapped_narration(
149
125
  "max_gap_limit_seconds": max_gap_seconds,
150
126
  }
151
127
  )
152
- clamped = [b for b in mapped if isinstance(b, dict) and b.get("clamped")]
128
+ clamped = [b for b in mapped if b["clamped"]]
153
129
  if clamped:
154
130
  warnings.append(
155
131
  {
@@ -168,7 +144,7 @@ def lint_mapped_narration(
168
144
  "mapped_count": mapped_count,
169
145
  "dropped": dropped,
170
146
  "drop_ratio": round(drop_ratio, 2),
171
- "output_duration": round(out_dur, 2),
147
+ "output_duration": round(output_duration, 2),
172
148
  "segments_per_minute": round(spm, 2),
173
149
  "max_gap_seconds": round(max_gap, 2),
174
150
  "coverage": round(coverage, 2),
@@ -180,12 +156,12 @@ def lint_mapped_narration(
180
156
 
181
157
  def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by=None):
182
158
  """Populate clip_plan_validated.json['qc'] as the single cut QC source."""
183
- qc = dict(plan.get("qc") or {})
184
- warnings = list(qc.get("warnings") or [])
185
- blocking = list(qc.get("blocking") or [])
186
- total = float(plan.get("total_duration") or 0.0)
187
- target = plan.get("target_duration")
188
- if target in (None, ""):
159
+ qc = dict(plan.get("qc", {}))
160
+ warnings = list(qc.get("warnings", []))
161
+ blocking = list(qc.get("blocking", []))
162
+ total = plan["total_duration"]
163
+ target = plan["target_duration"]
164
+ if target is None:
189
165
  target_status = "missing"
190
166
  target_qc = {
191
167
  "status": target_status,
@@ -193,7 +169,6 @@ def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by
193
169
  "total_duration": round(total, 3),
194
170
  }
195
171
  else:
196
- target = float(target)
197
172
  ratio = total / target if target > 0 else 0.0
198
173
  if ratio < 0.85:
199
174
  target_status = "under"
@@ -237,13 +212,13 @@ def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by
237
212
  qc["target_duration_status"] = target_status
238
213
  qc["target_duration"] = target_qc
239
214
  qc.setdefault("boundary_status", {})
240
- qc["clip_count"] = len(plan.get("clips") or [])
215
+ qc["clip_count"] = len(plan["clips"])
241
216
  qc["total_duration"] = round(total, 3)
242
217
  if warnings:
243
218
  qc["warnings"] = warnings
244
219
  if blocking:
245
220
  qc["blocking"] = blocking
246
- elif "blocking" in qc:
221
+ else:
247
222
  qc.pop("blocking", None)
248
223
  plan["qc"] = qc
249
224
  return plan