@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
@@ -1,320 +0,0 @@
1
- """Shared shift-left QC report contract for video-recap artifacts.
2
-
3
- This module is intentionally standalone: it defines a small schema and local
4
- validation helpers only. It does not call MiMo, read credentials, connect the
5
- pipeline, or attempt automatic fixes.
6
- """
7
- from __future__ import annotations
8
-
9
- import re
10
- from pathlib import Path
11
- from typing import Any
12
- from collections.abc import Mapping, Sequence
13
- from urllib.parse import urlsplit, urlunsplit
14
-
15
-
16
- SCHEMA_VERSION = 1
17
-
18
- STAGES = frozenset({
19
- "pre_cut",
20
- "post_cut",
21
- "pre_tts",
22
- "post_tts",
23
- "pre_assemble",
24
- "post_render",
25
- "golden",
26
- })
27
- SEVERITIES = frozenset({"info", "advisory", "warning", "blocker"})
28
- CONFIDENCES = frozenset({"low", "medium", "high", "objective"})
29
- SAMPLE_POLICIES = frozenset({"all", "deterministic", "sampled", "semantic", "aesthetic"})
30
- ARTIFACTS = frozenset({"final_qc.json", "golden_eval.json", "mimo_qc.json", "preflight_qc.json"})
31
-
32
- DETERMINISTIC_CATEGORIES = frozenset({
33
- "missing_artifact",
34
- "duration",
35
- "stream",
36
- "subtitle",
37
- "tts_placement",
38
- "schema_invalid",
39
- "placement",
40
- })
41
- NON_DETERMINISTIC_CATEGORIES = frozenset({"semantic", "aesthetic", "mimo_semantic", "mimo_aesthetic"})
42
- BLOCKING_SEVERITIES = frozenset({"blocker"})
43
- _SECRET_KEY_RE = re.compile(r"(?i)(api[_-]?key|token|secret|password|authorization|credential)")
44
- _SECRET_VALUE_RE = re.compile(r"\b(?:sk|tp)-[A-Za-z0-9_-]{6,}\b")
45
- _URL_SCHEMES_TO_SCRUB = frozenset({"http", "https", "ws", "wss"})
46
- REDACTED = "<redacted>"
47
-
48
-
49
- def _redact_url(value: str) -> str:
50
- """Strip URL credentials, query, and fragment while preserving useful host/path."""
51
- try:
52
- parts = urlsplit(value)
53
- except ValueError:
54
- return value
55
- if parts.scheme.lower() not in _URL_SCHEMES_TO_SCRUB or not parts.netloc:
56
- return value
57
- host = parts.hostname or ""
58
- if not host:
59
- return value
60
- if ":" in host and not host.startswith("["):
61
- host = f"[{host}]"
62
- netloc = host
63
- try:
64
- port = parts.port
65
- except ValueError:
66
- port = None
67
- if port is not None:
68
- netloc = f"{netloc}:{port}"
69
- return urlunsplit((parts.scheme, netloc, parts.path, "", ""))
70
-
71
-
72
- def redact_secrets(value: Any) -> Any:
73
- """Return a JSON-safe copy with likely credentials removed.
74
-
75
- Centralized for all video-recap QC reports: redacts secret-looking keys,
76
- common synthetic key/token values, and URL userinfo/query/fragment fields.
77
- """
78
- if isinstance(value, Mapping):
79
- redacted: dict[str, Any] = {}
80
- for key, item in value.items():
81
- key_s = str(key)
82
- redacted[key_s] = REDACTED if _SECRET_KEY_RE.search(key_s) else redact_secrets(item)
83
- return redacted
84
- if isinstance(value, list):
85
- return [redact_secrets(item) for item in value]
86
- if isinstance(value, tuple):
87
- return [redact_secrets(item) for item in value]
88
- if isinstance(value, Path):
89
- return str(value)
90
- if isinstance(value, str):
91
- return _SECRET_VALUE_RE.sub(REDACTED, _redact_url(value))
92
- return value
93
-
94
-
95
- _REQUIRED_FINDING_FIELDS = frozenset({
96
- "finding_id",
97
- "stage",
98
- "severity",
99
- "blocking",
100
- "deterministic",
101
- "confidence",
102
- "rule_id",
103
- "decision_reason",
104
- "location",
105
- "evidence",
106
- "sample_policy",
107
- "model_used",
108
- "next_action",
109
- # Helper fields kept for current rule semantics and compatibility.
110
- "category",
111
- "code",
112
- "message",
113
- "source",
114
- "objective_corroboration",
115
- })
116
- _REQUIRED_LOCATION_FIELDS = frozenset({"timecode", "source_span"})
117
- _REQUIRED_REPORT_FIELDS = frozenset({
118
- "schema_version",
119
- "artifact",
120
- "stage",
121
- "ok",
122
- "blocker_count",
123
- "finding_count",
124
- "findings",
125
- })
126
-
127
-
128
- class QCContractError(ValueError):
129
- """Raised when a QC report or finding violates the shared contract."""
130
-
131
-
132
- def build_finding(
133
- *,
134
- stage: str,
135
- severity: str,
136
- confidence: str,
137
- sample_policy: str | Mapping[str, Any],
138
- category: str,
139
- code: str,
140
- message: str,
141
- finding_id: str | None = None,
142
- id: str | None = None,
143
- rule_id: str | None = None,
144
- decision_reason: str | None = None,
145
- model_used: str | None = None,
146
- next_action: str | None = None,
147
- deterministic: bool | None = None,
148
- blocking: bool | None = None,
149
- source: Mapping[str, Any] | None = None,
150
- location: Mapping[str, Any] | None = None,
151
- evidence: Mapping[str, Any] | None = None,
152
- objective_corroboration: Mapping[str, Any] | None = None,
153
- **extra: Any,
154
- ) -> dict[str, Any]:
155
- """Build and validate one normalized QC finding.
156
-
157
- Deterministic objective findings may block. MiMo semantic/aesthetic findings
158
- default to advisory and may never block. Objective checks belong in the
159
- deterministic QC producers instead of upgrading subjective model findings.
160
- """
161
- if finding_id is None:
162
- finding_id = id
163
- if finding_id is None:
164
- raise QCContractError("finding_id is required")
165
- if rule_id is None:
166
- rule_id = code
167
- if decision_reason is None:
168
- decision_reason = message
169
- if model_used is None:
170
- model_used = "local_deterministic"
171
- if next_action is None:
172
- next_action = "manual_review"
173
- if isinstance(sample_policy, str):
174
- sample_policy = {"type": sample_policy}
175
-
176
- if deterministic is None:
177
- deterministic = category in DETERMINISTIC_CATEGORIES
178
- if source is None:
179
- source = {}
180
- source = redact_secrets(source)
181
- if location is None:
182
- location = {"timecode": None, "source_span": None}
183
- else:
184
- location = {"timecode": location.get("timecode"), "source_span": location.get("source_span")}
185
- if evidence is None:
186
- evidence = {}
187
- evidence = redact_secrets(evidence)
188
- objective_corroboration = redact_secrets(objective_corroboration or {})
189
-
190
- is_non_deterministic = (not deterministic) or category in NON_DETERMINISTIC_CATEGORIES
191
- if is_non_deterministic and blocking is None:
192
- blocking = False
193
- elif blocking is None:
194
- blocking = severity in BLOCKING_SEVERITIES
195
-
196
- if is_non_deterministic and blocking:
197
- raise QCContractError(f"non-deterministic blocking finding is not allowed: {category}/{code}")
198
-
199
- finding = {
200
- "finding_id": finding_id,
201
- "id": finding_id,
202
- "stage": stage,
203
- "severity": severity,
204
- "blocking": bool(blocking),
205
- "deterministic": bool(deterministic),
206
- "confidence": confidence,
207
- "rule_id": rule_id,
208
- "decision_reason": decision_reason,
209
- "location": dict(location),
210
- "evidence": dict(evidence),
211
- "sample_policy": dict(sample_policy),
212
- "model_used": model_used,
213
- "next_action": next_action,
214
- "category": category,
215
- "code": code,
216
- "message": message,
217
- "source": dict(source),
218
- "objective_corroboration": dict(objective_corroboration),
219
- }
220
- finding.update(redact_secrets(extra))
221
- _validate_finding(finding)
222
- return finding
223
-
224
-
225
- def build_report(
226
- *,
227
- artifact: str,
228
- stage: str,
229
- findings: Sequence[Mapping[str, Any]] | None = None,
230
- metadata: Mapping[str, Any] | None = None,
231
- ) -> dict[str, Any]:
232
- """Build and validate a minimal report for a supported QC artifact."""
233
- normalized_findings = [redact_secrets(dict(f)) for f in (findings or [])]
234
- blocker_count = sum(1 for f in normalized_findings if f.get("blocking") is True)
235
- report = {
236
- "schema_version": SCHEMA_VERSION,
237
- "artifact": artifact,
238
- "stage": stage,
239
- "ok": blocker_count == 0,
240
- "blocker_count": blocker_count,
241
- "finding_count": len(normalized_findings),
242
- "findings": normalized_findings,
243
- "metadata": redact_secrets(dict(metadata or {})),
244
- }
245
- validate_report(report)
246
- return report
247
-
248
-
249
- def validate_report(report: Mapping[str, Any]) -> bool:
250
- """Validate report shape, value domains, and blocking semantics."""
251
- if not isinstance(report, Mapping):
252
- raise QCContractError("report must be an object")
253
- missing = _REQUIRED_REPORT_FIELDS - set(report)
254
- if missing:
255
- raise QCContractError(f"report missing required fields: {sorted(missing)}")
256
- if report["schema_version"] != SCHEMA_VERSION:
257
- raise QCContractError(f"unsupported schema_version: {report['schema_version']!r}")
258
- if report["artifact"] not in ARTIFACTS:
259
- raise QCContractError(f"unsupported artifact: {report['artifact']!r}")
260
- if report["stage"] not in STAGES:
261
- raise QCContractError(f"unsupported stage: {report['stage']!r}")
262
- if not isinstance(report["findings"], list):
263
- raise QCContractError("findings must be a list")
264
- for finding in report["findings"]:
265
- _validate_finding(finding)
266
- blocker_count = sum(1 for f in report["findings"] if f.get("blocking") is True)
267
- if report["blocker_count"] != blocker_count:
268
- raise QCContractError("blocker_count does not match findings")
269
- if report["finding_count"] != len(report["findings"]):
270
- raise QCContractError("finding_count does not match findings")
271
- if report["ok"] != (blocker_count == 0):
272
- raise QCContractError("ok must be true exactly when blocker_count is zero")
273
- return True
274
-
275
-
276
- def _validate_finding(finding: Mapping[str, Any]) -> None:
277
- if not isinstance(finding, Mapping):
278
- raise QCContractError("finding must be an object")
279
- missing = _REQUIRED_FINDING_FIELDS - set(finding)
280
- if missing:
281
- raise QCContractError(f"finding missing required fields: {sorted(missing)}")
282
- if finding["stage"] not in STAGES:
283
- raise QCContractError(f"unsupported finding stage: {finding['stage']!r}")
284
- if finding["severity"] not in SEVERITIES:
285
- raise QCContractError(f"unsupported severity: {finding['severity']!r}")
286
- if finding["confidence"] not in CONFIDENCES:
287
- raise QCContractError(f"unsupported confidence: {finding['confidence']!r}")
288
- sample_policy = finding["sample_policy"]
289
- if not isinstance(sample_policy, Mapping):
290
- raise QCContractError("sample_policy must be an object")
291
- sample_policy_type = sample_policy.get("type")
292
- if sample_policy_type not in SAMPLE_POLICIES:
293
- raise QCContractError(f"unsupported sample_policy.type: {sample_policy_type!r}")
294
- if not isinstance(finding["deterministic"], bool):
295
- raise QCContractError("deterministic must be boolean")
296
- if not isinstance(finding["blocking"], bool):
297
- raise QCContractError("blocking must be boolean")
298
- if finding["blocking"] and finding["severity"] != "blocker":
299
- raise QCContractError("blocking findings must use severity='blocker'")
300
- location = finding["location"]
301
- if not isinstance(location, Mapping):
302
- raise QCContractError("location must be an object")
303
- missing_location = _REQUIRED_LOCATION_FIELDS - set(location)
304
- if missing_location:
305
- raise QCContractError(f"location missing required fields: {sorted(missing_location)}")
306
- if finding["source"] is None or not isinstance(finding["source"], Mapping):
307
- raise QCContractError("source must be an object")
308
- if finding["evidence"] is None or not isinstance(finding["evidence"], Mapping):
309
- raise QCContractError("evidence must be an object")
310
- if finding["objective_corroboration"] is None or not isinstance(finding["objective_corroboration"], Mapping):
311
- raise QCContractError("objective_corroboration must be an object")
312
- for field in ("finding_id", "rule_id", "decision_reason", "model_used", "next_action"):
313
- if not isinstance(finding[field], str) or not finding[field]:
314
- raise QCContractError(f"{field} must be a non-empty string")
315
-
316
- is_non_deterministic = (not finding["deterministic"]) or finding["category"] in NON_DETERMINISTIC_CATEGORIES
317
- if is_non_deterministic and finding["blocking"]:
318
- raise QCContractError(
319
- f"non-deterministic blocking finding is not allowed: {finding['category']}/{finding['code']}"
320
- )
@@ -1,108 +0,0 @@
1
- """Fuse scenes, ASR, and quiet windows for narration planning."""
2
-
3
- from lib import CONFIG
4
- from agent_text import _overlap_seconds, _recommended_char_budget
5
- from narration_lint import _validate_narration_budget
6
-
7
-
8
- def _quiet_windows(silence_periods):
9
- return [qp for qp in silence_periods if not qp["has_speech"]]
10
-
11
-
12
- def _align_narration_to_quiet(narration, scenes_analysis, silence_periods):
13
- """Recompute overlaps_speech from real quiet windows; keep the agent's timing.
14
-
15
- The dense continuous-bed design places narration ON the pictured beat over a
16
- ducked original bed, so segments are never relocated into silence gaps. Only
17
- the overlaps_speech flag that the ducking stage consumes is corrected, leaving
18
- the agent's start/end (and text) intact.
19
-
20
- Budget/dedup runs FIRST so a dedup-merged beat's overlaps_speech reflects its
21
- extended timing, not its original shorter span.
22
- """
23
- aligned = _validate_narration_budget(narration, scenes_analysis)
24
- quiet_windows = _quiet_windows(silence_periods)
25
- quiet_ratio_min = CONFIG["quiet_overlap_min_ratio"]
26
- for n in aligned:
27
- seg_dur = n["end"] - n["start"]
28
- quiet_overlap = sum(
29
- _overlap_seconds(n["start"], n["end"], qw["start"], qw["end"])
30
- for qw in quiet_windows
31
- )
32
- n["overlaps_speech"] = quiet_overlap < max(0.3, seg_dur * quiet_ratio_min)
33
- return aligned
34
-
35
-
36
- def _scene_asr_lines(asr_result, scene):
37
- lines = []
38
- for seg in asr_result:
39
- if scene["start"] < seg["end"] and scene["end"] > seg["start"] and seg["text"]:
40
- lines.append(f" [{seg['start']:.1f}-{seg['end']:.1f}] {seg['text']}")
41
- return lines
42
-
43
-
44
- def _quiet_windows_for_scene(silence_periods, scene):
45
- windows = []
46
- for qp in _quiet_windows(silence_periods):
47
- if qp["start"] < scene["end"] and qp["end"] > scene["start"]:
48
- start = max(qp["start"], scene["start"])
49
- end = min(qp["end"], scene["end"])
50
- if end > start:
51
- windows.append((start, end))
52
- return windows
53
-
54
-
55
- def _build_timeline_fusion(scenes, asr_segments, silence_periods):
56
- """Fuse VLM scenes, ASR dialogue and quiet narration slots on one timeline."""
57
- fusion = []
58
- quiet_windows = _quiet_windows(silence_periods)
59
- for scene in scenes:
60
- start, end = scene["start"], scene["end"]
61
- dialogue_segments = []
62
- dialogue_overlap = 0.0
63
- for seg in asr_segments:
64
- overlap = _overlap_seconds(start, end, seg["start"], seg["end"])
65
- if overlap <= 0:
66
- continue
67
- dialogue_overlap += overlap
68
- dialogue_segments.append(
69
- {
70
- "start": round(seg["start"], 2),
71
- "end": round(seg["end"], 2),
72
- "overlap_seconds": round(overlap, 2),
73
- "text": seg["text"],
74
- }
75
- )
76
-
77
- narration_slots = []
78
- for window in quiet_windows:
79
- overlap = _overlap_seconds(start, end, window["start"], window["end"])
80
- if overlap <= 0:
81
- continue
82
- slot_start = max(start, window["start"])
83
- slot_end = min(end, window["end"])
84
- narration_slots.append(
85
- {
86
- "start": round(slot_start, 2),
87
- "end": round(slot_end, 2),
88
- "duration": round(slot_end - slot_start, 2),
89
- "char_budget": _recommended_char_budget(slot_start, slot_end),
90
- }
91
- )
92
-
93
- fusion.append(
94
- {
95
- "scene_id": scene["scene_id"],
96
- "time_range": [round(start, 2), round(end, 2)],
97
- "visual_description": scene["description"],
98
- "depth_analysis": scene.get("depth_analysis", ""),
99
- "frame_facts": scene.get("frame_facts", {}),
100
- "dialogue_segments": dialogue_segments,
101
- "dialogue_overlap_seconds": round(dialogue_overlap, 2),
102
- "narration_slots": narration_slots,
103
- "recommended_mode": "quiet-slot"
104
- if narration_slots and dialogue_overlap < (end - start) * 0.4
105
- else "ducked-bed",
106
- }
107
- )
108
- return fusion
@@ -1,9 +0,0 @@
1
- """Public narration-brief API for this self-contained skill."""
2
-
3
- from briefing.builder import build_agent_brief
4
- from briefing.context import assess_understanding_substrate
5
-
6
- __all__ = [
7
- "assess_understanding_substrate",
8
- "build_agent_brief",
9
- ]
@@ -1,211 +0,0 @@
1
- """Deterministic narration readability/QC scanner.
2
-
3
- This module is intentionally report-only. It flags local readability and
4
- packaging hygiene risks for video narration; it is not an AIGC detector and it
5
- never rewrites text.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import re
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- CONTRACT = (
16
- "Local readability/QC report only: this is not an AIGC detector, does not "
17
- "claim AI-generation accuracy, and never rewrites text. Corrections remain "
18
- "human/agent rewrite work."
19
- )
20
-
21
- EM_DASH_RE = re.compile(r"——|—")
22
- NEGATIVE_FLIP_RE = re.compile(r"并?不是[^。!?!?;;\.]{0,40}而是")
23
- # Catch verbatim copy of the brief's example scaffolding without over-matching real text.
24
- # The bracketed role token 【主角】 is the placeholder; anchor the pronoun signal to the
25
- # example's exact phrase "TA要赌上" so the legitimate gender-neutral pronoun "TA" (他/她) +
26
- # 要 (idiomatic 解说 suspense device "凶手就在其中,TA要做的下一件事…") is NOT hard-blocked.
27
- PLACEHOLDER_RE = re.compile(r"【主角】|TA要赌上")
28
-
29
- CLICHE_TERMS = [
30
- "命运的齿轮", "殊不知", "与此同时", "一场阴谋", "背后真相", "不为人知",
31
- "故事就此展开", "命运", "真相", "秘密", "危机", "反转", "救赎",
32
- ]
33
- ABSTRACT_TERMS = ["人性", "成长", "救赎", "命运", "时代", "宿命", "选择", "意义", "价值", "情感"]
34
- REASONING_MARKERS = ["因为", "所以", "因此", "也就是说", "换句话说", "这意味着", "原因是", "可见"]
35
- METAPHOR_MARKERS = ["像", "仿佛", "好似", "犹如", "如同", "一把", "一场", "一张", "棋局", "风暴"]
36
- CONNECTIVE_MARKERS = ["但", "却", "而", "于是", "随后", "直到", "结果", "因为", "所以", "这时", "最后"]
37
-
38
-
39
- def _sentence_pieces(text: str) -> list[str]:
40
- """Split text into sentence-like pieces while keeping terminal punctuation."""
41
- text = re.sub(r"\s+", " ", text).strip()
42
- if not text:
43
- return []
44
- parts = re.split(r"([。!?!?;;.])", text)
45
- out: list[str] = []
46
- for idx in range(0, len(parts), 2):
47
- body = parts[idx].strip()
48
- punct = parts[idx + 1] if idx + 1 < len(parts) else ""
49
- if body or punct:
50
- out.append((body + punct).strip())
51
- return out or [text]
52
-
53
-
54
- def _text_units(text: str) -> int:
55
- """Length unit for prose: CJK characters, otherwise words."""
56
- if re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text):
57
- return len(re.sub(r"\s+", "", text))
58
- return len(re.findall(r"\b\w+\b", text))
59
-
60
-
61
- def _normalise_segments(payload: list[dict[str, Any]], *, source: str) -> list[dict[str, Any]]:
62
- key = "narration" if source == "narration" else "text"
63
- segments: list[dict[str, Any]] = []
64
- for idx, item in enumerate(payload):
65
- text = str(item.get(key, "")).strip()
66
- if text:
67
- segments.append({"source": source, "index": idx, "text": text})
68
- return segments
69
-
70
-
71
- def _load_original_subtitles(work_dir: Path | None) -> list[dict[str, Any]]:
72
- if work_dir is None:
73
- return []
74
- path = work_dir / "original_subtitles.json"
75
- if not path.exists():
76
- return []
77
- return _normalise_segments(json.loads(path.read_text(encoding="utf-8")), source="original_subtitles")
78
-
79
-
80
- def _style_card_requirement(work_dir: Path | None) -> tuple[bool, str]:
81
- """Read the explicit requirements contract; workspaces without one are advisory-only."""
82
- if work_dir is None:
83
- return False, "legacy_default"
84
- path = work_dir / "deslop_qc_requirements.json"
85
- if not path.exists():
86
- return False, "legacy_default"
87
- data = json.loads(path.read_text(encoding="utf-8"))
88
- return data["style_card_required"], "deslop_qc_requirements.json"
89
-
90
-
91
- def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] | None:
92
- if work_dir is None:
93
- return None
94
- severity = "blocker" if required else "advisory"
95
- path = work_dir / "style_card.json"
96
- if not path.exists():
97
- return {
98
- "severity": severity,
99
- "code": "missing_style_card",
100
- "source": "style_card",
101
- "index": None,
102
- "message": (
103
- "style_card.json is required by this expression/packaging run but is missing"
104
- if required else
105
- "style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it"
106
- ),
107
- }
108
- data = json.loads(path.read_text(encoding="utf-8"))
109
- if not isinstance(data, dict) or not data:
110
- return {
111
- "severity": severity,
112
- "code": "malformed_style_card",
113
- "source": "style_card",
114
- "index": None,
115
- "message": (
116
- "style_card.json is required but empty or not a JSON object"
117
- if required else
118
- "style_card.json is present but empty or not a JSON object; treat as migration warning unless the run requires it"
119
- ),
120
- }
121
- return None
122
-
123
-
124
- def _add_issue(bucket: list[dict[str, Any]], severity: str, code: str, source: str, index: int | None, message: str, **extra: Any) -> None:
125
- issue = {"severity": severity, "code": code, "source": source, "index": index, "message": message}
126
- issue.update({k: v for k, v in extra.items() if v is not None})
127
- bucket.append(issue)
128
-
129
-
130
- def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path | None = None, original_subtitles: list[dict[str, Any]] | None = None) -> dict[str, Any]:
131
- """Return a structured report; never mutates or rewrites input text."""
132
- work_path = Path(work_dir) if work_dir is not None else None
133
- segments = _normalise_segments(narration, source="narration")
134
- if original_subtitles is None:
135
- segments.extend(_load_original_subtitles(work_path))
136
- else:
137
- segments.extend(_normalise_segments(original_subtitles, source="original_subtitles"))
138
-
139
- blockers: list[dict[str, Any]] = []
140
- advisories: list[dict[str, Any]] = []
141
-
142
- required, requirement_source = _style_card_requirement(work_path)
143
- style_issue = _style_card_issue(work_path, required)
144
- if style_issue:
145
- (blockers if style_issue["severity"] == "blocker" else advisories).append(style_issue)
146
-
147
- all_text = "\n".join(seg["text"] for seg in segments)
148
- total_units = max(1, _text_units(all_text))
149
- sentences: list[str] = []
150
- for seg in segments:
151
- text = seg["text"]
152
- if EM_DASH_RE.search(text):
153
- _add_issue(blockers, "blocker", "em_dash", seg["source"], seg["index"], "破折号(—/——)不得出现在 narration/original_subtitles 中", matches=EM_DASH_RE.findall(text))
154
- if PLACEHOLDER_RE.search(text):
155
- _add_issue(blockers, "blocker", "placeholder_leakage", seg["source"], seg["index"], "示例占位内容泄漏到成稿中(如【主角】/TA要赌上)", matches=PLACEHOLDER_RE.findall(text))
156
- for sentence in _sentence_pieces(text):
157
- sentences.append(sentence)
158
- if NEGATIVE_FLIP_RE.search(sentence):
159
- _add_issue(advisories, "advisory", "negative_positive_flip", seg["source"], seg["index"], "“不是…而是…”模板化转折偏多,建议改成更具体的因果/行动表达", sentence=sentence[:120])
160
-
161
- def term_count(terms: list[str]) -> int:
162
- return sum(all_text.count(term) for term in terms)
163
-
164
- cliche_count = term_count(CLICHE_TERMS)
165
- abstract_count = term_count(ABSTRACT_TERMS)
166
- reasoning_count = term_count(REASONING_MARKERS)
167
- metaphor_count = term_count(METAPHOR_MARKERS)
168
- connective_count = term_count(CONNECTIVE_MARKERS)
169
-
170
- if cliche_count >= 4 or (cliche_count / total_units) > 0.025:
171
- _add_issue(advisories, "advisory", "cliche_density", "narration", None, "套话/高频抽象词偏密,建议换成具体行动、选择和后果", count=cliche_count)
172
- if abstract_count >= 5 or (abstract_count / total_units) > 0.03:
173
- _add_issue(advisories, "advisory", "abstract_summary", "narration", None, "抽象总结词偏多,容易像概括论文;补足画面证据和具体抉择", count=abstract_count)
174
- if reasoning_count >= 4:
175
- _add_issue(advisories, "advisory", "reasoning_chain", "narration", None, "解释链标记偏多,建议减少“因为/所以/这意味着”等讲理口吻", count=reasoning_count)
176
- if metaphor_count >= 4:
177
- _add_issue(advisories, "advisory", "metaphor_markers", "narration", None, "比喻/包装化标记偏多,确认没有遮住剧情事实", count=metaphor_count)
178
- if total_units >= 180 and connective_count <= 1:
179
- _add_issue(advisories, "advisory", "low_connective_density", "narration", None, "长文本缺少转折/因果连接,可能像平铺摘要", connective_count=connective_count)
180
-
181
- long_segments = [seg for seg in segments if seg["source"] == "narration" and _text_units(seg["text"]) > 180]
182
- if long_segments:
183
- _add_issue(advisories, "advisory", "overlong_narration_block", "narration", long_segments[0]["index"], "单个解说块过长,建议拆成更可听的段落", max_units=max(_text_units(seg["text"]) for seg in long_segments))
184
- long_sentences = [s for s in sentences if _text_units(s) > 90]
185
- if long_sentences:
186
- _add_issue(advisories, "advisory", "long_paragraph", "narration", None, "长句/长段落偏多,字幕阅读压力较大", max_units=max(_text_units(s) for s in long_sentences))
187
-
188
- return {
189
- "ok": not blockers,
190
- "contract": CONTRACT,
191
- "scanner": "deslop_qc.py",
192
- "style_card_required": required,
193
- "style_card_requirement_source": requirement_source,
194
- "blocker_count": len(blockers),
195
- "advisory_count": len(advisories),
196
- "blockers": blockers,
197
- "advisories": advisories,
198
- "metrics": {
199
- "segments_scanned": len(segments),
200
- "text_units": total_units if all_text else 0,
201
- "sentence_count": len(sentences),
202
- "cliche_count": cliche_count,
203
- "abstract_count": abstract_count,
204
- "reasoning_marker_count": reasoning_count,
205
- "metaphor_marker_count": metaphor_count,
206
- "connective_count": connective_count,
207
- },
208
- }
209
-
210
-
211
- __all__ = ["CONTRACT", "analyze_deslop_qc"]