@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
@@ -50,44 +50,30 @@ def _load_review_grounding(work_dir):
50
50
  """
51
51
  work_dir = Path(work_dir)
52
52
  manifest = _load(work_dir, "multi_source_manifest.json")
53
- sources = manifest.get("sources") if isinstance(manifest, dict) else None
54
- if isinstance(sources, list):
53
+ if manifest is not None:
55
54
  combined_vlm = []
56
55
  combined_asr = []
57
- for source in sources:
58
- if not isinstance(source, dict):
59
- continue
60
- source_id = str(source.get("source_id") or "").strip()
61
- relative_dir = source.get("source_work_dir")
62
- if not source_id or not relative_dir:
63
- continue
64
- source_dir = (work_dir / str(relative_dir)).resolve(strict=False)
56
+ for source in manifest["sources"]:
57
+ source_id = str(source["source_id"])
58
+ source_dir = (work_dir / source["source_work_dir"]).resolve(strict=False)
65
59
  try:
66
60
  source_dir.relative_to(work_dir.resolve(strict=False))
67
61
  except ValueError:
68
62
  continue
69
63
  for scene in _load(source_dir, "vlm_analysis.json") or []:
70
- if isinstance(scene, dict):
71
- item = dict(scene)
72
- item["source_id"] = source_id
73
- combined_vlm.append(item)
74
- clean_asr = _load(source_dir, "asr_clean.json")
75
- raw_asr = (
76
- clean_asr
77
- if clean_asr is not None
78
- else _load(source_dir, "asr_result.json")
79
- )
80
- for segment in _asr_segments(raw_asr):
81
- if isinstance(segment, dict):
82
- item = dict(segment)
83
- item["source_id"] = source_id
84
- combined_asr.append(item)
64
+ combined_vlm.append({**scene, "source_id": source_id})
65
+ for segment in _preferred_asr(source_dir):
66
+ combined_asr.append({**segment, "source_id": source_id})
85
67
  if combined_vlm or combined_asr:
86
68
  return combined_vlm, combined_asr
87
69
 
88
- return _load(work_dir, "vlm_analysis.json") or [], _asr_segments(
89
- _load(work_dir, "asr_result.json")
90
- )
70
+ return _load(work_dir, "vlm_analysis.json") or [], _preferred_asr(work_dir)
71
+
72
+
73
+ def _preferred_asr(work_dir):
74
+ """asr_clean.json wins over asr_result.json, as in lint, cut and assemble."""
75
+ clean = _load(work_dir, "asr_clean.json")
76
+ return _asr_segments(clean if clean is not None else _load(work_dir, "asr_result.json"))
91
77
 
92
78
 
93
79
  def _load_cut_clip_spans(work_dir):
@@ -176,13 +162,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
176
162
 
177
163
  remapped_scenes = []
178
164
  for scene in vlm_analysis or []:
179
- if not isinstance(scene, dict):
180
- continue
181
- try:
182
- start = float(scene.get("start", 0))
183
- end = float(scene.get("end", start))
184
- except (TypeError, ValueError):
185
- continue
165
+ start = float(scene["start"])
166
+ end = float(scene["end"])
186
167
  overlaps = _source_output_overlaps(
187
168
  start,
188
169
  end,
@@ -205,13 +186,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
205
186
 
206
187
  remapped_asr = []
207
188
  for seg in asr_result or []:
208
- if not isinstance(seg, dict):
209
- continue
210
- try:
211
- start = float(seg.get("start", 0))
212
- end = float(seg.get("end", start))
213
- except (TypeError, ValueError):
214
- continue
189
+ start = float(seg["start"])
190
+ end = float(seg["end"])
215
191
  text = str(seg.get("text", "")).strip()
216
192
  if not text:
217
193
  continue
@@ -231,8 +207,6 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
231
207
  item["output_segment_index"] = overlap.get("output_segment_index")
232
208
  remapped_asr.append(item)
233
209
 
234
- remapped_scenes.sort(
235
- key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0)))
236
- )
237
- remapped_asr.sort(key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0))))
210
+ remapped_scenes.sort(key=lambda x: (x["start"], x["end"]))
211
+ remapped_asr.sort(key=lambda x: (x["start"], x["end"]))
238
212
  return remapped_scenes, remapped_asr
@@ -13,7 +13,6 @@ from evidence_bundle import (
13
13
  _in_ranges,
14
14
  _safe_time,
15
15
  build_evidence_bundle,
16
- build_review_coverage_metadata,
17
16
  render_evidence_bundle,
18
17
  )
19
18
  from review_grounding import _load_agent_optional
@@ -42,48 +41,26 @@ CATEGORIES = [
42
41
  "other",
43
42
  ]
44
43
 
45
- SCORECARD_KEYS = [
46
- "promise_match",
47
- "hook_3s",
48
- "first_15s_delivery",
49
- "spine_clarity",
50
- "stakes_escalation",
51
- "information_gain",
52
- "spoken_language",
53
- "sentence_brevity",
54
- "tts_pacing",
55
- "grounding",
56
- "original_audio_use",
57
- "subtitle_readability",
58
- "ending_payoff",
59
- "style_consistency",
60
- "ai_flavor",
61
- "packaging_consistency",
62
- ]
63
-
64
44
  FACTUAL_CATEGORIES = {"hallucination", "incomplete"}
65
45
 
66
- COVERAGE_POLICY_VERSION = "coverage_policy_v1"
67
-
68
46
  RUBRIC = """你是中文视频解说的创作复核编辑。依据素材证据和已有创作计划审阅草稿,只指出真实问题,宁缺毋滥:
