@oh-story/dsh 0.1.11 → 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
@@ -3,18 +3,22 @@
3
3
  import json
4
4
  import os
5
5
  import shlex
6
+ import shutil
6
7
  import sys
7
8
  from pathlib import Path
8
9
 
9
10
  from lib import load_json
11
+ from lib import file_identity, ffmpeg_filters, read_json_object
12
+ import resources.project_binding as project_binding
13
+ from library import LIBRARY_ENV
10
14
  from recap_runtime import (
11
- _coerce_videos,
12
15
  _entry,
13
16
  _load_run_manifest,
14
17
  _multi_run_manifest_payload,
18
+ _resume_argv,
15
19
  _run_manifest_payload,
16
20
  )
17
- from recap_source import audio_binding, uses_narration
21
+ from recap_source import audio_binding
18
22
 
19
23
  ASSEMBLY_MANIFEST = "assembly_manifest.json"
20
24
 
@@ -66,6 +70,41 @@ def _canonical_visual_overlay(overlay, segment):
66
70
  return item
67
71
 
68
72
 
73
+ def _canonical_visual_overlays(narration_path):
74
+ return [
75
+ item
76
+ for segment in load_json(narration_path)
77
+ for overlay in segment.get("visual_overlays", [])
78
+ if (item := _canonical_visual_overlay(overlay, segment)) is not None
79
+ ]
80
+
81
+
82
+ def _preflight_visual_overlays(work_dir, *, narration):
83
+ """Fail before TTS and render when the run will draw text overlays but this ffmpeg has
84
+ no drawtext (libfreetype; stock Homebrew ffmpeg lacks it). Overlays are authored content,
85
+ never a default, so they are not dropped silently the way a default subtitle burn
86
+ degrades: the message says how to remove them. Narration runs read narration.json (the
87
+ overlays are only written after TTS); source modes read an existing visual_overlays.json."""
88
+ work_dir = Path(work_dir)
89
+ if shutil.which("ffmpeg") is None:
90
+ return
91
+ if narration:
92
+ narration_path = work_dir / "narration.json"
93
+ overlays = _canonical_visual_overlays(narration_path) if narration_path.exists() else []
94
+ remedy = "删掉 narration.json 各段的 visual_overlays"
95
+ else:
96
+ data = read_json_object(work_dir / _VISUAL_OVERLAYS) or {}
97
+ overlays = data.get("overlays") if isinstance(data.get("overlays"), list) else []
98
+ remedy = f"删掉 {_VISUAL_OVERLAYS} 里的 overlays"
99
+ if not overlays or "drawtext" in ffmpeg_filters():
100
+ return
101
+ raise SystemExit(
102
+ f"本次运行有 {len(overlays)} 个画面文字叠加(visual_overlays),但当前 ffmpeg 不支持 drawtext"
103
+ " 滤镜(需要 libfreetype),会在最后渲染时失败。\n"
104
+ f" 解决其一:(1) 安装带 drawtext/libfreetype 的 ffmpeg;(2) {remedy} 后续跑。"
105
+ )
106
+
107
+
69
108
  def _write_canonical_visual_overlays(work_dir, narration_path):
70
109
  """Write assemble's canonical work_dir/visual_overlays.json recap handoff.
71
110
 
@@ -76,32 +115,13 @@ def _write_canonical_visual_overlays(work_dir, narration_path):
76
115
  empty overlay list.
77
116
  """
78
117
  path = Path(work_dir) / _VISUAL_OVERLAYS
79
- overlays = [
80
- item
81
- for segment in load_json(narration_path)
82
- for overlay in segment.get("visual_overlays", [])
83
- if (item := _canonical_visual_overlay(overlay, segment)) is not None
84
- ]
118
+ overlays = _canonical_visual_overlays(narration_path)
85
119
  payload = {"schema_version": 1, "overlays": overlays}
86
120
  path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
87
121
  print(f"[video-recap] 🧩 visual overlays: {len(overlays)} → {path}", flush=True)
88
122
  return path
89
123
 
90
124
 
91
- def _print_grounding_qc_pointer(work_dir):
92
- qc_path = Path(work_dir) / "grounding_qc.json"
93
- if not qc_path.exists():
94
- return
95
- data = load_json(qc_path)
96
- ranges = data["review_coverage"]["time_ranges"]
97
- warnings = data["warnings"]
98
- suffix = f" · warnings {len(warnings)}" if warnings else ""
99
- print(
100
- f"[video-recap] 🧭 Grounding QC: {data['verdict']} · ranges {len(ranges)}{suffix} → {qc_path}",
101
- flush=True,
102
- )
103
-
104
-
105
125
  def _print_narration_review_pointer(work_dir, *, review_ran=True):
