@oh-story/dsh 0.1.7 → 0.1.9
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.
- package/README.md +30 -15
- package/lib/client.js +93 -16
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Merged from the shared core; reads the same env vars as the rest of the bundle."""
|
|
3
3
|
import json
|
|
4
4
|
import hashlib
|
|
5
|
+
import math
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import subprocess
|
|
@@ -40,34 +41,48 @@ def is_mimo_token_plan_key(api_key):
|
|
|
40
41
|
return str(api_key or "").strip().startswith("tp-")
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def default_mimo_api_url(
|
|
44
|
+
def default_mimo_api_url(is_token_plan, cluster=None):
|
|
44
45
|
"""Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
|
|
45
46
|
|
|
46
47
|
MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
|
|
47
48
|
(`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
|
|
48
49
|
not the pay-as-you-go `api.xiaomimimo.com` endpoint.
|
|
50
|
+
|
|
51
|
+
The caller classifies its own key with `is_mimo_token_plan_key` and passes only
|
|
52
|
+
that bit: a credential never reaches a function whose return value is logged.
|
|
49
53
|
"""
|
|
50
|
-
if
|
|
54
|
+
if is_token_plan:
|
|
51
55
|
cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
|
|
52
56
|
cluster_name = str(cluster_name).strip().lower()
|
|
53
|
-
|
|
57
|
+
if cluster_name not in MIMO_TOKEN_PLAN_API_URLS:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"MiMo token-plan cluster must be one of {sorted(MIMO_TOKEN_PLAN_API_URLS)}; "
|
|
60
|
+
f"got {cluster_name!r}"
|
|
61
|
+
)
|
|
62
|
+
return MIMO_TOKEN_PLAN_API_URLS[cluster_name]
|
|
54
63
|
return DEFAULT_MIMO_API_URL
|
|
55
64
|
|
|
56
65
|
|
|
57
|
-
def
|
|
58
|
-
"""Read an integer env var; ignore malformed values instead of crashing import."""
|
|
66
|
+
def _env_number(name, default, cast, minimum):
|
|
59
67
|
raw = os.environ.get(name)
|
|
60
68
|
if raw is None or raw == "":
|
|
61
69
|
return default
|
|
62
70
|
try:
|
|
63
|
-
value =
|
|
64
|
-
except
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
71
|
+
value = cast(raw)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise ValueError(f"{name} must be a {cast.__name__}; got {raw!r}") from exc
|
|
74
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
75
|
+
raise ValueError(f"{name} must be finite; got {raw!r}")
|
|
76
|
+
if minimum is not None and value < minimum:
|
|
77
|
+
raise ValueError(f"{name} must be >= {minimum}; got {value}")
|
|
68
78
|
return value
|
|
69
79
|
|
|
70
80
|
|
|
81
|
+
def env_int(name, default, *, minimum=None):
|
|
82
|
+
"""Read an integer env var, rejecting malformed or below-minimum values."""
|
|
83
|
+
return _env_number(name, default, int, minimum)
|
|
84
|
+
|
|
85
|
+
|
|
71
86
|
def env_bool(name, default=False):
|
|
72
87
|
"""Read common boolean env var forms."""
|
|
73
88
|
raw = os.environ.get(name)
|
|
@@ -77,17 +92,8 @@ def env_bool(name, default=False):
|
|
|
77
92
|
|
|
78
93
|
|
|
79
94
|
def env_float(name, default, *, minimum=None):
|
|
80
|
-
"""Read a float env var
|
|
81
|
-
|
|
82
|
-
if raw is None or raw == "":
|
|
83
|
-
return default
|
|
84
|
-
try:
|
|
85
|
-
value = float(raw)
|
|
86
|
-
except (TypeError, ValueError):
|
|
87
|
-
return default
|
|
88
|
-
if minimum is not None:
|
|
89
|
-
value = max(minimum, value)
|
|
90
|
-
return value
|
|
95
|
+
"""Read a float env var, rejecting malformed or below-minimum values."""
|
|
96
|
+
return _env_number(name, default, float, minimum)
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
# Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
|
|
@@ -97,16 +103,16 @@ def env_float(name, default, *, minimum=None):
|
|
|
97
103
|
_mimo_api_key = os.environ.get("MIMO_API_KEY", "")
|
|
98
104
|
_mimo_video_api_key = os.environ.get("MIMO_VIDEO_API_KEY", "") or _mimo_api_key
|
|
99
105
|
_mimo_asr_api_key = os.environ.get("MIMO_ASR_API_KEY", "") or _mimo_api_key
|
|
100
|
-
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
|
|
106
|
+
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
|
|
101
107
|
_raw_mimo_video_api_url = (
|
|
102
108
|
os.environ.get("MIMO_VIDEO_API_URL")
|
|
103
109
|
or os.environ.get("MIMO_API_URL")
|
|
104
|
-
or default_mimo_api_url(_mimo_video_api_key)
|
|
110
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_video_api_key))
|
|
105
111
|
)
|
|
106
112
|
_raw_mimo_asr_api_url = (
|
|
107
113
|
os.environ.get("MIMO_ASR_API_URL")
|
|
108
114
|
or os.environ.get("MIMO_API_URL")
|
|
109
|
-
or default_mimo_api_url(_mimo_asr_api_key)
|
|
115
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_asr_api_key))
|
|
110
116
|
)
|
|
111
117
|
|
|
112
118
|
CONFIG = {
|
|
@@ -410,8 +416,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
410
416
|
try:
|
|
411
417
|
req = urllib.request.Request(endpoint, data=data, headers=headers)
|
|
412
418
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
413
|
-
|
|
414
|
-
return result
|
|
419
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
415
420
|
except urllib.error.HTTPError as e:
|
|
416
421
|
body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
|
|
417
422
|
wait = min(2 ** attempt, 60)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""Enforce narration timing, evidence, budget, and sentence-integrity rules.
|
|
1
|
+
"""Enforce narration timing, evidence, budget, and sentence-integrity rules.
|
|
2
|
+
|
|
3
|
+
This is the single validator for the agent-authored narration.json: shape, numeric
|
|
4
|
+
timing and text checks live here, and every later consumer trusts its result.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
|
-
import importlib.util
|
|
4
7
|
import json
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
|
|
7
|
-
from lib import CONFIG
|
|
8
|
-
from lib import log
|
|
10
|
+
from lib import CONFIG, log
|
|
9
11
|
from agent_text import (
|
|
10
12
|
_clean_narration_punctuation,
|
|
11
13
|
_find_scene_for_midpoint,
|
|
@@ -16,24 +18,12 @@ from agent_text import (
|
|
|
16
18
|
_text_char_count,
|
|
17
19
|
_truncate_at_sentence,
|
|
18
20
|
)
|
|
21
|
+
from deslop_qc import analyze_deslop_qc
|
|
19
22
|
from speech_ownership import (
|
|
20
23
|
entry_overlaps_source_speech,
|
|
21
24
|
load_source_sentence_evidence,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
|
-
try:
|
|
25
|
-
from deslop_qc import analyze_deslop_qc
|
|
26
|
-
except ModuleNotFoundError:
|
|
27
|
-
_deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
|
|
28
|
-
_deslop_qc_spec = importlib.util.spec_from_file_location(
|
|
29
|
-
"deslop_qc", _deslop_qc_path
|
|
30
|
-
)
|
|
31
|
-
if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
|
|
32
|
-
raise
|
|
33
|
-
_deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
|
|
34
|
-
_deslop_qc_spec.loader.exec_module(_deslop_qc_module)
|
|
35
|
-
analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
|
|
36
|
-
|
|
37
27
|
|
|
38
28
|
def _lint_issue(level, index, code, message, **extra):
|
|
39
29
|
issue = {"level": level, "index": index, "code": code, "message": message}
|
|
@@ -41,11 +31,64 @@ def _lint_issue(level, index, code, message, **extra):
|
|
|
41
31
|
return issue
|
|
42
32
|
|
|
43
33
|
|
|
34
|
+
def _is_number(value):
|
|
35
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _visual_overlay_issues(index, seg):
|
|
39
|
+
"""Shape-check the renderer handoff recap reads verbatim from a validated segment.
|
|
40
|
+
|
|
41
|
+
recap owns which overlay `type` values it actually renders and drops the rest; lint owns
|
|
42
|
+
the shape, because nothing between here and that filter re-checks it — and the filter runs
|
|
43
|
+
after TTS, so a malformed overlay caught there would already have cost a synthesis pass.
|
|
44
|
+
"""
|
|
45
|
+
overlays = seg.get("visual_overlays")
|
|
46
|
+
if overlays is None:
|
|
47
|
+
return []
|
|
48
|
+
if not isinstance(overlays, list):
|
|
49
|
+
return [
|
|
50
|
+
_lint_issue(
|
|
51
|
+
"error", index, "invalid_visual_overlays", "visual_overlays must be an array"
|
|
52
|
+
)
|
|
53
|
+
]
|
|
54
|
+
issues = []
|
|
55
|
+
for position, overlay in enumerate(overlays):
|
|
56
|
+
if not isinstance(overlay, dict):
|
|
57
|
+
issues.append(
|
|
58
|
+
_lint_issue(
|
|
59
|
+
"error", index, "invalid_visual_overlay",
|
|
60
|
+
"visual_overlays entries must be objects",
|
|
61
|
+
overlay_index=position,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
65
|
+
for key in ("type", "text"):
|
|
66
|
+
value = overlay.get(key)
|
|
67
|
+
if not isinstance(value, str) or not value.strip():
|
|
68
|
+
issues.append(
|
|
69
|
+
_lint_issue(
|
|
70
|
+
"error", index, "invalid_visual_overlay",
|
|
71
|
+
f"visual_overlays[].{key} must be a non-empty string",
|
|
72
|
+
overlay_index=position,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
for key in ("start", "end"):
|
|
76
|
+
if key in overlay and not _is_number(overlay[key]):
|
|
77
|
+
issues.append(
|
|
78
|
+
_lint_issue(
|
|
79
|
+
"error", index, "invalid_visual_overlay",
|
|
80
|
+
f"visual_overlays[].{key} must be numeric",
|
|
81
|
+
overlay_index=position,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return issues
|
|
85
|
+
|
|
86
|
+
|
|
44
87
|
def _scene_bounds_for_midpoint(scenes_analysis, start, end):
|
|
45
|
-
scene = _find_scene_for_midpoint(scenes_analysis
|
|
88
|
+
scene = _find_scene_for_midpoint(scenes_analysis, start, end)
|
|
46
89
|
if not scene:
|
|
47
90
|
return None
|
|
48
|
-
return
|
|
91
|
+
return scene["start"], scene["end"]
|
|
49
92
|
|
|
50
93
|
|
|
51
94
|
def _frame_fact_times_for_segment(scenes_analysis, start, end):
|
|
@@ -55,53 +98,38 @@ def _frame_fact_times_for_segment(scenes_analysis, start, end):
|
|
|
55
98
|
many anchors is more likely to drift into "general story summary" instead
|
|
56
99
|
of staying attached to what is on screen.
|
|
57
100
|
"""
|
|
58
|
-
|
|
59
|
-
scene = _find_scene_for_midpoint(scenes_analysis or [], start, end)
|
|
101
|
+
scene = _find_scene_for_midpoint(scenes_analysis, start, end)
|
|
60
102
|
if not scene:
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
103
|
+
return []
|
|
104
|
+
return sorted(
|
|
105
|
+
ts
|
|
106
|
+
for ts in map(float, scene.get("frame_facts", {}))
|
|
107
|
+
if start <= ts <= end
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _clip_span(clip):
|
|
112
|
+
"""A validated plan clip carries source_start/source_end; a raw agent plan start/end."""
|
|
113
|
+
if "source_start" in clip:
|
|
114
|
+
return clip["source_start"], clip["source_end"]
|
|
115
|
+
return clip["start"], clip["end"]
|
|
70
116
|
|
|
71
117
|
|
|
72
118
|
def _clip_matches_for_segment(seg, clip_plan):
|
|
73
119
|
if not clip_plan:
|
|
74
120
|
return []
|
|
75
|
-
clips =
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
if not isinstance(clips, list):
|
|
79
|
-
return []
|
|
80
|
-
try:
|
|
81
|
-
start = float(seg.get("start"))
|
|
82
|
-
end = float(seg.get("end"))
|
|
83
|
-
except (TypeError, ValueError):
|
|
84
|
-
return []
|
|
85
|
-
midpoint = (start + end) / 2
|
|
121
|
+
clips = clip_plan["clips"]
|
|
122
|
+
midpoint = (seg["start"] + seg["end"]) / 2
|
|
86
123
|
if seg.get("source_clip_id") is not None:
|
|
87
124
|
try:
|
|
88
|
-
requested = int(seg
|
|
125
|
+
requested = int(seg["source_clip_id"])
|
|
89
126
|
except (TypeError, ValueError):
|
|
90
127
|
return []
|
|
91
|
-
|
|
92
|
-
clip
|
|
93
|
-
for clip in clips
|
|
94
|
-
if clip.get("clip_id") == requested
|
|
95
|
-
and float(clip.get("source_start", clip.get("start", 0)))
|
|
96
|
-
<= midpoint
|
|
97
|
-
<= float(clip.get("source_end", clip.get("end", 0)))
|
|
98
|
-
]
|
|
128
|
+
clips = [clip for clip in clips if clip.get("clip_id") == requested]
|
|
99
129
|
return [
|
|
100
130
|
clip
|
|
101
131
|
for clip in clips
|
|
102
|
-
if
|
|
103
|
-
<= midpoint
|
|
104
|
-
<= float(clip.get("source_end", clip.get("end", 0)))
|
|
132
|
+
if _clip_span(clip)[0] <= midpoint <= _clip_span(clip)[1]
|
|
105
133
|
]
|
|
106
134
|
|
|
107
135
|
|
|
@@ -132,17 +160,13 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
|
|
|
132
160
|
# already be several Chinese syllables into the next sentence.
|
|
133
161
|
for anchor in anchors:
|
|
134
162
|
when = anchor["time"]
|
|
135
|
-
|
|
136
|
-
pause_start = float(anchor.get("pause_start", when - 0.12))
|
|
137
|
-
except (TypeError, ValueError):
|
|
138
|
-
pause_start = when - 0.12
|
|
163
|
+
pause_start = anchor.get("pause_start", when - 0.12)
|
|
139
164
|
if pause_start - 0.05 <= start <= when + 0.08:
|
|
140
165
|
return None
|
|
141
166
|
|
|
142
167
|
suggested = next(
|
|
143
168
|
(anchor for anchor in anchors if anchor["time"] > start + 0.08), None
|
|
144
169
|
)
|
|
145
|
-
text_tail = str((suggested or {}).get("text_tail") or "").strip()
|
|
146
170
|
return _lint_issue(
|
|
147
171
|
"error",
|
|
148
172
|
index,
|
|
@@ -151,16 +175,31 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
|
|
|
151
175
|
"sentence-end anchor, or shorten/move/remove it when there is no later verified anchor, "
|
|
152
176
|
"then rerun lint before TTS. Source sentence interruption has no override.",
|
|
153
177
|
entry_time=round(start, 3),
|
|
154
|
-
suggested_start=round(
|
|
155
|
-
source_text_tail=text_tail,
|
|
156
|
-
anchor_confidence=suggested
|
|
178
|
+
suggested_start=round(suggested["time"], 3) if suggested else None,
|
|
179
|
+
source_text_tail=str(suggested.get("text_tail", "")).strip() if suggested else "",
|
|
180
|
+
anchor_confidence=suggested["confidence"] if suggested else None,
|
|
157
181
|
)
|
|
158
182
|
|
|
159
183
|
|
|
184
|
+
def _has_connected_predecessor(narration, idx, start):
|
|
185
|
+
"""A back-to-back narration handoff (<=150ms gap) does not expose the source track,
|
|
186
|
+
so the next TTS block is not a new source-speech entry."""
|
|
187
|
+
for other_idx, other in enumerate(narration):
|
|
188
|
+
if other_idx == idx or not isinstance(other, dict):
|
|
189
|
+
continue
|
|
190
|
+
other_start, other_end = other.get("start"), other.get("end")
|
|
191
|
+
if not _is_number(other_start) or not _is_number(other_end):
|
|
192
|
+
continue
|
|
193
|
+
if other_start < start and -0.001 <= start - other_end <= 0.15:
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
160
198
|
def lint_narration(
|
|
161
199
|
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
|
|
162
200
|
):
|
|
163
201
|
"""Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set."""
|
|
202
|
+
scenes_analysis = scenes_analysis or []
|
|
164
203
|
errors = []
|
|
165
204
|
warnings = []
|
|
166
205
|
normalized = []
|
|
@@ -187,10 +226,8 @@ def lint_narration(
|
|
|
187
226
|
)
|
|
188
227
|
)
|
|
189
228
|
continue
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
end = float(seg.get("end"))
|
|
193
|
-
except (TypeError, ValueError):
|
|
229
|
+
start, end = seg.get("start"), seg.get("end")
|
|
230
|
+
if not _is_number(start) or not _is_number(end):
|
|
194
231
|
errors.append(
|
|
195
232
|
_lint_issue(
|
|
196
233
|
"error", idx, "invalid_time", "start/end must be numeric"
|
|
@@ -198,19 +235,18 @@ def lint_narration(
|
|
|
198
235
|
)
|
|
199
236
|
continue
|
|
200
237
|
text = str(seg.get("narration", "")).strip()
|
|
201
|
-
pause_raw = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
|
|
202
238
|
try:
|
|
203
|
-
pause = int(
|
|
239
|
+
pause = int(seg.get("pause_after_ms", CONFIG["breath_ms"]))
|
|
204
240
|
except (TypeError, ValueError):
|
|
205
|
-
|
|
206
|
-
warnings.append(
|
|
241
|
+
errors.append(
|
|
207
242
|
_lint_issue(
|
|
208
|
-
"
|
|
243
|
+
"error",
|
|
209
244
|
idx,
|
|
210
245
|
"invalid_pause",
|
|
211
|
-
"pause_after_ms
|
|
246
|
+
"pause_after_ms must be an integer number of milliseconds",
|
|
212
247
|
)
|
|
213
248
|
)
|
|
249
|
+
continue
|
|
214
250
|
if pause < 0:
|
|
215
251
|
warnings.append(
|
|
216
252
|
_lint_issue(
|
|
@@ -221,6 +257,7 @@ def lint_narration(
|
|
|
221
257
|
pause_after_ms=pause,
|
|
222
258
|
)
|
|
223
259
|
)
|
|
260
|
+
errors.extend(_visual_overlay_issues(idx, seg))
|
|
224
261
|
if end <= start:
|
|
225
262
|
errors.append(
|
|
226
263
|
_lint_issue(
|
|
@@ -247,16 +284,12 @@ def lint_narration(
|
|
|
247
284
|
continue
|
|
248
285
|
|
|
249
286
|
char_count = _text_char_count(text)
|
|
250
|
-
budget = _recommended_char_budget(start, end
|
|
287
|
+
budget = _recommended_char_budget(start, end)
|
|
251
288
|
# estimate at the REAL playback rate (after the narration_speed atempo); otherwise a
|
|
252
289
|
# beat sized to its 1.3x-sped slot looks "over budget" when it actually fits.
|
|
253
|
-
play_rate =
|
|
254
|
-
CONFIG.get("speech_rate", 3.5)
|
|
255
|
-
* float(CONFIG.get("narration_speed", 1.0) or 1.0),
|
|
256
|
-
0.1,
|
|
257
|
-
)
|
|
290
|
+
play_rate = CONFIG["speech_rate"] * CONFIG["narration_speed"]
|
|
258
291
|
estimated_tts_seconds = char_count / play_rate
|
|
259
|
-
slot_seconds = _scene_available_seconds(start, end
|
|
292
|
+
slot_seconds = _scene_available_seconds(start, end)
|
|
260
293
|
if budget < 5:
|
|
261
294
|
warnings.append(
|
|
262
295
|
_lint_issue(
|
|
@@ -295,27 +328,16 @@ def lint_narration(
|
|
|
295
328
|
)
|
|
296
329
|
)
|
|
297
330
|
|
|
298
|
-
|
|
299
|
-
# the next TTS block is not a new source-speech entry. Keep this strict:
|
|
300
|
-
# only authored adjacency (<=150ms) bypasses the sentence anchor gate.
|
|
301
|
-
connected_predecessor = False
|
|
302
|
-
for other_idx, other in enumerate(narration):
|
|
303
|
-
if other_idx == idx or not isinstance(other, dict):
|
|
304
|
-
continue
|
|
305
|
-
try:
|
|
306
|
-
other_start = float(other.get("start"))
|
|
307
|
-
other_end = float(other.get("end"))
|
|
308
|
-
except (TypeError, ValueError):
|
|
309
|
-
continue
|
|
310
|
-
if other_start < start and -0.001 <= start - other_end <= 0.15:
|
|
311
|
-
connected_predecessor = True
|
|
312
|
-
break
|
|
313
|
-
if not connected_predecessor:
|
|
331
|
+
if not _has_connected_predecessor(narration, idx, start):
|
|
314
332
|
entry_issue = _source_sentence_entry_issue(
|
|
315
333
|
idx,
|
|
316
334
|
start,
|
|
317
335
|
source_sentence_anchors,
|
|
318
|
-
entry_overlaps_source_speech(
|
|
336
|
+
entry_overlaps_source_speech(
|
|
337
|
+
start,
|
|
338
|
+
source_evidence,
|
|
339
|
+
authored_overlap=seg.get("overlaps_speech", True),
|
|
340
|
+
),
|
|
319
341
|
)
|
|
320
342
|
if entry_issue:
|
|
321
343
|
errors.append(entry_issue)
|
|
@@ -349,13 +371,9 @@ def lint_narration(
|
|
|
349
371
|
frame_fact_times = _frame_fact_times_for_segment(
|
|
350
372
|
scenes_analysis, start, end
|
|
351
373
|
)
|
|
352
|
-
|
|
353
|
-
CONFIG.get("visual_beat_max_seconds", 18.0) or 18.0
|
|
354
|
-
)
|
|
355
|
-
max_visual_facts = int(CONFIG.get("visual_beat_max_facts", 3) or 3)
|
|
356
|
-
if (end - start) > max_visual_seconds and len(
|
|
374
|
+
if (end - start) > CONFIG["visual_beat_max_seconds"] and len(
|
|
357
375
|
frame_fact_times
|
|
358
|
-
) >
|
|
376
|
+
) > CONFIG["visual_beat_max_facts"]:
|
|
359
377
|
warnings.append(
|
|
360
378
|
_lint_issue(
|
|
361
379
|
"warning",
|
|
@@ -394,11 +412,7 @@ def lint_narration(
|
|
|
394
412
|
)
|
|
395
413
|
)
|
|
396
414
|
if len(matches) == 1:
|
|
397
|
-
|
|
398
|
-
clip_start = float(
|
|
399
|
-
clip.get("source_start", clip.get("start", start))
|
|
400
|
-
)
|
|
401
|
-
clip_end = float(clip.get("source_end", clip.get("end", end)))
|
|
415
|
+
clip_start, clip_end = _clip_span(matches[0])
|
|
402
416
|
if start < clip_start or end > clip_end:
|
|
403
417
|
warnings.append(
|
|
404
418
|
_lint_issue(
|
|
@@ -414,7 +428,7 @@ def lint_narration(
|
|
|
414
428
|
)
|
|
415
429
|
if seg.get("source_clip_id") is not None:
|
|
416
430
|
try:
|
|
417
|
-
int(seg
|
|
431
|
+
int(seg["source_clip_id"])
|
|
418
432
|
except (TypeError, ValueError):
|
|
419
433
|
errors.append(
|
|
420
434
|
_lint_issue(
|
|
@@ -462,23 +476,15 @@ def lint_narration(
|
|
|
462
476
|
if mode == "full" and len(sorted_segments) >= 2:
|
|
463
477
|
timeline_start = 0.0
|
|
464
478
|
timeline_end = sorted_segments[-1]["end"]
|
|
465
|
-
scene_ends = []
|
|
466
|
-
for scene in scenes_analysis or []:
|
|
467
|
-
try:
|
|
468
|
-
scene_ends.append(float(scene.get("end")))
|
|
469
|
-
except (AttributeError, TypeError, ValueError):
|
|
470
|
-
continue
|
|
479
|
+
scene_ends = [scene["end"] for scene in scenes_analysis]
|
|
471
480
|
if scene_ends:
|
|
472
481
|
timeline_end = max(timeline_end, max(scene_ends))
|
|
473
482
|
span = max(0.0, timeline_end - timeline_start)
|
|
474
483
|
# Score coverage at the SAME conservative rate the writer is budgeted at
|
|
475
484
|
# (_recommended_char_budget uses speech_rate * speech_safety_margin * narration_speed),
|
|
476
485
|
# else the metric scores ~18% stricter than its own budget and false-flags under_narrated.
|
|
477
|
-
play_rate =
|
|
478
|
-
CONFIG["speech_rate"]
|
|
479
|
-
* float(CONFIG.get("speech_safety_margin", 0.85) or 0.85)
|
|
480
|
-
* float(CONFIG.get("narration_speed", 1.0) or 1.0),
|
|
481
|
-
0.1,
|
|
486
|
+
play_rate = (
|
|
487
|
+
CONFIG["speech_rate"] * CONFIG["speech_safety_margin"] * CONFIG["narration_speed"]
|
|
482
488
|
)
|
|
483
489
|
spoken = [s["char_count"] / play_rate for s in sorted_segments]
|
|
484
490
|
spoken_ends = [
|
|
@@ -501,7 +507,7 @@ def lint_narration(
|
|
|
501
507
|
merged_intervals.append([start, end])
|
|
502
508
|
narrated_seconds = sum(end - start for start, end in merged_intervals)
|
|
503
509
|
coverage = narrated_seconds / span if span > 0 else 0.0
|
|
504
|
-
orig_min = CONFIG
|
|
510
|
+
orig_min = CONFIG["original_block_min_seconds"]
|
|
505
511
|
orig_gaps = [sorted_segments[0]["start"] - timeline_start]
|
|
506
512
|
orig_gaps.extend(
|
|
507
513
|
sorted_segments[i + 1]["start"] - spoken_ends[i]
|
|
@@ -510,10 +516,10 @@ def lint_narration(
|
|
|
510
516
|
orig_gaps.append(timeline_end - spoken_ends[-1])
|
|
511
517
|
original_blocks = sum(1 for g in orig_gaps if g >= orig_min)
|
|
512
518
|
avg_chars = sum(s["char_count"] for s in sorted_segments) / len(sorted_segments)
|
|
513
|
-
cov_target = CONFIG
|
|
514
|
-
cov_max = CONFIG
|
|
515
|
-
cov_min = CONFIG
|
|
516
|
-
block_min_chars = CONFIG
|
|
519
|
+
cov_target = CONFIG["narration_coverage_target"]
|
|
520
|
+
cov_max = CONFIG["narration_coverage_max"]
|
|
521
|
+
cov_min = CONFIG["narration_coverage_min"]
|
|
522
|
+
block_min_chars = CONFIG["narration_block_min_chars"]
|
|
517
523
|
metrics = {
|
|
518
524
|
"segment_count": len(sorted_segments),
|
|
519
525
|
"timeline_span_seconds": round(span, 2),
|
|
@@ -575,15 +581,20 @@ def lint_narration(
|
|
|
575
581
|
)
|
|
576
582
|
)
|
|
577
583
|
|
|
578
|
-
deslop_qc = analyze_deslop_qc(
|
|
579
|
-
|
|
584
|
+
deslop_qc = analyze_deslop_qc(
|
|
585
|
+
[seg for seg in narration if isinstance(seg, dict)]
|
|
586
|
+
if isinstance(narration, list)
|
|
587
|
+
else [],
|
|
588
|
+
work_dir=work_dir,
|
|
589
|
+
)
|
|
590
|
+
for blocker in deslop_qc["blockers"]:
|
|
580
591
|
errors.append(
|
|
581
592
|
_lint_issue(
|
|
582
593
|
"error",
|
|
583
|
-
blocker
|
|
584
|
-
blocker
|
|
585
|
-
blocker
|
|
586
|
-
source=blocker
|
|
594
|
+
blocker["index"],
|
|
595
|
+
blocker["code"],
|
|
596
|
+
blocker["message"],
|
|
597
|
+
source=blocker["source"],
|
|
587
598
|
matches=blocker.get("matches"),
|
|
588
599
|
sentence=blocker.get("sentence"),
|
|
589
600
|
)
|
|
@@ -616,7 +627,7 @@ def validate_narration_or_raise(
|
|
|
616
627
|
)
|
|
617
628
|
if report["errors"]:
|
|
618
629
|
sample = "; ".join(
|
|
619
|
-
f"#{e
|
|
630
|
+
f"#{e['index']}: {e['code']}" for e in report["errors"][:3]
|
|
620
631
|
)
|
|
621
632
|
raise ValueError(f"narration.json 预检失败: {sample}; 详见 narration_lint.json")
|
|
622
633
|
if report["warnings"]:
|
|
@@ -629,18 +640,12 @@ def validate_narration_or_raise(
|
|
|
629
640
|
|
|
630
641
|
|
|
631
642
|
def _validate_narration_budget(narration, scenes_analysis):
|
|
632
|
-
"""
|
|
633
|
-
|
|
634
|
-
raise ValueError("narration.json 必须是 JSON 数组")
|
|
635
|
-
|
|
643
|
+
"""Trim lint-validated narration to its timing budgets; drop what cannot be spoken."""
|
|
644
|
+
del scenes_analysis # scene boundaries are advisory (lint warns); authored timing is kept
|
|
636
645
|
cleaned = []
|
|
637
646
|
for raw in narration:
|
|
638
|
-
item = _normalise_narration_segment(raw
|
|
639
|
-
|
|
640
|
-
continue
|
|
641
|
-
max_chars = _recommended_char_budget(
|
|
642
|
-
item["start"], item["end"], item.get("pause_after_ms")
|
|
643
|
-
)
|
|
647
|
+
item = _normalise_narration_segment(raw)
|
|
648
|
+
max_chars = _recommended_char_budget(item["start"], item["end"])
|
|
644
649
|
if max_chars < 5:
|
|
645
650
|
log(f" 丢弃过短解说段 {item['start']:.1f}-{item['end']:.1f}s")
|
|
646
651
|
continue
|