@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
@@ -12,9 +12,10 @@ description: >
12
12
  本技能负责最终合成:
13
13
 
14
14
  1. 把各段旁白音频放到视频时间线上。
15
- 2. 在旁白窗口内压低原声,支持 fixed / sidechain / zone 模式。
16
- 3. 根据旁白位置生成 `subtitles.srt`;默认同时生成并烧录 `subtitles.ass`,`--no-burn-subtitles` 可关闭。
17
- 4. 可选把最终响度标准化到目标 LUFS。
15
+ 2. 在旁白窗口内用固定包络压低原声(盖住原声对白时与落在安静段时各用一档音量),间隙恢复原声。
16
+ 3. 根据旁白位置生成 `subtitles.srt`;默认同时生成并烧录 `subtitles.ass`,`--no-burn-subtitles` 可关闭。不烧录时(关闭或降级),`subtitles.srt` 复制到成片旁,名为 `recap_<stem>.srt`;烧录时删掉旧的同名外挂字幕。
17
+ 4. 可选把最终响度标准化到目标 LUFS:两遍 loudnorm,只用一个恒定增益,不做动态压缩;混音的真峰值放不下这么大的增益时,先过 4 倍过采样的真峰值限幅器(最多削 `LOUDNESS_LIMITER_MAX_DB`,默认 6 dB),超出部分才下调目标响度。真峰值目标针对交付的 AAC 文件:AAC 编码会让真峰值比 PCM 混音高 0.3–1.4 dB,所以首次渲染瞄准 TP 下 0.5 dB,渲染后解码成片实测;仍超过 -1 dBTP 时按超出量再降 0.1 dB 只重编码音频(画面流复制),最多两次,仍超出则以 `delivered_true_peak_over_target` 阻断。ffmpeg 实际用的模式、限幅量与成片实测值记在 `assembly_qc.json` 的 `loudness_mode` 与 `loudnorm_final_pass`(`delivered`)。
18
+ 5. 成片不带原片的容器元数据(`title`、`comment` 等标签与章节)。
18
19
 
19
20
  ## 2. 声音收尾契约
20
21
 
@@ -69,21 +70,26 @@ python3 scripts/assemble.py <video> --work-dir <work_dir> \
69
70
  - 音频模式的处理与冻结语义见 `references/audio-modes.md`。默认仍为 `narration`;另外两种模式必须显式选择。
70
71
  - `--audio-mix-adoption` 只与显式 `--tts-meta`、`--narration-adoption` 同时使用;它保留 `narration` 模式名,但跳过旧速度/适配、原声 handoff、环境 BGM、duck、loudnorm 和 limiter。
71
72
  - 音频按轨道混合:原声、可选 BGM 与旁白各自独立。
73
+ - 段落首块(与上一块作者留白超过 1.6 秒)从写的 `start` 放置,但不早于上一块实际结尾加它的 `pause_after_ms`,上一块超时会把它往后推;段落内后续块紧接上一块的实际结尾(间隔 0.35 秒),最多比写的 `start` 提前 1.2 秒。这一提前发生在旁白校验之后,所以提前的那一段不能进入原声对白:有对白时最多提前到最后一段对白结束处。真的提前了的块在 `source_entry_status` 记 `paragraph_tightened`,并在 `written_start` 记写的 `start`。
72
74
  - 旁白不做任何容差裁尾;温和加速后仍放不下即 `no_safe_fit`。每段 `_placed_*.wav`
73
75
  必须与序列化后的时间线区间等长或更短,否则 `timeline_audio_mismatch` 阻断。
74
76
  - 已采用配音的 v1 合同只支持原速、禁止段内适速;不能让环境默认 1.15 倍速或旧缓存覆盖它。放不下就修订安排,不裁尾。严格运行使用新工作目录与新输出路径;输入/实际混音来源变动或 QC 失败时,不发布候选成片。没有采用文件的旧入口仍是兼容模式,不自动获得同等证据。