106
126
  """Surface the advisory narration review produced by this run, if any.
107
127
 
@@ -109,7 +129,6 @@ def _print_narration_review_pointer(work_dir, *, review_ran=True):
109
129
  stale narration_review.md from an older run is never surfaced. When it ran, video-script's
110
130
  review_runner has written both narration_review.json and .md.
111
131
  """
112
- _print_grounding_qc_pointer(work_dir)
113
132
  if not review_ran:
114
133
  return
115
134
  review_md = Path(work_dir) / "narration_review.md"
@@ -191,20 +210,16 @@ def _read_assembly_output(work_dir):
191
210
 
192
211
 
193
212
  def _read_phase_ledger(work_dir):
194
- """Phase ledger (cut mode): which artifacts exist and the clip_plan/narration they match.
195
-
196
- Lets resume be driven by recorded phase state rather than bare file existence — the
197
- prerequisite for the cut-first/narrate-second two-pause flow, and the guard that keeps a
198
- narration written for one clip_plan from silently driving a different cut into TTS.
199
- None before the first cut pass has recorded anything.
213
+ """Phase ledger (cut mode with narration): the clip_plan identity the narration is
214
+ written against — the guard that keeps a narration written for one clip_plan from
215
+ silently driving a different cut into TTS. None before pass 2 has recorded it.
200
216
  """
201
217
  path = Path(work_dir) / PHASE_LEDGER
202
218
  return load_json(path) if path.exists() else None
203
219
 
204
220
 
205
- def _write_phase_ledger(work_dir, **fields):
206
- ledger = _read_phase_ledger(work_dir) or {}
207
- ledger.update(fields)
221
+ def _write_phase_ledger(work_dir, clip_plan_identity):
222
+ ledger = {"clip_plan_identity": clip_plan_identity}
208
223
  (Path(work_dir) / PHASE_LEDGER).write_text(
209
224
  json.dumps(ledger, ensure_ascii=False, indent=2), encoding="utf-8"
210
225
  )
@@ -219,86 +234,9 @@ def _cut_narration_is_stale(ledger, current_clip_plan_identity):
219
234
  return ledger is not None and ledger["clip_plan_identity"] != current_clip_plan_identity
220
235
 
221
236
 
222
- def _continuation_command(video, work_dir, args):
223
- # Values a project binding filled in are re-derived from --project on resume.
224
- bound = getattr(args, "_bound_from_project", frozenset())
225
- parts = [
226
- sys.executable,
227
- str(_entry("video-recap", "recap.py")),
228
- *[str(v) for v in _coerce_videos(video)],
229
- "--work-dir",
230
- str(work_dir),
231
- ]
232
- if args.context:
233
- parts += ["--context", args.context]
234
- if args.scene_threshold is not None:
235
- parts += ["--scene-threshold", str(args.scene_threshold)]
236
- if args.style != "纪录片":
237
- parts += ["--style", args.style]
238
- if args.edit_mode != "full":
239
- parts += ["--edit-mode", args.edit_mode]
240
- if args.audio_mode != "narration":
241
- parts += ["--audio-mode", args.audio_mode]
242
- if args.audio_stream_index != 0:
243
- parts += ["--audio-stream-index", str(args.audio_stream_index)]
244
- if args.target_duration:
245
- parts += ["--target-duration", args.target_duration]
246
- if args.allow_duration_drift:
247
- parts.append("--allow-duration-drift")
248
- if args.skip_asr:
249
- parts.append("--skip-asr")
250
- if args.mimo_video_overview:
251
- parts.append("--mimo-video-overview")
252
- if args.mimo_qc != "off":
253
- parts += ["--mimo-qc", args.mimo_qc]
254
- if args.mimo_qc_refresh:
255
- parts.append("--mimo-qc-refresh")
256
- if not args.consolidate: # default is ON; only the opt-out needs to round-trip
257
- parts.append("--no-consolidate")
258
- if args.consolidate_asr:
259
- parts.append("--consolidate-asr")
260
- if uses_narration(args):
261
- if args.mimo_tts_voice and "mimo_tts_voice" not in bound:
262
- parts += ["--mimo-tts-voice", args.mimo_tts_voice]
263
- if args.tts_provider != "auto" and "tts_provider" not in bound:
264
- parts += ["--tts-provider", args.tts_provider]
265
- if args.voice_ref and "voice_ref" not in bound:
266
- parts += ["--voice-ref", args.voice_ref]
267
- if args.allow_partial_tts:
268
- parts.append("--allow-partial-tts")
269
- if args.preserve_approved_text:
270
- parts.append("--preserve-approved-text")
271
- if args.burn_subtitles is not None:
272
- parts.append("--burn-subtitles" if args.burn_subtitles else "--no-burn-subtitles")
273
- if args.subtitle_y_top is not None:
274
- parts += ["--subtitle-y-top", str(args.subtitle_y_top)]
275
- if args.subtitle_y_bot is not None:
276
- parts += ["--subtitle-y-bot", str(args.subtitle_y_bot)]
277
- if args.output_dir:
278
- parts += ["--output-dir", args.output_dir]
279
- if args.export_jianying:
280
- parts.append("--export-jianying")
281
- if args.jianying_bundle_media:
282
- parts.append("--jianying-bundle-media")
283
- if args.jianying_no_bundle_media:
284
- parts.append("--jianying-no-bundle-media")
285
- if uses_narration(args):
286
- if args.review_narration is not None:
287
- parts.append(
288
- "--review-narration" if args.review_narration else "--no-review-narration"
289
- )
290
- if args.require_narration_review:
291
- parts.append("--require-narration-review")
292
- if args.material_library_dir and "material_library_dir" not in bound:
293
- parts += ["--material-library-dir", args.material_library_dir]
294
- if getattr(args, "project", None):
295
- parts += ["--project", args.project]
296
- if args.use_materials:
297
- parts.append("--use-materials")
298
- if args.save_materials:
299
- parts.append("--save-materials")
300
- if args.require_final_qc:
301
- parts.append("--require-final-qc")
237
+ def _continuation_command(work_dir, args):
238
+ """Shell command that resumes this run: the original argv, replayable from any cwd."""
239
+ parts = [sys.executable, str(_entry("video-recap", "recap.py")), *_resume_argv(work_dir, args)]
302
240
  return " ".join(shlex.quote(part) for part in parts)
303
241
 
304
242
 
@@ -454,26 +392,57 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
454
392
  )
455
393
  return cache[source_id]
456
394
 
395
+ spans_by_source = {}
396
+ for clip in plan["clips"]:
397
+ spans_by_source.setdefault(clip["source_id"], []).append(
398
+ (float(clip["source_start"]), float(clip["source_end"])))
399
+
457
400
  mapped_anchors, mapped_speech, mapped_quiet = [], [], []
401
+ output_duration = max(float(clip["output_end"]) for clip in plan["clips"])
458
402
  for clip in plan["clips"]:
459
403
  source_id = clip["source_id"]
460
404
  source_start = float(clip["source_start"])
461
405
  source_end = float(clip["source_end"])
462
406
  output_start = float(clip["output_start"])
407
+ output_end = float(clip["output_end"])
408
+
409
+ def to_output(when):
410
+ # A source time on this clip's OUTPUT clock, never past its own span: the next
411
+ # clip on the output timeline plays a different range (or source).
412
+ when = min(max(when, source_start), source_end)
413
+ return round(min(output_start + when - source_start, output_end), 3)
414
+
463
415
  anchors, speech_rows, quiet_rows = load_source(source_id)
464
416
  for anchor in anchors:
465
417
  when = float(anchor["time"])
466
418
  if not (source_start - 0.05 <= when <= source_end + 0.05):
467
419
  continue
468
- pause = max(source_start, min(float(anchor["pause_start"]), when))
420
+ # The 0.05 s source slack can carry an anchor past the output's ends (a clip
421
+ # end frame-snapped 181.42 -> 181.40): narration cannot start there.
422
+ if not 0 <= round(output_start + when - source_start, 3) <= output_duration:
423
+ continue
424
+ # A pause end just past a frame-snapped edge belongs to this clip at that edge,
425
+ # unless another clip of the same source plays that instant itself.
426
+ if not source_start <= when <= source_end and any(
427
+ start <= when <= end for start, end in spans_by_source[source_id]):
428
+ continue
429
+ # Same default as the single-source remap in video-understanding's timeline brief.
430
+ pause = max(source_start, min(float(anchor.get("pause_start", when - 0.12)), when))
469
431
  item = dict(anchor)
470
432
  item.update(
471
433
  source_id=source_id,
472
434
  source_time=round(when, 3),
473
- time=round(output_start + when - source_start, 3),
474
- source_pause_start=round(pause, 3),
475
- pause_start=round(output_start + pause - source_start, 3),
435
+ time=to_output(when),
436
+ source_pause_start=round(min(pause, source_end), 3),
437
+ pause_start=to_output(pause),
438
+ # `time` IS the pause end; keep the source value apart (one clock per field).
439
+ source_pause_end=round(float(anchor.get("pause_end", when)), 3),
476
440
  )
441
+ item["pause_end"] = item["time"]
442
+ if "expected_time" in anchor:
443
+ expected = float(anchor["expected_time"])
444
+ item["source_expected_time"] = round(expected, 3)
445
+ item["expected_time"] = to_output(expected)
477
446
  mapped_anchors.append(item)
478
447
  for rows, destination, require_text in (
479
448
  (speech_rows, mapped_speech, True),
@@ -493,8 +462,8 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
493
462
  source_id=source_id,
494
463
  source_start=round(start, 3),
495
464
  source_end=round(end, 3),
496
- start=round(output_start + start - source_start, 3),
497
- end=round(output_start + end - source_start, 3),
465
+ start=to_output(start),
466
+ end=to_output(end),
498
467
  )
499
468
  destination.append(item)
500
469
 
@@ -503,6 +472,8 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
503
472
  "artifact": "speech_boundary_anchors_output.json",
504
473
  "timeline": "cut_output",
505
474
  "source_artifact": "multi_source_manifest.json",
475
+ # video-script's cut_output lint trusts this evidence only for this exact plan file.
476
+ "clip_plan_identity": file_identity(Path(work_dir) / "clip_plan_validated.json"),
506
477
  "sentence_anchors": sorted(mapped_anchors, key=lambda row: row["time"]),
507
478
  "speech_spans": sorted(mapped_speech, key=lambda row: (row["start"], row["end"])),
508
479
  "quiet_windows": sorted(mapped_quiet, key=lambda row: (row["start"], row["end"])),
@@ -552,14 +523,31 @@ def _write_multi_source_output_brief(work_dir, source_records, validated_plan_pa
552
523
  f"{c['source_id']} `{src['source_path']}` "
553
524
  f"source {_fmt_range(c['source_start'], c['source_end'])}{reason}"
554
525
  )
555
- anchors = speech_evidence["sentence_anchors"]
556
- if anchors:
557
- lines += ["", "## 原声句末安全切入点"]
558
- lines.extend(
559
- f"- {row['time']:.3f}s ({row['source_id']})"
560
- for row in anchors
561
- if row["confidence"] in {"high", "medium"}
526
+ anchors = []
527
+ for row in speech_evidence["sentence_anchors"]:
528
+ # Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
529
+ # labels there are usable but unverified.
530
+ use = row.get("boundary_use") or (
531
+ "unverified" if row["confidence"] in {"high", "medium"} else "none"
562
532
  )
533
+ if use != "none":
534
+ anchors.append((row, use))
535
+ if anchors:
536
+ lines += [
537
+ "",
538
+ "## 原声句末安全切入点",
539
+ "",
540
+ "`unverified` 点是粗粒度 ASR 窗口里的标点位置估计、再吸附到短停顿:不切断单词,"
541
+ "但不保证原声句子已说完(误差上限见 `±`);门禁照常生效。",
542
+ ]
543
+ for row, use in anchors:
544
+ if use == "verified":
545
+ label = row["confidence"]
546
+ elif "timing_bound_seconds" in row:
547
+ label = f"unverified ±{float(row['timing_bound_seconds']):.1f}s"
548
+ else: # schema-1 anchors carry no error bound
549
+ label = "unverified"
550
+ lines.append(f"- {row['time']:.3f}s [{label}] ({row['source_id']})")
563
551
  lines += ["", "## Source work dirs"]
564
552
  for s in source_records:
565
553
  lines.append(f"- {s['source_id']}: `{_source_work_dir(work_dir, s)}`")
@@ -595,7 +583,7 @@ def _fmt_range(start, end):
595
583
 
596
584
 
597
585
  def _material_library_dir(args):
598
- return args.material_library_dir or os.environ.get("VIDEO_RECAP_MATERIAL_LIBRARY_DIR") or None
586
+ return args.material_library_dir or os.environ.get(LIBRARY_ENV) or None
599
587
 
600
588
 
601
589
  def _materials_enabled(args):
@@ -606,7 +594,7 @@ def _save_materials_enabled(args):
606
594
  return bool(_material_library_dir(args) and args.save_materials)
607
595
 
608
596
 
609
- def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
597
+ def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None, project=None):
610
598
  brief = Path(work_dir) / "agent_narration_brief.md"
611
599
  print("=" * 50)
612
600
  if "Research the story FIRST" in brief.read_text(encoding="utf-8"):
@@ -617,5 +605,8 @@ def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
617
605
  print(f"[video-recap] ⏸ 阅读 {brief}(按 video-script 规则)后写入 {need_text}")
618
606
  if inspect_hint:
619
607
  print(f"[video-recap] 先核对状态/时间轴(建议性): {inspect_hint}")
608
+ reference_note = project_binding.bound_reference_note(work_dir, project)
609
+ if reference_note:
610
+ print(f"[video-recap] {reference_note}")
620
611
  print(f"[video-recap] 写完后重跑继续: {cont}")
621
612
  print("=" * 50)
@@ -0,0 +1,4 @@
1
+ """Resource-library consumers on the run path: project binding and the per-run resource lock.
2
+
3
+ The library entry itself stays top-level as ``library.py`` (a ``library/`` package would shadow it).
4
+ """
@@ -12,7 +12,7 @@ from datetime import datetime, timezone
12
12
  from pathlib import Path
13
13
 
14
14
  import library as library_lib
15
- from materials import file_identity
15
+ from lib import file_identity
16
16
 
17
17
  LOCK_NAME = "resource_lock.json"
18
18
  LOCK_SCHEMA = "video-recap.resource-lock.v1"
@@ -2,7 +2,8 @@
2
2
 
3
3
  Only video-recap reads the library. A bound subtitle style becomes ``SUBTITLE_*`` env for
4
4
  video-assemble, a bound voice becomes the voiceover provider / voice arguments, a bound
5
- BGM becomes ``BGM_PATH``. Stage skills keep seeing concrete paths and values; they never
5
+ BGM becomes ``BGM_PATH``, a bound production reference becomes a marked copy in the work_dir
6
+ that only the writing agent reads. Stage skills keep seeing concrete paths and values; they never
6
7
  learn about templates. Anything the caller already set explicitly must agree with the
7
8
  binding — a silent override would make the delivered file disagree with the project.
8
9
  """