69
47
  1. 反幻觉(最重要):解说里的人物、动作、因果、关系必须由带标签的 evidence 支撑。画面/对白是 timeline evidence(clock=SOURCE 或 OUTPUT);背景资料/user_context 只能作为 context-only(clock=null)辅助识别/消歧。research-only 不能升级成当前画面强事实;若与 research 一致但画面/对白里看不到,最多 severity=suggestion/category=grounding_risk,不要判 error;只有与全部可得证据矛盾才是 severity=error, category=hallucination,并指出冲突证据。
70
48
  2. 导演意图:若提供 recap_story_plan.json,检查草稿是否兑现 viewer promise、POV、dramatic question、情绪路径和 chosen_hypothesis;不要另起一条更“吸睛”但不属于该计划的故事。偏离主线 → no_throughline;承诺不兑现 → promise_mismatch/weak_payoff。
71
49
  3. change-based beats:每个 beat 应改变知识、权力、目标、关系、情绪或风险。精简时不能只留下“发生了什么”,还要保住人物动机、接受条件及随后犹豫/行动的必要前提。若一段只重复上一段、删除后什么都不损失,可报 low_information_gain/pacing;不要用固定段数或秒数代替判断。
72
50
  4. 钩子:开头要提出正文真实兑现的戏剧问题/利害,不是交代场景,也不是无关的留存话术。弱钩子 → weak_hook。
73
51
  5. 给信息而非念画面:观众看得见动作表情;解说只增加上下文、因果、预期、证据支持的解释或跨越。复述画面 → narrating_picture。
74
- 6. 视听分工:若提供 visual_audio_board.json,检查 narration_job=none 或 audio_owner=original_dialogue/action_sound/ambience/music/silence 的拍是否被旁白无故覆盖;必须听见的原声被盖住 → original_audio_conflict。沉默和低旁白覆盖本身不是问题。
52
+ 6. 视听分工:若提供 visual_audio_board.json,检查 narration_job=none 或 audio_owner=original_dialogue/action_sound/ambience/music/silence 的拍是否被旁白无故覆盖;必须听见的原声被盖住 → original_audio_conflict。沉默和低旁白覆盖本身不是问题。这些拍与 original_subtitles 的原声字幕块按计划没有旁白,不是跳过或漏写。
75
53
  7. 人物、反应与动作兑现:不要用旁白解释掉素材中已经能成立的表演、停顿或反应。有情感回应或知情变化的反应镜不可机械删;反打是否保留取决于它是否提供新增信息,而非是否达到统一时长。以某个可见结果为看点时,只写 evidence 已呈现的结果,不推断更强的结果(例:证据只到受击,就不能写成倒地或胜负)。当前评审只能检查计划/稿件一致性,不能凭少量帧声称最终剪点一定好坏。
76
54
  8. 密度/节奏与因果边界:7:3 不是配额。只在旁白没有任务、墙到墙压住原声、碎成一句一停,或无意长空档导致因果断裂时,报 density/pacing。不得把跨场镜头拼成同场动作/反应的虚假因果,也不用花字替补源证据或提前宣布结果。
77
55
  9. 去废词:删空泛形容("危机四伏""震撼人心")→ cliche。
78
- 10. 完整句子:半句话/未收尾 → incomplete。
56
+ 10. 完整句子:旁白本身半句话/未收尾 → incomplete。某拍没有旁白不算 incomplete。
79
57
  11. 段落衔接:解说块要为随后的原声留白铺垫,下一块要承接原声刚呈现的变化;若两块各说各的、原声进来接不上 → disjoint_handoff。
80
58
  12. 结尾回收:结尾要兑现开头承诺/主线情绪,不要突然停、只复述最后画面、没有情绪/信息回报;弱回收 → weak_payoff。
81
59
  13. 风格一致性与修改范围:若提供 style_card.json,把它当作表达意图/语气/节奏边界;不符合意图 → style_mismatch。不要把 style_card 当标题/封面/首句包装计划。只提能定位到具体段落、beat 或镜头边界的具体局部修法;REVISION 未点名层默认冻结,不借局部问题重做故事、声音或包装。