75
- - 原声在旁白结束后保持压低到下一可靠句末的 `pause_start`,只在实测停顿内渐强,
76
- 于 `source_restore_at` 回满;无后续锚点时保持压低到时间线末端,而不是放出半句。
77
+ - 原声在旁白结束后最多再压低 3 秒,等这段时间内的下一个句末锚点:压到它的 `pause_start`,只在实测停顿内渐强,
78
+ 于 `source_restore_at` 回满(锚点为 `unverified` 时状态记 `sentence_boundary_unverified`)。3 秒内没有锚点时,
79
+ 离时间线末端不足 3 秒就压到末端(`held_to_timeline_end`),否则在旁白结束处直接回满(`bounded_release`),
80
+ 不为远处的锚点长时间压住原声对白。`assembly_qc.json` 的 `summary.max_source_duck_hold_seconds` 记录最长的压低延续。
77
81
  - `--export-jianying` / `EXPORT_JIANYING=1` 可把 `timeline.json` 导出为可编辑草稿。cut 模式应传 `--source-video <orig>`,让草稿引用真实原片区间。
78
82
  - 剪映导出默认把视频、音频与图片复制到 `Resources/local/{video,audio,image}`,保持草稿可搬迁;`--jianying-no-bundle-media` 只适合原路径始终可访问的情况。
79
83
  - 重叠覆盖物会拆到编号轨道;非空目标目录不会覆盖,而会创建编号兄弟目录。
80
- - 常速、倒放、变换、富文本、转场、蒙版、LUT、绿幕复合草稿及显式特效轨道通过 timeline v2 扩展表达。需要素材包的功能只接受调用方合法提供的离线资源。
84
+ - 导出只接受 `schema_version: 2` 的 `timeline.json`,只映射视频、音频、字幕和图片叠层(`scale` / `position`);变速、转场、蒙版、富文本、特效轨等手写扩展字段会被明确拒绝。
81
85
  - 剪映草稿引用未烧录的源视频,因此原片硬字幕仍会保留,必要时在剪映内另行遮罩。
82
86
  - 字幕外观可用 `SUBTITLE_FONT_SIZE`、`SUBTITLE_MARGIN_V`、`SUBTITLE_MAX_CHARS` 等控制。
83
- - `SUBTITLE_Y_TOP/BOT` 把 ASS 基线放到测得的原片字幕区域,坐标为半开 `[top, bot)`;显式遮罩策略下默认 `SUBTITLE_MASK_OPACITY=0.6`,`SOURCE_SUBTITLE_MASK_TIMING=narration`。
84
- - 原声在旁白间隙回到 `IDLE_ORIG_VOLUME`,旁白下压到 `SPEECH_DUCKING_VOLUME`;`DUCK_FADE_SECONDS` 控制过渡。还可配置 `DUCKING_MODE`、`ZONE_DUCKING_VOLUME`、`FINAL_LOUDNORM` 与 `TARGET_LUFS`。
87
+ - `SUBTITLE_Y_TOP/BOT` 把 ASS 基线放到测得的原片字幕区域,坐标为显示画布上的半开 `[top, bot)`,只接受方形或近方形像素(SAR 与 1:1 相差不超过 2%,未标注的 `0:1` 按方形);显式遮罩策略下默认 `SUBTITLE_MASK_OPACITY=0.6`,`SOURCE_SUBTITLE_MASK_TIMING=narration`。
88
+ - 原声在旁白间隙回到 `IDLE_ORIG_VOLUME`,旁白下压到 `SPEECH_DUCKING_VOLUME`;`DUCK_FADE_SECONDS` 控制过渡。还可配置 `DUCK_BRIDGE_SECONDS`、`ZONE_DUCKING_VOLUME`、`FINAL_LOUDNORM`、`TARGET_LUFS` 与 `LOUDNESS_LIMITER_MAX_DB`。
85
89
  - 可通过 `BGM_PATH` 指定 BGM;它会循环到成片长度,并按 `BGM_VOLUME` / `BGM_DUCKING_VOLUME` 混音。不要在没有创作依据时设置通用 BGM。