@@ -17,7 +18,8 @@ import library as library_lib
17
18
  PROJECT_FILE = "recap_project.json"
18
19
  PROJECT_SCHEMA = "video-recap.project.v1"
19
20
  PROJECT_KEYS = {"schema", "name", "library", "bindings", "notes"}
20
- BINDING_KINDS = ("subtitle_style", "packaging", "voice", "bgm")
21
+ BINDING_KINDS = ("subtitle_style", "packaging", "voice", "bgm", "production_reference")
22
+ GEOMETRY_ROLES = {"subtitle_style", "packaging"}
21
23
 
22
24
 
23
25
  class BindingError(SystemExit):
@@ -188,6 +190,14 @@ def resolve_project(value, args, environ=None, *, include_voice=True) -> dict:
188
190
  used_templates.append({"role": "packaging", "id": packaging["id"],
189
191
  "version": packaging["version"], "status": packaging["status"],
190
192
  "canvas": packaging["canvas"]})
193
+ reference = template("production_reference")
194
+ if reference:
195
+ used_templates.append({"role": "production_reference", "id": reference["id"],
196
+ "version": reference["version"], "status": reference["status"],
197
+ "canvas": reference["canvas"]})
198
+ reference = {"template": {"id": reference["id"], "version": reference["version"]},
199
+ "payload": json.loads((Path(reference["record"]).parent / library_lib.REFERENCE_FILE)
200
+ .read_text(encoding="utf-8"))}
191
201
  voice = resource("voice") if include_voice else None