82
60
  14. 包装一致性:只有提供 packaging_plan.json 时才评估标题/封面/首句/卖点承诺与正文兑现;缺失不扣分。不一致 → packaging_mismatch。不要让包装反过来改写故事判断。
83
- 15. 去AI味:若出现模板化、空泛拔高、过度对仗、机械转折、明显 agent 示例残留,可报 ai_flavor;若出现示例人物/占位实体泄漏(如未替换示例名、模板角色)→ example_entity_leak。“不是 A,而是 B”本身不是错误,只有在先虚构旧判断再制造假洞察、或反复机械使用时才建议改写。deslop_qc.json 是 deterministic local report-only QC,不是 AIGC detector,不自动重写;只能作为证据参考,不能仅凭它判定。
84
- 另外给一份内容效果 scorecard(1-5,advisory:除事实矛盾/残句外不要据此给 error;缺项可以省略,系统会保留为 null/未评分):promise_match/hook_3s/first_15s_delivery/spine_clarity/stakes_escalation/information_gain/spoken_language/sentence_brevity/tts_pacing/grounding/original_audio_use/subtitle_readability/ending_payoff/style_consistency/ai_flavor/packaging_consistency。`sentence_brevity` 衡量的是句子是否简洁而完整,不是越短越高;连续短句造成串珠式 TTS 应降低分数。ai_flavor 分数含义:5=自然、人味强,1=AI味明显。
61
+ 15. 去AI味:若出现模板化、空泛拔高、过度对仗、机械转折、明显 agent 示例残留,可报 ai_flavor;若出现示例人物/占位实体泄漏(如未替换示例名、模板角色)→ example_entity_leak。“不是 A,而是 B”本身不是错误,只有在先虚构旧判断再制造假洞察、或反复机械使用时才建议改写。
85
62
  只返回 JSON(不要额外解释),格式:
86
- {"verdict":"PASS|REVISE|FAIL","summary":"一两句总体判断","scorecard":{"promise_match":1-5,"hook_3s":1-5,"first_15s_delivery":1-5,"spine_clarity":1-5,"stakes_escalation":1-5,"information_gain":1-5,"spoken_language":1-5,"sentence_brevity":1-5,"tts_pacing":1-5,"grounding":1-5,"original_audio_use":1-5,"subtitle_readability":1-5,"ending_payoff":1-5,"style_consistency":1-5,"ai_flavor":1-5,"packaging_consistency":1-5},"hook_candidates_review":[{"candidate":"首句","type":"suspense|contrast|stakes","score":1-5,"keep":true}],"retention_risk_points":[{"time":"00:28","risk":"为什么可能掉人","fix":"怎么改"}],"highest_return_edits":["最值得改的动作"],"information_gain_notes":[{"segment":0,"label":"motive|relationship|stakes|foreshadowing|payoff|context|visual_restatement","note":"证据/改法"}],"spoken_language_rewrites":[{"segment":0,"original":"原句","rewrite":"口语改写","why":"为什么更适合听"}],"grounding_assertions":[{"segment":0,"assertion":"人物/关系/因果断言","source":"visual|asr|research|user_context|unsupported","risk":"谨慎说明"}],"findings":[{"segment":<草稿段号(从0起)或null表示整体>,"severity":"error|warning|suggestion","category":"<上面类别之一>","issue":"问题","fix":"具体改法"}]}"""
63
+ {"verdict":"PASS|REVISE|FAIL","summary":"一两句总体判断","findings":[{"segment":<草稿段号(从0起)或null表示整体>,"severity":"error|warning|suggestion","category":"<上面类别之一>","issue":"问题","fix":"具体改法"}]}"""
87
64
 
88
65
 
89
66
  def merge_review_findings(chunks):
@@ -173,7 +150,7 @@ def _merge_chunk_reviews(chunk_reviews):
173
150
  return parse_review_response("")
174
151
  if len(chunk_reviews) == 1:
175
152
  return chunk_reviews[0]
176
- verdict_rank = {"PASS": 0, "OK": 0, "REVISE": 1, "FAIL": 2}
153
+ verdict_rank = {"PASS": 0, "REVISE": 1, "FAIL": 2}
177
154
  best = max(chunk_reviews, key=lambda r: verdict_rank[r["verdict"]])
178
155
  merged = dict(best)
179
156
  merged["findings"] = merge_review_findings(chunk_reviews)
@@ -189,33 +166,6 @@ def _merge_chunk_reviews(chunk_reviews):
189
166
  return merged
190
167
 
191
168
 
