@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
@@ -154,6 +154,44 @@ def _remap_frame_facts_for_brief(frame_facts, overlap):
154
154
  return out
155
155
 
156
156
 
157
+ # A time the VLM quoted in its prose: "12.0s" (the frame-tag form), "12秒", "01:05",
158
+ # "1:02:03". Only a value inside the scene's own source range counts as a source timestamp;
159
+ # anything else (a duration, an on-screen clock) is left as written.
160
+ _PROSE_TIME_RE = re.compile(
161
+ r"(?<![\d.:])(?:(?:(\d+):)?(\d{1,2}):(\d{2}(?:\.\d+)?)(?![\d:])"
162
+ r"|(\d+(?:\.\d+)?)\s*(?:s|秒)(?![A-Za-z]))"
163
+ )
164
+ _PROSE_TIME_SLACK = 0.25
165
+ _CUT_AWAY_TIME = "[cut-away moment]"
166
+
167
+
168
+ def _remap_prose_times_for_brief(text, scene, overlap):
169
+ """Rewrite source timestamps quoted in VLM prose onto the OUTPUT clock.
170
+
171
+ A time inside this part's kept source range becomes its output time ("3.2s"); a time
172
+ elsewhere in the scene was cut away and is replaced by a marker, so an OUTPUT section
173
+ never quotes a source-clock time."""
174
+
175
+ def replace(match):
176
+ hours, minutes, secs, bare = match.groups()
177
+ if bare is not None:
178
+ when = float(bare)
179
+ else:
180
+ when = int(hours or 0) * 3600 + int(minutes) * 60 + float(secs)
181
+ if not (scene["start"] - _PROSE_TIME_SLACK <= when <= scene["end"] + _PROSE_TIME_SLACK):
182
+ return match.group(0)
183
+ if not (
184
+ overlap["source_start"] - _PROSE_TIME_SLACK
185
+ <= when
186
+ <= overlap["source_end"] + _PROSE_TIME_SLACK
187
+ ):
188
+ return _CUT_AWAY_TIME
189
+ out = overlap["output_start"] + (when - overlap["source_start"])
190
+ return f"{min(max(out, overlap['output_start']), overlap['output_end']):.1f}s"
191
+
192
+ return _PROSE_TIME_RE.sub(replace, text) if isinstance(text, str) else text
193
+
194
+
157
195
  def _remap_scenes_to_output_for_brief(scenes, spans):
158
196
  out = []
159
197
  for scene in scenes:
@@ -165,6 +203,9 @@ def _remap_scenes_to_output_for_brief(scenes, spans):
165
203
  item["frame_facts"] = _remap_frame_facts_for_brief(
166
204
  scene.get("frame_facts", {}), overlap
167
205
  )
206
+ for field in ("description", "depth_analysis"):
207
+ if field in item:
208
+ item[field] = _remap_prose_times_for_brief(item[field], scene, overlap)
168
209
  if len(overlaps) > 1:
169
210
  item["scene_id"] = f"{scene['scene_id']}.{part_idx}"
170
211
  out.append(item)
@@ -186,6 +227,38 @@ def _remap_segments_to_output_for_brief(segments, spans):
186
227
  return out
187
228
 
188
229
 
230
+ _COVERAGE_SLACK = 0.05
231
+ # No "." inside: the ASR chunker splits sentences on it.
232
+ _PARTIAL_WINDOW_TEXT = "[partial ASR window: only part of it is in the cut, text withheld]"
233
+
234
+
235
+ def _remap_asr_to_output_for_brief(segments, spans):
236
+ """ASR rows on the OUTPUT clock for the brief's writing evidence.
237
+
238
+ A coarse window the cut keeps only part of cannot say which of its words survived, so
239
+ each kept piece shows a partial-window marker instead of the window's text: the
240
+ brief never quotes dialogue that may not be in the cut. A window the kept clips cover
241
+ completely (even across a seam) keeps its text. `speech_spans` in
242
+ speech_boundary_anchors_output.json are remapped separately and keep the raw text."""
243
+ out = []
244
+ for seg in segments:
245
+ overlaps = _source_output_overlaps_for_brief(seg["start"], seg["end"], spans)
246
+ kept = sum(o["source_end"] - o["source_start"] for o in overlaps)
247
+ window = seg["end"] - seg["start"]
248
+ partial = bool(str(seg.get("text") or "").strip()) and kept < window - _COVERAGE_SLACK
249
+ for overlap in overlaps:
250
+ item = dict(seg)
251
+ item["start"] = round(overlap["output_start"], 3)
252
+ item["end"] = round(overlap["output_end"], 3)
253
+ if "duration" in item:
254
+ item["duration"] = round(item["end"] - item["start"], 3)
255
+ if partial:
256
+ item["text"] = _PARTIAL_WINDOW_TEXT
257
+ out.append(item)
258
+ out.sort(key=lambda x: (x["start"], x["end"]))
259
+ return out
260
+
261
+
189
262
  def _remap_brief_evidence_to_output_timeline(
190
263
  work_dir, scenes_analysis, asr_result, silence_periods, *, required=False
191
264
  ):
