@oh-story/dsh 0.1.10 → 0.1.12
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 +11 -11
- package/lib/client.js +7 -3
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +212 -142
- package/lib/drama/skills/short-drama/SKILL.md +14 -2
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
- package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
- package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
- package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
- package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
- package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
- package/lib/index.js +71 -21
- package/lib/index.js.map +3 -3
- package/lib/oh-story/manifest.json +545 -310
- package/lib/oh-story/roles/chapter-extractor.md +11 -11
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +11 -9
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +9 -61
- package/lib/oh-story/roles/story-explorer.md +20 -125
- package/lib/oh-story/roles/story-researcher.md +0 -1
- package/lib/oh-story/skills/story/SKILL.md +6 -6
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
- package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +63 -579
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
- package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
- package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
- package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
- package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
- package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
- package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
- package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
- package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
- package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
- package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-review/SKILL.md +19 -273
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
- package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
- package/lib/oh-story/skills/story-review/references/solo.md +52 -0
- package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
- package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
- package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
- package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
- package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
- package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +2 -2
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +60 -1
- package/lib/video-recap/README.en.md +348 -137
- package/lib/video-recap/README.md +340 -136
- package/lib/video-recap/manifest.json +406 -251
- package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
- package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
- package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
- package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
- package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
- package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
- package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
- package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
- package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
- package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
- package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
- package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
- package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
- package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
- package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
- package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
- package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
- package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
- package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
- package/lib/video-recap/skills/video-script/SKILL.md +16 -13
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
- package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
- package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
- package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
- package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
- package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
- package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
- package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
- package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
- package/package.json +41 -41
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
- package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
- /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
- /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Self-contained config + utilities for this skill (no cross-skill imports).
|
|
2
2
|
Merged from the shared core; reads the same env vars as the rest of the bundle."""
|
|
3
3
|
import json
|
|
4
|
-
import hashlib
|
|
5
4
|
import math
|
|
6
5
|
import os
|
|
7
6
|
import re
|
|
@@ -25,12 +24,11 @@ MIMO_TOKEN_PLAN_API_URLS = {
|
|
|
25
24
|
}
|
|
26
25
|
DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
|
|
27
26
|
DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
|
|
28
|
-
DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
def normalize_api_url(raw_url):
|
|
32
30
|
"""Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
|
|
33
|
-
url =
|
|
31
|
+
url = raw_url.rstrip("/")
|
|
34
32
|
if url.endswith("/chat/completions"):
|
|
35
33
|
return url
|
|
36
34
|
return f"{url}/chat/completions"
|
|
@@ -118,14 +116,14 @@ _raw_mimo_asr_api_url = (
|
|
|
118
116
|
CONFIG = {
|
|
119
117
|
"api_url": normalize_api_url(_raw_api_url),
|
|
120
118
|
"api_key": _mimo_api_key,
|
|
121
|
-
"
|
|
119
|
+
"api_env_var": "MIMO_API_KEY",
|
|
122
120
|
"mimo_api_url": normalize_api_url(_raw_api_url),
|
|
123
121
|
"mimo_api_key": _mimo_api_key,
|
|
124
122
|
"mimo_video_api_url": normalize_api_url(_raw_mimo_video_api_url),
|
|
125
123
|
"mimo_video_api_key": _mimo_video_api_key,
|
|
126
124
|
"mimo_asr_api_url": normalize_api_url(_raw_mimo_asr_api_url),
|
|
127
125
|
"mimo_asr_api_key": _mimo_asr_api_key,
|
|
128
|
-
"
|
|
126
|
+
"mimo_asr_env_var": "MIMO_ASR_API_KEY" if os.environ.get("MIMO_ASR_API_KEY") else "MIMO_API_KEY",
|
|
129
127
|
"mimo_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
|
|
130
128
|
"mimo_video_model": os.environ.get("MIMO_VIDEO_MODEL") or os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
|
|
131
129
|
"vlm_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
|
|
@@ -208,24 +206,6 @@ CONFIG = {
|
|
|
208
206
|
SCRIPT_DIR = Path(__file__).parent
|
|
209
207
|
PROMPTS_DIR = SCRIPT_DIR.parent / "references"
|
|
210
208
|
|
|
211
|
-
def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
|
|
212
|
-
"""Return the canonical tempo budget shared by voiceover and assemble."""
|
|
213
|
-
cfg = config or CONFIG
|
|
214
|
-
global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
|
|
215
|
-
rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
|
|
216
|
-
cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
|
|
217
|
-
hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
|
|
218
|
-
legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
|
|
219
|
-
segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
|
|
220
|
-
return {
|
|
221
|
-
"global_narration_speed": global_speed,
|
|
222
|
-
"tts_rate_factor": rate_factor,
|
|
223
|
-
"cumulative_tempo_max": cumulative_max,
|
|
224
|
-
"cumulative_tempo_hard_max": hard_max,
|
|
225
|
-
"segment_tempo_max": segment_tempo_max,
|
|
226
|
-
"max_raw_duration_factor": global_speed * segment_tempo_max,
|
|
227
|
-
}
|
|
228
|
-
|
|
229
209
|
def log(msg):
|
|
230
210
|
print(f"[video-recap] {msg}", flush=True)
|
|
231
211
|
|
|
@@ -250,64 +230,31 @@ def get_video_duration(video_path):
|
|
|
250
230
|
"-of", "csv=p=0", str(video_path)]
|
|
251
231
|
result = run_cmd(cmd)
|
|
252
232
|
if result.returncode != 0:
|
|
253
|
-
|
|
233
|
+
raise RuntimeError(f"ffprobe 无法读取时长 {video_path}: {result.stderr.strip()[-500:]}")
|
|
254
234
|
try:
|
|
255
235
|
return float(result.stdout.strip())
|
|
256
|
-
except
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def stable_json_dumps(value):
|
|
260
|
-
"""Serialize values deterministically for non-secret cache fingerprints."""
|
|
261
|
-
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
262
|
-
|
|
263
|
-
def stable_hash(value):
|
|
264
|
-
"""Return an md5 digest for deterministic JSON-serializable values."""
|
|
265
|
-
return hashlib.md5(stable_json_dumps(value).encode("utf-8")).hexdigest()
|
|
236
|
+
except ValueError as exc:
|
|
237
|
+
raise RuntimeError(f"ffprobe 时长输出无法解析 {video_path}: {result.stdout.strip()[:100]!r}") from exc
|
|
266
238
|
|
|
267
|
-
_FILE_FINGERPRINT_MEMO = {}
|
|
268
239
|
|
|
240
|
+
def load_background_research(work_dir):
|
|
241
|
+
"""Load the agent-authored background_research.json: missing → {}, malformed → raise."""
|
|
242
|
+
path = Path(work_dir) / "background_research.json"
|
|
243
|
+
if not path.exists():
|
|
244
|
+
return {}
|
|
245
|
+
try:
|
|
246
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
247
|
+
except ValueError as exc:
|
|
248
|
+
raise ValueError(f"background_research.json 不是合法 JSON,请修复后重试: {exc}") from exc
|
|
249
|
+
if not isinstance(data, dict):
|
|
250
|
+
raise ValueError("background_research.json 顶层必须是 JSON 对象({...})")
|
|
251
|
+
return data
|
|
269
252
|
|
|
270
|
-
def
|
|
271
|
-
"""
|
|
253
|
+
def file_identity(path):
|
|
254
|
+
"""{"size", "mtime_ns"} of a file: the cache identity recorded instead of content hashes."""
|
|
272
255
|
st = os.stat(os.fspath(path))
|
|
273
|
-
return
|
|
274
|
-
|
|
256
|
+
return {"size": st.st_size, "mtime_ns": st.st_mtime_ns}
|
|
275
257
|
|
|
276
|
-
def file_fingerprint(path, chunk_size=1024 * 1024):
|
|
277
|
-
"""Return a full-content fingerprint for cache-correct identity checks.
|
|
278
|
-
|
|
279
|
-
The digest covers CONTENT only — never the path or mtime — so a copied video or
|
|
280
|
-
artifact is still recognised as the same asset, while any byte change invalidates
|
|
281
|
-
the cache even if timestamps, size, head, or tail bytes are misleading.
|
|
282
|
-
|
|
283
|
-
Identity metadata is used ONLY to memoize within a single process. One understanding
|
|
284
|
-
run fingerprints the same source video 8-10 times and the whole extracted frame set
|
|
285
|
-
2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
|
|
286
|
-
any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
|
|
287
|
-
re-hashed, so the memo can never serve a stale digest.
|
|
288
|
-
"""
|
|
289
|
-
key = _file_identity(path)
|
|
290
|
-
memoized = _FILE_FINGERPRINT_MEMO.get(key)
|
|
291
|
-
if memoized is not None:
|
|
292
|
-
return memoized
|
|
293
|
-
h = hashlib.sha256()
|
|
294
|
-
with open(os.fspath(path), "rb") as f:
|
|
295
|
-
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
296
|
-
h.update(chunk)
|
|
297
|
-
digest = h.hexdigest()
|
|
298
|
-
_FILE_FINGERPRINT_MEMO[key] = digest
|
|
299
|
-
return digest
|
|
300
|
-
def video_fingerprint(video_path):
|
|
301
|
-
"""Full video content fingerprint used as the root pipeline asset print."""
|
|
302
|
-
return file_fingerprint(video_path)
|
|
303
|
-
|
|
304
|
-
def step_cache_key(video_path, step_name, params_fingerprint=""):
|
|
305
|
-
"""Build a cache key from video content, step name and step parameters."""
|
|
306
|
-
params_digest = params_fingerprint
|
|
307
|
-
if not isinstance(params_digest, str):
|
|
308
|
-
params_digest = stable_hash(params_digest)
|
|
309
|
-
payload = f"{video_fingerprint(video_path)}_{step_name}_{params_digest}"
|
|
310
|
-
return hashlib.md5(payload.encode("utf-8")).hexdigest()
|
|
311
258
|
|
|
312
259
|
def _retry_after_seconds(value, fallback):
|
|
313
260
|
"""Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
|
|
@@ -342,7 +289,7 @@ def _sanitize_api_error(value, limit=500):
|
|
|
342
289
|
def _api_headers(api_provider=None, api_url=None, api_key=None):
|
|
343
290
|
"""Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
|
|
344
291
|
del api_provider, api_url # MiMo is the only provider; signature kept for call sites
|
|
345
|
-
key = CONFIG
|
|
292
|
+
key = CONFIG["api_key"] if api_key is None else api_key
|
|
346
293
|
return {
|
|
347
294
|
"Content-Type": "application/json",
|
|
348
295
|
"User-Agent": "video-recap/1.0",
|
|
@@ -357,7 +304,7 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
|
|
|
357
304
|
normalized["max_completion_tokens"] = normalized.pop("max_tokens")
|
|
358
305
|
model = str(normalized.get("model") or "")
|
|
359
306
|
if (
|
|
360
|
-
CONFIG
|
|
307
|
+
CONFIG["mimo_disable_thinking"]
|
|
361
308
|
and not model.endswith(("-tts", "-asr"))
|
|
362
309
|
and "thinking" not in normalized
|
|
363
310
|
):
|
|
@@ -369,9 +316,9 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
|
|
|
369
316
|
def _mimo_endpoint(kind):
|
|
370
317
|
"""Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
|
|
371
318
|
by_kind = {
|
|
372
|
-
"video": ("mimo_video_api_url", "mimo_video_api_key", "
|
|
373
|
-
"tts": ("mimo_tts_api_url", "mimo_tts_api_key", "
|
|
374
|
-
"asr": ("mimo_asr_api_url", "mimo_asr_api_key", "
|
|
319
|
+
"video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_env_var"),
|
|
320
|
+
"tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_env_var"),
|
|
321
|
+
"asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_env_var"),
|
|
375
322
|
}
|
|
376
323
|
if kind not in by_kind:
|
|
377
324
|
raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
|
|
@@ -379,7 +326,7 @@ def _mimo_endpoint(kind):
|
|
|
379
326
|
return {
|
|
380
327
|
"api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
|
|
381
328
|
"api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
|
|
382
|
-
"
|
|
329
|
+
"api_env_var": CONFIG.get(src_key, "MIMO_API_KEY"),
|
|
383
330
|
}
|
|
384
331
|
|
|
385
332
|
def _call_mimo_endpoint(kind, payload, max_retries=10):
|
|
@@ -390,7 +337,7 @@ def _call_mimo_endpoint(kind, payload, max_retries=10):
|
|
|
390
337
|
api_provider="mimo",
|
|
391
338
|
api_url=settings["api_url"],
|
|
392
339
|
api_key=settings["api_key"],
|
|
393
|
-
|
|
340
|
+
api_env_var=settings["api_env_var"],
|
|
394
341
|
)
|
|
395
342
|
|
|
396
343
|
def mimo_video_api_call(payload, max_retries=10):
|
|
@@ -401,7 +348,7 @@ def mimo_asr_api_call(payload, max_retries=10):
|
|
|
401
348
|
"""Call the MiMo speech-recognition (ASR) endpoint."""
|
|
402
349
|
return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
|
|
403
350
|
|
|
404
|
-
def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None,
|
|
351
|
+
def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_env_var=None):
|
|
405
352
|
"""调用 OpenAI-compatible API,带重试。
|
|
406
353
|
|
|
407
354
|
长视频理解会发出数百次 VLM/ASR 调用,集群的 429 限流是常态而非错误,所以重试更耐心
|
|
@@ -425,7 +372,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
425
372
|
wait = _retry_after_seconds(retry_after, max(wait, 10))
|
|
426
373
|
log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
|
|
427
374
|
elif e.code == 401:
|
|
428
|
-
key_name =
|
|
375
|
+
key_name = api_env_var or CONFIG["api_env_var"]
|
|
429
376
|
raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
|
|
430
377
|
elif e.code == 403:
|
|
431
378
|
hint = "API 访问被拒绝 (403)。"
|
|
@@ -459,9 +406,6 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
|
|
|
459
406
|
time.sleep(wait)
|
|
460
407
|
else:
|
|
461
408
|
raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
|
|
462
|
-
# Unreachable for max_retries >= 1; guards against a silent `None` return (and the
|
|
463
|
-
# TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
|
|
464
|
-
raise ValueError(f"max_retries must be >= 1, got {max_retries}")
|
|
465
409
|
|
|
466
410
|
def load_prompt(name):
|
|
467
411
|
"""加载 prompt 模板"""
|
|
@@ -5,6 +5,7 @@ timing and text checks live here, and every later consumer trusts its result.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import math
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from lib import CONFIG, log
|
|
@@ -32,7 +33,11 @@ def _lint_issue(level, index, code, message, **extra):
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def _is_number(value):
|
|
35
|
-
return
|
|
36
|
+
return (
|
|
37
|
+
isinstance(value, (int, float))
|
|
38
|
+
and not isinstance(value, bool)
|
|
39
|
+
and math.isfinite(value)
|
|
40
|
+
)
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
def _visual_overlay_issues(index, seg):
|
|
@@ -115,17 +120,13 @@ def _clip_span(clip):
|
|
|
115
120
|
return clip["start"], clip["end"]
|
|
116
121
|
|
|
117
122
|
|
|
118
|
-
def _clip_matches_for_segment(seg, clip_plan):
|
|
123
|
+
def _clip_matches_for_segment(seg, clip_plan, requested_clip_id):
|
|
119
124
|
if not clip_plan:
|
|
120
125
|
return []
|
|
121
126
|
clips = clip_plan["clips"]
|
|
122
127
|
midpoint = (seg["start"] + seg["end"]) / 2
|
|
123
|
-
if
|
|
124
|
-
|
|
125
|
-
requested = int(seg["source_clip_id"])
|
|
126
|
-
except (TypeError, ValueError):
|
|
127
|
-
return []
|
|
128
|
-
clips = [clip for clip in clips if clip.get("clip_id") == requested]
|
|
128
|
+
if requested_clip_id is not None:
|
|
129
|
+
clips = [clip for clip in clips if clip.get("clip_id") == requested_clip_id]
|
|
129
130
|
return [
|
|
130
131
|
clip
|
|
131
132
|
for clip in clips
|
|
@@ -196,9 +197,13 @@ def _has_connected_predecessor(narration, idx, start):
|
|
|
196
197
|
|
|
197
198
|
|
|
198
199
|
def lint_narration(
|
|
199
|
-
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
|
|
200
|
+
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
|
|
201
|
+
require_chronological=False,
|
|
200
202
|
):
|
|
201
|
-
"""Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set.
|
|
203
|
+
"""Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set.
|
|
204
|
+
|
|
205
|
+
Segments are sorted by start for the timing checks; ``require_chronological`` (the
|
|
206
|
+
--preserve-approved-text contract) additionally rejects input that is not already in order."""
|
|
202
207
|
scenes_analysis = scenes_analysis or []
|
|
203
208
|
errors = []
|
|
204
209
|
warnings = []
|
|
@@ -215,6 +220,7 @@ def lint_narration(
|
|
|
215
220
|
)
|
|
216
221
|
)
|
|
217
222
|
else:
|
|
223
|
+
previous_start = None
|
|
218
224
|
for idx, seg in enumerate(narration):
|
|
219
225
|
if not isinstance(seg, dict):
|
|
220
226
|
errors.append(
|
|
@@ -230,33 +236,47 @@ def lint_narration(
|
|
|
230
236
|
if not _is_number(start) or not _is_number(end):
|
|
231
237
|
errors.append(
|
|
232
238
|
_lint_issue(
|
|
233
|
-
"error", idx, "invalid_time", "start/end must be
|
|
239
|
+
"error", idx, "invalid_time", "start/end must be finite numbers"
|
|
234
240
|
)
|
|
235
241
|
)
|
|
236
242
|
continue
|
|
237
|
-
|
|
238
|
-
try:
|
|
239
|
-
pause = int(seg.get("pause_after_ms", CONFIG["breath_ms"]))
|
|
240
|
-
except (TypeError, ValueError):
|
|
243
|
+
if require_chronological and previous_start is not None and start < previous_start:
|
|
241
244
|
errors.append(
|
|
242
245
|
_lint_issue(
|
|
243
246
|
"error",
|
|
244
247
|
idx,
|
|
245
|
-
"
|
|
246
|
-
"
|
|
248
|
+
"out_of_order",
|
|
249
|
+
"Segments must be listed in chronological order",
|
|
250
|
+
start=start,
|
|
251
|
+
previous_start=previous_start,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
previous_start = start
|
|
255
|
+
text = seg.get("narration")
|
|
256
|
+
if not isinstance(text, str):
|
|
257
|
+
errors.append(
|
|
258
|
+
_lint_issue(
|
|
259
|
+
"error",
|
|
260
|
+
idx,
|
|
261
|
+
"invalid_narration",
|
|
262
|
+
"narration must be a string",
|
|
263
|
+
start=start,
|
|
264
|
+
end=end,
|
|
247
265
|
)
|
|
248
266
|
)
|
|
249
267
|
continue
|
|
250
|
-
|
|
251
|
-
|
|
268
|
+
text = text.strip()
|
|
269
|
+
pause = seg.get("pause_after_ms", CONFIG["breath_ms"])
|
|
270
|
+
if not isinstance(pause, int) or isinstance(pause, bool) or pause < 0:
|
|
271
|
+
errors.append(
|
|
252
272
|
_lint_issue(
|
|
253
|
-
"
|
|
273
|
+
"error",
|
|
254
274
|
idx,
|
|
255
|
-
"
|
|
256
|
-
"pause_after_ms
|
|
257
|
-
pause_after_ms=pause,
|
|
275
|
+
"invalid_pause",
|
|
276
|
+
"pause_after_ms must be a non-negative integer number of milliseconds",
|
|
258
277
|
)
|
|
259
278
|
)
|
|
279
|
+
continue
|
|
260
280
|
errors.extend(_visual_overlay_issues(idx, seg))
|
|
261
281
|
if end <= start:
|
|
262
282
|
errors.append(
|
|
@@ -388,7 +408,21 @@ def lint_narration(
|
|
|
388
408
|
)
|
|
389
409
|
|
|
390
410
|
if mode == "cut":
|
|
391
|
-
|
|
411
|
+
requested_clip_id = seg.get("source_clip_id")
|
|
412
|
+
if requested_clip_id is not None:
|
|
413
|
+
try:
|
|
414
|
+
requested_clip_id = int(requested_clip_id)
|
|
415
|
+
except (TypeError, ValueError):
|
|
416
|
+
errors.append(
|
|
417
|
+
_lint_issue(
|
|
418
|
+
"error",
|
|
419
|
+
idx,
|
|
420
|
+
"invalid_source_clip_id",
|
|
421
|
+
"source_clip_id must be an integer",
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
continue
|
|
425
|
+
matches = _clip_matches_for_segment(seg, clip_plan, requested_clip_id)
|
|
392
426
|
if not matches:
|
|
393
427
|
errors.append(
|
|
394
428
|
_lint_issue(
|
|
@@ -400,7 +434,7 @@ def lint_narration(
|
|
|
400
434
|
end=end,
|
|
401
435
|
)
|
|
402
436
|
)
|
|
403
|
-
elif len(matches) > 1 and
|
|
437
|
+
elif len(matches) > 1 and requested_clip_id is None:
|
|
404
438
|
errors.append(
|
|
405
439
|
_lint_issue(
|
|
406
440
|
"error",
|
|
@@ -426,18 +460,6 @@ def lint_narration(
|
|
|
426
460
|
clip_end=round(clip_end, 3),
|
|
427
461
|
)
|
|
428
462
|
)
|
|
429
|
-
if seg.get("source_clip_id") is not None:
|
|
430
|
-
try:
|
|
431
|
-
int(seg["source_clip_id"])
|
|
432
|
-
except (TypeError, ValueError):
|
|
433
|
-
errors.append(
|
|
434
|
-
_lint_issue(
|
|
435
|
-
"error",
|
|
436
|
-
idx,
|
|
437
|
-
"invalid_source_clip_id",
|
|
438
|
-
"source_clip_id must be an integer",
|
|
439
|
-
)
|
|
440
|
-
)
|
|
441
463
|
|
|
442
464
|
normalized.append(
|
|
443
465
|
{"index": idx, "start": start, "end": end, "char_count": char_count}
|
|
@@ -620,10 +642,12 @@ def lint_narration(
|
|
|
620
642
|
|
|
621
643
|
|
|
622
644
|
def validate_narration_or_raise(
|
|
623
|
-
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
|
|
645
|
+
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
|
|
646
|
+
require_chronological=False,
|
|
624
647
|
):
|
|
625
648
|
report = lint_narration(
|
|
626
|
-
narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir
|
|
649
|
+
narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir,
|
|
650
|
+
require_chronological=require_chronological,
|
|
627
651
|
)
|
|
628
652
|
if report["errors"]:
|
|
629
653
|
sample = "; ".join(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from lib import CONFIG,
|
|
6
|
+
from lib import CONFIG, file_identity
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _empty_evidence(mode):
|
|
@@ -21,11 +21,11 @@ def _read_json(path):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _output_payload_is_current(payload, work_dir):
|
|
24
|
-
|
|
24
|
+
plan_path = Path(work_dir) / "clip_plan_validated.json"
|
|
25
25
|
return (
|
|
26
26
|
payload is not None
|
|
27
|
-
and
|
|
28
|
-
and payload["
|
|
27
|
+
and plan_path.exists()
|
|
28
|
+
and payload["clip_plan_identity"] == file_identity(plan_path)
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
@@ -24,12 +24,7 @@ import subprocess
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
26
|
from extract import frame_number_for_time, parse_frame_number
|
|
27
|
-
from lib import CONFIG, run_cmd, log
|
|
28
|
-
|
|
29
|
-
try: # file_fingerprint is the project's content-fingerprint helper; reuse when cheap.
|
|
30
|
-
from lib import file_fingerprint
|
|
31
|
-
except ImportError: # pragma: no cover - lib always ships it; degrade gracefully if not.
|
|
32
|
-
file_fingerprint = None
|
|
27
|
+
from lib import CONFIG, run_cmd, log, get_video_duration
|
|
33
28
|
|
|
34
29
|
|
|
35
30
|
# Candidate font files probed (in order) for burning mm:ss labels. The first that exists
|
|
@@ -138,14 +133,11 @@ def _scene_anchor_timestamps(scenes, max_tiles):
|
|
|
138
133
|
long. Total is capped at max_tiles by evenly subsampling the ordered anchor list, so the
|
|
139
134
|
sheet stays legible (D2: one bounded contact sheet, not dozens of frame reads).
|
|
140
135
|
"""
|
|
141
|
-
long_scene_seconds = float(CONFIG
|
|
136
|
+
long_scene_seconds = float(CONFIG["storyboard_long_scene_seconds"])
|
|
142
137
|
anchors = []
|
|
143
138
|
for scene_id, scene in enumerate(scenes or []):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end = float(scene["end"])
|
|
147
|
-
except (KeyError, TypeError, ValueError):
|
|
148
|
-
continue
|
|
139
|
+
start = float(scene["start"])
|
|
140
|
+
end = float(scene["end"])
|
|
149
141
|
if end <= start:
|
|
150
142
|
continue
|
|
151
143
|
mid = (start + end) / 2.0
|
|
@@ -164,15 +156,6 @@ def _scene_anchor_timestamps(scenes, max_tiles):
|
|
|
164
156
|
return anchors
|
|
165
157
|
|
|
166
158
|
|
|
167
|
-
def _file_fp(path):
|
|
168
|
-
if file_fingerprint is None:
|
|
169
|
-
return None
|
|
170
|
-
try:
|
|
171
|
-
return file_fingerprint(path)
|
|
172
|
-
except (OSError, ValueError):
|
|
173
|
-
return None
|
|
174
|
-
|
|
175
|
-
|
|
176
159
|
def _labelled_frame(frame_path, label, font_path, scratch_dir, out_name):
|
|
177
160
|
"""Burn `label` onto a copy of frame_path via drawtext; return the labelled path or None.
|
|
178
161
|
|
|
@@ -205,7 +188,7 @@ def _tile_pages(frame_paths, columns, out_dir, out_stem, scratch_dir):
|
|
|
205
188
|
spill into _001.jpg, _002.jpg… Returns [] on any ffmpeg failure (caller degrades to None).
|
|
206
189
|
"""
|
|
207
190
|
columns = max(1, int(columns))
|
|
208
|
-
rows_per_page =
|
|
191
|
+
rows_per_page = int(CONFIG["storyboard_rows_per_page"])
|
|
209
192
|
per_page = columns * rows_per_page
|
|
210
193
|
pages = []
|
|
211
194
|
total = len(frame_paths)
|
|
@@ -234,13 +217,9 @@ def _tile_pages(frame_paths, columns, out_dir, out_stem, scratch_dir):
|
|
|
234
217
|
"-vf", f"tile={cols}x{rows}",
|
|
235
218
|
str(page_path),
|
|
236
219
|
]
|
|
237
|
-
|
|
238
|
-
result = run_cmd(cmd)
|
|
239
|
-
except Exception as exc: # noqa: BLE001
|
|
240
|
-
log(f"storyboard tile 异常: {exc}")
|
|
241
|
-
return []
|
|
220
|
+
result = run_cmd(cmd)
|
|
242
221
|
if result.returncode != 0 or not page_path.exists():
|
|
243
|
-
log(f"storyboard tile 失败: {
|
|
222
|
+
log(f"storyboard tile 失败: {result.stderr[-300:]}")
|
|
244
223
|
return []
|
|
245
224
|
pages.append(page_path)
|
|
246
225
|
return pages
|
|
@@ -282,7 +261,7 @@ def _render_storyboard(work_dir, tiles, out_stem):
|
|
|
282
261
|
|
|
283
262
|
try:
|
|
284
263
|
pages = _tile_pages(
|
|
285
|
-
render_frames, CONFIG
|
|
264
|
+
render_frames, CONFIG["storyboard_columns"],
|
|
286
265
|
storyboard_dir, out_stem, scratch_dir,
|
|
287
266
|
)
|
|
288
267
|
finally:
|
|
@@ -305,8 +284,8 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
|
|
|
305
284
|
if not paths:
|
|
306
285
|
log("storyboard 跳过 source:frames/ 为空或缺失")
|
|
307
286
|
return None
|
|
308
|
-
max_tiles = int(CONFIG
|
|
309
|
-
columns = int(CONFIG
|
|
287
|
+
max_tiles = int(CONFIG["storyboard_max_tiles"])
|
|
288
|
+
columns = int(CONFIG["storyboard_columns"])
|
|
310
289
|
anchors = _scene_anchor_timestamps(scenes, max_tiles)
|
|
311
290
|
if not anchors:
|
|
312
291
|
log("storyboard 跳过 source:无可用场景锚点")
|
|
@@ -339,7 +318,6 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
|
|
|
339
318
|
"schema_version": 1,
|
|
340
319
|
"timeline": "source",
|
|
341
320
|
"video_path": str(video_path),
|
|
342
|
-
"video_fingerprint": _file_fp(video_path),
|
|
343
321
|
"fps": float(fps) if fps else None,
|
|
344
322
|
"labels_burned": labels_burned,
|
|
345
323
|
"page_images": [str(p) for p in pages],
|
|
@@ -350,9 +328,10 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
|
|
|
350
328
|
},
|
|
351
329
|
"tiles": tiles,
|
|
352
330
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
331
|
+
try:
|
|
332
|
+
payload["duration"] = round(get_video_duration(video_path), 3)
|
|
333
|
+
except RuntimeError as exc:
|
|
334
|
+
log(f"storyboard source:无法读取视频时长,sidecar 省略 duration(忽略): {exc}")
|
|
356
335
|
json_path = work_dir / "storyboard" / "source_storyboard.json"
|
|
357
336
|
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
358
337
|
log(f"storyboard source: {len(tiles)} tiles → {len(pages)} page(s), labels_burned={labels_burned}")
|
|
@@ -362,32 +341,6 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
|
|
|
362
341
|
return None
|
|
363
342
|
|
|
364
343
|
|
|
365
|
-
def get_video_duration_safe(video_path):
|
|
366
|
-
"""Best-effort source duration via ffprobe; returns None on any failure (advisory)."""
|
|
367
|
-
if not shutil.which("ffprobe"):
|
|
368
|
-
return None
|
|
369
|
-
try:
|
|
370
|
-
result = subprocess.run(
|
|
371
|
-
["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
|
372
|
-
"-of", "csv=p=0", str(video_path)],
|
|
373
|
-
capture_output=True, text=True, timeout=30,
|
|
374
|
-
)
|
|
375
|
-
if result.returncode != 0:
|
|
376
|
-
return None
|
|
377
|
-
return float((result.stdout or "").strip())
|
|
378
|
-
except (ValueError, OSError, subprocess.SubprocessError):
|
|
379
|
-
return None
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _clip_plan_clips(clip_plan_validated):
|
|
383
|
-
"""Return the clips list from a validated plan (list or {"clips":[...]})."""
|
|
384
|
-
if isinstance(clip_plan_validated, dict):
|
|
385
|
-
clips = clip_plan_validated.get("clips", [])
|
|
386
|
-
else:
|
|
387
|
-
clips = clip_plan_validated
|
|
388
|
-
return clips if isinstance(clips, list) else []
|
|
389
|
-
|
|
390
|
-
|
|
391
344
|
def _source_to_output(source_time, clip):
|
|
392
345
|
"""Forward affine source→output map for ONE clip (reimplements cut.py:319 locally).
|
|
393
346
|
|
|
@@ -416,12 +369,12 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
|
|
|
416
369
|
if not paths:
|
|
417
370
|
log("storyboard 跳过 edited:frames/ 为空或缺失")
|
|
418
371
|
return None
|
|
419
|
-
clips =
|
|
372
|
+
clips = clip_plan_validated["clips"]
|
|
420
373
|
if not clips:
|
|
421
374
|
log("storyboard 跳过 edited:clip_plan_validated 无 clips")
|
|
422
375
|
return None
|
|
423
|
-
columns = int(CONFIG
|
|
424
|
-
max_tiles = int(CONFIG
|
|
376
|
+
columns = int(CONFIG["storyboard_columns"])
|
|
377
|
+
max_tiles = int(CONFIG["storyboard_max_tiles"])
|
|
425
378
|
|
|
426
379
|
tiles = []
|
|
427
380
|
seen_frames = set() # frame-identity dedupe (NOT luma de-dupe; that stays deferred)
|
|
@@ -474,7 +427,6 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
|
|
|
474
427
|
"timeline": "output",
|
|
475
428
|
"source_video_path": str(source_video_path),
|
|
476
429
|
"edited_video_path": str(edited_source) if edited_source.exists() else None,
|
|
477
|
-
"clip_plan_fingerprint": _clip_plan_fingerprint(clip_plan_validated),
|
|
478
430
|
"labels_burned": labels_burned,
|
|
479
431
|
"page_images": [str(p) for p in pages],
|
|
480
432
|
"sample_policy": {
|
|
@@ -491,17 +443,3 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
|
|
|
491
443
|
except Exception as exc: # noqa: BLE001 - advisory: never propagate
|
|
492
444
|
log(f"storyboard edited 失败(忽略): {exc}")
|
|
493
445
|
return None
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def _clip_plan_fingerprint(clip_plan_validated):
|
|
497
|
-
"""Stable fingerprint of the validated plan that drives the edited tiles."""
|
|
498
|
-
try:
|
|
499
|
-
return _stable_hash(clip_plan_validated)
|
|
500
|
-
except Exception: # noqa: BLE001
|
|
501
|
-
return None
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
def _stable_hash(value):
|
|
505
|
-
import hashlib
|
|
506
|
-
blob = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
507
|
-
return hashlib.md5(blob.encode("utf-8")).hexdigest()
|