192
- def _research_guardrail_qc(review, context_items):
193
- # Assertion keys are whatever the judge returned; only the list itself is normalized.
194
- assertions = review["grounding_assertions"]
195
- research_context_assertions = [
196
- a
197
- for a in assertions
198
- if str(a.get("source", "")).lower() == "research"
199
- or a.get("support") == "context_only"
200
- or a.get("clock") is None
201
- ]
202
- risk_assertions = [
203
- a
204
- for a in research_context_assertions
205
- if "spoiler" in str(a.get("risk", "")).lower()
206
- or "research-only" in str(a.get("risk", "")).lower()
207
- or "current-timeline" in str(a.get("risk", "")).lower()
208
- ]
209
- grounding_risk_findings = [
210
- f for f in review["findings"] if f["category"] == "grounding_risk"
211
- ]
212
- return {
213
- "context_only_assertions": len(context_items) + len(research_context_assertions),
214
- "spoiler_risk_assertions": len(risk_assertions) + len(grounding_risk_findings),
215
- "policy": "research evidence is context_only unless visual/asr-supported",
216
- }
217
-
218
-
219
169
  def _load_review_research_context(work_dir):
220
170
  """Agent-authored background_research.json: {} when absent; corrupt JSON raises
221
171
  (same policy as the brief's loader), a non-object document fails open to {}."""
@@ -317,41 +267,89 @@ def _format_json_context(title, value, limit=3000):
317
267
  return f"## {title}\n{text[:limit]}"
318
268
 
319
269
 
320
- def _clamp_score(value, default=3):
321
- try:
322
- score = int(round(float(value)))
323
- except (TypeError, ValueError):
324
- score = default
325
- return max(1, min(5, score))
326
-
327
-
328
- def _normalise_scorecard(raw):
329
- # Keep a stable scorecard schema, but do NOT fabricate a judge-looking score for a dimension
330
- # the model omitted: those stay None ("未评分") rather than a neutral-looking 3.
331
- source = raw if isinstance(raw, dict) else {}
332
- return {
333
- key: (_clamp_score(source[key]) if key in source else None)
334
- for key in SCORECARD_KEYS
335
- }
336
-
270
+ # audio_owner values that leave a beat to the source track (playbook: visual_audio_board).
271
+ ORIGINAL_AUDIO_OWNERS = frozenset(
272
+ ("original_dialogue", "action_sound", "ambience", "music", "silence")
273
+ )
274
+ _MAX_ORIGINAL_AUDIO_LINES = 40
275
+
276
+
277
+ def _beat_window(beat, clock):
278
+ """(label, start, end) on `clock` when the beat carries it, else on the other clock."""
279
+ for name in (clock, "source" if clock == "output" else "output"):
280
+ start, end = beat.get(f"{name}_start"), beat.get(f"{name}_end")
281
+ if (
282
+ isinstance(start, (int, float))
283
+ and isinstance(end, (int, float))
284
+ and not isinstance(start, bool)
285
+ and not isinstance(end, bool)
286
+ and end > start
287
+ ):
288
+ return name.upper(), float(start), float(end)
289
+ return None
290
+
291
+
292
+ def _original_audio_beats(board, plan):
293
+ """Beats the plan leaves to the source track: audio_owner is original audio or
294
+ narration_job is none. visual_audio_board owns audio decisions, so a story-plan beat
295
+ counts only when the board does not list the same beat_id."""
296
+ board_items = board.get("items") if isinstance(board, dict) else None
297
+ plan_beats = plan.get("beats") if isinstance(plan, dict) else None
298
+ board_items = [b for b in board_items or [] if isinstance(b, dict)]
299
+ board_ids = {str(b.get("beat_id")) for b in board_items if b.get("beat_id") is not None}
300
+ candidates = board_items + [
301
+ b
302
+ for b in plan_beats or []
303
+ if isinstance(b, dict) and str(b.get("beat_id")) not in board_ids
304
+ ]
305
+ return [
306
+ beat
307
+ for beat in candidates
308
+ if str(beat.get("audio_owner", "")).strip() in ORIGINAL_AUDIO_OWNERS
309
+ or str(beat.get("narration_job", "")).strip() == "none"
310
+ ]
337
311
 
338
- def _normalise_list_of_dicts(value, allowed_keys):
339
- out = []
340
- for item in value or []:
341
- if isinstance(item, dict):
342
- out.append({key: item.get(key) for key in allowed_keys if key in item})
343
- return out
344
312
 