@@ -194,7 +267,7 @@ def _remap_brief_evidence_to_output_timeline(
194
267
  return scenes_analysis, asr_result, silence_periods
195
268
  return (
196
269
  _remap_scenes_to_output_for_brief(scenes_analysis, spans),
197
- _remap_segments_to_output_for_brief(asr_result, spans),
270
+ _remap_asr_to_output_for_brief(asr_result, spans),
198
271
  _remap_segments_to_output_for_brief(silence_periods, spans),
199
272
  )
200
273
 
@@ -204,6 +277,13 @@ def _read_json(path, default):
204
277
  return json.loads(path.read_text(encoding="utf-8")) if path.exists() else default
205
278
 
206
279
 
280
+ def _source_to_span_output(source_time, span):
281
+ """`source_time` on one clip span's OUTPUT clock, clamped to that span: the next span on
282
+ the output timeline plays a different source range."""
283
+ when = min(max(source_time, span["source_start"]), span["source_end"])
284
+ return round(min(span["output_start"] + when - span["source_start"], span["output_end"]), 3)
285
+
286
+
207
287
  def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
208
288
  """Load sentence anchors and remap them to cut OUTPUT time when needed."""
209
289
  work_dir = Path(work_dir)
@@ -217,26 +297,41 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
217
297
  return anchors
218
298
 
219
299
  spans = _load_cut_output_spans_for_brief(work_dir, required=True)
300
+ output_duration = max(span["output_end"] for span in spans)
220
301
  remapped = []
221
302
  for anchor in anchors:
222
303
  source_time = anchor["time"]
223
- for span in spans:
224
- if span["source_start"] - 0.05 <= source_time <= span["source_end"] + 0.05:
225
- item = dict(anchor)
226
- item["source_time"] = round(source_time, 3)
227
- item["time"] = round(
228
- span["output_start"] + source_time - span["source_start"], 3
229
- )
230
- # Preserve the measured safe pause in OUTPUT time too, so cut-mode lint
231
- # compares one clock.
232
- source_pause_start = max(
233
- span["source_start"], min(anchor.get("pause_start", source_time - 0.12), source_time)
234
- )
235
- item["source_pause_start"] = round(source_pause_start, 3)
236
- item["pause_start"] = round(
237
- span["output_start"] + source_pause_start - span["source_start"], 3
238
- )
239
- remapped.append(item)
304
+ # A pause end a few ms past a frame-snapped clip edge belongs to that clip, at the
305
+ # edge; only when no clip plays the instant itself.
306
+ owners = [span for span in spans
307
+ if span["source_start"] <= source_time <= span["source_end"]] or [
308
+ span for span in spans
309
+ if span["source_start"] - 0.05 <= source_time <= span["source_end"] + 0.05]
310
+ for span in owners:
311
+ # The 0.05 s match slack can carry an anchor past the output's ends (a clip
312
+ # end frame-snapped 181.42 -> 181.40): narration cannot start there.
313
+ unclamped = round(span["output_start"] + source_time - span["source_start"], 3)
314
+ if not 0 <= unclamped <= output_duration:
315
+ continue
316
+ item = dict(anchor)
317
+ item["source_time"] = round(source_time, 3)
318
+ item["time"] = _source_to_span_output(source_time, span)
319
+ # Preserve the measured safe pause in OUTPUT time too, so cut-mode lint
320
+ # compares one clock.
321
+ source_pause_start = max(
322
+ span["source_start"], min(anchor.get("pause_start", source_time - 0.12), source_time)
323
+ )
324
+ item["source_pause_start"] = round(min(source_pause_start, span["source_end"]), 3)
325
+ item["pause_start"] = _source_to_span_output(source_pause_start, span)
326
+ # `time` IS the pause end; keep the source value apart so the artifact
327
+ # never mixes clocks.
328
+ item["source_pause_end"] = round(anchor.get("pause_end", source_time), 3)
329
+ item["pause_end"] = item["time"]
330
+ if "expected_time" in anchor:
331
+ expected = float(anchor["expected_time"])
332
+ item["source_expected_time"] = round(expected, 3)
333
+ item["expected_time"] = _source_to_span_output(expected, span)
334
+ remapped.append(item)
240
335
 
241
336
  speech_rows = [
242
337
  row for row in _read_json(work_dir / "asr_result.json", []) if row["text"]
@@ -262,19 +357,30 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
262
357
  return out_payload["sentence_anchors"]
263
358
 
264
359
 
360
+ def _anchor_boundary_use(anchor):
361
+ # Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
362
+ # labels there are usable but unverified.
363
+ return anchor.get("boundary_use") or (
364
+ "unverified" if anchor["confidence"] in {"high", "medium"} else "none"
365
+ )
366
+
367
+
265
368
  def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
266
369
  anchors = [
267
370
  anchor
268
371
  for anchor in _sentence_entry_anchors_for_brief(work_dir, edit_mode)
269
- if anchor["confidence"] in {"high", "medium"}
372
+ if _anchor_boundary_use(anchor) != "none"
270
373
  ]
271
374
  if not anchors:
272
375
  return []
273
376
  lines = [
274
377
  "## 原声句末安全切入点",
275
378
  "",
276
- "这些时间是 ASR 句末标点与短声学停顿对齐后的旁白安全入口。旁白在原声已开始后切入时,"
379
+ "这些时间是 ASR 句末标点与短声学停顿对齐后的旁白入口。旁白在原声已开始后切入时,"
277
380
  "必须从其中一个点开始;否则会在 TTS 前被 `interrupts_source_sentence` 硬阻断。",
381
+ "- 标 `unverified` 的点是由粗粒度 ASR 窗口里的标点位置估计、再吸附到短停顿得到的:"
382
+ "落在停顿里,不会切断单词,但不保证原声句子已经说完(误差上限见 `±`)。请自己听一下再定;"
383
+ "门禁本身照常生效。",
278
384
  "- 调整方式:优先把 `start` 移到建议锚点;放不下时缩短文本、移动整块或删除该旁白,不能让脚本静默挪音频。",
279
385
  '- 原声句子完整性是硬约束:切入块写 `"source_entry_policy": "sentence_boundary"`;'
280
386
  "不存在 `intentional_interrupt` 绕过方式。没有后续可靠句末锚点时,移动、缩短或删除旁白块。",
@@ -284,8 +390,17 @@ def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
284
390
  f" (SOURCE {anchor['source_time']:.2f}s)" if "source_time" in anchor else ""
285
391
  )
286
392
  text_tail = str(anchor.get("text_tail", "")).strip()
393
+ if _anchor_boundary_use(anchor) == "verified":
394
+ label = anchor["confidence"]
395
+ else:
396
+ # Schema-1 anchors carry no error bound to print.
397
+ label = (
398
+ f"unverified ±{float(anchor['timing_bound_seconds']):.1f}s"
399
+ if "timing_bound_seconds" in anchor
400
+ else "unverified"
401
+ )
287
402
  lines.append(
288
- f"- {anchor['time']:.2f}s [{anchor['confidence']}]{source_suffix} {text_tail}".rstrip()
403
+ f"- {anchor['time']:.2f}s [{label}]{source_suffix} {text_tail}".rstrip()
289
404
  )
290
405
  lines.append("")
291
406
  return lines
@@ -308,19 +423,3 @@ def _format_output_clip_list(work_dir):
308
423
  )
309
424
  out.append("")
310
425
  return out if len(out) > 2 else []
311
-
312
-
313
- def _write_deslop_qc_requirements(work_dir):
314
- """Write the stable deslop QC contract consumed by deslop_qc.py.
315
-
316
- style_card_required defaults to False (advisory): a missing style_card.json is a
317
- warning, not a render-blocking error. A future opt-in run can set it True to make
318
- style_card.json a hard requirement — deslop_qc.py reads that field.
319
- """
320
- payload = {
321
- "schema_version": 1,
322
- "style_card_required": False,
323
- }
324
- path = Path(work_dir) / "deslop_qc_requirements.json"
325
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
326
- return path
@@ -10,7 +10,8 @@ Optional, synchronous, in-pipeline. Two independent LLM passes over the video's
10
10
  cleanup pass can never shift the timing-bearing spans downstream chunking depends on.
11
11
 
12
12
  Both passes are idempotent (a fresh artifact is reused); a failure raises and the runner
13
- records it in consolidation.status.json. Mirrors review.py's pure-seam + thin-driver shape so it is
13
+ records it in consolidation.status.json. A cut-off or non-JSON model reply is a failure too
14
+ (ConsolidateIncomplete), never an empty artifact reported as done. Mirrors review.py's pure-seam + thin-driver shape so it is
14
15
  unit-testable with a mocked api_call. NON-required: the pipeline runs unchanged without it.
15
16
  """
16
17
 
@@ -20,6 +21,7 @@ import re
20
21
  from pathlib import Path
21
22
 
22
23
  from lib import CONFIG, log, api_call, file_identity, load_background_research
24
+ from index_normalize import coerce_character_aliases, normalize_index, scenes_end
23
25
  from understanding_cache import _fresh
24
26
 
25
27
  # Shared tolerance for the per-segment span check. The brief-side gate inlines the SAME
@@ -28,6 +30,29 @@ from understanding_cache import _fresh
28
30
  _ASR_SPAN_TOL = 0.05
29
31
  ASR_CLEAN_POSTPROCESS_VERSION = 2
30
32
 
33
+ # Output budgets. A 5-min recap's index ran past 3000 tokens and came back cut off
34
+ # (finish_reason=length); a cut-off reply parses to nothing, so it must never be written as
35
+ # a finished artifact. One retry doubles the budget; a second cut-off raises.
36
+ _CLEAN_MAX_TOKENS = 8000
37
+ _INDEX_MAX_TOKENS = 8000
38
+ _RETRY_BUDGET_FACTOR = 2
39
+
40
+
41
+ class ConsolidateIncomplete(RuntimeError):
42
+ """The model reply was cut off or is not JSON; nothing was written for that pass."""
43
+
44
+
45
+ class ConsolidateNoKey(RuntimeError):
46
+ """A pass needs the model but no API key is set; nothing was sent or written."""
47
+
48
+
49
+ def _require_api_key(label):
50
+ """Offline runs reuse fresh artifacts; a pass that would call the model skips instead of
51
+ sending an unauthenticated request that can only 401."""
52
+ if not CONFIG["api_key"]:
53
+ log(f"consolidate({label}): 未设置 {CONFIG['api_env_var']},跳过(不发送请求)")
54
+ raise ConsolidateNoKey(label)
55
+
31
56
  CLEAN_PROMPT = """你在清洗中文视频的 ASR 逐段转写。对【每一段】做:补标点、修明显同音/错别字、(能判断时)在句首轻标说话人,让长段连读文本变成清晰可读的句子。
32
57
  铁律:
33
58
  - 不要合并或拆分段落,输出段数必须与输入完全一致,顺序一致。
@@ -44,6 +69,7 @@ INDEX_PROMPT = """你在根据逐场景画面分析、ASR对白、background_res
44
69
  - visual/asr 是当前视频事实证据;每个人物、关系、剧情节点、物件尽量给 evidence_ids。
45
70
  - background_research 只能用于人名/别名/术语/身份消歧,默认 support=context_only;不能把后续剧情或未出现关系升级为当前画面事实。
46
71
  - ASR 中出现但画面描述未命名的人名,应进入 characters[*].asr_mentions。
72
+ - 输出要紧凑、必须是完整 JSON:只收对解说有用的角色/关系/节点;description/relation/text 每条不超过 40 字;visual_descriptions 最多 3 条;每项 evidence_ids 最多 6 个;plot_points 最多 20 条。
47
73
  只返回 JSON:
48
74
  {"characters":[{"name":"角色名或外观指代","description":"身份/特征","aliases":[],"visual_descriptions":[],"asr_mentions":[],"research_role":"","evidence_ids":[],"confidence":"high|medium|low"}],
49
75
  "relationships":[{"a":"角色","b":"角色","relation":"关系","evidence_ids":[],"support":"direct|indirect|context_only"}],
@@ -339,6 +365,7 @@ def parse_index_response(text):
339
365
  ):
340
366
  val = data.get(key)
341
367
  out[key] = val if isinstance(val, list) else []
368
+ out["characters"] = coerce_character_aliases(out["characters"])
342
369
  for item in out["research_glossary"]:
343
370
  if isinstance(item, dict):
344
371
  item["support"] = "context_only"
@@ -479,15 +506,10 @@ def consolidate_transcript(work_dir):
479
506
  ):
480
507
  log("consolidate(asr): asr_clean.json 已最新,跳过")
481
508
  return existing
482
- resp = api_call(
483
- {
484
- "model": CONFIG["vlm_model"],
485
- "messages": build_clean_messages(asr_result),
486
- "max_tokens": 4000,
487
- "temperature": 0.2,
488
- }
509
+ _require_api_key("asr")
510
+ content = _complete_json_reply(
511
+ "asr", build_clean_messages(asr_result), _CLEAN_MAX_TOKENS
489
512
  )
490
- content = _response_text(resp)
491
513
  segments = parse_clean_response(content, asr_result)
492
514
  payload = {
493
515
  "source": _input_identity(work_dir, "asr_result.json"),
@@ -513,62 +535,124 @@ def consolidate_index(work_dir):
513
535
  if _fresh(out_path, work_dir / "vlm_analysis.json") and _index_cache_matches(
514
536
  work_dir, vlm_analysis
515
537
  ):
538
+ cached = _load(work_dir, "understanding_index.json")
539
+ repaired = _normalized_index(cached, vlm_analysis) if isinstance(cached, dict) else cached
540
+ if repaired != cached:
541
+ # An index written before the deterministic repairs existed: fix it in place
542
+ # instead of paying for a new model call.
543
+ _store_index(work_dir, repaired, vlm_analysis)
516
544
  log("consolidate(index): understanding_index.json 已最新,跳过")
517
- return _load(work_dir, "understanding_index.json")
545
+ return repaired
546
+ _require_api_key("index")
518
547
  asr_result = _load(work_dir, "asr_result.json") or []
519
548
  asr_clean = _load(work_dir, "asr_clean.json") or {}
520
549
  background_research = load_background_research(work_dir)
521
- resp = api_call(
522
- {
523
- "model": CONFIG["vlm_model"],
524
- "messages": build_index_messages(
525
- vlm_analysis,
526
- asr_result=asr_result,
527
- asr_clean=asr_clean,
528
- background_research=background_research,
529
- ),
530
- "max_tokens": 3000,
531
- "temperature": 0.2,
532
- }
550
+ content = _complete_json_reply(
551
+ "index",
552
+ build_index_messages(
553
+ vlm_analysis,
554
+ asr_result=asr_result,
555
+ asr_clean=asr_clean,
556
+ background_research=background_research,
557
+ ),
558
+ _INDEX_MAX_TOKENS,
533
559
  )
534
- index = parse_index_response(_response_text(resp))
560
+ index = parse_index_response(content)
535
561
  index = _apply_deterministic_asr_research_fallback(
536
562
  index,
537
563
  asr_result=asr_result,
538
564
  asr_clean=asr_clean,
539
565
  background_research=background_research,
540
566
  )
541
- out_path.write_text(
567
+ index = _normalized_index(index, vlm_analysis)
568
+ _store_index(work_dir, index, vlm_analysis)
569
+ log(
570
+ f"consolidate(index): 写出 understanding_index.json(角色 {len(index['characters'])})"
571
+ )
572
+ return index
573
+
574
+
575
+ def _normalized_index(index, vlm_analysis):
576
+ """Merge split characters and canonicalize plot times (index_normalize)."""
577
+ index, repairs = normalize_index(index, duration=scenes_end(vlm_analysis))
578
+ if repairs["merged_characters"] or repairs["dropped_plot_times"]:
579
+ log(
580
+ f"consolidate(index): 合并重复角色 {repairs['merged_characters']} 个,"
581
+ f"丢弃无效剧情时间 {repairs['dropped_plot_times']} 个"
582
+ )
583
+ return index
584
+
585
+
586
+ def _store_index(work_dir, index, vlm_analysis):
587
+ (work_dir / "understanding_index.json").write_text(
542
588
  json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8"
543
589
  )
544
590
  _write_index_meta(work_dir, vlm_analysis)
545
591
  (work_dir / "understanding_index.md").write_text(
546
592
  format_index_md(index), encoding="utf-8"
547
593
  )
548
- log(
549
- f"consolidate(index): 写出 understanding_index.json(角色 {len(index['characters'])})"
550
- )
551
- return index
552
594
 
553
595
 
554
596
  def consolidate(work_dir, do_asr=False, do_index=True):
555
597
  """Default = index-only (Pass B, zero timing risk). Pass A (asr) is opt-in.
556
598
 
557
- Failures propagate; understanding_runner catches them and writes the "failed" status."""
558
- result = {}
559
- if do_asr:
560
- result["asr_clean"] = consolidate_transcript(work_dir)
599
+ Failures propagate; understanding_runner catches them and writes the "failed" status.
600
+ A pass that needs the model while no key is set is listed under "skipped_no_key"."""
601
+ result = {"skipped_no_key": []}
602
+ passes = [("asr", "asr_clean", consolidate_transcript)] if do_asr else []
561
603
  if do_index:
562
- result["index"] = consolidate_index(work_dir)
604
+ passes.append(("index", "index", consolidate_index))
605
+ for label, key, run in passes:
606
+ try:
607
+ result[key] = run(work_dir)
608
+ except ConsolidateNoKey:
609
+ result[key] = None
610
+ result["skipped_no_key"].append(label)
563
611
  return result
564
612
 
565
613
 
566
614
  def _response_text(resp):
615
+ """(content, finish_reason) of the first choice; ("", None) on a malformed response."""
567
616
  try:
568
- return resp["choices"][0]["message"]["content"]
569
- except (KeyError, IndexError, TypeError):
617
+ choice = resp["choices"][0]
618
+ return choice["message"]["content"] or "", choice.get("finish_reason")
619
+ except (KeyError, IndexError, TypeError, AttributeError):
570
620
  log("consolidate: API 返回结构异常")
571
- return ""
621
+ return "", None
622
+
623
+
624
+ def _complete_json_reply(label, messages, max_tokens):
625
+ """Call the model and return a reply that is complete JSON, or raise ConsolidateIncomplete.
626
+
627
+ A cut-off reply (finish_reason=length) is retried once with a larger budget; an
628
+ unparseable reply is not retried (a bigger budget would not fix it)."""
629
+ budget = max_tokens
630
+ for attempt in range(2):
631
+ content, finish = _response_text(
632
+ api_call(
633
+ {
634
+ "model": CONFIG["vlm_model"],
635
+ "messages": messages,
636
+ "max_tokens": budget,
637
+ "temperature": 0.2,
638
+ }
639
+ )
640
+ )
641
+ if finish == "length":
642
+ if attempt == 0:
643
+ log(f"consolidate({label}): 输出被截断(max_tokens={budget}),加大预算重试一次")
644
+ budget *= _RETRY_BUDGET_FACTOR
645
+ continue
646
+ raise ConsolidateIncomplete(
647
+ f"consolidate({label}): 输出被截断(finish_reason=length,"
648
+ f"max_tokens={budget}),未写出结果"
649
+ )
650
+ if not isinstance(_extract_json(content), dict):
651
+ raise ConsolidateIncomplete(
652
+ f"consolidate({label}): 模型返回的不是可解析的 JSON"
653
+ f"(finish_reason={finish}),未写出结果"
654
+ )
655
+ return content
572
656
 
573
657
 
574
658
  def main():
@@ -583,7 +667,7 @@ def main():
583
667
  print(
584
668
  json.dumps(
585
669
  {
586
- "status": "consolidated",
670
+ "status": "skipped_no_key" if res["skipped_no_key"] else "consolidated",
587
671
  "index": bool(res.get("index")),
588
672
  "asr_clean": bool(res.get("asr_clean")),
589
673
  },