192
202
  if voice:
193
203
  voice_updates, voice_env = _voice_updates(voice, args)
@@ -221,6 +231,7 @@ def resolve_project(value, args, environ=None, *, include_voice=True) -> dict:
221
231
  "templates": used_templates,
222
232
  "resources": [],
223
233
  "packaging": packaging and {"record": _record(packaging), "resources": resources},
234
+ "production_reference": reference,
224
235
  }
225
236
 
226
237
 
@@ -244,6 +255,8 @@ def apply_project(resolved, args, environ=None) -> None:
244
255
  def check_canvas(resolved, width, height) -> None:
245
256
  """Templates only apply to the canvas they were calibrated on."""
246
257
  for template in resolved["templates"]:
258
+ if template["role"] not in GEOMETRY_ROLES: # a production reference carries no geometry
259
+ continue
247
260
  canvas = template["canvas"]
248
261
  if (canvas["width"], canvas["height"]) != (width, height):
249
262
  raise BindingError(
@@ -253,34 +266,82 @@ def check_canvas(resolved, width, height) -> None:
253
266
 
254
267
 
255
268
  PACKAGING_LAYERS = "packaging_layers.json"
269
+ REFERENCE_COPY = "production_reference.json"
256
270
  _WRITTEN_BY = "video-recap --project"
257
271
 
258
272
 
259
- def sync_packaging_layers(work_dir, resolved) -> None:
260
- """Write the bound packaging template's layers for video-assemble, or retire our old copy.
273
+ def _read_json(path):
274
+ try:
275
+ return json.loads(Path(path).read_text(encoding="utf-8"))
276
+ except (OSError, ValueError):
277
+ return None
278
+
261
279
 
262
- A caller-authored ``packaging_layers.json`` (no ``written_by`` marker) is left alone.
280
+ def _ours(data) -> bool:
281
+ return isinstance(data, dict) and data.get("written_by") == _WRITTEN_BY
282
+
283
+
284
+ def _sync_bound_file(work_dir, name, payload) -> None:
285
+ """Write ``payload`` with our marker, or retire our old copy when nothing is bound.
286
+
287
+ A caller-authored file (no ``written_by`` marker) is never deleted.
263
288
  """
264
- path = Path(work_dir) / PACKAGING_LAYERS
289
+ path = Path(work_dir) / name
290
+ if payload is None:
291
+ if path.exists() and _ours(_read_json(path)):
292
+ path.unlink()
293
+ return
294
+ # The marker goes last so a payload key of the same name can never unmark our copy.
295
+ path.write_text(json.dumps({**payload, "written_by": _WRITTEN_BY}, ensure_ascii=False, indent=2),
296
+ encoding="utf-8")
297
+
298
+
299
+ def sync_packaging_layers(work_dir, resolved) -> None:
300
+ """Write the bound packaging template's layers for video-assemble, or retire our old copy."""
265
301
  packaging = (resolved or {}).get("packaging")
266
302
  if not packaging:
267
- if path.exists():
268
- try:
269
- plan = json.loads(path.read_text(encoding="utf-8"))
270
- except ValueError:
271
- plan = None
272
- ours = isinstance(plan, dict) and plan.get("written_by") == _WRITTEN_BY
273
- if ours:
274
- path.unlink()
303
+ _sync_bound_file(work_dir, PACKAGING_LAYERS, None)
275
304
  return
276
305
  record, resources = packaging["record"], packaging["resources"]
277
306
  layers = [{"name": layer["name"],
278
307
  "path": resources[layer["image"]["resource"]]["files"][0]["path"],
279
308
  "rect": layer["rect"]}
280
309
  for layer in record["params"]["layers"]]
281
- path.write_text(json.dumps({
282
- "written_by": _WRITTEN_BY,
310
+ _sync_bound_file(work_dir, PACKAGING_LAYERS, {
283
311
  "template": {"id": record["id"], "version": record["version"]},
284
312
  "canvas": record["canvas"],
285
313
  "layers": layers,
286
- }, ensure_ascii=False, indent=2), encoding="utf-8")
314
+ })
315
+
316
+
317
+ def sync_production_reference(work_dir, resolved) -> None:
318
+ """Hand the bound reference to the writing agent before its first pause; no script reads it.
319
+
320
+ A caller's own copy is never rewritten: identical to the binding it is left as is, different
321
+ from it is a conflict, like any other setting.
322
+ """
323
+ bound = (resolved or {}).get("production_reference")
324
+ path = Path(work_dir) / REFERENCE_COPY
325
+ if bound and path.exists():
326
+ current = _read_json(path)
327
+ if not _ours(current):
328
+ if current == bound["payload"]:
329
+ return
330
+ template = bound["template"]
331
+ raise BindingError(f"{path} 不是 --project 写的,与绑定的 {template['id']}@v{template['version']} "
332
+ "不同;删掉其中一处")
333
+ _sync_bound_file(work_dir, REFERENCE_COPY,
334
+ bound and {**bound["payload"], "template": bound["template"]})
335
+
336
+
337
+ def bound_reference_note(work_dir, resolved=None) -> str | None:
338
+ """The pause-banner line for the bound reference in work_dir (our copy or the caller's identical one)."""
339
+ data = _read_json(Path(work_dir) / REFERENCE_COPY)
340
+ bound = (resolved or {}).get("production_reference")
341
+ if _ours(data):
342
+ template = data.get("template") or {}
343
+ elif bound and data == bound["payload"]:
344
+ template = bound["template"]
345
+ else:
346
+ return None
347
+ return f"本轮带制作参考 {template.get('id')}@v{template.get('version')}(可选,取舍写入 reference_methods)"
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: video-reference
3
+ user-invocable: true
4
+ description: >
5
+ 按需把一部成片拆成可复用的制作参考:测镜头节奏与响度,标注段落与音轨分工,把原片事实与可迁移方法分开,
6
+ 导出不含原片人名台词的 production_reference.json 供下次制作参考。不在默认生产路径上。
7
+ 触发词:拆片、拆解成片、制作参考、参考模板、production reference。
8
+ ---
9
+
10
+ ## 1. 定位
11
+
12
+ 本技能把一部**已完成的成片**拆成下一次制作能直接参考的方法与数值。它是按需的参考分析,不是生产阶段,也不是质检:
13
+ 不调用 MiMo,不改其他产物,不给任何运行打分或拦截。
14
+
15
+ Agent 的角色是**拆片编辑**:先如实记录"这部片子怎么做的"(事实),再提炼"换一部素材还能怎么用"(方法)。
16
+ 两者必须分开:事实只留在本地工作目录,导出物只含方法和测得的数值。
17
+
18
+ ## 2. 输入
19
+
20
+ - 成片文件(`.mp4 / .mov / .mkv / .webm`)。
21
+ - 对**这部成片**跑出的视频理解产物目录 `U`(建议 `ASR_SEGMENT_SECONDS=5`,窗口越短,旁白语速越准)。
22
+ 本技能读取其中可选的 `asr_result.json`、`asr_timing_evidence.json`、`background_research.json`、`understanding_index.json`
23
+ (其 `characters` 的名字、别名与 ASR 提及都进入人名泄漏扫描);
24
+ 标注时 Agent 还应看故事板 / contact sheet 与 `vlm_analysis.json`。
25
+
26
+ 没有理解产物也能跑,但旁白语速为空,泄漏扫描只剩 Agent 自己写的 `entities`,check 会给出警告;
27
+ 看画面改用 `frames --span 0,<时长> --step 2` 的接触表。
28
+
29
+ 下面的 `scripts/...` 均相对于本技能目录。
30
+
31
+ ## 3. 流程
32
+
33
+ ```bash
34
+ python3 scripts/reference.py measure <成片> --work-dir U # 一次 ffmpeg:切点 + 响度,按文件身份缓存
35
+ python3 scripts/reference.py frames <成片> --work-dir U --review # 逐帧看被压下的疑似切点
36
+ python3 scripts/reference.py frames <成片> --work-dir U --longest 5 # 看最长镜头里有没有漏切
37
+ python3 scripts/reference.py check --work-dir U [--json] # 校验并打印派生值
38
+ python3 scripts/reference.py export --work-dir U --out <下次运行的 work_dir>/production_reference.json
39
+ ```
40
+
41
+ 1. **measure** 写 `U/reference_measurements.json`(只由脚本写):镜头切点、镜长分布、每分钟切点数、10 秒切点曲线、
42
+ 整体响度 / LRA / 真峰值、逐秒短期响度。它要解码整片:5 分钟 720p 约 15–70 秒,4K 约 7 分钟;成片不变时复用,
43
+ 只改 `--soft-score/--hard-score` 不重新解码。切点规则:scdet 分数 ≥10 必算;≥4 且是前后 0.3 秒内其他帧(相邻帧除外)
44
+ 两倍以上的孤立峰才算。运动镜头、急推拉、闪光会被压下,并列进 `shots.review_windows`;scdet 分数会减去前一帧的帧差,
45
+ 从快速运动切进静止镜头的硬切可能只有 1 分,所以帧差本身的单侧峰(`mafd_peaks`)也进待复核窗口,但不会自动算切点。
46
+ 2. **复核切点**:固定阈值在真实成片上两头都错(暗场硬切只有 5–7 分;快速运动的单个镜头每 0.2 秒一个 7–8 分的峰),
47
+ 所以每次都要看图。`frames --review` 逐帧拼出每个待复核窗口,`frames --longest 5` 在最长的 5 个镜头里均匀取 12 帧;
48
+ 页面在 `U/reference_frames/`,命令打印每格对应的秒数。看完写 `labels.cut_fixes`:`add` 漏掉的切点秒数,
49
+ `remove` 误报的切点秒数(±0.1 秒内对上测得的切点);看过无需改动就写 `{}`。重新 `measure` 后待复核窗口数变了,旧的 `cut_fixes` 不再算数,按新窗口重看一遍。之后所有镜头数值、段内切点密度和导出都用复核后的切点。
50
+ 3. **标注**:Agent 写 `U/reference_breakdown.json` 的 `labels`——音轨归属 `audio_spans`、叙事段落 `sections`、
51
+ 字幕形态 `subtitles`、标注依据 `basis`。`audio_spans` 的边界放在**声音实际起止**处(听得到的人声起点与止点),
52
+ 不放在字幕或旁白块的开始处(按字幕出现帧定的旁白结束点实测晚 0.24–0.42 秒)。`U/speech_boundary_anchors.json` 存在时,
53
+ 用它的 `acoustic_pauses`(`start` 是人声停下处,`end` 是下一句开口处)对齐边界;`switch_on_cut_share` 只容差 ±0.25 秒,
54
+ 边界放错它就只反映标注习惯。原片自带的画外音、内心独白也是原片音轨,记 `original_dialogue`;要区分时写进 fact 和方法。
55
+ 只有 labels 时就可以跑 check,它会打印 `derived`(各音轨占比、段内切点密度、
56
+ 旁白语速、声音切换与画面切点的对齐比例、分数位置的结构、第一次原声出现位置)。
57
+ 4. **事实与方法**:在同一文件写 `source_facts`(带时间或测量锚点、显式 `entities`)和 `methods`
58
+ (`rule`、`applies_when`、`avoid_when`、`applies_to`、`evidence`、`targets`)。`targets` 只写 `{"from": "<测量路径>"}`,
59
+ 数值由 export 从当前测量填入,Agent 永远不手写数字。
60
+ 5. **check → export**:零 error 才导出;导出后对产物再做一次泄漏扫描。
61
+
62
+ 字段、枚举和好/坏方法示例见 `references/reference-schema.md`。
63
+
64
+ ## 4. 分离规则
65
+
66
+ check 的 error(退出码 1):
67
+
68
+ | 规则 | 要求 |
69
+ |---|---|
70
+ | R1 | 顶层、labels、fact、method、target、`cut_fixes` 都是封闭键集与封闭枚举;fact 不能带方法字段,method 不能带事实字段;id 为 `f1…` / `m1…` 且唯一;`subtitles` 值类型固定;`skipped_dimensions` 的值都是非空字符串 |
71
+ | R2 | `audio_spans`、`sections` 按时间排序、不重叠、间隙 ≤0.5s、覆盖整片;字幕证据时间在时长内;`cut_fixes.remove` 对得上测得的切点,`add` 不与测得的切点重复 |
72
+ | R3 | 每条 fact 二选一锚定:`t:[a,b]` 在时长内,或 `measure:[路径]` 解析到 `shots` / `loudness` / `derived` 下的非字符串值;必须显式写 `entities` |
73
+ | R4 | 每条 method 至少一条证据:已有 fact id 或同样只认这三个根的 `measure:<路径>` |
74
+ | R5 | target 只写 `from`:`shots` / `loudness` / `derived` 下的数值叶子,或白名单派生对象(见 schema);不得带列表下标 |
75
+ | R6 | `rule` / `applies_when` / `avoid_when` 和 `skipped_dimensions` 的原因不得含:原片实体名(忽略空白)、与台词、背景资料或事实共有的连续 8 个汉字(标点隔开、跨相邻 ASR 窗口也算)或 5 个英文词、绝对时间码、"第 N 秒"或"N 分 M 秒"(含中文数字)、绝对路径 |
76
+ | R7 | 五个维度各至少一条 method,或在 `skipped_dimensions` 写明原因 |
77
+ | R8 | 导出物的每个键和字符串再扫一遍 R6,且不得出现 `source_facts`、`labels`、`entities`、`evidence`、`statement`、`from`、`path` 键 |
78
+
79
+ 实体名来自 fact `entities`、`understanding_index.json` 的 `characters` / `entities` / `research_glossary` 名字与别名、`background_research.json` 的角色与
80
+ `character_details` 别名、≤5 字的 `cultural_notes` 条目,以及资料里《》「」引号中的短词。与事实共有 8 字时错误会注明来自
81
+ source_facts:通用剪辑措辞改写任一侧即可。
82
+
83
+ 警告不阻断:method 缺 `applies_when`、rule 正文写了数字、ASR 不是 `AVAILABLE_COARSE`、有中文对白但理解产物与背景资料都没给出名字(此时只扫 fact `entities`)、`basis` 为空、
84
+ 理解产物记录的成片身份与测量的成片不一致、有待复核窗口却没写 `cut_fixes`。
85
+
86
+ R6 只能拦住字面泄漏,拦不住改写过的剧情;写方法时要写"什么情况下怎么做",不要复述"这部片里发生了什么"。
87
+ 规则也不判断语义:方法的方向必须与 check 打印的派生值一致(例如先看各音轨的 `cuts_per_min` 再写"哪类段落切得密")。
88
+
89
+ ## 5. 交付与使用
90
+
91
+ `production_reference.json` 只含:时长与画布、`cut_detection`(切点规则的参数与 Agent 增删的切点数)、
92
+ `profile`(节奏数值,`provenance` 为 `measured` / `reviewed`(写了 `cut_fixes`,`{}` 也算)/ `labeled`)、
93
+ 分数位置的 `structure`、字幕形态、`methods`、`skipped_dimensions`。
94
+
95
+ 使用方式:把它复制进下一次制作的 `work_dir`。写稿阶段只在文件存在时阅读它,
96
+ 并可在 `recap_story_plan.json` 的可选字段 `reference_methods` 记录每条方法的 adopt / adapt / skip。
97
+ 它是参考,不是配额:与新素材的证据冲突时以素材为准。没有任何脚本读取它,也不会因为它的存在改变任何阶段的通过与否。
98
+
99
+ `U/` 里的 breakdown 与 `reference_frames/` 含原片事实和画面,只留在本地,不要复制或提交。
100
+
101
+ ## 6. 能力边界
102
+
103
+ - 不调用 MiMo 或任何 LLM,不改动理解产物,不给成片打分,不做"新片与参考的差距"判定。
104
+ - 切点来自 ffmpeg `scdet` 分数:慢叠化测不到;相隔 0.3 秒内分数相近的两个真切点、刚好压在 soft 线上的切点(不同 ffmpeg 构建
105
+ 4.00 与 3.989 分)、运动后的暗场硬切(0.9 分)都不算切点,只进待复核窗口。在一部 5 分钟剧集解说上测得 82 个切点,
106
+ 11 个待复核窗口里 4 个含漏掉的 5 个真切点,其余是急推拉、翻页和运动镜头;补上后,三段逐帧真值(开场、暗场、中段共 27 个)全部对上、无误报。
107
+ - 旁白占比与语速是 `labeled` 精度:来自 Agent 的音轨标注与粗 ASR 窗口,不是逐词对齐。
108
+ - 泄漏扫描基于字面匹配,Agent 仍需自查方法是否在复述原片剧情。