313
+ def _format_original_audio_holds(board, plan, subtitles, clock):
314
+ """Name every interval the plan deliberately leaves without narration.
345
315
 
346
- def _normalise_string_list(value, limit=12):
347
- out = []
348
- for item in value or []:
349
- text = str(item).strip()
350
- if text:
351
- out.append(text)
352
- if len(out) >= limit:
353
- break
354
- return out
316
+ The planning JSON is clipped to a few thousand characters, so without this list a
317
+ reviewer can see a story beat with no narration and report it as skipped.
318
+ """
319
+ lines = []
320
+ for beat in _original_audio_beats(board, plan):
321
+ window = _beat_window(beat, clock)
322
+ when = f"{window[0]} {window[1]:.1f}-{window[2]:.1f}s" if window else "时间未标"
323
+ owner = str(beat.get("audio_owner", "")).strip() or "?"
324
+ job = str(beat.get("narration_job", "")).strip() or "?"
325
+ anchor = _clip_text(
326
+ beat.get("original_audio_anchor") or beat.get("must_keep_moment"), 60
327
+ )
328
+ tail = f" 原声锚点:{anchor}" if anchor else ""
329
+ lines.append(
330
+ f"- [{when}] beat {beat.get('beat_id', '?')} audio_owner={owner} narration_job={job}{tail}"
331
+ )
332
+ for row in subtitles if isinstance(subtitles, list) else []:
333
+ if not isinstance(row, dict):
334
+ continue
335
+ start, end = _safe_time(row, "start", None), _safe_time(row, "end", None)
336
+ text = _clip_text(row.get("text"), 60)
337
+ if start is None or end is None or end <= start or not text:
338
+ continue
339
+ lines.append(f"- [{clock.upper()} {start:.1f}-{end:.1f}s] 原声字幕块「{text}」")
340
+ if not lines:
341
+ return ""
342
+ dropped = len(lines) - _MAX_ORIGINAL_AUDIO_LINES
343
+ lines = lines[:_MAX_ORIGINAL_AUDIO_LINES]
344
+ if dropped > 0:
345
+ lines.append(f"- …另有 {dropped} 条同类区间未列出")
346
+ return (
347
+ "## 计划内留给原声的区间(不是漏写)\n"
348
+ "以下拍在 visual_audio_board/recap_story_plan 里由原声或沉默拥有,或是 original_subtitles 的原声字幕块。"
349
+ "这些区间没有旁白是有意的:不要报为跳过、缺失或漏写,也不要据此给 incomplete、no_throughline、"
350
+ "low_information_gain 或 density。只有旁白闯入并盖住必须听见的原声时报 original_audio_conflict;"
351
+ "相邻旁白块与这段原声接不上时报 disjoint_handoff。\n" + "\n".join(lines)
352
+ )
355
353
 
356
354
 
357
355
  def _format_draft(narration):
@@ -393,27 +391,33 @@ def build_review_messages(
393
391
  research=research_obj,
394
392
  )
395
393
  evidence_text = render_evidence_bundle(bundle)
396
- # Optional, agent-authored planning/QC artifacts. Bad JSON returns None via _load, matching
397
- # existing fail-open optional artifact behavior; these only sharpen advisory scoring.
394
+ # Optional, agent-authored planning artifacts. Bad JSON returns None via _load, matching
395
+ # existing fail-open optional artifact behavior; these only sharpen the craft findings.
398
396
  packaging = _load_optional_json(work_dir, "packaging_plan.json")
399
397
  story_plan = _load_optional_json(work_dir, "recap_story_plan.json")
400
398
  av_board = _load_optional_json(work_dir, "visual_audio_board.json")
401
399
  style_card = _load_optional_json(work_dir, "style_card.json")
