@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
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import socket
|
|
5
5
|
import urllib.error
|
|
6
6
|
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
@@ -25,7 +26,7 @@ DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
|
|
|
25
26
|
|
|
26
27
|
def normalize_api_url(raw_url):
|
|
27
28
|
"""Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
|
|
28
|
-
url =
|
|
29
|
+
url = raw_url.rstrip("/")
|
|
29
30
|
if url.endswith("/chat/completions"):
|
|
30
31
|
return url
|
|
31
32
|
return f"{url}/chat/completions"
|
|
@@ -33,34 +34,40 @@ def normalize_api_url(raw_url):
|
|
|
33
34
|
|
|
34
35
|
def is_mimo_token_plan_key(api_key):
|
|
35
36
|
"""Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
|
|
36
|
-
return
|
|
37
|
+
return api_key.startswith("tp-")
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def default_mimo_api_url(
|
|
40
|
+
def default_mimo_api_url(is_token_plan):
|
|
40
41
|
"""Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
|
|
41
42
|
|
|
42
43
|
MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
|
|
43
44
|
(`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
|
|
44
45
|
not the pay-as-you-go `api.xiaomimimo.com` endpoint.
|
|
46
|
+
|
|
47
|
+
The caller classifies its own key with `is_mimo_token_plan_key` and passes only
|
|
48
|
+
that bit: a credential never reaches a function whose return value is logged.
|
|
45
49
|
"""
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if not is_token_plan:
|
|
51
|
+
return DEFAULT_MIMO_API_URL
|
|
52
|
+
cluster = (os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER).strip().lower()
|
|
53
|
+
if cluster not in MIMO_TOKEN_PLAN_API_URLS:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"MIMO_TOKEN_PLAN_CLUSTER must be one of {sorted(MIMO_TOKEN_PLAN_API_URLS)}; got {cluster!r}"
|
|
56
|
+
)
|
|
57
|
+
return MIMO_TOKEN_PLAN_API_URLS[cluster]
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
def env_int(name, default, *, minimum=None):
|
|
54
|
-
"""Read an integer env var;
|
|
61
|
+
"""Read an integer env var; a malformed or out-of-range value is a clear error."""
|
|
55
62
|
raw = os.environ.get(name)
|
|
56
63
|
if raw is None or raw == "":
|
|
57
64
|
return default
|
|
58
65
|
try:
|
|
59
66
|
value = int(raw)
|
|
60
|
-
except
|
|
61
|
-
|
|
62
|
-
if minimum is not None:
|
|
63
|
-
|
|
67
|
+
except ValueError:
|
|
68
|
+
raise ValueError(f"{name} must be an integer; got {raw!r}") from None
|
|
69
|
+
if minimum is not None and value < minimum:
|
|
70
|
+
raise ValueError(f"{name} must be >= {minimum}; got {value}")
|
|
64
71
|
return value
|
|
65
72
|
|
|
66
73
|
|
|
@@ -72,18 +79,8 @@ def env_bool(name, default=False):
|
|
|
72
79
|
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
73
80
|
|
|
74
81
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
raw = os.environ.get(name)
|
|
78
|
-
if raw is None or raw == "":
|
|
79
|
-
return default
|
|
80
|
-
try:
|
|
81
|
-
value = float(raw)
|
|
82
|
-
except (TypeError, ValueError):
|
|
83
|
-
return default
|
|
84
|
-
if minimum is not None:
|
|
85
|
-
value = max(minimum, value)
|
|
86
|
-
return value
|
|
82
|
+
def load_json(path):
|
|
83
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
87
84
|
|
|
88
85
|
|
|
89
86
|
# Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
|
|
@@ -94,21 +91,21 @@ _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
|
|
|
94
91
|
_mimo_video_api_key = os.environ.get("MIMO_VIDEO_API_KEY", "") or _mimo_api_key
|
|
95
92
|
_mimo_tts_api_key = os.environ.get("MIMO_TTS_API_KEY", "") or _mimo_api_key
|
|
96
93
|
_mimo_asr_api_key = os.environ.get("MIMO_ASR_API_KEY", "") or _mimo_api_key
|
|
97
|
-
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
|
|
94
|
+
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
|
|
98
95
|
_raw_mimo_video_api_url = (
|
|
99
96
|
os.environ.get("MIMO_VIDEO_API_URL")
|
|
100
97
|
or os.environ.get("MIMO_API_URL")
|
|
101
|
-
or default_mimo_api_url(_mimo_video_api_key)
|
|
98
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_video_api_key))
|
|
102
99
|
)
|
|
103
100
|
_raw_mimo_tts_api_url = (
|
|
104
101
|
os.environ.get("MIMO_TTS_API_URL")
|
|
105
102
|
or os.environ.get("MIMO_API_URL")
|
|
106
|
-
or default_mimo_api_url(_mimo_tts_api_key)
|
|
103
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_tts_api_key))
|
|
107
104
|
)
|
|
108
105
|
_raw_mimo_asr_api_url = (
|
|
109
106
|
os.environ.get("MIMO_ASR_API_URL")
|
|
110
107
|
or os.environ.get("MIMO_API_URL")
|
|
111
|
-
or default_mimo_api_url(_mimo_asr_api_key)
|
|
108
|
+
or default_mimo_api_url(is_mimo_token_plan_key(_mimo_asr_api_key))
|
|
112
109
|
)
|
|
113
110
|
|
|
114
111
|
CONFIG = {
|
|
@@ -174,24 +171,6 @@ CONFIG = {
|
|
|
174
171
|
"vlm_workers": env_int("VLM_WORKERS", 8, minimum=1), # VLM 并行分析线程数
|
|
175
172
|
}
|
|
176
173
|
|
|
177
|
-
def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
|
|
178
|
-
"""Return the canonical tempo budget shared by voiceover and assemble."""
|
|
179
|
-
cfg = config or CONFIG
|
|
180
|
-
global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
|
|
181
|
-
rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
|
|
182
|
-
cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
|
|
183
|
-
hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
|
|
184
|
-
legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
|
|
185
|
-
segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
|
|
186
|
-
return {
|
|
187
|
-
"global_narration_speed": global_speed,
|
|
188
|
-
"tts_rate_factor": rate_factor,
|
|
189
|
-
"cumulative_tempo_max": cumulative_max,
|
|
190
|
-
"cumulative_tempo_hard_max": hard_max,
|
|
191
|
-
"segment_tempo_max": segment_tempo_max,
|
|
192
|
-
"max_raw_duration_factor": global_speed * segment_tempo_max,
|
|
193
|
-
}
|
|
194
|
-
|
|
195
174
|
|
|
196
175
|
class MiMoQCRequestError(RuntimeError):
|
|
197
176
|
"""Sanitized, fail-open transport error for the advisory QC request."""
|
|
@@ -213,19 +192,18 @@ def mimo_qc_api_call(payload, *, config=None, timeout=60):
|
|
|
213
192
|
endpoint = normalize_api_url(
|
|
214
193
|
cfg.get("mimo_video_api_url") or cfg.get("mimo_api_url") or cfg.get("api_url")
|
|
215
194
|
)
|
|
216
|
-
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
217
195
|
request = urllib.request.Request(
|
|
218
196
|
endpoint,
|
|
219
|
-
data=
|
|
197
|
+
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
220
198
|
headers={
|
|
221
199
|
"Content-Type": "application/json",
|
|
222
200
|
"User-Agent": "video-recap/mimo-qc",
|
|
223
|
-
"api-key":
|
|
201
|
+
"api-key": api_key,
|
|
224
202
|
},
|
|
225
203
|
method="POST",
|
|
226
204
|
)
|
|
227
205
|
try:
|
|
228
|
-
with urllib.request.urlopen(request, timeout=
|
|
206
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
229
207
|
raw = response.read().decode("utf-8")
|
|
230
208
|
except urllib.error.HTTPError as exc:
|
|
231
209
|
raise MiMoQCRequestError(f"http_{exc.code}") from None
|
|
@@ -235,7 +213,7 @@ def mimo_qc_api_call(payload, *, config=None, timeout=60):
|
|
|
235
213
|
raise MiMoQCRequestError("network_error") from None
|
|
236
214
|
try:
|
|
237
215
|
result = json.loads(raw)
|
|
238
|
-
except
|
|
216
|
+
except ValueError:
|
|
239
217
|
raise MiMoQCRequestError("invalid_json") from None
|
|
240
218
|
if not isinstance(result, dict):
|
|
241
219
|
raise MiMoQCRequestError("invalid_response")
|
|
@@ -23,6 +23,8 @@ import tempfile
|
|
|
23
23
|
from datetime import datetime, timezone
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
|
+
from lib import load_json
|
|
27
|
+
|
|
26
28
|
ALLOWED_ARTIFACTS = {
|
|
27
29
|
"scenes.json",
|
|
28
30
|
"asr_result.json",
|
|
@@ -106,11 +108,11 @@ def stable_json_dumps(value) -> str:
|
|
|
106
108
|
|
|
107
109
|
|
|
108
110
|
def settings_fingerprint(settings) -> str:
|
|
109
|
-
return hashlib.sha256(stable_json_dumps(settings
|
|
111
|
+
return hashlib.sha256(stable_json_dumps(settings).encode("utf-8")).hexdigest()
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
def source_id_from_fingerprint(fingerprint: str) -> str:
|
|
113
|
-
return f"src_{
|
|
115
|
+
return f"src_{fingerprint[:12]}"
|
|
114
116
|
|
|
115
117
|
|
|
116
118
|
def assign_source_ids(sources: list[dict]) -> list[dict]:
|
|
@@ -125,21 +127,16 @@ def assign_source_ids(sources: list[dict]) -> list[dict]:
|
|
|
125
127
|
assigned = []
|
|
126
128
|
for raw in sources:
|
|
127
129
|
item = dict(raw)
|
|
128
|
-
fp =
|
|
129
|
-
if not fp:
|
|
130
|
-
raise ValueError("source fingerprint is required")
|
|
130
|
+
fp = item["source_video_fingerprint"]
|
|
131
131
|
base = source_id_from_fingerprint(fp)
|
|
132
|
-
path = str(Path(item
|
|
132
|
+
path = str(Path(item["source_path"]).resolve())
|
|
133
133
|
used = seen.setdefault(fp, set())
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
suffix = hashlib.sha256(path.encode("utf-8")).hexdigest()[:6]
|
|
138
|
-
sid = f"{base}_{suffix}"
|
|
134
|
+
sid = base
|
|
135
|
+
if used:
|
|
136
|
+
sid = f"{base}_{hashlib.sha256(path.encode('utf-8')).hexdigest()[:6]}"
|
|
139
137
|
# Avoid accidental path-hash collisions within one manifest.
|
|
140
138
|
while sid in used:
|
|
141
|
-
|
|
142
|
-
sid = f"{base}_{suffix}"
|
|
139
|
+
sid = f"{base}_{hashlib.sha256((path + sid).encode('utf-8')).hexdigest()[:6]}"
|
|
143
140
|
used.add(sid)
|
|
144
141
|
item["source_id"] = sid
|
|
145
142
|
item["source_path"] = path
|
|
@@ -148,34 +145,21 @@ def assign_source_ids(sources: list[dict]) -> list[dict]:
|
|
|
148
145
|
|
|
149
146
|
|
|
150
147
|
def _slug(text: str, max_len: int = 48) -> str:
|
|
151
|
-
raw = Path(
|
|
148
|
+
raw = Path(text).stem.lower()
|
|
152
149
|
raw = re.sub(r"[^a-z0-9\u4e00-\u9fff._-]+", "-", raw).strip("-._")
|
|
153
150
|
return (raw or "material")[:max_len].strip("-._") or "material"
|
|
154
151
|
|
|
155
152
|
|
|
156
153
|
def material_id_for(source_path: str | Path, source_fingerprint: str) -> str:
|
|
157
|
-
return f"{_slug(str(source_path))}-{
|
|
154
|
+
return f"{_slug(str(source_path))}-{source_fingerprint[:12]}"
|
|
158
155
|
|
|
159
156
|
|
|
160
157
|
def material_dir(library_dir: str | Path, material_id: str) -> Path:
|
|
161
158
|
return Path(library_dir) / "materials" / material_id
|
|
162
159
|
|
|
163
160
|
|
|
164
|
-
def _load_json(path: Path):
|
|
165
|
-
try:
|
|
166
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
167
|
-
except (OSError, ValueError, TypeError):
|
|
168
|
-
return None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _safe_text(value, limit: int = 600) -> str:
|
|
172
|
-
text = str(value or "").replace("\x00", " ").strip()
|
|
173
|
-
return _redact_text(text)[:limit]
|
|
174
|
-
|
|
175
|
-
|
|
176
161
|
def _redact_text(text: str) -> str:
|
|
177
162
|
"""Redact credential value shapes only; leave ordinary words (secret/token/…) intact."""
|
|
178
|
-
text = str(text or "")
|
|
179
163
|
for rx in SECRET_VALUE_RES:
|
|
180
164
|
text = rx.sub("[redacted-token]", text)
|
|
181
165
|
return SECRET_ASSIGN_RE.sub(lambda m: f"{m.group(1)}{m.group(2)}[redacted-key]{m.group(4)}", text)
|
|
@@ -185,13 +169,12 @@ def _redact_json(value):
|
|
|
185
169
|
if isinstance(value, dict):
|
|
186
170
|
out = {}
|
|
187
171
|
for key, item in value.items():
|
|
188
|
-
key_text = str(key)
|
|
189
172
|
# Drop only the value of an exact credential-named key; keep the key name and
|
|
190
173
|
# never coalesce distinct keys (so benign fields like token_economy survive).
|
|
191
|
-
if
|
|
192
|
-
out[
|
|
174
|
+
if key.strip().lower() in SECRET_KEY_NAMES:
|
|
175
|
+
out[key] = "[redacted]"
|
|
193
176
|
else:
|
|
194
|
-
out[
|
|
177
|
+
out[key] = _redact_json(item)
|
|
195
178
|
return out
|
|
196
179
|
if isinstance(value, list):
|
|
197
180
|
return [_redact_json(item) for item in value]
|
|
@@ -202,54 +185,36 @@ def _redact_json(value):
|
|
|
202
185
|
|
|
203
186
|
def copy_artifact_redacted(src: Path, dst: Path) -> None:
|
|
204
187
|
"""Copy an allowed JSON/MD artifact without persisting obvious secret markers."""
|
|
205
|
-
|
|
206
|
-
text = src.read_text(encoding="utf-8")
|
|
207
|
-
except UnicodeDecodeError as exc:
|
|
208
|
-
raise ValueError(f"refusing to persist non-text material artifact: {src.name}") from exc
|
|
188
|
+
text = src.read_text(encoding="utf-8")
|
|
209
189
|
if src.suffix.lower() == ".json":
|
|
210
|
-
|
|
211
|
-
data = json.loads(text)
|
|
212
|
-
except ValueError:
|
|
213
|
-
dst.write_text(_redact_text(text), encoding="utf-8")
|
|
214
|
-
else:
|
|
215
|
-
dst.write_text(json.dumps(_redact_json(data), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
190
|
+
dst.write_text(json.dumps(_redact_json(json.loads(text)), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
216
191
|
else:
|
|
217
192
|
dst.write_text(_redact_text(text), encoding="utf-8")
|
|
218
193
|
|
|
219
194
|
|
|
220
195
|
def summarize_work_dir(work_dir: str | Path, *, source_name: str = "") -> dict:
|
|
221
|
-
"""
|
|
196
|
+
"""Grep-friendly tags for material.md/index: character and entity names plus artifact counts."""
|
|
222
197
|
work = Path(work_dir)
|
|
223
|
-
|
|
198
|
+
summary = f"Analyzed video material: {source_name or work.name}"
|
|
224
199
|
tags = []
|
|
225
|
-
|
|
226
|
-
if
|
|
200
|
+
index_path = work / "understanding_index.json"
|
|
201
|
+
if index_path.exists():
|
|
202
|
+
index = load_json(index_path)
|
|
227
203
|
for key in ("summary", "story_summary", "overall_summary", "one_sentence"):
|
|
228
|
-
if
|
|
229
|
-
|
|
204
|
+
if index.get(key):
|
|
205
|
+
summary = _redact_text(str(index[key]))[:600]
|
|
230
206
|
break
|
|
231
207
|
for key in ("characters", "entities", "keywords", "tags"):
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
tags.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if isinstance(scenes, list):
|
|
243
|
-
tags.append(f"scenes:{len(scenes)}")
|
|
244
|
-
asr = _load_json(work / "asr_result.json")
|
|
245
|
-
if isinstance(asr, list):
|
|
246
|
-
tags.append(f"asr:{len(asr)}")
|
|
247
|
-
summary = " | ".join(p for p in summary_parts if p) or f"Analyzed video material: {source_name or work.name}"
|
|
248
|
-
dedup_tags = []
|
|
249
|
-
for tag in tags:
|
|
250
|
-
if tag and tag not in dedup_tags:
|
|
251
|
-
dedup_tags.append(tag)
|
|
252
|
-
return {"summary": summary, "tags": dedup_tags[:20]}
|
|
208
|
+
for item in index.get(key, [])[:12]:
|
|
209
|
+
text = item.get("name", "") if isinstance(item, dict) else item
|
|
210
|
+
tags.append(_redact_text(str(text))[:80])
|
|
211
|
+
for name, label in (("scenes.json", "scenes"), ("asr_result.json", "asr")):
|
|
212
|
+
if (work / name).exists():
|
|
213
|
+
tags.append(f"{label}:{len(load_json(work / name))}")
|
|
214
|
+
return {
|
|
215
|
+
"summary": summary,
|
|
216
|
+
"tags": list(dict.fromkeys(tag for tag in tags if tag))[:20],
|
|
217
|
+
}
|
|
253
218
|
|
|
254
219
|
|
|
255
220
|
def allowed_artifact_paths(work_dir: str | Path) -> list[Path]:
|
|
@@ -258,19 +223,19 @@ def allowed_artifact_paths(work_dir: str | Path) -> list[Path]:
|
|
|
258
223
|
|
|
259
224
|
|
|
260
225
|
def write_material_md(path: Path, metadata: dict, summary: str, tags: list[str]) -> None:
|
|
261
|
-
artifact_lines = "\n".join(f"- `{a
|
|
226
|
+
artifact_lines = "\n".join(f"- `{a['name']}` → `{a['path']}`" for a in metadata["artifacts"])
|
|
262
227
|
tags_text = ", ".join(tags) if tags else "(none)"
|
|
263
|
-
text = f"""# Material: {metadata
|
|
228
|
+
text = f"""# Material: {metadata['source_name']}
|
|
264
229
|
|
|
265
|
-
- material_id: `{metadata
|
|
266
|
-
- source: `{metadata
|
|
267
|
-
- source_fingerprint: `{metadata
|
|
268
|
-
- settings_fingerprint: `{metadata
|
|
269
|
-
- updated_at: `{metadata
|
|
230
|
+
- material_id: `{metadata['material_id']}`
|
|
231
|
+
- source: `{metadata['source_path']}`
|
|
232
|
+
- source_fingerprint: `{metadata['source_video_fingerprint']}`
|
|
233
|
+
- settings_fingerprint: `{metadata['settings_fingerprint']}`
|
|
234
|
+
- updated_at: `{metadata['updated_at']}`
|
|
270
235
|
- tags: {tags_text}
|
|
271
236
|
|
|
272
237
|
## Summary
|
|
273
|
-
{
|
|
238
|
+
{summary}
|
|
274
239
|
|
|
275
240
|
## Artifacts
|
|
276
241
|
{artifact_lines or '- (none)'}
|
|
@@ -278,6 +243,23 @@ def write_material_md(path: Path, metadata: dict, summary: str, tags: list[str])
|
|
|
278
243
|
path.write_text(text, encoding="utf-8")
|
|
279
244
|
|
|
280
245
|
|
|
246
|
+
def _read_material_metadata(path: Path) -> dict | None:
|
|
247
|
+
try:
|
|
248
|
+
data = load_json(path)
|
|
249
|
+
except (OSError, ValueError):
|
|
250
|
+
return None
|
|
251
|
+
return data if isinstance(data, dict) else None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _material_cache_entry(path: Path) -> dict | None:
|
|
255
|
+
data = _read_material_metadata(path)
|
|
256
|
+
if not data or not all(data.get(key) for key in (
|
|
257
|
+
"source_video_fingerprint", "settings_fingerprint", "updated_at", "material_id",
|
|
258
|
+
)) or not isinstance(data.get("artifacts"), list):
|
|
259
|
+
return None
|
|
260
|
+
return data
|
|
261
|
+
|
|
262
|
+
|
|
281
263
|
def save_material(
|
|
282
264
|
library_dir: str | Path,
|
|
283
265
|
work_dir: str | Path,
|
|
@@ -302,7 +284,7 @@ def save_material(
|
|
|
302
284
|
fresh_names = {src.name for src in sources}
|
|
303
285
|
# Reconcile: drop allowed artifacts left from a previous (larger) save so the on-disk
|
|
304
286
|
# artifacts/ dir always matches material.json — a stale orphan must not surface in greps.
|
|
305
|
-
for stale in artifacts_dir.iterdir()
|
|
287
|
+
for stale in artifacts_dir.iterdir():
|
|
306
288
|
if stale.is_file() and stale.name in ALLOWED_ARTIFACTS and stale.name not in fresh_names:
|
|
307
289
|
stale.unlink()
|
|
308
290
|
copied = []
|
|
@@ -311,8 +293,11 @@ def save_material(
|
|
|
311
293
|
copy_artifact_redacted(src, dst)
|
|
312
294
|
copied.append({"name": src.name, "path": f"artifacts/{src.name}", "sha256": file_fingerprint(dst)})
|
|
313
295
|
|
|
314
|
-
|
|
315
|
-
|
|
296
|
+
meta_path = dest / "material.json"
|
|
297
|
+
previous = _read_material_metadata(meta_path)
|
|
298
|
+
created_at = previous.get("created_at") if previous else None
|
|
299
|
+
if not isinstance(created_at, str) or not created_at:
|
|
300
|
+
created_at = now
|
|
316
301
|
source_path = Path(source_path).resolve()
|
|
317
302
|
summary_info = summarize_work_dir(work_dir, source_name=source_path.name)
|
|
318
303
|
metadata = {
|
|
@@ -328,7 +313,7 @@ def save_material(
|
|
|
328
313
|
"created_at": created_at,
|
|
329
314
|
"updated_at": now,
|
|
330
315
|
}
|
|
331
|
-
|
|
316
|
+
meta_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
332
317
|
write_material_md(dest / "material.md", metadata, summary_info["summary"], summary_info["tags"])
|
|
333
318
|
|
|
334
319
|
index_record = {
|
|
@@ -356,15 +341,15 @@ def find_material_by_fingerprint(library_dir: str | Path, source_fingerprint: st
|
|
|
356
341
|
return None
|
|
357
342
|
candidates = []
|
|
358
343
|
for meta_path in root.glob("*/material.json"):
|
|
359
|
-
data =
|
|
360
|
-
if
|
|
344
|
+
data = _material_cache_entry(meta_path)
|
|
345
|
+
if data is not None and data["source_video_fingerprint"] == source_fingerprint:
|
|
361
346
|
data["material_dir"] = str(meta_path.parent)
|
|
362
347
|
candidates.append(data)
|
|
363
348
|
if not candidates:
|
|
364
349
|
return None
|
|
365
350
|
# Deterministic fallback policy for legacy/manual callers that do not know
|
|
366
351
|
# the expected material_id: newest wins, then material_id for stable ties.
|
|
367
|
-
candidates.sort(key=lambda d: (
|
|
352
|
+
candidates.sort(key=lambda d: (d["updated_at"], d["material_id"]), reverse=True)
|
|
368
353
|
return candidates[0]
|
|
369
354
|
|
|
370
355
|
|
|
@@ -391,51 +376,43 @@ def restore_material(
|
|
|
391
376
|
"""
|
|
392
377
|
lib = Path(library_dir)
|
|
393
378
|
if material_id:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
379
|
+
meta_path = material_dir(lib, material_id) / "material.json"
|
|
380
|
+
meta = _material_cache_entry(meta_path)
|
|
381
|
+
if meta is None:
|
|
382
|
+
return {"restored": False, "reason": "material missing or invalid"}
|
|
383
|
+
meta["material_dir"] = str(meta_path.parent)
|
|
397
384
|
else:
|
|
398
385
|
meta = find_material_by_fingerprint(lib, source_fingerprint)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if meta
|
|
402
|
-
return {"restored": False, "reason": "source fingerprint mismatch", "material_id": meta
|
|
403
|
-
if meta
|
|
404
|
-
return {"restored": False, "reason": "settings fingerprint mismatch", "material_id": meta
|
|
405
|
-
|
|
406
|
-
src_dir = Path(meta
|
|
386
|
+
if meta is None:
|
|
387
|
+
return {"restored": False, "reason": "material not found"}
|
|
388
|
+
if meta["source_video_fingerprint"] != source_fingerprint:
|
|
389
|
+
return {"restored": False, "reason": "source fingerprint mismatch", "material_id": meta["material_id"]}
|
|
390
|
+
if meta["settings_fingerprint"] != settings_fp:
|
|
391
|
+
return {"restored": False, "reason": "settings fingerprint mismatch", "material_id": meta["material_id"]}
|
|
392
|
+
|
|
393
|
+
src_dir = Path(meta["material_dir"]) / "artifacts"
|
|
407
394
|
if not src_dir.exists():
|
|
408
|
-
return {"restored": False, "reason": "material artifacts missing", "material_id": meta
|
|
395
|
+
return {"restored": False, "reason": "material artifacts missing", "material_id": meta["material_id"]}
|
|
409
396
|
dest = Path(work_dir)
|
|
410
397
|
dest.mkdir(parents=True, exist_ok=True)
|
|
411
|
-
staged = []
|
|
412
398
|
with tempfile.TemporaryDirectory(prefix=".material_restore_", dir=str(dest)) as tmp_name:
|
|
413
399
|
tmp = Path(tmp_name)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if not staged:
|
|
424
|
-
return {
|
|
425
|
-
"restored": False,
|
|
426
|
-
"reason": "material artifacts empty",
|
|
427
|
-
"material_id": meta.get("material_id"),
|
|
428
|
-
}
|
|
400
|
+
staged = [
|
|
401
|
+
artifact["name"]
|
|
402
|
+
for artifact in meta["artifacts"]
|
|
403
|
+
if isinstance(artifact, dict) and artifact.get("name") in ALLOWED_ARTIFACTS
|
|
404
|
+
]
|
|
405
|
+
if not staged or any(not (src_dir / name).is_file() for name in staged):
|
|
406
|
+
return {"restored": False, "reason": "material artifacts missing", "material_id": meta["material_id"]}
|
|
407
|
+
for name in staged:
|
|
408
|
+
copy_artifact_redacted(src_dir / name, tmp / name)
|
|
429
409
|
|
|
430
410
|
pruned = []
|
|
431
411
|
if prune_stale_allowed:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
# delete it and then skip the copy, losing the file.
|
|
437
|
-
if name in staged_set:
|
|
438
|
-
continue
|
|
412
|
+
# Never prune an artifact we are about to restore: the restore loop below honors
|
|
413
|
+
# `overwrite`, so pruning a staged name would (with overwrite=False) delete it and
|
|
414
|
+
# then skip the copy, losing the file.
|
|
415
|
+
for name in sorted(ALLOWED_ARTIFACTS - set(staged)):
|
|
439
416
|
out = dest / name
|
|
440
417
|
if out.exists():
|
|
441
418
|
out.unlink()
|
|
@@ -450,7 +427,7 @@ def restore_material(
|
|
|
450
427
|
restored.append(name)
|
|
451
428
|
return {
|
|
452
429
|
"restored": bool(restored),
|
|
453
|
-
"material_id": meta
|
|
430
|
+
"material_id": meta["material_id"],
|
|
454
431
|
"artifacts": restored,
|
|
455
432
|
"pruned_artifacts": pruned,
|
|
456
433
|
"material": meta,
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
"""Load this skill's
|
|
1
|
+
"""Load this skill's MiMo client without depending on the ambient ``lib`` module."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"video_recap_mimo_qc_lib", _LOCAL_LIB_PATH
|
|
6
|
+
_SPEC = importlib.util.spec_from_file_location(
|
|
7
|
+
"video_recap_mimo_qc_lib", Path(__file__).with_name("lib.py")
|
|
9
8
|
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
): # pragma: no cover - import invariant
|
|
13
|
-
raise ImportError(f"cannot load local MiMo QC client: {_LOCAL_LIB_PATH}")
|
|
14
|
-
_LOCAL_LIB = importlib.util.module_from_spec(_LOCAL_LIB_SPEC)
|
|
15
|
-
_LOCAL_LIB_SPEC.loader.exec_module(_LOCAL_LIB)
|
|
9
|
+
_LIB = importlib.util.module_from_spec(_SPEC)
|
|
10
|
+
_SPEC.loader.exec_module(_LIB)
|
|
16
11
|
|
|
17
|
-
DEFAULT_CONFIG =
|
|
18
|
-
mimo_qc_api_call =
|
|
12
|
+
DEFAULT_CONFIG = _LIB.CONFIG
|
|
13
|
+
mimo_qc_api_call = _LIB.mimo_qc_api_call
|