@oh-story/dsh 0.1.12 → 0.1.13

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 (264) hide show
  1. package/README.md +17 -11
  2. package/lib/client.js +85 -35
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +41 -36
  5. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
  6. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
  7. package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
  8. package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
  9. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
  10. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
  12. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
  13. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
  14. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
  15. package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
  16. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
  17. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
  18. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
  19. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
  20. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  21. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
  22. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
  23. package/lib/index.js +16 -7
  24. package/lib/index.js.map +2 -2
  25. package/lib/novel-to-game/README.md +98 -44
  26. package/lib/novel-to-game/README_ZH.md +100 -46
  27. package/lib/novel-to-game/VERSION +1 -1
  28. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
  29. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
  30. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
  31. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
  32. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
  33. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
  34. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
  35. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
  36. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
  37. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
  38. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
  39. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
  40. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
  41. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
  42. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
  43. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
  44. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
  45. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
  46. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
  57. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
  58. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
  59. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
  60. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
  61. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
  62. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
  63. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
  64. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
  65. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
  66. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
  91. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
  92. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
  93. package/lib/novel-to-game/manifest.json +303 -78
  94. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
  95. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
  96. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
  98. package/lib/types/client/drama-production-view.d.ts +11 -0
  99. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  100. package/lib/types/client/file-activity.d.ts +1 -1
  101. package/lib/types/client/file-activity.d.ts.map +1 -1
  102. package/lib/types/client/production-prompts.d.ts +7 -1
  103. package/lib/types/client/production-prompts.d.ts.map +1 -1
  104. package/lib/types/client/production-runtime.d.ts +7 -2
  105. package/lib/types/client/production-runtime.d.ts.map +1 -1
  106. package/lib/types/drama-adapters.d.ts.map +1 -1
  107. package/lib/types/skill-provider.d.ts.map +1 -1
  108. package/lib/types/video-project.d.ts.map +1 -1
  109. package/lib/video-recap/CHANGELOG.md +108 -1
  110. package/lib/video-recap/README.en.md +18 -14
  111. package/lib/video-recap/README.md +18 -14
  112. package/lib/video-recap/manifest.json +297 -321
  113. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  114. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  115. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  116. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  117. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  118. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  119. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  120. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  121. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  122. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  123. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  124. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  125. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  126. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  127. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  128. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  129. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  130. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  131. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  132. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  133. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  135. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  136. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  137. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  138. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  139. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  140. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  141. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  142. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  143. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  144. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  145. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  146. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  147. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  148. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  149. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  150. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  151. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  152. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  153. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  154. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  155. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  156. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  157. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  158. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  159. package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
  160. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  161. package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
  162. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  163. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  164. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  165. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  166. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  167. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  168. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  169. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
  170. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  171. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
  172. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  173. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  174. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  175. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  176. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  177. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  178. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  179. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  180. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  181. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  182. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  183. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  184. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  185. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  186. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  187. package/lib/video-recap/skills/video-script/SKILL.md +9 -7
  188. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  189. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  190. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  191. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  192. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  193. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  194. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  195. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  196. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  197. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  198. package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
  199. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  200. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  201. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  202. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  203. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  204. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  205. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  206. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  207. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  208. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  209. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  210. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  211. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  212. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  213. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  214. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  215. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  216. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  217. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  218. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  219. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  220. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  221. package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
  222. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  223. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  224. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
  225. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  226. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  227. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  228. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  229. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  230. package/package.json +45 -45
  231. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  232. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  233. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  234. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  235. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  239. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  240. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  241. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  242. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  243. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  244. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  245. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  246. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  247. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  248. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  249. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  250. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  251. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  252. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  253. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  254. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  255. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  258. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  259. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  260. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  262. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  263. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  264. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -1,15 +1,20 @@
1
- """Render edited source media and write delivery QC."""
2
-
3
- import json
4
-
1
+ """Render edited source media."""
5
2
 
3
+ from fractions import Fraction
6
4
  from pathlib import Path
7
5
 
6
+ from frame_grid import frame_count, parse_frame_rate
8
7
  from lib import CONFIG, filter_file_args, get_video_duration, log, run_cmd
9
8
 
10
9
  from cut_contract import _write_edited_source_meta