402
- deslop_qc = _load_optional_json(work_dir, "deslop_qc.json")
400
+ holds = _format_original_audio_holds(
401
+ av_board,
402
+ story_plan,
403
+ _load_optional_json(work_dir, "original_subtitles.json"),
404
+ bundle["clock"],
405
+ )
406
+ holds_block = f"{holds}\n\n" if holds else ""
403
407
  user = (
404
408
  f"{RUBRIC}\n\n"
405
- "## Scorecard 评估提示\n"
406
- "若 work_dir 提供了 packaging_plan/recap_story_plan/visual_audio_board/style_card/deslop_qc,则结合评估:"
409
+ "## 创作计划参考\n"
410
+ "若 work_dir 提供了 packaging_plan/recap_story_plan/visual_audio_board/style_card,则结合评审:"
407
411
  "packaging_plan 只负责标题/封面/首句/卖点承诺与正文兑现;style_card 只负责表达意图、语气、节奏和禁忌;"
408
- "recap_story_plan 是导演意图/备选假设/chosen POV/change-based beats 的基线;visual_audio_board 是画面/表演/原声/audio_owner/narration_job/剪辑锚点的基线;deslop_qc 是 deterministic local report-only QC,不是 AIGC detector,也不会自动重写,只能当证据参考。"
409
- "若未提供,则基于解说本身与画面/对白证据评分,但不得因计划文件缺失给 error。统一评估:hook 是否真实兑现;每段是否产生变化/信息增量而非看图说话;"
412
+ "recap_story_plan 是导演意图/备选假设/chosen POV/change-based beats 的基线;visual_audio_board 是画面/表演/原声/audio_owner/narration_job/剪辑锚点的基线。"
413
+ "若未提供,则基于解说本身与画面/对白证据评审,但不得因计划文件缺失给 error。统一评估:hook 是否真实兑现;每段是否产生变化/信息增量而非看图说话;"
410
414
  "结尾是否兑现开头承诺/主线情绪;是否写给耳朵听(连续完整思路、自然口语、TTS可呼吸,而非一句一停);人物/关系/因果断言是否有 visual/ASR timeline evidence;research/user_context 仅可作为 context-only 辅助。\n"
411
415
  "审美/风格/包装/去AI味项是 advisory:可 REVISE,但除事实矛盾/残句外不要给 error。\n\n"
412
416
  f"{_format_json_context('packaging_plan.json(标题/封面/首句/卖点包装承诺,可能为空)', packaging)}\n\n"
413
417
  f"{_format_json_context('recap_story_plan.json(主线/beats/original moments,可能为空)', story_plan)}\n\n"
414
418
  f"{_format_json_context('visual_audio_board.json(画面/原声/字幕/剪辑锚点,可能为空)', av_board)}\n\n"
415
419
  f"{_format_json_context('style_card.json(表达意图/语气/节奏/禁忌,不负责包装,可能为空)', style_card)}\n\n"
416
- f"{_format_json_context('deslop_qc.json(deterministic report-only QC;非AIGC检测器;不自动重写,可能为空)', deslop_qc)}\n\n"
420
+ f"{holds_block}"
417
421
  f"## 背景资料(context-only/advisory:只辅助识别/消歧/弱背景,不是当前画面强事实)\n"
418
422
  f"Guardrail: clock=null/context_only;不得把未来剧情或 research-only 关系/因果升级为当前事实。\n"
419
423
  f"{research_context or '(无)'}\n\n"
@@ -423,23 +427,6 @@ def build_review_messages(
423
427
  return [{"role": "user", "content": user}]
424
428
 
425
429
 
426
- def _downgrade_context_assertions(assertions):
427
- out = []
428
- for item in assertions or []:
429
- src = str(item.get("source", "")).strip().lower()
430
- if src in {"research", "user_context"}:
431
- item = dict(item)
432
- item["support"] = "context_only"
433
- item["clock"] = None
434
- risk = str(item.get("risk", "")).strip()
435
- label = "research-only" if src == "research" else "user_context-only"
436
- item["risk"] = (
437
- risk + "; " if risk else ""
438
- ) + f"{label}: advisory/context_only, not a strong current-timeline fact"
439
- out.append(item)
440
- return out
441
-
442
-
443
430
  def parse_review_response(text):
444
431
  """Pure: robustly extract the reviewer JSON; fall back to a REVISE-unknown shell."""
445
432
  raw = str(text or "")
@@ -466,8 +453,10 @@ def parse_review_response(text):
466
453
 
467
454
  def _normalise_review(data):
468
455
  verdict = str(data.get("verdict", "REVISE")).upper()
469
- # PASS/REVISE/FAIL is the new vocabulary; OK is kept as a backward-compatible alias.
470
- if verdict not in ("PASS", "REVISE", "FAIL", "OK"):
456
+ # The prompt asks for PASS/REVISE/FAIL only; a stray "OK" from the judge means approval.
457
+ if verdict == "OK":
458
+ verdict = "PASS"
459
+ if verdict not in ("PASS", "REVISE", "FAIL"):
471
460
  verdict = "REVISE"
472
461
  findings = []
473
462
  for f in data.get("findings", []) or []:
@@ -493,42 +482,29 @@ def _normalise_review(data):
493
482
  "fix": str(f.get("fix", "")).strip(),
494
483
  }
495
484
  )
496
- # The scorecard and the lists below are PURE ADVISORY enrichment: they never mutate the
497
- # judge's verdict (deliberately not the colleague's auto-downgrade) and never gate. The
498
- # hard pre-TTS gate stays exactly where it was — error findings counted in recap.py.
499
485
  return {
500
486
  "verdict": verdict,
501
487
  "summary": str(data.get("summary", "")).strip(),
502
- "scorecard": _normalise_scorecard(data.get("scorecard")),
503
- "hook_candidates_review": _normalise_list_of_dicts(
504
- data.get("hook_candidates_review"),
505
- ["candidate", "type", "score", "keep", "reason"],
506
- ),
507
- "retention_risk_points": _normalise_list_of_dicts(
508
- data.get("retention_risk_points"), ["time", "risk", "fix", "evidence"]
509
- ),
510
- "highest_return_edits": _normalise_string_list(
511
- data.get("highest_return_edits")
512
- ),
513
- "information_gain_notes": _normalise_list_of_dicts(
514
- data.get("information_gain_notes"), ["segment", "label", "note", "rewrite"]
515
- ),
516
- "spoken_language_rewrites": _normalise_list_of_dicts(
517
- data.get("spoken_language_rewrites"),
518
- ["segment", "original", "rewrite", "why"],
519
- ),
520
- "grounding_assertions": _downgrade_context_assertions(
521
- _normalise_list_of_dicts(
522
- data.get("grounding_assertions"),
523
- ["segment", "assertion", "source", "risk", "support", "clock"],
524
- )
525
- ),
526
488
  "findings": findings,
527
489
  }