86
- - 烧录字幕需要带 `subtitles` / libass 的 ffmpeg;合成阶段会预检并在缺失时明确失败。
90
+ - 烧录字幕需要带 `subtitles` / libass 的 ffmpeg,合成阶段在渲染前预检。显式要求烧录(`--burn-subtitles` 或环境变量 `BURN_SUBTITLES`)时缺 libass 直接失败;只是默认开启时降级:不烧录,交付外挂 `.srt`(留白里的 `「」` 原声对白仅在有 `user_subtitles.*` 时照常写进去;原本由遮罩触发的对白随遮罩一起关闭,原片硬字幕可见),`visual_qc.json` 的 `warnings` 与 `assembly_manifest.json` 的 `warnings` 记一条 `subtitle_burn_degraded`,`subtitles.burn_degraded_reason` 写原因。降级后遮罩照旧关闭(`mask.trigger` 为 `burn_subtitles_degraded`)。
91
+ - `visual_overlays.json` 的文字叠加用 ffmpeg `drawtext`(libfreetype)。缺 drawtext 时合成在渲染前失败;叠加是写稿时明确加的内容,不会被静默丢掉。
92
+ - 成片画面一律是 H.264 8-bit 4:2:0 并带 `+faststart`:不需要滤镜、且源画面已是 H.264 8-bit 4:2:0(`yuv420p`,或全范围的 `yuvj420p`)、宽高为偶数时才流复制,原样交付;否则重编码为 `yuv420p`。色彩标记:源未标记或已是 BT.709 时标为 BT.709(`-colorspace/-color_primaries/-color_trc bt709`),其它已声明的色彩空间原样保留;全范围(`pc`)源保留 `pc`,其余标 `tv`。YUV 源只改标记,不转换像素;RGB 源(ffprobe 报 `gbr`,如 PNG/QuickTime RLE 封装的 MOV)没有 YUV 矩阵可保留,按 BT.709 limited 转换。结果记在 `assembly_qc.json` 的 `delivery_qc.color_tags`(RGB 源多一个 `from_rgb: true`)。
87
93
  - 原声留白中的对白字幕优先读取 Agent 校对的 `original_subtitles.json`;否则保守映射 ASR。只有遮罩覆盖留白或用户字幕明确要求替换时才烧录原声对白,并用 `「」` 与旁白区分。
88
94
 
89
95
  ### 按原片区间准备声音,而不是整体压低旧成片
@@ -96,9 +102,9 @@ python3 scripts/assemble.py <video> --work-dir <work_dir> \
96
102
  ## 7. 字幕与可选包装
97
103
 
98
104
  先锁定画面、剪点、旁白和混音,再投入字幕动画或边框包装;字幕样式不能掩盖叙事、剪点或声音问题。
99
- 普通交付优先使用现有 ASS 路径;只有用户需要逐 cue 排版、动画或透明图层时,才用项目级代码渲染器,
100
- 并按 `references/foreground-compose.md` 把它生成的 RGBA 序列叠到锁定母版。包装顺序与样帧抽检清单见
101
- `references/packaging.md`。
105
+ 普通交付优先使用现有 ASS 路径;整片不动的包框、标题条和角标用 `packaging_layers.json` 静态叠加;
106
+ 只有用户需要逐 cue 排版、动画或透明图层时,才用项目级代码渲染器,由该项目自己把透明层合成到锁定母版。
107
+ 包装顺序与样帧抽检清单见 `references/packaging.md`。
102
108
 
103
109
  ## 8. 能力边界
104
110
 
@@ -106,7 +112,3 @@ python3 scripts/assemble.py <video> --work-dir <work_dir> \
106
112
  - 字幕烧录默认开启;关闭时不会重编码绘制字幕区域。
107
113
 
108
114
  显式输出轴字幕轨的独立合同、完整替换语义和当前边界见 `references/subtitle-track.md`。
109
-
110
- 画面回原片重建后,若需保留另一文件中的已采用完整混音,先按
111
- `references/pair-media.md` 显式配对独立画面与音轨。配对只复制流,不补字幕或片名卡;
112
- 后续字幕轨必须重新绑定配对后的容器与 `a:0`,不能继续沿用旧版本身份。
@@ -64,7 +64,9 @@ narration binding path, the final decoded PCM facts, the final AAC decoder/packe
64
64
  count/payload bytes, and the actual output picture decoder/frame clock. The output
65
65
  picture must preserve fps, frame count, zero start, and duration. A video-copy path
66
66
  reports `packet_identity: EXACT` when decoder parameters and packet sizes/timestamps
67
- are unchanged; an allowed visual re-encode reports `REENCODED_CLOCK_MATCH`. Timeline,
67
+ are unchanged; an allowed visual re-encode reports `REENCODED_CLOCK_MATCH`. The colour
68
+ labels (`color_range` / `color_space` / `color_transfer` / `color_primaries`) are left out of
69
+ that comparison, because the final render deliberately labels an untagged picture BT.709. Timeline,
68
70
  settings, QC, and manifest identify the explicit path rather than reporting ambient
69
71
  ducking/BGM/loudnorm operations.