11
- from media_geometry import _has_audio_stream
12
- from sentence_boundaries import _continuous_source_join
10
+ from media_geometry import (
11
+ _color_tag_args,
12
+ _color_tag_filter,
13
+ _has_audio_stream,
14
+ _output_color_tags,
15
+ _probe_video_format,
16
+ )
17
+ from sentence_gate import _continuous_source_join
13
18
 
14
19
 
15
20
  def _audio_segment_filter(
@@ -18,7 +23,7 @@ def _audio_segment_filter(
18
23
  max_fade = duration / 2
19
24
  fade_in = max(0.0, min(fade_in_ms / 1000.0, max_fade))
20
25
  fade_out = max(0.0, min(fade_out_ms / 1000.0, max_fade))
21
- base = f"{label_in}atrim=start={start:.3f}:end={end:.3f},asetpts=PTS-STARTPTS"
26
+ base = f"{label_in}atrim=start={start:.6f}:end={end:.6f},asetpts=PTS-STARTPTS"
22
27
  if fade_in > 0:
23
28
  base += f",afade=t=in:st=0:d={fade_in:.3f}"
24
29
  if fade_out > 0:
@@ -43,89 +48,95 @@ def _clip_audio_edge_fades(clips, idx, fade_ms):
43
48
  return fade_in, fade_out
44
49
 
45
50
 
46
- def _probe_audio_sample_rate(video_path):
47
- """Sample rate of the first audio stream, or None when it cannot be observed.
51
+ def _video_segment_filter(label_in, label_out, start, end, frames, out_rate, source_rate, norm=""):
52
+ """Exactly `frames` frames at `out_rate` from source [start, end).
48
53
 
49
- Delivery QC is observational and runs even on a plan that was never rendered, so an
50
- ffprobe that is absent (OSError) reads the same as one that reports no audio stream.
54
+ The trim points sit half a source frame early so the frame ON a grid edge is selected
55
+ despite millisecond rounding or container pts jitter. fps resamples onto the output
56
+ grid; tpad clones the last frame without limit, covering a segment that came up short
57
+ (one frame from resampling, or many when the clip runs past the end of a video stream
58
+ shorter than its audio); and the final trim cuts to the exact count, so every segment
59
+ advances the concat by whole frames and edited_source.mp4 stays constant frame rate.
51
60
  """
52
- cmd = [
53
- "ffprobe",
54
- "-v",
55
- "error",
56
- "-select_streams",
57
- "a:0",
58
- "-show_entries",
59
- "stream=sample_rate",
60
- "-of",
61
- "csv=p=0",
62
- str(video_path),
63
- ]
61
+ half = 0.5 / source_rate if source_rate else 0.0
62
+ return (
63
+ f"{label_in}trim=start={max(0.0, start - half):.6f}:end={end - half:.6f},"
64
+ f"setpts=PTS-STARTPTS,{norm}fps={out_rate},tpad=stop_mode=clone:stop=-1,"
65
+ f"trim=end_frame={frames},setpts=PTS-STARTPTS{label_out}"
66
+ )
67
+
68
+
69
+ def _warn_on_frame_count_mismatch(path, expected):
70
+ """Log when the encoded video holds a different frame count than qc.frame_grid records.
71
+
72
+ The exact count rests on fps/tpad/trim behaviour verified on ffmpeg 8 and 9; an older
73
+ ffmpeg that behaves differently would otherwise leave a short or VFR file unnoticed.
74
+ A warning, not a block: final_qc owns blockers.
75
+ """
76
+ result = run_cmd([
77
+ "ffprobe", "-v", "error", "-select_streams", "v:0", "-count_packets",
78
+ "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", str(path),
79
+ ])
64
80
  try:
65
- result = run_cmd(cmd)
66
- except OSError:
67
- return None
68
- text = result.stdout.strip()
69
- if result.returncode != 0 or not text:
70
- return None
71
- return int(float(text))
81
+ rendered = int(str(result.stdout).strip())
82
+ except (AttributeError, ValueError):
83
+ return
84
+ if rendered != expected:
85
+ log(f"警告: {path.name} 渲染出 {rendered} 帧,"
86
+ f"qc.frame_grid.frame_count 记录的是 {expected} 帧;"
87
+ "当前 ffmpeg 的 fps/tpad/trim 行为可能不同,请检查帧率是否恒定")
72
88
 
73
89
 
74
- def _delivery_reencode_reason(source_paths, clips):
75
- reasons = ["trim_concat_filter_requires_reencode"]
76
- if len(source_paths) > 1:
77
- reasons.append("multi_source_geometry_audio_normalization")
78
- if any("source_path" not in clip for clip in clips):
79
- reasons.append("single_source_filter_concat_no_stream_copy")
80
- return "+".join(reasons)
90
+ def _edited_source_color_tags(source_formats, *, rgb_converted_per_clip=False):
91
+ """One set of colour tags for the concatenated picture, from each source's probe.
81
92
 
93
+ Sources that agree keep their shared tags; sources that disagree cannot be described by
94
+ one label, so the picture is BT.709 limited, the same default an untagged source gets,
95
+ and `_clip_color_filter` converts every clip into it. With `rgb_converted_per_clip` the
96
+ RGB sources (by colour space or pixel format) are converted to BT.709 limited yuv420p
97
+ clip by clip, so their tags lose `from_rgb` and match a BT.709 limited YUV source.
98
+ """
99
+ tags = [_output_color_tags(fmt) for fmt in source_formats]
100
+ if rgb_converted_per_clip:
101
+ tags = [{k: v for k, v in item.items() if k != "from_rgb"} for item in tags]
102
+ if all(item == tags[0] for item in tags):
103
+ return tags[0]
104
+ log("剪辑源视频: 各来源色彩标记不一致,各段统一转换为 BT.709 limited")
105
+ return _output_color_tags({})
82
106
 
83
- def update_delivery_qc(validated_plan, *, source_paths, output_path=None, rendered=False):
84
- """Attach cut delivery facts to qc.delivery_qc without writing visual_qc."""
85
- qc = validated_plan["qc"]
86
- clips = validated_plan["clips"]
87
- target_sample_rate = 48000
88
- probed_sample_rate = (
89
- _probe_audio_sample_rate(output_path)
90
- if output_path and Path(output_path).exists()
91
- else None
92
- )
93
- delivery_qc = {
94
- "schema_version": 1,
95
- "video_encode_passes": 1,
96
- "reencode_reason": _delivery_reencode_reason(source_paths, clips),
97
- "stream_copy_risk": {
98
- "status": "avoided",
99
- "reason": "cut uses trim/concat/filtergraph with explicit libx264/aac encode; no risky stream-copy path",
100
- },
101
- "audio_sample_rate": {
102
- "target": target_sample_rate,
103
- "probed": probed_sample_rate,
104
- },
105
- "final_compat_notes": [
106
- "video encoded with libx264/yuv420p-compatible filter path",
107
- "audio encoded as AAC with 48000 Hz target for delivery compatibility",
108
- "edited_source.mp4 is an intermediate; downstream assembly may perform another intentional encode",
109
- ],
110
- "output_geometry": qc["output_geometry"],
111
- "rendered": rendered,
112
- "planned": not rendered,
113
- }
114
- if probed_sample_rate and probed_sample_rate != target_sample_rate:
115
- delivery_qc["final_compat_notes"].append(
116
- f"probed audio sample rate {probed_sample_rate} differs from target {target_sample_rate}"
117
- )
118
- qc["delivery_qc"] = delivery_qc
119
- return delivery_qc
120
107
 
108
+ # ffprobe colour-space names the scale filter can convert from/to (in/out_color_matrix).
109
+ _SCALE_COLOR_MATRIX = {
110
+ "bt709": "bt709", "bt470bg": "bt470", "smpte170m": "smpte170m", "fcc": "fcc",
111
+ "smpte240m": "smpte240m", "bt2020nc": "bt2020",
112
+ }
121
113
 
122
- def write_cut_delivery_qc(work_dir, validated_plan):
123
- path = Path(work_dir) / "cut_delivery_qc.json"
124
- path.write_text(
125
- json.dumps(validated_plan["qc"]["delivery_qc"], ensure_ascii=False, indent=2),
126
- encoding="utf-8",
127
- )
128
- return path
114
+
115
+ def _clip_color_filter(source_format, target_tags):
116
+ """Per-clip conversion of one source's picture into the shared yuv420p picture.
117
+
118
+ The conversion is explicit for every source: the concat filter needs one pixel format,
119
+ and ffmpeg 8 also negotiates one colour space and range across its inputs, silently
120
+ converting the others with its own pick while the stamped label says something else.
121
+ An RGB source converts with the target's matrix; a YUV source converts from its own
122
+ matrix and range (untagged counts as BT.709, the label it would get alone). Only the
123
+ matrix and range are converted; primaries and transfer are relabelled, not remapped.
124
+ """
125
+ tags = _output_color_tags(source_format)
126
+ out_matrix = _SCALE_COLOR_MATRIX.get(target_tags.get("colorspace"))
127
+ out_range = target_tags["color_range"]
128
+ if tags.get("from_rgb"):
129
+ return f"scale=out_color_matrix={out_matrix or 'bt709'}:out_range={out_range},format=yuv420p,"
130
+ in_matrix = _SCALE_COLOR_MATRIX.get(tags.get("colorspace"))
131
+ if in_matrix and out_matrix:
132
+ return (
133
+ f"scale=in_color_matrix={in_matrix}:in_range={tags['color_range']}"
134
+ f":out_color_matrix={out_matrix}:out_range={out_range},format=yuv420p,"
135
+ )
136
+ if tags != target_tags:
137
+ log(f"剪辑源视频: 色彩空间 {tags.get('colorspace')} 无法转换为 "
138
+ f"{target_tags.get('colorspace')},该来源只改标记")
139
+ return "format=yuv420p,"
129
140
 
130
141
 
131
142
  def build_edited_source_video(input_video, validated_plan, work_dir, output_path=None):
@@ -133,7 +144,8 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
133
144
 
134
145
  `validated_plan["qc"]["output_geometry"]` is required: the caller (cut_cli, or any
135
146
  public user of this API) selects the canvas with `_select_output_geometry` first, so the
136
- same geometry is recorded in clip_plan_validated.json and used for the render."""
147
+ same geometry is recorded in clip_plan_validated.json and used for the render. Each clip
148
+ renders `frame_count(duration, output frame_rate)` frames and exactly that much audio."""
137
149
  work_dir = Path(work_dir)
138
150
  output_path = Path(output_path or work_dir / "edited_source.mp4")
139
151
  clips = validated_plan["clips"]
@@ -143,6 +155,11 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
143
155
  "validated_plan['qc']['output_geometry'] is required: select the canvas with "
144
156
  "media_geometry._select_output_geometry before build_edited_source_video"
145
157
  )
158
+ geometry_qc = qc["output_geometry"]
159
+ out_rate = Fraction(geometry_qc["frame_rate"])
160
+ source_rates = {
161
+ row["path"]: parse_frame_rate(row.get("frame_rate")) for row in geometry_qc["sources"]
162
+ }
146
163
 
147
164
  source_paths = []
148
165
  for clip in clips:
@@ -151,106 +168,99 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
151
168
  source_paths.append(source_path)
152
169
  source_index = {path: idx for idx, path in enumerate(source_paths)}
153
170
  audio_by_input = {path: _has_audio_stream(path) for path in source_paths}
171
+ format_by_input = {path: _probe_video_format(path) for path in source_paths}
154
172
  join_fade_ms = CONFIG["clip_join_audio_fade_ms"]
155
173
  qc["join_fade_ms"] = round(join_fade_ms, 3)
156
174
 
175
+ color_tags = _edited_source_color_tags(
176
+ [format_by_input[path] for path in source_paths],
177
+ rgb_converted_per_clip=len(source_paths) > 1,
178
+ )
157
179
  parts = []
158
180
  concat_inputs = []
159
181
  extra_inputs = []
182
+ vnorm_by_input = {}
160
183
  if len(source_paths) > 1:
161
184
  # Distinct sources almost always differ in resolution/SAR/fps/pixel-format (and
162
185
  # some may lack audio), which the bare concat filter rejects. Normalize every video
163
186
  # segment to one canvas and give every clip an audio segment (real or synthesized
164
187
  # silence) so concat always succeeds with a continuous track and no source's audio
165
188
  # is dropped just because a sibling source is silent.
166
- geometry_qc = qc["output_geometry"]
167
- canvas_w, canvas_h, canvas_fps = (
168
- geometry_qc["width"],
169
- geometry_qc["height"],
170
- geometry_qc["fps"],
171
- )
189
+ canvas_w, canvas_h = geometry_qc["width"], geometry_qc["height"]
172
190
  vnorm = (
173
191
  f"scale={canvas_w}:{canvas_h}:force_original_aspect_ratio=decrease,"
174
192
  f"pad={canvas_w}:{canvas_h}:(ow-iw)/2:(oh-ih)/2,setsar=1,"
175
- f"fps={canvas_fps},format=yuv420p"
176
193
  )
177
- for clip_pos, clip in enumerate(clips):
178
- idx = clip["clip_id"]
179
- clip_source = clip.get("source_path", str(input_video))
180
- input_idx = source_index[clip_source]
181
- start = clip["source_start"]
182
- end = clip["source_end"]
183
- dur = end - start
194
+ # Each clip becomes yuv420p in the shared colour space and range here, before the
195
+ # shared tags are stamped; left to `format=yuv420p` alone, ffmpeg picks the matrix
196
+ # (BT.601 for RGB) while the file is labelled with the shared tags.
197
+ vnorm_by_input = {
198
+ path: vnorm + _clip_color_filter(fmt, color_tags)
199
+ for path, fmt in format_by_input.items()
200
+ }
201
+ has_audio = len(source_paths) > 1 or all(audio_by_input.values())
202
+ total_frames = 0
203
+ end = None
204
+ for clip_pos, clip in enumerate(clips):
205
+ idx = clip["clip_id"]
206
+ clip_source = clip.get("source_path", str(input_video))
207
+ input_idx = source_index[clip_source]
208
+ # A lossless join starts exactly where the previous clip's render ended, not at the
209
+ # plan's millisecond-rounded copy of it, so the two atrims stay sample-contiguous.
210
+ joined = clip_pos > 0 and _continuous_source_join(clips[clip_pos - 1], clip)
211
+ start = end if joined else clip["source_start"]
212
+ frames = clip.get("frame_count") or frame_count(clip["duration"], out_rate)
213
+ total_frames += frames
214
+ dur = float(frames / out_rate)
215
+ end = start + dur
216
+ parts.append(_video_segment_filter(
217
+ f"[{input_idx}:v]", f"[v{idx}]", start, end, frames, out_rate,
218
+ source_rates.get(clip_source), vnorm_by_input.get(clip_source, ""),
219
+ ))
220
+ concat_inputs.append(f"[v{idx}]")
221
+ if not has_audio:
222
+ continue
223
+ if audio_by_input[clip_source]:
184
224
  fade_in_ms, fade_out_ms = _clip_audio_edge_fades(clips, clip_pos, join_fade_ms)
185
225
  parts.append(
186
- f"[{input_idx}:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS,{vnorm}[v{idx}]"
187
- )
188
- if audio_by_input[clip_source]:
189
- parts.append(
190
- _audio_segment_filter(
191
- f"[{input_idx}:a]",
192
- f"[a{idx}]",
193
- start,
194
- end,
195
- dur,
196
- fade_in_ms,
197
- fade_out_ms,
198
- extra_filters="aresample=48000,aformat=sample_rates=48000:channel_layouts=stereo",
199
- )
226
+ _audio_segment_filter(
227
+ f"[{input_idx}:a]",
228
+ f"[a{idx}]",
229
+ start,
230
+ end,
231
+ dur,
232
+ fade_in_ms,
233
+ fade_out_ms,
234
+ extra_filters=(
235
+ "aresample=48000,aformat=sample_rates=48000:channel_layouts=stereo"
236
+ if len(source_paths) > 1 else ""
237
+ ),
200
238
  )
201
- else:
202
- parts.append(
203
- f"anullsrc=r=48000:cl=stereo,atrim=duration={dur:.3f},asetpts=PTS-STARTPTS,"
204
- f"aformat=sample_rates=48000:channel_layouts=stereo[a{idx}]"
205
- )
206
- concat_inputs.append(f"[v{idx}][a{idx}]")
207
- parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=1[v][a]")
208
- maps = ["-map", "[v]", "-map", "[a]"]
209
- else:
210
- has_audio = all(audio_by_input.values())
211
- for clip_pos, clip in enumerate(clips):
212
- idx = clip["clip_id"]
213
- input_idx = source_index[clip.get("source_path", str(input_video))]
214
- start = clip["source_start"]
215
- end = clip["source_end"]
216
- parts.append(
217
- f"[{input_idx}:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS[v{idx}]"
218
239
  )
219
- concat_inputs.append(f"[v{idx}]")
220
- if has_audio:
221
- fade_in_ms, fade_out_ms = _clip_audio_edge_fades(
222
- clips, clip_pos, join_fade_ms
223
- )
224
- parts.append(
225
- _audio_segment_filter(
226
- f"[{input_idx}:a]",
227
- f"[a{idx}]",
228
- start,
229
- end,
230
- end - start,
231
- fade_in_ms,
232
- fade_out_ms,
233
- )
234
- )
235
- concat_inputs.append(f"[a{idx}]")
236
-
237
- if has_audio:
240
+ else:
238
241
  parts.append(
239
- "".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=1[v][a]"
242
+ f"anullsrc=r=48000:cl=stereo,atrim=duration={dur:.6f},asetpts=PTS-STARTPTS,"
243
+ f"aformat=sample_rates=48000:channel_layouts=stereo[a{idx}]"
240
244
  )
241
- maps = ["-map", "[v]", "-map", "[a]"]
242
- else:
243
- parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=0[v]")
244
- maps = ["-map", "[v]", "-map", f"{len(source_paths)}:a", "-shortest"]
245
- extra_inputs = [
246
- "-f",
247
- "lavfi",
248
- "-t",
249
- f"{validated_plan['total_duration']:.3f}",
250
- "-i",
251
- "anullsrc=channel_layout=stereo:sample_rate=48000",
252
- ]
245
+ concat_inputs.append(f"[a{idx}]")
253
246
 
247
+ if has_audio:
248
+ parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=1[v][a]")
249
+ maps = ["-map", "[v]", "-map", "[a]"]
250
+ else:
251
+ parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=0[v]")
252
+ maps = ["-map", "[v]", "-map", f"{len(source_paths)}:a", "-shortest"]
253
+ extra_inputs = [
254
+ "-f",
255
+ "lavfi",
256
+ "-t",
257
+ f"{float(total_frames / out_rate):.6f}",
258
+ "-i",
259
+ "anullsrc=channel_layout=stereo:sample_rate=48000",
260
+ ]
261
+
262
+ parts.append(f"[v]{_color_tag_filter(color_tags)}[vtagged]")
263
+ maps[maps.index("[v]")] = "[vtagged]"
254
264
  filter_complex = ";".join(parts)
255
265
  if len(filter_complex.encode("utf-8")) > 7000:
256
266
  filter_script = work_dir / "edit_filter_complex.txt"
@@ -285,19 +295,20 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
285
295
  "48000",
286
296
  "-movflags",
287
297
  "+faststart",
298
+ # The source's container/stream tags (a scraper's title, comment or URL) and its
299
+ # chapters would otherwise be copied into edited_source.mp4 and on into the recap.
300
+ "-map_metadata",
301
+ "-1",
302
+ "-map_chapters",
303
+ "-1",
304
+ *_color_tag_args(color_tags),
288
305
  str(output_path),
289
306
  ]
290
307
  result = run_cmd(cmd)
291
308
  if result.returncode != 0:
292
309
  raise RuntimeError(f"剪辑源视频失败: {result.stderr}")
293
310
 
294
- update_delivery_qc(
295
- validated_plan,
296
- source_paths=source_paths,
297
- output_path=output_path,
298
- rendered=True,
299
- )
300
- write_cut_delivery_qc(work_dir, validated_plan)
311
+ _warn_on_frame_count_mismatch(output_path, total_frames)
301
312
  _write_edited_source_meta(output_path, validated_plan, input_video)
302
313
  duration = get_video_duration(output_path)
303
314
  log(f"剪辑源视频: {output_path} ({duration:.1f}s, {len(clips)} clips)")