@oh-story/dsh 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +17 -11
  2. package/lib/client.js +85 -35
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +41 -36
  5. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
  6. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
  7. package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
  8. package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
  9. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
  10. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
  12. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
  13. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
  14. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
  15. package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
  16. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
  17. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
  18. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
  19. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
  20. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  21. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
  22. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
  23. package/lib/index.js +16 -7
  24. package/lib/index.js.map +2 -2
  25. package/lib/novel-to-game/README.md +98 -44
  26. package/lib/novel-to-game/README_ZH.md +100 -46
  27. package/lib/novel-to-game/VERSION +1 -1
  28. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
  29. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
  30. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
  31. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
  32. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
  33. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
  34. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
  35. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
  36. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
  37. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
  38. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
  39. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
  40. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
  41. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
  42. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
  43. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
  44. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
  45. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
  46. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
  57. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
  58. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
  59. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
  60. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
  61. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
  62. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
  63. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
  64. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
  65. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
  66. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
  91. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
  92. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
  93. package/lib/novel-to-game/manifest.json +303 -78
  94. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
  95. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
  96. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
  98. package/lib/types/client/drama-production-view.d.ts +11 -0
  99. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  100. package/lib/types/client/file-activity.d.ts +1 -1
  101. package/lib/types/client/file-activity.d.ts.map +1 -1
  102. package/lib/types/client/production-prompts.d.ts +7 -1
  103. package/lib/types/client/production-prompts.d.ts.map +1 -1
  104. package/lib/types/client/production-runtime.d.ts +7 -2
  105. package/lib/types/client/production-runtime.d.ts.map +1 -1
  106. package/lib/types/drama-adapters.d.ts.map +1 -1
  107. package/lib/types/skill-provider.d.ts.map +1 -1
  108. package/lib/types/video-project.d.ts.map +1 -1
  109. package/lib/video-recap/CHANGELOG.md +108 -1
  110. package/lib/video-recap/README.en.md +18 -14
  111. package/lib/video-recap/README.md +18 -14
  112. package/lib/video-recap/manifest.json +297 -321
  113. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  114. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  115. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  116. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  117. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  118. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  119. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  120. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  121. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  122. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  123. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  124. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  125. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  126. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  127. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  128. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  129. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  130. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  131. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  132. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  133. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  135. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  136. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  137. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  138. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  139. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  140. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  141. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  142. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  143. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  144. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  145. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  146. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  147. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  148. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  149. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  150. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  151. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  152. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  153. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  154. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  155. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  156. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  157. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  158. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  159. package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
  160. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  161. package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
  162. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  163. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  164. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  165. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  166. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  167. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  168. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  169. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
  170. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  171. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
  172. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  173. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  174. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  175. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  176. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  177. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  178. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  179. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  180. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  181. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  182. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  183. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  184. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  185. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  186. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  187. package/lib/video-recap/skills/video-script/SKILL.md +9 -7
  188. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  189. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  190. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  191. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  192. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  193. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  194. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  195. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  196. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  197. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  198. package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
  199. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  200. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  201. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  202. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  203. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  204. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  205. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  206. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  207. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  208. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  209. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  210. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  211. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  212. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  213. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  214. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  215. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  216. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  217. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  218. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  219. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  220. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  221. package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
  222. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  223. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  224. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
  225. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  226. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  227. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  228. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  229. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  230. package/package.json +45 -45
  231. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  232. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  233. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  234. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  235. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  239. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  240. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  241. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  242. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  243. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  244. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  245. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  246. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  247. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  248. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  249. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  250. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  251. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  252. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  253. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  254. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  255. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  258. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  259. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  260. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  262. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  263. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  264. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -0,0 +1,277 @@