528
490
 
529
491
 
492
+ def _segment_label(segment):
493
+ """The draft lists blocks from 0 (`segment` keeps that index); readers see 段 N from 1."""
494
+ if segment is None:
495
+ return "整体"
496
+ if isinstance(segment, str) and segment.strip().isdigit():
497
+ segment = int(segment)
498
+ elif isinstance(segment, float) and segment.is_integer():
499
+ segment = int(segment)
500
+ if isinstance(segment, int) and not isinstance(segment, bool) and segment >= 0:
501
+ return f"段 {segment + 1}"
502
+ # Any other shape is not a block index; show the raw value, never as a 段 number.
503
+ return f"段 ?(模型返回 {json.dumps(segment, ensure_ascii=False, default=str)})"
504
+
505
+
530
506
  def format_review_md(review):
531
- """Render a parse_review_response() review; list-item keys stay judge-optional."""
507
+ """Render a parse_review_response() review as markdown."""
532
508
  order = {"error": 0, "warning": 1, "suggestion": 2}
533
509
  findings = sorted(review["findings"], key=lambda f: order[f["severity"]])
534
510
  counts = {
@@ -543,127 +519,13 @@ def format_review_md(review):
543
519
  "",
544
520
  review["summary"] or "_(no summary)_",
545
521
  "",
546
- "## Scorecard",
522
+ "## Findings",
547
523
  ]
548
- for key, v in review["scorecard"].items():
549
- out.append(f"- {key}: {v}/5" if v is not None else f"- {key}: 未评分")
550
- out.extend(["", "## Highest-return edits"])
551
- out.extend([f"- {edit}" for edit in review["highest_return_edits"]] or ["- (none)"])
552
- out.extend(["", "## Retention risk points"])
553
- risks = review["retention_risk_points"]
554
- if risks:
555
- for item in risks:
556
- out.append(
557
- f"- {item.get('time', '?')}: {item.get('risk', '')} — {item.get('fix', '')}"
558
- )
559
- else:
560
- out.append("- (none)")
561
- out.extend(["", "## Hook candidates review"])
562
- hooks = review["hook_candidates_review"]
563
- if hooks:
564
- for item in hooks:
565
- out.append(
566
- f"- {item.get('type', '?')} {item.get('score', '-')}/5: {item.get('candidate', '')} ({'keep' if item.get('keep') else 'drop'}) {item.get('reason', '')}"
567
- )
568
- else:
569
- out.append("- (none)")
570
- out.extend(
571
- ["", "## Information gain / write-for-ear / grounding", "### Information gain"]
572
- )
573
- notes = review["information_gain_notes"]
574
- out.extend(
575
- [
576
- f"- 段 {n.get('segment')}: {n.get('label')} — {n.get('note', '')} {n.get('rewrite', '')}"
577
- for n in notes
578
- ]
579
- or ["- (none)"]
580
- )
581
- out.append("### Spoken rewrites")
582
- rewrites = review["spoken_language_rewrites"]
583
- out.extend(
584
- [
585
- f"- 段 {r.get('segment')}: {r.get('original', '')} → {r.get('rewrite', '')}({r.get('why', '')})"
586
- for r in rewrites
587
- ]
588
- or ["- (none)"]
589
- )
590
- out.append("### Grounding assertions")
591
- assertions = review["grounding_assertions"]
592
- out.extend(
593
- [
594
- f"- 段 {a.get('segment')}: {a.get('assertion', '')} [{a.get('source', '')}] {a.get('risk', '')}"
595
- for a in assertions
596
- ]
597
- or ["- (none)"]
598
- )
599
- out.extend(["", "## Findings"])
600
524
  if not findings:
601
525
  out.append("- (none)")
602
526
  for f in findings:
603
- seg = "整体" if f["segment"] is None else f"段 {f['segment']}"
527
+ seg = _segment_label(f["segment"])
604
528
  out.append(f"- **[{f['severity']}/{f['category']}] {seg}** — {f['issue']}")
605
529
  if f["fix"]:
606
530
  out.append(f" - 改法: {f['fix']}")
607
531
  return "\n".join(out) + "\n"