70
72
 
@@ -4,12 +4,12 @@ These JSON protocol templates are pinned to
4
4
  [`duoec/duo-video`](https://github.com/duoec/duo-video) commit
5
5
  `ef4eb46c823910553f901649f2f13fd7575e748f`, under its MIT license. They are
6
6
  data/schema baselines, not executable upstream code. Runtime builders deep-copy
7
- the templates and replace authored values such as IDs, paths, timings, canvas
8
- dimensions, and resource configuration.
7
+ the templates and replace authored values such as IDs, paths, timings, and canvas
8
+ dimensions. Only the templates the exporter builds from are kept; the nested
9
+ compound-draft templates were dropped together with green-screen export.
9
10
 
10
11
  The required copyright and permission notice is preserved in
11
12
  `LICENSE.duo-video` in this directory.
12
13
 
13
- The exporter never uses duo-video's embedded example credentials. Resource-ID
14
- features accept an offline `material` or `resource_config` payload so official
15
- resource packages can be supplied legally by the caller.
14
+ The exporter never uses duo-video's embedded example credentials and never
15
+ emits resource-ID materials (stickers, effects, transitions, text templates).
@@ -13,9 +13,9 @@
13
13
  4. 渲染完整透明字幕层,再 overlay 到锁定母版;合成后确认音轨未被意外改写。
14
14
  5. 完整播放实际最终文件,并复查字幕遮脸、跳字、断行、首尾帧和边界处残影。
15
15
 
16
- 已有项目级渲染器能输出精确包装时,使用 `foreground-compose.md` 将它生成的 RGBA 序列叠到锁定母版,
17
- 而不是用通用白字黑框近似品牌样式。该操作保留实际帧钟和 AAC 包,不生成字体或文案;只有通过验证的
18
- 新文件才写入新目录。片名卡有渐显或动画时须提供完整序列,不能冻结最后一张图代替。
16
+ 已有项目级渲染器能输出精确包装时,由该项目自己把它生成的透明层合成到锁定母版,而不是用通用白字黑框
17
+ 近似品牌样式;本技能不提供逐帧前景合成命令。合成时音轨按流复制,合成后核对帧率、时长与音轨未被改写,
18
+ 再写入新文件。片名卡有渐显或动画时须渲染完整序列,不能冻结最后一张图代替。
19
19
 
20
20
  ## 静态包装图层 `packaging_layers.json`
21
21
 
@@ -37,7 +37,7 @@
37
37
  - `timeline.json` 同时得到对应的 image 轨,剪映草稿里的包框可单独编辑。
38
38
  - `assembly_manifest.json` 的 `video_filters.packaging_layers` 记录每张图片的路径与 `{size, mtime_ns}`。
39
39
  - 编排器按项目绑定的 `packaging` 模板写出这个文件时会带 `written_by` 标记;
40
- 手写的文件不会被它删除。有渐显或动画的包装仍走 `foreground-compose.md`。
40
+ 手写的文件不会被它删除。有渐显或动画的包装仍由项目级渲染器自行合成。
41
41
 
42
42
  ## 原则
43
43
 
@@ -15,7 +15,7 @@ from subtitles.track import load_subtitle_track
15
15
  loaded = load_subtitle_track(
16
16
  "subtitle_track.json",
17
17
  expected_picture_identity={
18
- "path": "/project/paired.mp4",
18
+ "path": "/project/master.mp4",
19
19
  "edit_plan": "/project/edit_plan.json",
20
20
  },
21
21
  expected_audio_identity={
@@ -66,7 +66,7 @@ reports the labels present; it is not an aggregate precision verdict.
66
66
  "overlap_policy": "forbid",
67
67
  "bindings": {
68
68
  "picture": {
69
- "path": "/project/paired.mp4",
69
+ "path": "/project/master.mp4",
70
70
  "edit_plan": "/project/edit_plan.json"
71
71
  },
72
72
  "audio": {
@@ -105,8 +105,8 @@ reports the labels present; it is not an aggregate precision verdict.
105
105
  - `attribution.kind` is `source` or `narration`; `ref` identifies the source
106
106
  utterance or narration item without changing its text.
107
107
  - Unknown fields and schema versions other than integer `1` are rejected, so a
108
- newer producer cannot be silently interpreted as v1. Legacy `sha256` /
109
- `edit_sha256` binding keys are the one exception: they are ignored.
108
+ newer producer cannot be silently interpreted as v1. That includes `sha256` /
109
+ `edit_sha256` binding keys.
110
110
 
111
111
  ## Independent binding checks
112
112
 
@@ -175,12 +175,12 @@ as current facts.
175
175
  Actual decoded frame PTS are read before projection. Integer cue ticks and the
176
176
  rational timebase are retained until each boundary is resolved to the first
177
177
  frame at or after it. `subtitle_track_validation.json` records original ticks,
178
- resolved frame indexes/PTS, quantization deltas, ASS thresholds, validation
179
- schema and projector version. SRT and timeline consume resolved frame seconds;
180
- ASS thresholds are chosen to switch on those same frames despite ASS's 10ms
181
- clock. A cue with no visible frame, or a boundary that ASS cannot distinguish,
182
- is rejected rather than silently dropped. The original author file is not
183
- rewritten. Legacy subtitles keep their previous rendering behavior.
178
+ resolved frame indexes/PTS, quantization deltas and ASS thresholds. SRT and
179
+ timeline consume resolved frame seconds; ASS thresholds are chosen to switch on
180
+ those same frames despite ASS's 10ms clock. A cue with no visible frame, or a
181
+ boundary that ASS cannot distinguish, is rejected rather than silently dropped.
182
+ The original author file is not rewritten. Legacy subtitles keep their previous
183
+ rendering behavior.
184
184
 
185
185
  Each consumption re-checks the track/media/optional edit plan `{size, mtime_ns}`
186
186
  and the consumer duration; a rewritten input is stale and must be prepared
@@ -7,7 +7,7 @@ import subprocess
7
7
  from assemble_constants import frame_clock_samples
8
8
  from adoption.frozen_audio import probe_audio_packets
9
9
  import adoption.narration_binding as narration_binding
10
- from pair_media import probe_picture, validate_pair_timing
10
+ from adoption.av_clock import probe_picture, validate_pair_timing
11
11
  import source_score
12
12
  from adoption.strict_inputs import (
13
13
  read_json_bytes, require_fields, require_integer, require_local_path, require_number,
@@ -204,6 +204,16 @@ def render_explicit_mix(context, narration_context, tts_segments, work_dir):
204
204
  return runtime
205
205
 
206
206
 
207
+ _COLOR_TAG_KEYS = ("color_range", "color_space", "color_transfer", "color_primaries")
208
+
209
+
210
+ def _without_color_tags(picture):
211
+ """The picture identity minus the colour labels the final render writes on purpose
212
+ (an untagged copied picture gains BT.709 tags; its packets stay the same)."""
213
+ decoder = {k: v for k, v in picture["decoder"].items() if k not in _COLOR_TAG_KEYS}
214
+ return {**picture, "decoder": decoder}
215
+
216
+
207
217
  def finalize_binding(context, narration_record, rendered_output, final_output, work_dir):
208
218
  """Probe the rendered candidate and write ``audio_mix_binding.json`` into work_dir."""
209
219
  if not context.get("runtime"):
@@ -216,7 +226,8 @@ def finalize_binding(context, narration_record, rendered_output, final_output, w
216
226
  if output_clock[key] != input_clock[key]:
217
227
  raise ValueError("rendered output picture frame clock changed")
218
228
  packet_identity = (
219
- "EXACT" if output_picture == context["picture_identity"]
229
+ "EXACT"
230
+ if _without_color_tags(output_picture) == _without_color_tags(context["picture_identity"])
220
231
  else "REENCODED_CLOCK_MATCH"
221
232
  )
222
233
  audio = probe_audio_packets(rendered, 0)
@@ -0,0 +1,109 @@
1
+ """Exact picture and AAC clocks for the frozen-audio adoption paths.
2
+
3
+ ``probe_picture`` reads the full CFR presentation clock of a picture stream;
4
+ ``validate_aac_packet_interval`` proves an AAC stream's packets tile its header
5
+ interval; ``validate_pair_timing`` checks the two intervals agree within one frame
6
+ or one AAC packet. These are narrow container-clock gates, not a perceptual
7
+ synchronization verdict.
8
+ """
9
+
10
+ from fractions import Fraction
11
+
12
+ from assemble_constants import SUPPORTED_PICTURE_CODECS
13
+ from adoption.strict_inputs import probe_json
14
+
15
+
16
+ def _time(value):
17
+ if value in (None, 'N/A'):
18
+ raise ValueError('Missing media timing')
19
+ try:
20
+ return Fraction(str(value))
21
+ except (ValueError, ZeroDivisionError) as exc:
22
+ raise ValueError('Invalid media timing') from exc
23
+
24
+
25
+ def probe_picture(path):
26
+ """Actual full CFR presentation clock plus codec-order packet sizes and timestamps."""
27
+ data = probe_json(path, '-select_streams', 'v:0', '-show_streams', '-show_packets',
28
+ '-show_format', '-show_entries',
29
+ 'format=format_name:stream=codec_name,profile,level,width,height,pix_fmt,'
30
+ 'sample_aspect_ratio,field_order,color_range,color_space,color_transfer,'
31
+ 'color_primaries,chroma_location,time_base,start_pts,duration_ts,avg_frame_rate'
32
+ ':packet=pts,dts,duration,size,side_data_list')
33
+ streams = data.get('streams', [])
34
+ if len(streams) != 1 or streams[0].get('codec_name') not in SUPPORTED_PICTURE_CODECS:
35
+ raise ValueError('Picture clock requires one selected H264/HEVC picture stream')
36
+ if 'mp4' not in data.get('format', {}).get('format_name', '').split(','):
37
+ raise ValueError('Picture clock currently requires an MP4-family container')
38
+ v = streams[0]
39
+ fps, tb = _time(v.get('avg_frame_rate')), _time(v.get('time_base'))
40
+ if not 1 <= fps <= 120 or tb <= 0 or v.get('start_pts') != 0:
41
+ raise ValueError('Picture requires positive CFR and a known zero start')
42
+ duration = _time(v.get('duration_ts')) * tb
43
+ frames = probe_json(path, '-select_streams', 'v:0', '-show_frames',
44
+ '-show_entries', 'frame=pts')['frames']
45
+ pts = [_time(frame.get('pts')) * tb for frame in frames]
46
+ if not pts or any(t != Fraction(i, fps) for i, t in enumerate(pts)) or duration != len(pts) / fps:
47
+ raise ValueError('Picture requires complete zero-origin CFR frame clock and exact duration')
48
+ decoder_keys = ['codec_name', 'profile', 'level', 'width', 'height', 'pix_fmt',
49
+ 'sample_aspect_ratio', 'field_order', 'color_range', 'color_space',
50
+ 'color_transfer', 'color_primaries', 'chroma_location']
51
+ packets = []
52
+ for packet in data.get('packets', []):
53
+ if packet.get('size') is None:
54
+ raise ValueError('Picture packet missing size')
55
+ ticks = {key: str(_time(packet.get(key)) * tb) for key in ['pts', 'dts', 'duration']}
56
+ if _time(ticks['duration']) <= 0:
57
+ raise ValueError('Picture packet has invalid duration')
58
+ packets.append({**ticks, 'size': int(packet['size']),
59
+ 'side_data_list': packet.get('side_data_list', [])})
60
+ if len(packets) != len(pts):
61
+ raise ValueError('Picture packet/frame counts disagree')
62
+ return {'decoder': {key: v.get(key) for key in decoder_keys}, 'packets': packets,
63
+ 'frame_pts': [str(t) for t in pts], 'frame_count': len(pts), 'fps': str(fps),
64
+ 'duration': str(duration), 'start': '0'}
65
+
66
+
67
+ def validate_aac_packet_interval(audio):
68
+ """Validate exact AAC packet continuity against the stream-header interval."""
69
+ if audio['codec'] != 'aac':
70
+ raise ValueError('Adopted audio must be AAC')
71
+ packets = audio['packets']
72
+ rate = audio['sample_rate']
73
+ if type(rate) is not int or rate <= 0 or len(packets) < 2:
74
+ raise ValueError('AAC packet timing is incomplete')
75
+ nominal = _time(packets[0]['duration'])
76
+ # Accept known AAC frame sample counts, never an arbitrary huge duration.
77
+ if nominal * rate not in {960, 1024, 1920, 2048}:
78
+ raise ValueError('Unsupported AAC nominal packet duration')
79
+ for i, packet in enumerate(packets):
80
+ d = _time(packet['duration'])
81
+ if d <= 0 or d > nominal or (i < len(packets) - 1 and d != nominal):
82
+ raise ValueError('Nonuniform or invalid AAC packet duration')
83
+ p, t = _time(packet['pts']), _time(packet['dts'])
84
+ if p != t:
85
+ raise ValueError('Unsupported AAC PTS/DTS difference')
86
+ if i and p != _time(packets[i-1]['pts']) + _time(packets[i-1]['duration']):
87
+ raise ValueError('AAC packet clock has gaps or overlaps')
88
+ audio_start, audio_duration = _time(audio['start_time']), _time(audio['duration'])
89
+ if audio_duration <= 0:
90
+ raise ValueError('Invalid audio interval')
91
+ audio_end = audio_start + audio_duration
92
+ first_pts = _time(packets[0]['pts'])
93
+ last_end = _time(packets[-1]['pts']) + _time(packets[-1]['duration'])
94
+ if (not 0 <= audio_start - first_pts <= nominal
95
+ or abs(last_end - audio_end) > Fraction(1, rate)):
96
+ raise ValueError('AAC packet clock does not match stream interval')
97
+ return audio_start, audio_end, nominal
98
+
99
+
100
+ def validate_pair_timing(picture, audio):
101
+ """Narrow AAC/CFR compatibility; not a perceptual synchronization verdict."""
102
+ audio_start, audio_end, nominal = validate_aac_packet_interval(audio)
103
+ tolerance = max(1 / Fraction(picture['fps']), nominal)
104
+ end_delta = audio_end - Fraction(picture['duration'])
105
+ if abs(audio_start) > tolerance or abs(end_delta) > tolerance:
106
+ raise ValueError('Picture/audio interval mismatch; no implicit trim, padding, offset or retime')
107
+ return {'start_delta': str(audio_start),
108
+ 'end_delta': str(end_delta),
109
+ 'tolerance': str(tolerance), 'nominal_aac_packet': str(nominal)}
@@ -30,7 +30,8 @@ def current_audio_mix_binding(work_dir, audio_mode):
30
30
  def publish_render(*, work_dir, binding, explicit_mix, tts_segments, narration_wav,
31
31
  render_output, published_output, audio_mode, audio_operations,
32
32
  adopted_audio, loudness_mode, loudnorm_measurement, visual_qc,
33
- source_has_audio, video_duration, render_delivery, source_audio_status):
33
+ source_has_audio, video_duration, render_delivery, source_audio_status,
34
+ loudnorm_final_pass=None):
34
35
  """Gate the rendered candidate on assembly QC and publish it with its bindings.
35
36
 
36
37
  Returns the final output path. Inactive-binding renders only write their
@@ -77,6 +78,7 @@ def publish_render(*, work_dir, binding, explicit_mix, tts_segments, narration_w
77
78
  source_has_audio=source_has_audio,
78
79
  loudness_mode=loudness_mode,
79
80
  loudnorm_measurement=loudnorm_measurement,
81
+ loudnorm_final_pass=loudnorm_final_pass,
80
82
  visual_qc=visual_qc,
81
83
  audio_mode=audio_mode,
82
84
  audio_operations=audio_operations,
@@ -103,6 +105,7 @@ def publish_render(*, work_dir, binding, explicit_mix, tts_segments, narration_w
103
105
  tts_segments, video_duration, output_path=render_output,
104
106
  source_has_audio=source_has_audio,
105
107
  loudness_mode=loudness_mode, loudnorm_measurement=loudnorm_measurement,
108
+ loudnorm_final_pass=loudnorm_final_pass,
106
109
  visual_qc=visual_qc, audio_mode=audio_mode,
107
110
  audio_operations=audio_operations, adopted_audio=adopted_audio,
108
111
  narration_input_binding=current_binding,
@@ -18,16 +18,9 @@ def _artifact_identity(path):
18
18
  return file_identity(path) if path.exists() else None
19
19
 
20
20
 
21
- def _explicit_source_video():
22
- """Return the cut-mode source video only when the caller opted in explicitly."""
23
- if not CONFIG["source_video_explicit"]:
24
- return ""
25
- return CONFIG["source_video"]
26
-
27
-
28
21
  def _source_video_identity():
29
- """``{path, size, mtime_ns}`` of the explicit cut-mode source video, else None."""
30
- source_video = _explicit_source_video()
22
+ """``{path, size, mtime_ns}`` of the cut-mode --source-video, else None."""
23
+ source_video = CONFIG["source_video"]
31
24
  if not source_video:
32
25
  return None
33
26
  path = Path(source_video)