1
+ // 覆盖层菜单:回看记录、存读档、设置、系统菜单与心意、鉴赏(CG/回想/音乐/结局)、退出。
2
+ import { CAST, CG, MUSIC, ENDINGS, ENDING_KIND } from './story/index.js';
3
+ import { HEROINES, ROUTE_MIN } from './adv.js';
4
+ import { config, saveConfig, global, loadSaves, deleteSave, SLOT_PAGES, SLOTS_PER_PAGE, DEFAULT_CONFIG } from './store.js';
5
+ import { audio } from './audio.js';
6
+ import { cgVisible, cgClass } from './stage.js';
7
+ import { story, toTitle, loadSnapshot, saveTo, startReplay, setAuto, setSkip, afterMenuClose, applyBoxAlpha } from './main.js';
8
+
9
+ const $ = (sel) => document.querySelector(sel);
10
+ const overlay = () => $('#overlay');
11
+ let current = null;
12
+ const trail = []; // 从哪一层进来的:Esc 与「返回」退回上一层
13
+ let savePage = 1;
14
+ let extrasTab = 'cg';
15
+ const backlog = [];
16
+ export let closedAt = 0;
17
+
18
+ export const menuOpen = () => current !== null;
19
+ export function pushBacklog(entry) { backlog.push(entry); if (backlog.length > 300) backlog.shift(); }
20
+ export function clearBacklog(entries = []) { backlog.splice(0, backlog.length, ...entries); }
21
+ export const backlogTail = () => backlog.slice(-80);
22
+
23
+ export function closeMenu() {
24
+ const was = current;
25
+ current = null;
26
+ trail.length = 0;
27
+ clearInterval(preview.timer);
28
+ overlay().hidden = true;
29
+ overlay().innerHTML = '';
30
+ closedAt = performance.now();
31
+ if (was === null) return;
32
+ audio.play(ingame() ? story.stage.bgm : 'title');
33
+ if (ingame()) afterMenuClose();
34
+ }
35
+
36
+ export function openMenu(kind, sub) {
37
+ setAuto(false);
38
+ setSkip(false);
39
+ trail.length = 0;
40
+ if (kind === 'extras' && sub) extrasTab = sub;
41
+ if (['backlog', 'save', 'load'].includes(kind)) audio.se('page');
42
+ go(kind, false);
43
+ }
44
+
45
+ function go(kind, remember = true) {
46
+ if (remember && current) trail.push(current);
47
+ current = kind;
48
+ overlay().hidden = false;
49
+ overlay().className = `menu-${kind}`;
50
+ render();
51
+ }
52
+
53
+ export function backMenu() {
54
+ const prev = trail.pop();
55
+ if (prev) { current = prev; overlay().className = `menu-${prev}`; render(); } else closeMenu();
56
+ }
57
+
58
+ export function confirmBox(text, yes) {
59
+ confirmState.text = text;
60
+ confirmState.yes = yes;
61
+ if (current) go('confirm');
62
+ else { setAuto(false); setSkip(false); go('confirm', false); }
63
+ }
64
+
65
+ const ingame = () => document.body.dataset.screen === 'game';
66
+ const frame = (title, body, extra = '', cls = '') => `
67
+ <div class="panel ${cls}" role="dialog" aria-label="${title}">
68
+ <header><h2>${title}</h2>${extra}<button class="close" data-act="close" aria-label="返回">返回</button></header>
69
+ <div class="panel-body">${body}</div>
70
+ </div>`;
71
+
72
+ function render() {
73
+ const o = overlay();
74
+ clearInterval(preview.timer);
75
+ if (current === 'backlog') o.innerHTML = frame('回看', `<ol class="backlog">${backlog.map((b) => `<li class="${b.choice ? 'chosen' : ''}">${b.name ? `<b style="color:${b.color ?? '#6b4b2e'}">${b.name}</b>` : ''}<p>${b.text}</p></li>`).join('') || '<li><p>还没有读过的句子。</p></li>'}</ol>`);
76
+ else if (current === 'save' || current === 'load') o.innerHTML = frame(current === 'save' ? '存档' : '读档', slotsHtml(), pagesHtml(), 'slots-panel');
77
+ else if (current === 'config') o.innerHTML = frame('设置', configHtml());
78
+ else if (current === 'system') o.innerHTML = frame('菜单', systemHtml());
79
+ else if (current === 'peek') o.innerHTML = frame('心意', peekHtml());
80
+ else if (current === 'extras') o.innerHTML = frame('鉴赏', extrasHtml(), tabsHtml(), 'tall');
81
+ else if (current === 'exit') o.innerHTML = `<div class="panel small"><h2>已离开</h2><p>浏览器不允许网页自行关闭窗口,你可以直接关掉这个标签页。</p><div class="row"><button class="btn primary" data-act="close">回到标题</button></div></div>`;
82
+ else if (current === 'confirm') o.innerHTML = `<div class="panel small"><p>${confirmState.text}</p><div class="row"><button class="btn primary" data-act="yes">确定</button><button class="btn" data-act="close">取消</button></div></div>`;
83
+ else if (current === 'cgview') o.innerHTML = `<figure class="cgview" data-act="view"><div class="cg-frame"><img class="${cgClass(viewing)}" src="${CG[viewing].src}" alt="${CG[viewing].name}"></div><figcaption>${CG[viewing].name} · ← → 翻看 点中间关闭</figcaption></figure>`;
84
+ if (current === 'backlog') { const body = o.querySelector('.panel-body'); body.scrollTop = body.scrollHeight; }
85
+ if (current === 'config') startPreview();
86
+ o.querySelector('[data-focus], .panel button:not(.close), .panel button')?.focus({ preventScroll: true });
87
+ }
88
+
89
+ // ---------- 存读档 ----------
90
+ function pagesHtml() {
91
+ const pages = [...Array(SLOT_PAGES)].map((_, i) => `<button class="tab ${savePage === i + 1 ? 'on' : ''}" data-page="${i + 1}">${i + 1}</button>`).join('');
92
+ return `<nav class="tabs">${pages}<button class="tab ${savePage === 'q' ? 'on' : ''}" data-page="q">快存/自动</button></nav>`;
93
+ }
94
+ const slotName = (id) => (id === 'quick' ? '快速存档' : id === 'auto' ? '自动存档' : `No.${id.padStart(2, '0')}`);
95
+ function slotsHtml() {
96
+ const saves = loadSaves();
97
+ const ids = savePage === 'q' ? ['quick', 'auto'] : [...Array(SLOTS_PER_PAGE)].map((_, i) => String((savePage - 1) * SLOTS_PER_PAGE + i + 1));
98
+ return `<div class="slots">${ids.map((id) => {
99
+ const s = saves[id];
100
+ const locked = current === 'save' && (id === 'auto' || !ingame() || story.replay);
101
+ return `<button class="slot ${s ? '' : 'empty'}" data-slot="${id}" ${(current === 'load' && !s) || locked ? 'disabled' : ''}>
102
+ <span class="thumb" ${s?.thumb ? `style="background-image:url(${s.thumb})"` : ''}>${s ? '' : '空'}</span>
103
+ <span class="meta"><b>${slotName(id)}</b><em>${s ? s.chapter : '——'}</em><small>${s ? s.date : ''}</small><q>${s?.text ?? ''}</q></span>
104
+ </button>`;
105
+ }).join('')}</div><p class="count slot-hint">右键点一份存档,可以删掉它。</p>`;
106
+ }
107
+
108
+ // ---------- 设置 ----------
109
+ const preview = { timer: null };
110
+ const PREVIEW_LINE = '金莲:「官人,这字走得快些,还是慢些?」';
111
+ function startPreview() {
112
+ clearInterval(preview.timer);
113
+ const el = $('#cfg-type');
114
+ if (!el) return;
115
+ let start = performance.now();
116
+ preview.timer = setInterval(() => {
117
+ if (config.textSpeed >= 100) { el.textContent = PREVIEW_LINE; return; }
118
+ const shown = Math.floor(((performance.now() - start) * config.textSpeed) / 1000);
119
+ el.textContent = PREVIEW_LINE.slice(0, shown);
120
+ if (shown > PREVIEW_LINE.length + config.textSpeed) start = performance.now(); // 停一秒再重打
121
+ }, 16);
122
+ }
123
+ function configHtml() {
124
+ const out = (key) => (key === 'textSpeed' && config[key] >= 100 ? '瞬间' : config[key]);
125
+ const range = (key, label, min = 0, max = 100) => `<label class="cfg"><span>${label}</span><input type="range" min="${min}" max="${max}" value="${config[key]}" data-cfg="${key}"><output>${out(key)}</output></label>`;
126
+ const toggle = (key, label, hint) => `<label class="cfg toggle"><span>${label}<small>${hint}</small></span><input type="checkbox" ${config[key] ? 'checked' : ''} data-cfg="${key}"></label>`;
127
+ return `<div class="cfg-grid">
128
+ ${range('textSpeed', '文字速度', 10)}
129
+ ${range('autoSpeed', '自动速度')}
130
+ ${range('bgm', '背景音乐')}
131
+ ${range('se', '音效')}
132
+ ${range('boxAlpha', '文字框浓淡', 40)}
133
+ ${toggle('skipUnread', '快进未读文字', '关闭时,快进只跳过读过的句子')}
134
+ ${toggle('adult', '成人画面', '关闭后,亲密 CG 以帘幕代替')}
135
+ </div>
136
+ <p class="cfg-preview"><span id="cfg-type"></span></p>
137
+ <p class="cfg-keys">点击推进 右键隐藏 滚轮上翻回看 按住 Ctrl 快进 A 自动 ← 上一选项 Esc 菜单</p>
138
+ <div class="row"><button class="btn" data-act="fullscreen">全屏切换</button><button class="btn" data-act="defaults">恢复默认</button></div>`;
139
+ }
140
+
141
+ // ---------- 系统菜单与心意 ----------
142
+ function systemHtml() {
143
+ return `<div class="sys">
144
+ <button class="btn" data-go="save" ${story.replay ? 'disabled' : ''}>存档</button>
145
+ <button class="btn" data-go="load">读档</button>
146
+ <button class="btn" data-go="config">设置</button>
147
+ <button class="btn" data-go="peek" ${story.replay ? 'disabled' : ''}>心意</button>
148
+ <button class="btn" data-go="backlog">回看记录</button>
149
+ <button class="btn" data-act="title">回到标题</button>
150
+ <button class="btn primary" data-act="close" data-focus>继续游戏</button>
151
+ </div>`;
152
+ }
153
+ // 心意一瞥只给她的神色和院门灯,不给档位
154
+ function peekHtml() {
155
+ return `<p class="peek-note">只是一眼。清明席散之后,只有还为你亮着灯的门,才会开。</p><div class="peek">${HEROINES.map((h) => {
156
+ const v = story.vars.aff[h] ?? 0;
157
+ const face = v >= 6 ? 'blush' : v >= 1 ? 'smile' : 'neutral';
158
+ const lit = v >= ROUTE_MIN;
159
+ return `<div class="peek-card" style="--accent:${CAST[h].color}"><img src="assets/sprite/${h}/${face}.webp" alt=""><b>${CAST[h].full}</b><span class="lantern ${lit ? 'lit' : 'dim'}">${lit ? '灯亮着' : '灯将熄'}</span></div>`;
160
+ }).join('')}</div>`;
161
+ }
162
+
163
+ // ---------- 鉴赏 ----------
164
+ const TABS = { cg: 'CG', scene: '回想', music: '音乐', ending: '结局' };
165
+ function tabsHtml() {
166
+ return `<nav class="tabs">${Object.entries(TABS).map(([k, v]) => `<button class="tab ${extrasTab === k ? 'on' : ''}" data-tab="${k}">${v}</button>`).join('')}</nav>`;
167
+ }
168
+ const SCENES = [
169
+ ['yue_night', 'yue_good', 'yue', '月下焚香'], ['pan_night', 'pan_good', 'pan', '琵琶不冷'], ['pinger_night', 'pinger_good', 'pinger', '钥匙在她手里'],
170
+ ['meng_night', 'meng_good', 'meng', '她自己开的门'], ['xuee_night', 'xuee_good', 'xuee', '天亮前的两碗粥'],
171
+ ];
172
+ const cgGroup = (id) => CG[id].group ?? HEROINES.find((h) => id.startsWith(`${h}_`)) ?? 'common';
173
+ const GROUPS = ['common', ...HEROINES];
174
+ // 鉴赏里的顺序:先共通,再按五人分组;大图左右翻看也按这个顺序
175
+ const galleryIds = () => Object.keys(CG).filter((id) => id !== 'title').sort((a, b) => GROUPS.indexOf(cgGroup(a)) - GROUPS.indexOf(cgGroup(b)));
176
+ const viewable = () => galleryIds().filter((id) => global.cg.has(id) && cgVisible(id));
177
+ function extrasHtml() {
178
+ if (extrasTab === 'cg') {
179
+ const ids = galleryIds();
180
+ const open = ids.filter((id) => global.cg.has(id)).length;
181
+ const groups = GROUPS.map((g) => {
182
+ const members = ids.filter((id) => cgGroup(id) === g);
183
+ const got = members.filter((id) => global.cg.has(id)).length;
184
+ return `<h3 class="cg-group">${g === 'common' ? '共通' : CAST[g].name}<small>${got} / ${members.length}</small></h3><div class="cg-grid">${members.map((id) => {
185
+ const unlocked = global.cg.has(id);
186
+ const show = unlocked && cgVisible(id);
187
+ return `<button class="cg-cell ${unlocked ? '' : 'locked'}" data-cg="${id}" ${show ? '' : 'disabled'}>${show ? `<span class="cg-thumb"><img loading="lazy" class="${cgClass(id)}" src="${CG[id].src}" alt=""></span>` : `<span>${unlocked ? '帘' : '?'}</span>`}<small>${unlocked ? CG[id].name : '未解锁'}</small></button>`;
188
+ }).join('')}</div>`;
189
+ }).join('');
190
+ return `<p class="count">已收集 ${open} / ${ids.length}</p>${groups}`;
191
+ }
192
+ if (extrasTab === 'scene') {
193
+ return `<div class="scene-list">${SCENES.map(([id, label, who, title]) => {
194
+ const ok = global.scene.has(id);
195
+ return `<button class="scene-cell" style="--accent:${CAST[who].color}" data-replay="${label}" ${ok ? '' : 'disabled'}><img src="assets/sprite/${who}/${ok ? 'blush' : 'neutral'}.webp" alt=""><b>${ok ? title : '???'}</b><small>${CAST[who].full} · ${ok ? '良缘之夜' : '走到她的良缘结局后解锁'}</small></button>`;
196
+ }).join('')}</div>`;
197
+ }
198
+ if (extrasTab === 'music') {
199
+ return `<ol class="music">${Object.entries(MUSIC).map(([id, name], i) => {
200
+ const ok = global.music.has(id);
201
+ return `<li><button class="btn ${ok && audio.track?.id === id ? 'primary' : ''}" data-music="${id}" ${ok ? '' : 'disabled'}>${String(i + 1).padStart(2, '0')} ${ok ? name : '???'}</button></li>`;
202
+ }).join('')}</ol>`;
203
+ }
204
+ const cell = (id) => {
205
+ const e = ENDINGS[id];
206
+ const ok = global.ending.has(id);
207
+ return `<div class="end-cell ${e.kind} ${ok ? 'ok' : ''}"><small>${ENDING_KIND[e.kind]}</small><b>${ok ? e.title : '???'}</b></div>`;
208
+ };
209
+ const total = Object.keys(ENDINGS).length;
210
+ return `<p class="count">已到达 ${[...global.ending].filter((id) => ENDINGS[id]).length} / ${total}</p>
211
+ <div class="flow"><div class="flow-root">共通线 · 序章~清明荷亭</div>
212
+ <div class="flow-routes">${HEROINES.map((h) => `<div class="flow-col" style="--accent:${CAST[h].color}"><div class="flow-head">${CAST[h].full}篇</div>${['good', 'normal', 'bad'].map((k) => cell(`${h}_${k}`)).join('')}</div>`).join('')}
213
+ <div class="flow-col lonely"><div class="flow-head">无人留灯</div>${cell('lonely')}</div></div></div>`;
214
+ }
215
+
216
+ // ---------- 交互 ----------
217
+ const confirmState = { text: '', yes: null };
218
+ let viewing = null;
219
+ function stepCg(dir) {
220
+ const ids = viewable();
221
+ if (!ids.length) return;
222
+ viewing = ids[(ids.indexOf(viewing) + dir + ids.length) % ids.length];
223
+ render();
224
+ }
225
+
226
+ overlay().addEventListener('click', async (e) => {
227
+ const t = e.target.closest('button, figure');
228
+ if (!t) return;
229
+ audio.se('click');
230
+ const d = t.dataset;
231
+ if (d.act === 'close') backMenu();
232
+ else if (d.act === 'view') { const x = e.clientX / window.innerWidth; if (x < 1 / 3) stepCg(-1); else if (x > 2 / 3) stepCg(1); else backMenu(); }
233
+ else if (d.act === 'yes') { const fn = confirmState.yes; confirmState.yes = null; await fn?.(); }
234
+ else if (d.act === 'title') confirmBox('回到标题?未存档的进度会丢失。', toTitle);
235
+ else if (d.act === 'fullscreen') { if (document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen?.(); }
236
+ else if (d.act === 'defaults') { Object.assign(config, DEFAULT_CONFIG, { adult: config.adult }); saveConfig(); audio.setVolumes(config.bgm, config.se); applyBoxAlpha(); render(); }
237
+ else if (d.go) go(d.go);
238
+ else if (d.page) { savePage = d.page === 'q' ? 'q' : Number(d.page); render(); }
239
+ else if (d.tab) { extrasTab = d.tab; render(); }
240
+ else if (d.slot) {
241
+ const record = loadSaves()[d.slot];
242
+ if (current === 'load') loadSnapshot(record);
243
+ else if (record) confirmBox(`覆盖 ${slotName(d.slot)} 的存档?`, async () => { await saveTo(d.slot); backMenu(); });
244
+ else { await saveTo(d.slot); render(); }
245
+ } else if (d.cg) { viewing = d.cg; go('cgview'); }
246
+ else if (d.replay) startReplay(d.replay);
247
+ else if (d.music) { audio.play(d.music); render(); }
248
+ });
249
+
250
+ overlay().addEventListener('contextmenu', (e) => {
251
+ e.preventDefault();
252
+ const slot = e.target.closest('[data-slot]')?.dataset.slot;
253
+ if (slot && loadSaves()[slot] && (current === 'save' || current === 'load')) {
254
+ confirmBox(`删除 ${slotName(slot)} 的存档?`, () => { deleteSave(slot); backMenu(); });
255
+ } else backMenu();
256
+ });
257
+ overlay().addEventListener('wheel', (e) => {
258
+ if (current !== 'backlog' || e.deltaY <= 0) return;
259
+ const body = overlay().querySelector('.panel-body');
260
+ if (body.scrollTop + body.clientHeight >= body.scrollHeight - 2) closeMenu();
261
+ }, { passive: true });
262
+ overlay().addEventListener('input', (e) => {
263
+ const key = e.target.dataset.cfg;
264
+ if (!key) return;
265
+ config[key] = e.target.type === 'checkbox' ? e.target.checked : Number(e.target.value);
266
+ saveConfig();
267
+ audio.setVolumes(config.bgm, config.se);
268
+ if (key === 'boxAlpha') applyBoxAlpha();
269
+ if (key === 'textSpeed') startPreview();
270
+ const out = e.target.parentElement.querySelector('output');
271
+ if (out) out.textContent = key === 'textSpeed' && config[key] >= 100 ? '瞬间' : config[key];
272
+ });
273
+ document.addEventListener('keydown', (e) => {
274
+ if (current !== 'cgview') return;
275
+ if (e.key === 'ArrowLeft') stepCg(-1);
276
+ else if (e.key === 'ArrowRight') stepCg(1);
277
+ });
@@ -0,0 +1,67 @@
1
+ // 标题飘落桃花瓣(canvas),并让标题底图随指针轻微视差。系统要求减少动效时只画静止花瓣。
2
+ let raf = 0;
3
+ let petals = [];
4
+ const reduce = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
5
+
6
+ function petal(w, h, fresh) {
7
+ return {
8
+ x: Math.random() * w,
9
+ y: fresh ? Math.random() * h : -20,
10
+ r: 5 + Math.random() * 7,
11
+ vx: 0.3 + Math.random() * 0.8,
12
+ vy: 0.6 + Math.random() * 1.1,
13
+ a: Math.random() * Math.PI * 2,
14
+ va: (Math.random() - 0.5) * 0.04,
15
+ hue: 340 + Math.random() * 18,
16
+ };
17
+ }
18
+
19
+ export function startPetals(canvas) {
20
+ stopPetals();
21
+ const ctx = canvas.getContext('2d');
22
+ const fit = () => {
23
+ canvas.width = canvas.clientWidth * devicePixelRatio;
24
+ canvas.height = canvas.clientHeight * devicePixelRatio;
25
+ };
26
+ fit();
27
+ window.addEventListener('resize', fit);
28
+ petals = Array.from({ length: 46 }, () => petal(canvas.width, canvas.height, true));
29
+ const draw = () => {
30
+ const { width: w, height: h } = canvas;
31
+ ctx.clearRect(0, 0, w, h);
32
+ for (const p of petals) {
33
+ if (!reduce()) {
34
+ p.x += p.vx * devicePixelRatio + Math.sin(p.a) * 0.4;
35
+ p.y += p.vy * devicePixelRatio;
36
+ p.a += p.va;
37
+ if (p.y > h + 20 || p.x > w + 20) Object.assign(p, petal(w, h, false), { x: Math.random() * w * 0.8 - w * 0.1 });
38
+ }
39
+ ctx.save();
40
+ ctx.translate(p.x, p.y);
41
+ ctx.rotate(p.a);
42
+ ctx.scale(1, 0.62 + Math.sin(p.a * 2) * 0.25);
43
+ ctx.fillStyle = `hsla(${p.hue}, 78%, 86%, 0.85)`;
44
+ ctx.beginPath();
45
+ ctx.ellipse(0, 0, p.r * devicePixelRatio, p.r * 0.62 * devicePixelRatio, 0, 0, Math.PI * 2);
46
+ ctx.fill();
47
+ ctx.restore();
48
+ }
49
+ raf = reduce() ? 0 : requestAnimationFrame(draw);
50
+ };
51
+ draw();
52
+ const bg = canvas.parentElement.querySelector('.title-bg');
53
+ stopPetals.move = (e) => {
54
+ const x = e.clientX / innerWidth - 0.5;
55
+ const y = e.clientY / innerHeight - 0.5;
56
+ bg.style.transform = `scale(1.06) translate(${-x * 14}px, ${-y * 10}px)`;
57
+ };
58
+ window.addEventListener('pointermove', stopPetals.move);
59
+ stopPetals.fit = fit;
60
+ }
61
+
62
+ export function stopPetals() {
63
+ cancelAnimationFrame(raf);
64
+ raf = 0;
65
+ if (stopPetals.move) window.removeEventListener('pointermove', stopPetals.move);
66
+ if (stopPetals.fit) window.removeEventListener('resize', stopPetals.fit);
67
+ }
@@ -0,0 +1,180 @@
1
+ // 舞台:背景交叉淡入、立绘进出场与差分、说话人高亮、CG 与成人画面门控、存档缩略图合成。
2
+ import { BG, CG, CAST, EXPRESSIONS } from './story/index.js';
3
+ import { config, ageConfirmed } from './store.js';
4
+
5
+ const $ = (sel) => document.querySelector(sel);
6
+ const POS_X = { left: 0.24, center: 0.5, right: 0.76 };
7
+ const FOCUS_ZOOM = 1.3;
8
+
9
+ export const spriteSrc = (who, expr) => `assets/sprite/${who}/${EXPRESSIONS.includes(expr) ? expr : 'neutral'}.webp`;
10
+ // 构图偏右、左侧留白的 CG:按 focus 裁到人物那一侧(舞台、鉴赏大图、缩略图共用)
11
+ export const cgClass = (id) => (CG[id]?.focus ? `focus-${CG[id].focus}` : '');
12
+ export const cgVisible = (id) => Boolean(id && CG[id] && (!CG[id].adult || (config.adult && ageConfirmed())));
13
+
14
+ let shownBg = null;
15
+ let activeBgLayer = 'a';
16
+ let shownCg = null;
17
+
18
+ export function syncStage(stage, { instant = false, speaker = null } = {}) {
19
+ // 背景
20
+ if (stage.bg !== shownBg) {
21
+ shownBg = stage.bg;
22
+ const next = activeBgLayer === 'a' ? 'b' : 'a';
23
+ const nextEl = $(`#bg-${next}`);
24
+ const prevEl = $(`#bg-${activeBgLayer}`);
25
+ nextEl.style.backgroundImage = stage.bg && BG[stage.bg] ? `url("${BG[stage.bg]}")` : 'none';
26
+ nextEl.dataset.bg = stage.bg ?? '';
27
+ nextEl.classList.toggle('night', /night|chamber|rain/.test(stage.bg ?? ''));
28
+ const fast = instant || stage.bgFx === 'cut';
29
+ nextEl.style.transitionDuration = prevEl.style.transitionDuration = fast ? '0ms' : '';
30
+ nextEl.classList.add('on');
31
+ prevEl.classList.remove('on');
32
+ activeBgLayer = next;
33
+ if (stage.bgFx === 'flash' && !instant) flash();
34
+ }
35
+
36
+ // 立绘:按 id 增量更新,新登场者淡入上滑,离场者淡出
37
+ const layer = $('#sprites');
38
+ const want = stage.sprites;
39
+ layer.dataset.count = Object.keys(want).length;
40
+ for (const el of [...layer.children]) {
41
+ if (!want[el.dataset.who] && !el.classList.contains('leaving')) {
42
+ el.classList.add('leaving');
43
+ setTimeout(() => el.remove(), instant ? 0 : 420);
44
+ }
45
+ }
46
+ for (const [who, { pos, expr }] of Object.entries(want)) {
47
+ let el = layer.querySelector(`.sprite[data-who="${who}"]:not(.leaving)`);
48
+ if (!el) {
49
+ el = document.createElement('div');
50
+ el.className = `sprite entering${instant ? ' no-anim' : ''}`;
51
+ el.dataset.who = who;
52
+ el.innerHTML = `<img alt="${CAST[who].full}" draggable="false">`;
53
+ el.querySelector('img').src = spriteSrc(who, expr);
54
+ el.dataset.expr = expr;
55
+ layer.append(el);
56
+ requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove('entering')));
57
+ }
58
+ el.dataset.pos = pos;
59
+ if (el.dataset.expr !== expr) {
60
+ // 差分交叉淡入:新图叠在上面淡入后替换
61
+ const prev = el.querySelector('img:last-child');
62
+ const img = document.createElement('img');
63
+ img.alt = CAST[who].full;
64
+ img.draggable = false;
65
+ img.className = 'swap';
66
+ img.src = spriteSrc(who, expr);
67
+ el.append(img);
68
+ requestAnimationFrame(() => img.classList.add('in'));
69
+ setTimeout(() => { prev.remove(); img.className = ''; }, instant ? 0 : 260);
70
+ el.dataset.expr = expr;
71
+ if (expr === 'surprised' && !instant) jolt(el);
72
+ }
73
+ const speaking = speaker && speaker === who;
74
+ el.classList.toggle('speaking', Boolean(speaking));
75
+ el.classList.toggle('dim', Boolean(speaker && CAST[speaker] && !speaking));
76
+ }
77
+
78
+ $('#weather').className = stage.weather ?? '';
79
+
80
+ syncCg(stage);
81
+ }
82
+
83
+ // CG:成人 CG 仅在年龄确认且设置开启时加载;否则以帘幕代替
84
+ function syncCg(stage) {
85
+ if (stage.cg === shownCg) return;
86
+ shownCg = stage.cg;
87
+ const box = $('#cg');
88
+ if (!stage.cg) { box.classList.remove('on'); return; }
89
+ const visible = cgVisible(stage.cg);
90
+ const img = box.querySelector('img');
91
+ img.src = visible ? CG[stage.cg].src : '';
92
+ img.className = visible ? cgClass(stage.cg) : '';
93
+ box.classList.toggle('veiled', !visible);
94
+ box.classList.add('on');
95
+ if (!visible) onVeil?.();
96
+ }
97
+
98
+ let onVeil = null;
99
+ export const setVeilHook = (fn) => { onVeil = fn; };
100
+ // 设置里改了「成人画面」:正在显示的 CG 立刻按新设置重画
101
+ export function refreshCg(stage) {
102
+ shownCg = undefined;
103
+ syncCg(stage);
104
+ }
105
+
106
+ export function resetStage() {
107
+ shownBg = undefined;
108
+ shownCg = undefined;
109
+ $('#sprites').innerHTML = '';
110
+ }
111
+
112
+ export function jolt(el) {
113
+ el.classList.remove('jolt');
114
+ void el.offsetWidth;
115
+ el.classList.add('jolt');
116
+ }
117
+
118
+ export function flash() {
119
+ const f = $('#flash');
120
+ f.classList.remove('go');
121
+ void f.offsetWidth;
122
+ f.classList.add('go');
123
+ }
124
+
125
+ export function shake() {
126
+ const s = $('#stage');
127
+ s.classList.remove('shake');
128
+ void s.offsetWidth;
129
+ s.classList.add('shake');
130
+ }
131
+
132
+ const imageCache = new Map();
133
+ function loadImage(src) {
134
+ if (!imageCache.has(src)) {
135
+ imageCache.set(src, new Promise((resolve) => {
136
+ const img = new Image();
137
+ img.onload = () => resolve(img);
138
+ img.onerror = () => resolve(null);
139
+ img.src = src;
140
+ }));
141
+ }
142
+ return imageCache.get(src);
143
+ }
144
+
145
+ // 存档缩略图:用当前背景+立绘(或非成人 CG)重新合成,绝不截入成人画面
146
+ export async function thumbnail(stage) {
147
+ const W = 320;
148
+ const H = 180;
149
+ const canvas = document.createElement('canvas');
150
+ canvas.width = W;
151
+ canvas.height = H;
152
+ const ctx = canvas.getContext('2d');
153
+ ctx.fillStyle = '#1d1a24';
154
+ ctx.fillRect(0, 0, W, H);
155
+ const cover = (img, zoom = 0) => {
156
+ // zoom>0:与舞台 .focus-right 相同,只取右侧 1/zoom 宽、居中 1/zoom 高
157
+ const sw = zoom ? img.width / zoom : img.width;
158
+ const sh = zoom ? img.height / zoom : img.height;
159
+ const sx = img.width - sw;
160
+ const sy = (img.height - sh) / 2;
161
+ const s = Math.max(W / sw, H / sh);
162
+ ctx.drawImage(img, sx, sy, sw, sh, (W - sw * s) / 2, (H - sh * s) / 2, sw * s, sh * s);
163
+ };
164
+ const cg = stage.cg && CG[stage.cg] && !CG[stage.cg].adult ? await loadImage(CG[stage.cg].src) : null;
165
+ if (cg) cover(cg, CG[stage.cg].focus ? FOCUS_ZOOM : 0);
166
+ else {
167
+ const bg = stage.bg && BG[stage.bg] ? await loadImage(BG[stage.bg]) : null;
168
+ if (bg) cover(bg);
169
+ if (!stage.cg) {
170
+ for (const [who, { pos, expr }] of Object.entries(stage.sprites)) {
171
+ const img = await loadImage(spriteSrc(who, expr));
172
+ if (!img) continue;
173
+ const h = H * 1.15;
174
+ const w = img.width * (h / img.height);
175
+ ctx.drawImage(img, W * POS_X[pos] - w / 2, H * 0.06, w, h);
176
+ }
177
+ }
178
+ }
179
+ try { return canvas.toDataURL('image/jpeg', 0.72); } catch { return null; }
180
+ }
@@ -0,0 +1,70 @@
1
+ // 本地持久化:设置、跨周目解锁(CG/回想/曲目/结局/已读/已选)、存档槽。全部容错,存储不可用时照常可玩。
2
+ const KEYS = { config: 'jpm2_config', global: 'jpm2_global', saves: 'jpm2_saves' };
3
+ export const AGE_KEY = 'jpm_age_confirmed';
4
+ export const SLOT_PAGES = 4;
5
+ export const SLOTS_PER_PAGE = 9;
6
+
7
+ const read = (key, fallback) => {
8
+ try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; }
9
+ };
10
+ const write = (key, value) => {
11
+ try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; }
12
+ };
13
+
14
+ export const DEFAULT_CONFIG = Object.freeze({
15
+ textSpeed: 45, // 字/秒;100 = 瞬间
16
+ autoSpeed: 50, // 0 慢 … 100 快
17
+ bgm: 60,
18
+ se: 70,
19
+ skipUnread: false,
20
+ adult: true,
21
+ boxAlpha: 90, // 文字框浓淡 40…100
22
+ });
23
+
24
+ export const config = { ...DEFAULT_CONFIG, ...read(KEYS.config, {}) };
25
+ export const saveConfig = () => write(KEYS.config, config);
26
+
27
+ const g = read(KEYS.global, {});
28
+ export const global = {
29
+ cg: new Set(g.cg ?? []),
30
+ scene: new Set(g.scene ?? []),
31
+ music: new Set(g.music ?? []),
32
+ ending: new Set(g.ending ?? []),
33
+ read: new Set(g.read ?? []),
34
+ chosen: new Set(g.chosen ?? []), // 「标签:选项文字」,后续周目给走过的选项标「已选」
35
+ };
36
+ let globalTimer = null;
37
+ export function saveGlobal(now = false) {
38
+ const flush = () => {
39
+ globalTimer = null;
40
+ write(KEYS.global, Object.fromEntries(Object.entries(global).map(([k, v]) => [k, [...v]])));
41
+ };
42
+ if (now) { clearTimeout(globalTimer); flush(); } else if (!globalTimer) globalTimer = setTimeout(flush, 800);
43
+ }
44
+
45
+ export function ageConfirmed() {
46
+ try { return sessionStorage.getItem(AGE_KEY) === 'yes'; } catch { return false; }
47
+ }
48
+ export function confirmAge() {
49
+ try { sessionStorage.setItem(AGE_KEY, 'yes'); } catch { /* 本次会话内仍按已确认处理 */ }
50
+ }
51
+
52
+ // 存档:{ [slotId]: { snap, chapter, text, date, thumb } };slotId 为 1..36、'quick'、'auto'
53
+ export const loadSaves = () => read(KEYS.saves, {});
54
+ export function writeSave(slot, record) {
55
+ const saves = loadSaves();
56
+ saves[slot] = record;
57
+ if (write(KEYS.saves, saves)) return true;
58
+ // 空间不足时先丢掉缩略图再试一次
59
+ saves[slot] = { ...record, thumb: null };
60
+ return write(KEYS.saves, saves);
61
+ }
62
+ export function deleteSave(slot) {
63
+ const saves = loadSaves();
64
+ delete saves[slot];
65
+ write(KEYS.saves, saves);
66
+ }
67
+ export function latestSave() {
68
+ const saves = loadSaves();
69
+ return Object.entries(saves).sort((a, b) => (b[1].time ?? 0) - (a[1].time ?? 0))[0] ?? null;
70
+ }