@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
|
|
@@ -13,8 +14,6 @@ from email.utils import parsedate_to_datetime
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
16
|
-
_EXISTING_CONFIG_REF = globals().get("CONFIG")
|
|
17
|
-
|
|
18
17
|
DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
|
|
19
18
|
DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
|
|
20
19
|
MIMO_TOKEN_PLAN_API_URLS = {
|
|
@@ -22,7 +21,6 @@ MIMO_TOKEN_PLAN_API_URLS = {
|
|
|
22
21
|
"sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
|
|
23
22
|
"ams": "https://token-plan-ams.xiaomimimo.com/v1",
|
|
24
23
|
}
|
|
25
|
-
DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
|
|
26
24
|
DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
|
|
27
25
|
DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
|
|
28
26
|
DEFAULT_FISH_TTS_API_URL = "https://api.fish.audio/v1/tts"
|
|
@@ -32,7 +30,7 @@ DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
|
|
|
32
30
|
|
|
33
31
|
def normalize_api_url(raw_url):
|
|
34
32
|
"""Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
|
|
35
|
-
url =
|
|
33
|
+
url = raw_url.rstrip("/")
|
|
36
34
|
if url.endswith("/chat/completions"):
|
|
37
35
|
return url
|
|
38
36
|
return f"{url}/chat/completions"
|
|
@@ -40,76 +38,80 @@ def normalize_api_url(raw_url):
|
|
|
40
38
|
|
|
41
39
|
def is_mimo_token_plan_key(api_key):
|
|
42
40
|
"""Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
|
|
43
|
-
return
|
|
41
|
+
return api_key.startswith("tp-")
|
|
44
42
|
|
|
45
43
|
|
|
46
|
-
def default_mimo_api_url(
|
|
44
|
+
def default_mimo_api_url(is_token_plan):
|
|
47
45
|
"""Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
|
|
48
46
|
|
|
49
47
|
MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
|
|
50
48
|
(`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
|
|
51
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.
|
|
52
53
|
"""
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if not is_token_plan:
|
|
55
|
+
return DEFAULT_MIMO_API_URL
|
|
56
|
+
cluster = os.environ.get("MIMO_TOKEN_PLAN_CLUSTER", DEFAULT_MIMO_TOKEN_PLAN_CLUSTER).strip().lower()
|
|
57
|
+
if cluster 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)}, got {cluster!r}"
|
|
60
|
+
)
|
|
61
|
+
return MIMO_TOKEN_PLAN_API_URLS[cluster]
|
|
58
62
|
|
|
59
63
|
|
|
60
|
-
def
|
|
61
|
-
"""Read an integer env var; ignore malformed values instead of crashing import."""
|
|
64
|
+
def _env_number(name, default, cast, minimum):
|
|
62
65
|
raw = os.environ.get(name)
|
|
63
66
|
if raw is None or raw == "":
|
|
64
67
|
return default
|
|
65
68
|
try:
|
|
66
|
-
value =
|
|
67
|
-
except
|
|
68
|
-
|
|
69
|
-
if
|
|
70
|
-
|
|
69
|
+
value = cast(raw)
|
|
70
|
+
except ValueError as exc:
|
|
71
|
+
raise ValueError(f"{name} must be a {cast.__name__}, got {raw!r}") from exc
|
|
72
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
73
|
+
raise ValueError(f"{name} must be finite, got {raw!r}")
|
|
74
|
+
if minimum is not None and value < minimum:
|
|
75
|
+
raise ValueError(f"{name} must be >= {minimum}, got {raw!r}")
|
|
71
76
|
return value
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
def
|
|
75
|
-
"""Read
|
|
76
|
-
|
|
77
|
-
if raw is None or raw == "":
|
|
78
|
-
return default
|
|
79
|
-
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
79
|
+
def env_int(name, default, *, minimum=None):
|
|
80
|
+
"""Read an integer env var, rejecting malformed or below-minimum values."""
|
|
81
|
+
return _env_number(name, default, int, minimum)
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
def env_float(name, default, *, minimum=None):
|
|
83
|
-
"""Read a float env var
|
|
85
|
+
"""Read a float env var, rejecting malformed or below-minimum values."""
|
|
86
|
+
return _env_number(name, default, float, minimum)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def env_bool(name, default=False):
|
|
90
|
+
"""Read common boolean env var forms (1/true/yes/y/on, 0/false/no/n/off)."""
|
|
84
91
|
raw = os.environ.get(name)
|
|
85
92
|
if raw is None or raw == "":
|
|
86
93
|
return default
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return value
|
|
94
|
+
text = raw.strip().lower()
|
|
95
|
+
if text in {"1", "true", "yes", "y", "on"}:
|
|
96
|
+
return True
|
|
97
|
+
if text in {"0", "false", "no", "n", "off"}:
|
|
98
|
+
return False
|
|
99
|
+
raise ValueError(f"{name} must be a boolean (1/true/yes/on or 0/false/no/off), got {raw!r}")
|
|
94
100
|
|
|
95
101
|
|
|
96
|
-
# Single MiMo credential powers ASR +
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# route to the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
|
|
102
|
+
# Single MiMo credential powers ASR + TTS. The TTS override (MIMO_TTS_API_KEY / MIMO_TTS_API_URL)
|
|
103
|
+
# is optional and falls back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-route to
|
|
104
|
+
# the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
|
|
100
105
|
_mimo_api_key = os.environ.get("MIMO_API_KEY", "")
|
|
101
106
|
_mimo_tts_api_key = os.environ.get("MIMO_TTS_API_KEY", "") or _mimo_api_key
|
|
102
|
-
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
|
|
107
|
+
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
|
|
103
108
|
_raw_mimo_tts_api_url = (
|
|
104
109
|
os.environ.get("MIMO_TTS_API_URL")
|
|
105
110
|
or os.environ.get("MIMO_API_URL")
|
|
106
|
-
or default_mimo_api_url(_mimo_tts_api_key)
|
|
111
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_tts_api_key))
|
|
107
112
|
)
|
|
108
113
|
|
|
109
114
|
CONFIG = {
|
|
110
|
-
"api_url": normalize_api_url(_raw_api_url),
|
|
111
|
-
"api_key": _mimo_api_key,
|
|
112
|
-
"api_key_source": "MIMO_API_KEY",
|
|
113
115
|
"mimo_api_url": normalize_api_url(_raw_api_url),
|
|
114
116
|
"mimo_api_key": _mimo_api_key,
|
|
115
117
|
"mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
|
|
@@ -145,19 +147,15 @@ CONFIG = {
|
|
|
145
147
|
"tts_segment_target_rms_dbfs": env_float("TTS_SEGMENT_TARGET_RMS_DBFS", -20.0),
|
|
146
148
|
"tts_segment_peak_limit": env_float("TTS_SEGMENT_PEAK_LIMIT", 0.98, minimum=0.1),
|
|
147
149
|
}
|
|
148
|
-
if isinstance(_EXISTING_CONFIG_REF, dict):
|
|
149
|
-
_EXISTING_CONFIG_REF.clear()
|
|
150
|
-
_EXISTING_CONFIG_REF.update(CONFIG)
|
|
151
|
-
CONFIG = _EXISTING_CONFIG_REF
|
|
152
150
|
|
|
153
|
-
|
|
151
|
+
|
|
152
|
+
def narration_tempo_budget(tts_rate_offset=0.0):
|
|
154
153
|
"""Return the canonical tempo budget shared by voiceover and assemble."""
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
|
|
154
|
+
global_speed = CONFIG["narration_speed"]
|
|
155
|
+
rate_factor = 1.0 + tts_rate_offset
|
|
156
|
+
cumulative_max = CONFIG["narration_cumulative_tempo_max"]
|
|
157
|
+
hard_max = max(cumulative_max, CONFIG["narration_cumulative_tempo_hard_max"])
|
|
158
|
+
legacy_segment_cap = CONFIG["tts_segment_tempo_max"]
|
|
161
159
|
segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
|
|
162
160
|
return {
|
|
163
161
|
"global_narration_speed": global_speed,
|
|
@@ -168,43 +166,35 @@ def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
|
|
|
168
166
|
"max_raw_duration_factor": global_speed * segment_tempo_max,
|
|
169
167
|
}
|
|
170
168
|
|
|
169
|
+
|
|
171
170
|
def log(msg):
|
|
172
171
|
print(f"[video-recap] {msg}", flush=True)
|
|
173
172
|
|
|
173
|
+
|
|
174
174
|
def run_cmd(cmd, **kwargs):
|
|
175
|
-
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
text = str(part)
|
|
180
|
-
display_parts.append(text if len(text) <= 240 else text[:237] + "...")
|
|
181
|
-
display = " ".join(display_parts)
|
|
182
|
-
else:
|
|
183
|
-
display = str(cmd)
|
|
184
|
-
if len(display) > 2000:
|
|
185
|
-
display = display[:1997] + "..."
|
|
175
|
+
"""运行命令列表,返回 CompletedProcess"""
|
|
176
|
+
display = " ".join(
|
|
177
|
+
str(part) if len(str(part)) <= 240 else str(part)[:237] + "..." for part in cmd
|
|
178
|
+
)
|
|
186
179
|
log(f"运行: {display}")
|
|
187
180
|
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
|
188
181
|
|
|
182
|
+
|
|
189
183
|
def get_video_duration(video_path):
|
|
190
|
-
"""
|
|
191
|
-
cmd = ["ffprobe", "-v", "
|
|
184
|
+
"""获取媒体时长(秒)"""
|
|
185
|
+
cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
192
186
|
"-of", "csv=p=0", str(video_path)]
|
|
193
187
|
result = run_cmd(cmd)
|
|
194
188
|
if result.returncode != 0:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return float(result.stdout.strip())
|
|
198
|
-
except (TypeError, ValueError):
|
|
199
|
-
return 0.0
|
|
189
|
+
raise RuntimeError(f"ffprobe 无法读取时长: {video_path}: {result.stderr.strip()}")
|
|
190
|
+
return float(result.stdout.strip())
|
|
200
191
|
|
|
201
|
-
def stable_json_dumps(value):
|
|
202
|
-
"""Serialize values deterministically for non-secret cache fingerprints."""
|
|
203
|
-
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
204
192
|
|
|
205
193
|
def stable_hash(value):
|
|
206
|
-
"""Return an md5 digest
|
|
207
|
-
|
|
194
|
+
"""Return an md5 digest of a deterministic JSON serialization (non-secret cache fingerprints)."""
|
|
195
|
+
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
196
|
+
return hashlib.md5(encoded.encode("utf-8")).hexdigest()
|
|
197
|
+
|
|
208
198
|
|
|
209
199
|
_FILE_FINGERPRINT_MEMO = {}
|
|
210
200
|
|
|
@@ -239,6 +229,8 @@ def file_fingerprint(path, chunk_size=1024 * 1024):
|
|
|
239
229
|
digest = h.hexdigest()
|
|
240
230
|
_FILE_FINGERPRINT_MEMO[key] = digest
|
|
241
231
|
return digest
|
|
232
|
+
|
|
233
|
+
|
|
242
234
|
def _retry_after_seconds(value, fallback):
|
|
243
235
|
"""Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
|
|
244
236
|
if not value:
|
|
@@ -265,33 +257,22 @@ _ERROR_KEY_RE = re.compile(r"\b(?:tp|sk)-[A-Za-z0-9_-]{8,}\b")
|
|
|
265
257
|
|
|
266
258
|
def _sanitize_api_error(value, limit=500, *, extra_secrets=()):
|
|
267
259
|
"""Bound transport diagnostics without echoing request media or credentials."""
|
|
268
|
-
text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value
|
|
260
|
+
text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value))
|
|
269
261
|
text = _ERROR_KEY_RE.sub("<redacted-key>", text)
|
|
270
262
|
for secret in extra_secrets:
|
|
271
263
|
if secret:
|
|
272
264
|
text = text.replace(str(secret), "<redacted-key>")
|
|
273
265
|
return text[:limit]
|
|
274
266
|
|
|
275
|
-
def _api_headers(api_provider=None, api_url=None, api_key=None):
|
|
276
|
-
"""Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
|
|
277
|
-
del api_provider, api_url # MiMo is the only provider; signature kept for call sites
|
|
278
|
-
key = CONFIG.get("api_key", "") if api_key is None else api_key
|
|
279
|
-
return {
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
|
-
"User-Agent": "video-recap/1.0",
|
|
282
|
-
"api-key": key,
|
|
283
|
-
}
|
|
284
267
|
|
|
285
|
-
def _prepare_api_payload(payload
|
|
268
|
+
def _prepare_api_payload(payload):
|
|
286
269
|
"""Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
|
|
287
|
-
del api_provider, api_url
|
|
288
270
|
normalized = dict(payload)
|
|
289
271
|
if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
|
|
290
272
|
normalized["max_completion_tokens"] = normalized.pop("max_tokens")
|
|
291
|
-
model = str(normalized.get("model") or "")
|
|
292
273
|
if (
|
|
293
|
-
CONFIG
|
|
294
|
-
and not model.endswith(("-tts", "-asr"))
|
|
274
|
+
CONFIG["mimo_disable_thinking"]
|
|
275
|
+
and not normalized["model"].endswith(("-tts", "-asr"))
|
|
295
276
|
and "thinking" not in normalized
|
|
296
277
|
):
|
|
297
278
|
# MiMo V2.5 may spend small max_completion_tokens budgets on reasoning_content.
|
|
@@ -299,57 +280,48 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
|
|
|
299
280
|
normalized["thinking"] = {"type": "disabled"}
|
|
300
281
|
return normalized
|
|
301
282
|
|
|
302
|
-
def _mimo_endpoint(kind):
|
|
303
|
-
"""Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
|
|
304
|
-
by_kind = {
|
|
305
|
-
"video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_api_key_source"),
|
|
306
|
-
"tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_api_key_source"),
|
|
307
|
-
"asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_api_key_source"),
|
|
308
|
-
}
|
|
309
|
-
if kind not in by_kind:
|
|
310
|
-
raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
|
|
311
|
-
url_key, key_key, src_key = by_kind[kind]
|
|
312
|
-
return {
|
|
313
|
-
"api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
|
|
314
|
-
"api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
|
|
315
|
-
"api_key_source": CONFIG.get(src_key, "MIMO_API_KEY"),
|
|
316
|
-
}
|
|
317
283
|
|
|
318
|
-
def
|
|
319
|
-
|
|
284
|
+
def mimo_tts_api_call(payload):
|
|
285
|
+
"""Call the MiMo TTS endpoint."""
|
|
320
286
|
return api_call(
|
|
321
287
|
payload,
|
|
322
|
-
max_retries=
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
api_key_source=settings["api_key_source"],
|
|
288
|
+
max_retries=10,
|
|
289
|
+
api_url=CONFIG["mimo_tts_api_url"],
|
|
290
|
+
api_key=CONFIG["mimo_tts_api_key"],
|
|
291
|
+
api_key_source=CONFIG["mimo_tts_api_key_source"],
|
|
327
292
|
)
|
|
328
293
|
|
|
329
|
-
def mimo_tts_api_call(payload, max_retries=10):
|
|
330
|
-
"""Call the MiMo TTS endpoint."""
|
|
331
|
-
return _call_mimo_endpoint("tts", payload, max_retries=max_retries)
|
|
332
294
|
|
|
333
|
-
def mimo_asr_api_call(payload
|
|
295
|
+
def mimo_asr_api_call(payload):
|
|
334
296
|
"""Call the MiMo speech-recognition (ASR) endpoint."""
|
|
335
|
-
return
|
|
297
|
+
return api_call(
|
|
298
|
+
payload,
|
|
299
|
+
max_retries=10,
|
|
300
|
+
api_url=CONFIG["mimo_api_url"],
|
|
301
|
+
api_key=CONFIG["mimo_api_key"],
|
|
302
|
+
api_key_source="MIMO_API_KEY",
|
|
303
|
+
)
|
|
336
304
|
|
|
337
|
-
|
|
305
|
+
|
|
306
|
+
def api_call(payload, *, api_url, api_key, api_key_source, max_retries=8):
|
|
338
307
|
"""调用 OpenAI-compatible API,带重试。
|
|
339
308
|
|
|
340
309
|
集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
|
|
341
310
|
避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
|
|
342
311
|
"""
|
|
343
|
-
endpoint = normalize_api_url(api_url
|
|
344
|
-
headers =
|
|
345
|
-
|
|
312
|
+
endpoint = normalize_api_url(api_url)
|
|
313
|
+
headers = {
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
"User-Agent": "video-recap/1.0",
|
|
316
|
+
"api-key": api_key,
|
|
317
|
+
}
|
|
318
|
+
data = json.dumps(_prepare_api_payload(payload)).encode("utf-8")
|
|
346
319
|
|
|
347
320
|
for attempt in range(max_retries):
|
|
348
321
|
try:
|
|
349
322
|
req = urllib.request.Request(endpoint, data=data, headers=headers)
|
|
350
323
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
351
|
-
|
|
352
|
-
return result
|
|
324
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
353
325
|
except urllib.error.HTTPError as e:
|
|
354
326
|
body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
|
|
355
327
|
wait = min(2 ** attempt, 60)
|
|
@@ -358,8 +330,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
358
330
|
wait = _retry_after_seconds(retry_after, max(wait, 10))
|
|
359
331
|
log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
|
|
360
332
|
elif e.code == 401:
|
|
361
|
-
|
|
362
|
-
raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
|
|
333
|
+
raise RuntimeError(f"API 认证失败 (401)。请检查 {api_key_source} 和 API URL 是否匹配。")
|
|
363
334
|
elif e.code == 403:
|
|
364
335
|
hint = "API 访问被拒绝 (403)。"
|
|
365
336
|
if "1010" in body or "cloudflare" in body.lower():
|
|
@@ -382,8 +353,6 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
382
353
|
else:
|
|
383
354
|
raise RuntimeError(f"API 调用失败 {max_retries} 次: HTTP {e.code} — {body}")
|
|
384
355
|
except Exception as e: # noqa: BLE001 - transport/decode faults are all retryable here
|
|
385
|
-
# (Deliberately broad, but no longer written as `(URLError, Exception)`, which
|
|
386
|
-
# read as a tuple while `Exception` already subsumed the first member.)
|
|
387
356
|
wait = min(2 ** attempt, 60)
|
|
388
357
|
safe_error = _sanitize_api_error(e)
|
|
389
358
|
log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): {safe_error}")
|
|
@@ -392,13 +361,11 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
392
361
|
time.sleep(wait)
|
|
393
362
|
else:
|
|
394
363
|
raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
|
|
395
|
-
|
|
396
|
-
# TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
|
|
397
|
-
raise ValueError(f"max_retries must be >= 1, got {max_retries}")
|
|
364
|
+
|
|
398
365
|
|
|
399
366
|
def _text_char_count(text):
|
|
400
367
|
"""计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
|
|
401
|
-
return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text
|
|
368
|
+
return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text))
|
|
402
369
|
|
|
403
370
|
|
|
404
371
|
def _truncate_at_sentence(text, max_chars):
|
|
@@ -11,7 +11,7 @@ import sys
|
|
|
11
11
|
import wave
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from lib import CONFIG
|
|
14
|
+
from lib import CONFIG
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak_limit=0.98):
|
|
@@ -36,10 +36,12 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
|
|
|
36
36
|
# OUTPUT just to measure it) — about 420ms for a 12s block, times every narration
|
|
37
37
|
# segment. Same arithmetic, same rounding, one decode.
|
|
38
38
|
samples = array.array("h")
|
|
39
|
-
samples.frombytes(data
|
|
39
|
+
samples.frombytes(data)
|
|
40
40
|
if sys.byteorder != "little": # wave data is little-endian regardless of host
|
|
41
41
|
samples.byteswap()
|
|
42
42
|
if not samples:
|
|
43
|
+
# A silent block has no RMS to normalize toward; copy it through with neutral
|
|
44
|
+
# metadata rather than dividing by a zero sample count.
|
|
43
45
|
output_wav.write_bytes(input_wav.read_bytes())
|
|
44
46
|
return {
|
|
45
47
|
"rms_dbfs_before": None,
|
|
@@ -51,10 +53,10 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
|
|
|
51
53
|
rms = math.sqrt(sum(s * s for s in samples) / count)
|
|
52
54
|
peak = max(max(samples), -min(samples)) / 32768.0
|
|
53
55
|
rms_dbfs_before = 20 * math.log10(max(rms, 1e-9) / 32768.0)
|
|
54
|
-
target_linear = 10 ** (
|
|
56
|
+
target_linear = 10 ** (target_rms_dbfs / 20.0) * 32768.0
|
|
55
57
|
gain = target_linear / max(rms, 1e-9)
|
|
56
58
|
if peak > 0:
|
|
57
|
-
gain = min(gain,
|
|
59
|
+
gain = min(gain, peak_limit / peak)
|
|
58
60
|
|
|
59
61
|
# Accumulate the output statistics while normalizing instead of decoding the result back.
|
|
60
62
|
normalized = array.array("h", bytes(2 * count))
|
|
@@ -64,8 +66,7 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
|
|
|
64
66
|
value = max(-32768, min(32767, int(round(samples[i] * gain))))
|
|
65
67
|
normalized[i] = value
|
|
66
68
|
out_square_sum += value * value
|
|
67
|
-
|
|
68
|
-
out_peak_raw = abs(value)
|
|
69
|
+
out_peak_raw = max(out_peak_raw, abs(value))
|
|
69
70
|
out_bytes = normalized
|
|
70
71
|
if sys.byteorder != "little":
|
|
71
72
|
out_bytes = array.array("h", normalized)
|
|
@@ -86,28 +87,16 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
|
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
def _maybe_normalize_tts_wav(output_wav):
|
|
89
|
-
"""Normalize a synthesized TTS block in-place when
|
|
90
|
-
|
|
91
|
-
Unit tests often stub TTS with text bytes rather than real WAV. In that case
|
|
92
|
-
normalization is skipped safely; real MiMo WAV output gets RMS/peak metadata.
|
|
93
|
-
"""
|
|
94
|
-
if not CONFIG.get("tts_segment_normalize", True):
|
|
90
|
+
"""Normalize a synthesized TTS block in-place; None when normalization is disabled."""
|
|
91
|
+
if not CONFIG["tts_segment_normalize"]:
|
|
95
92
|
return None
|
|
96
93
|
output_wav = Path(output_wav)
|
|
97
94
|
tmp = output_wav.with_name(f"{output_wav.stem}_norm{output_wav.suffix}")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return meta
|
|
107
|
-
except Exception as exc:
|
|
108
|
-
try:
|
|
109
|
-
tmp.unlink(missing_ok=True)
|
|
110
|
-
except OSError:
|
|
111
|
-
pass
|
|
112
|
-
log(f" TTS RMS 归一跳过: {exc}")
|
|
113
|
-
return None
|
|
95
|
+
meta = _normalize_tts_wav_rms(
|
|
96
|
+
output_wav,
|
|
97
|
+
tmp,
|
|
98
|
+
target_rms_dbfs=CONFIG["tts_segment_target_rms_dbfs"],
|
|
99
|
+
peak_limit=CONFIG["tts_segment_peak_limit"],
|
|
100
|
+
)
|
|
101
|
+
os.replace(tmp, output_wav)
|
|
102
|
+
return meta
|