608
-
609
-
610
- def build_grounding_qc(work_dir, review, bundle, *, timeline="source"):
611
- """Pure-ish compatibility seam: build grounding QC payload without writing it.
612
-
613
- It reads optional QC sidecars from work_dir to preserve the existing artifact
614
- contract, but has no side effects.
615
- """
616
- work_dir = Path(work_dir)
617
- items = bundle["items"]
618
- context = bundle["context_items"]
619
- # The runner hands the same pipeline warnings to the bundle and the review.
620
- warnings = list(bundle["warnings"])
621
- verdict = "warn" if warnings else "pass"
622
- if any(f["severity"] == "error" for f in review["findings"]):
623
- verdict = "fail"
624
- coverage_meta = build_review_coverage_metadata(bundle)
625
- visual_items = [item for item in items if item["source"] == "visual"]
626
- asr_items = [item for item in items if item["source"] == "asr"]
627
- return {
628
- "schema_version": 1,
629
- "owner": "video-script.review",
630
- "timeline": timeline,
631
- "coverage_policy_version": COVERAGE_POLICY_VERSION,
632
- "review_coverage": {
633
- "time_ranges": coverage_meta["time_ranges"],
634
- "scene_count": coverage_meta["scene_count"],
635
- "asr_count": coverage_meta["asr_count"],
636
- "dropped_ranges": coverage_meta["dropped_ranges"],
637
- },
638
- "evidence_contract": {
639
- # Every timeline item carries the bundle clock; context items are unclocked.
640
- "source_items": len(items) if bundle["clock"] == "source" else 0,
641
- "output_items": len(items) if bundle["clock"] == "output" else 0,
642
- "unclocked_items": 0,
643
- "context_only_items": len(context),
644
- },
645
- "index_inputs": {
646
- "vlm": bool(visual_items),
647
- "asr": bool(asr_items),
648
- "research": (work_dir / "background_research.json").exists(),
649
- },
650
- "speech_window_qc": _load_optional_json(work_dir, "silence_periods.qc.json")
651
- or {"coarse_asr_windows": 0, "low_confidence_speech_flags": 0},
652
- "research_guardrail": _research_guardrail_qc(review, context),
653
- "warnings": warnings,
654
- "verdict": verdict,
655
- }
656
-
657
-
658
- def write_grounding_qc(work_dir, qc):
659
- """Compatibility seam: write a prebuilt grounding_qc.json payload."""
660
- work_dir = Path(work_dir)
661
- (work_dir / "grounding_qc.json").write_text(
662
- json.dumps(qc, ensure_ascii=False, indent=2), encoding="utf-8"
663
- )
664
- return qc
665
-
666
-
667
- def _write_grounding_qc(work_dir, review, bundle, *, timeline="source"):
668
- qc = build_grounding_qc(work_dir, review, bundle, timeline=timeline)
669
- return write_grounding_qc(work_dir, qc)
@@ -1,4 +1,4 @@
1
- """Run narration review and write its advisory artifacts."""
1
+ """Run narration review and write narration_review.json / .md."""
2
2
 
3
3
  import argparse
4
4
 
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
 
10
10
  from lib import CONFIG, log, api_call
11
11
 
12
- from evidence_bundle import build_evidence_bundle
12
+ from evidence_bundle import EVIDENCE_CONTRACT_VERSION, build_evidence_bundle
13
13
  from review_grounding import (
14
14
  _load_cut_clip_spans,
15
15
  _load_required,
@@ -20,16 +20,11 @@ from review_response import (
20
20
  _chunk_evidence_bundle,
21
21
  _load_review_research_context,
22
22
  _merge_chunk_reviews,
23
- _write_grounding_qc,
24
23
  build_review_messages,
25
24
  format_review_md,
26
25
  parse_review_response,
27
26
  )
28
27
 
29
- EVIDENCE_CONTRACT_VERSION = 1
30
-
31
- COVERAGE_POLICY_VERSION = "coverage_policy_v1"
32
-
33
28
 
34
29
  def review_narration(work_dir, *, timeline="source", strict_evidence=False):
35
30
  work_dir = Path(work_dir)
@@ -94,13 +89,10 @@ def review_narration(work_dir, *, timeline="source", strict_evidence=False):
94
89
  "schema_version": EVIDENCE_CONTRACT_VERSION,
95
90
  "timeline": timeline,
96
91
  "clock": bundle["clock"],
97
- "coverage_policy_version": COVERAGE_POLICY_VERSION,
98
92
  "selected_ranges": bundle["coverage"]["selected_ranges"],
99
93
  "chunk_count": len(chunks),
100
94
  "warnings": warnings,
101
95
  }
102
- _write_grounding_qc(work_dir, review, bundle, timeline=timeline)
103
-
104
96
  (work_dir / "narration_review.json").write_text(
105
97
  json.dumps(review, ensure_ascii=False, indent=2), encoding="utf-8"
106
98
  )