@oh-story/dsh 0.1.0
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/LICENSE +21 -0
- package/README.md +38 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1201 -0
- package/lib/client.js.map +7 -0
- package/lib/drama/LICENSE.upstream +21 -0
- package/lib/drama/manifest.json +1012 -0
- package/lib/drama/skills/short-drama/SKILL.md +153 -0
- package/lib/drama/skills/short-drama/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama/assets/creator-decision.example.jsonl +4 -0
- package/lib/drama/skills/short-drama/assets/production-observation.example.jsonl +2 -0
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +36 -0
- package/lib/drama/skills/short-drama/assets/reference-observation.example.jsonl +2 -0
- package/lib/drama/skills/short-drama/references/audience-reveal.md +74 -0
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +94 -0
- package/lib/drama/skills/short-drama/references/creator-workflow.md +82 -0
- package/lib/drama/skills/short-drama/references/form-cards/Q/347/211/210/350/241/250/350/276/276.md +73 -0
- package/lib/drama/skills/short-drama/references/form-cards//344/272/214/347/273/264/345/212/250/346/200/201/346/274/253.md +74 -0
- package/lib/drama/skills/short-drama/references/form-cards//345/233/275/346/274/253/344/272/214/346/254/241/345/205/203.md +79 -0
- package/lib/drama/skills/short-drama/references/form-cards//345/256/236/346/213/215.md +75 -0
- package/lib/drama/skills/short-drama/references/form-cards//346/260/264/345/242/250/347/254/224/350/247/246.md +85 -0
- package/lib/drama/skills/short-drama/references/form-cards//351/243/216/346/240/274/345/214/226/344/270/211/347/273/264.md +72 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +80 -0
- package/lib/drama/skills/short-drama/references/lifecycle-commands.md +130 -0
- package/lib/drama/skills/short-drama/references/look-development.md +52 -0
- package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +58 -0
- package/lib/drama/skills/short-drama/references/production-form-profiles.md +126 -0
- package/lib/drama/skills/short-drama/references/reference-roles.md +142 -0
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +27 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +1963 -0
- package/lib/drama/skills/short-drama/scripts/selftest.py +287 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +187 -0
- package/lib/drama/skills/short-drama-assets/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +3 -0
- package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +4 -0
- package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +5 -0
- package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +3 -0
- package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +5 -0
- package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +3 -0
- package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +2 -0
- package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +2 -0
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +89 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +164 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +123 -0
- package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +146 -0
- package/lib/drama/skills/short-drama-assets/references/location-and-view.md +84 -0
- package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +111 -0
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +107 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +136 -0
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +171 -0
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +267 -0
- package/lib/drama/skills/short-drama-assets/scripts/selftest.py +89 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +157 -0
- package/lib/drama/skills/short-drama-develop/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -0
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +120 -0
- package/lib/drama/skills/short-drama-develop/assets/episode-map.jsonl +1 -0
- package/lib/drama/skills/short-drama-develop/assets/story-engine.md +181 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +219 -0
- package/lib/drama/skills/short-drama-develop/references/creative-reference-intake.md +57 -0
- package/lib/drama/skills/short-drama-develop/references/director-brief-craft.md +113 -0
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +509 -0
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +240 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//344/272/262/345/255/220/351/232/220/347/247/230.md +112 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//344/273/231/344/276/240/344/277/256/347/234/237.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/212/250/344/275/234/344/273/273/345/212/241.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/217/244/350/243/205/346/235/203/350/260/213.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/256/266/345/272/255/345/205/263/347/263/273.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//346/202/254/347/226/221/350/247/204/345/210/231.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/224/237/346/264/273/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/240/264/351/225/234/351/207/215/345/234/206.md +109 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//350/201/214/345/234/272/345/226/234/345/211/247.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//350/261/252/351/227/250/345/251/232/346/201/213.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//350/272/253/344/273/275/351/224/231/344/275/215.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +92 -0
- package/lib/drama/skills/short-drama-develop/references/mechanism-loop.md +282 -0
- package/lib/drama/skills/short-drama-develop/references/multi-episode-intake.md +128 -0
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +211 -0
- package/lib/drama/skills/short-drama-develop/references/reveal-reversal-payoff.md +99 -0
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +293 -0
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +88 -0
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +359 -0
- package/lib/drama/skills/short-drama-develop/scripts/episode_intake.py +699 -0
- package/lib/drama/skills/short-drama-develop/scripts/selftest.py +47 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +181 -0
- package/lib/drama/skills/short-drama-image-prompts/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompt-spec.jsonl.md +187 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +37 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-frame-spec.jsonl.md +128 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +31 -0
- package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +154 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +231 -0
- package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +122 -0
- package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +152 -0
- package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +96 -0
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +38 -0
- package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +177 -0
- package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +140 -0
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +92 -0
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +89 -0
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +443 -0
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +93 -0
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +245 -0
- package/lib/drama/skills/short-drama-novel-analyze/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +116 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +124 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +155 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +141 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +67 -0
- package/lib/drama/skills/short-drama-novel-analyze/scripts/novel_index.py +651 -0
- package/lib/drama/skills/short-drama-novel-analyze/scripts/selftest.py +51 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +103 -0
- package/lib/drama/skills/short-drama-produce/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +140 -0
- package/lib/drama/skills/short-drama-produce/references/providers/gpt-image-2.md +24 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-music.md +25 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +40 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +60 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +1635 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +964 -0
- package/lib/drama/skills/short-drama-produce/scripts/selftest.py +134 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +178 -0
- package/lib/drama/skills/short-drama-review/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +2 -0
- package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +42 -0
- package/lib/drama/skills/short-drama-review/assets/verdict-template.json +32 -0
- package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +2 -0
- package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +34 -0
- package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +114 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +270 -0
- package/lib/drama/skills/short-drama-review/references/project-calibration.md +85 -0
- package/lib/drama/skills/short-drama-review/references/review-method.md +82 -0
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +115 -0
- package/lib/drama/skills/short-drama-review/references/rubric-source-analysis.md +108 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +164 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +166 -0
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +87 -0
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +325 -0
- package/lib/drama/skills/short-drama-review/scripts/selftest.py +87 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +264 -0
- package/lib/drama/skills/short-drama-storyboard/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +2 -0
- package/lib/drama/skills/short-drama-storyboard/assets/coverage-template.json +49 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +32 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-template.jsonl +2 -0
- package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +12 -0
- package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +2 -0
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +241 -0
- package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +169 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +264 -0
- package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +70 -0
- package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +157 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +192 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +29 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +110 -0
- package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +191 -0
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +824 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +245 -0
- package/lib/drama/skills/short-drama-video-prompts/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +21 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/delivery-container.jsonl.md +101 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +212 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +2 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/music-spec.jsonl.md +31 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +38 -0
- package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +46 -0
- package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +2 -0
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +240 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +255 -0
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +93 -0
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +304 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +191 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +312 -0
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +168 -0
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +110 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +329 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +450 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +327 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +82 -0
- package/lib/drama/skills/short-drama-write/SKILL.md +284 -0
- package/lib/drama/skills/short-drama-write/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-write/assets/beats.jsonl +2 -0
- package/lib/drama/skills/short-drama-write/assets/episode-card-standalone.json +120 -0
- package/lib/drama/skills/short-drama-write/assets/episode-card.json +55 -0
- package/lib/drama/skills/short-drama-write/assets/screenplay.md +36 -0
- package/lib/drama/skills/short-drama-write/assets/voice-record-sheet.jsonl.md +89 -0
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +519 -0
- package/lib/drama/skills/short-drama-write/references/production-format-dialect.md +273 -0
- package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +51 -0
- package/lib/drama/skills/short-drama-write/references/scene-sound-dramaturgy.md +43 -0
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +260 -0
- package/lib/drama/skills/short-drama-write/references/script-craft.md +558 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +94 -0
- package/lib/drama/skills/short-drama-write/references/substitutable-realization.md +107 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +309 -0
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +1091 -0
- package/lib/drama/skills/short-drama-write/scripts/selftest.py +229 -0
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +416 -0
- package/lib/index.js +756 -0
- package/lib/index.js.map +7 -0
- package/lib/oh-story/LICENSE.upstream +21 -0
- package/lib/oh-story/manifest.json +1351 -0
- package/lib/oh-story/roles/chapter-extractor.md +314 -0
- package/lib/oh-story/roles/character-designer.md +169 -0
- package/lib/oh-story/roles/consistency-checker.md +190 -0
- package/lib/oh-story/roles/narrative-writer.md +294 -0
- package/lib/oh-story/roles/story-architect.md +195 -0
- package/lib/oh-story/roles/story-explorer.md +364 -0
- package/lib/oh-story/roles/story-researcher.md +304 -0
- package/lib/oh-story/skills/browser-cdp/SKILL.md +175 -0
- package/lib/oh-story/skills/story/SKILL.md +116 -0
- package/lib/oh-story/skills/story/VERSION +1 -0
- package/lib/oh-story/skills/story-cover/SKILL.md +327 -0
- package/lib/oh-story/skills/story-cover/references/cover-styles.md +210 -0
- package/lib/oh-story/skills/story-deslop/SKILL.md +445 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +582 -0
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +1371 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +330 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +241 -0
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +410 -0
- package/lib/oh-story/skills/story-import/SKILL.md +634 -0
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +82 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +169 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +134 -0
- package/lib/oh-story/skills/story-import/references/state-tracking.md +108 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +442 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +175 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +161 -0
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +1139 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +329 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +270 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +530 -0
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +559 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +80 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +149 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +133 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +342 -0
- package/lib/oh-story/skills/story-long-scan/references/genre-trends.md +197 -0
- package/lib/oh-story/skills/story-long-scan/references/publishing-guide.md +114 -0
- package/lib/oh-story/skills/story-long-scan/references/reader-profiling.md +105 -0
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +207 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +78 -0
- package/lib/oh-story/skills/story-long-write/SKILL.md +327 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +582 -0
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +302 -0
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-long-write/references/character-basics.md +445 -0
- package/lib/oh-story/skills/story-long-write/references/character-design-methods.md +428 -0
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +385 -0
- package/lib/oh-story/skills/story-long-write/references/commercial-core-methods.md +502 -0
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +62 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +282 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +426 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +209 -0
- package/lib/oh-story/skills/story-long-write/references/female-audience-writing.md +188 -0
- package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +169 -0
- package/lib/oh-story/skills/story-long-write/references/genre-catalog.md +480 -0
- package/lib/oh-story/skills/story-long-write/references/genre-core-mechanics.md +415 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +91 -0
- package/lib/oh-story/skills/story-long-write/references/genre-readers.md +235 -0
- package/lib/oh-story/skills/story-long-write/references/genre-writing-formulas.md +444 -0
- package/lib/oh-story/skills/story-long-write/references/hooks-chapter.md +225 -0
- package/lib/oh-story/skills/story-long-write/references/hooks-paragraph.md +146 -0
- package/lib/oh-story/skills/story-long-write/references/hooks-suspense.md +352 -0
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +349 -0
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +457 -0
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +287 -0
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +389 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +326 -0
- package/lib/oh-story/skills/story-long-write/references/plot-core-methods.md +522 -0
- package/lib/oh-story/skills/story-long-write/references/plot-emotion-system.md +354 -0
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +711 -0
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +622 -0
- package/lib/oh-story/skills/story-long-write/references/quality-checklist.md +184 -0
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +109 -0
- package/lib/oh-story/skills/story-long-write/references/reversal-toolkit.md +349 -0
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +409 -0
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +347 -0
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +510 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +161 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +140 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +99 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +284 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +421 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +1371 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +330 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +241 -0
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +1139 -0
- package/lib/oh-story/skills/story-review/SKILL.md +483 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +582 -0
- package/lib/oh-story/skills/story-review/references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +385 -0
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +282 -0
- package/lib/oh-story/skills/story-review/references/plot-core-methods.md +522 -0
- package/lib/oh-story/skills/story-review/references/quality-checklist.md +250 -0
- package/lib/oh-story/skills/story-review/references/quality-rubric.md +55 -0
- package/lib/oh-story/skills/story-review/references/rubrics/fanqie.md +18 -0
- package/lib/oh-story/skills/story-review/references/rubrics/qidian.md +19 -0
- package/lib/oh-story/skills/story-review/references/rubrics/zhihu.md +20 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +161 -0
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +1371 -0
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +330 -0
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +410 -0
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +1139 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +495 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +582 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-basics.md +445 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-design-methods.md +428 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +385 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +282 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +426 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-methods.md +179 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +169 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-catalog.md +480 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-core-mechanics.md +415 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +91 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +235 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +444 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/hooks-chapter.md +225 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/hooks-suspense.md +352 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +349 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +457 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +287 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +389 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/plot-core-methods.md +522 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +247 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/reversal-toolkit.md +349 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +409 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +510 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +421 -0
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +278 -0
- package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +582 -0
- package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-short-analyze/references/character-basics.md +453 -0
- package/lib/oh-story/skills/story-short-analyze/references/character-design-methods.md +438 -0
- package/lib/oh-story/skills/story-short-analyze/references/character-relations.md +395 -0
- package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +79 -0
- package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +490 -0
- package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +425 -0
- package/lib/oh-story/skills/story-short-analyze/references/genre-readers.md +245 -0
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +453 -0
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-techniques.md +475 -0
- package/lib/oh-story/skills/story-short-analyze/references/hooks-chapter.md +225 -0
- package/lib/oh-story/skills/story-short-analyze/references/hooks-paragraph.md +146 -0
- package/lib/oh-story/skills/story-short-analyze/references/hooks-suspense.md +352 -0
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +685 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +165 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +561 -0
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +242 -0
- package/lib/oh-story/skills/story-short-analyze/references/real-market-data.md +90 -0
- package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +54 -0
- package/lib/oh-story/skills/story-short-scan/SKILL.md +212 -0
- package/lib/oh-story/skills/story-short-scan/references/real-market-data.md +90 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +433 -0
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +147 -0
- package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +62 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +282 -0
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +179 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +159 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +156 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +163 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +167 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +178 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +166 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +159 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +151 -0
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +153 -0
- 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 +210 -0
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +444 -0
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +488 -0
- package/lib/oh-story/skills/story-short-write/references/hooks-chapter.md +225 -0
- package/lib/oh-story/skills/story-short-write/references/hooks-paragraph.md +146 -0
- package/lib/oh-story/skills/story-short-write/references/hooks-suspense.md +352 -0
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +165 -0
- package/lib/oh-story/skills/story-short-write/references/quality-checklist.md +222 -0
- package/lib/oh-story/skills/story-short-write/references/reversal-toolkit.md +349 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +185 -0
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +133 -0
- package/lib/oh-story/skills/story-short-write/references/short-format.md +167 -0
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +35 -0
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +158 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +153 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +1371 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +330 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +241 -0
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +410 -0
- package/lib/types/client/file-activity.d.ts +30 -0
- package/lib/types/client/file-activity.d.ts.map +1 -0
- package/lib/types/client/file-tree.d.ts +20 -0
- package/lib/types/client/file-tree.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/jsonl-preview.d.ts +15 -0
- package/lib/types/client/jsonl-preview.d.ts.map +1 -0
- package/lib/types/client/markdown-preview.d.ts +38 -0
- package/lib/types/client/markdown-preview.d.ts.map +1 -0
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/native-hooks.d.ts +16 -0
- package/lib/types/native-hooks.d.ts.map +1 -0
- package/lib/types/role-provider.d.ts +6 -0
- package/lib/types/role-provider.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts +10 -0
- package/lib/types/role-tool.d.ts.map +1 -0
- package/lib/types/skill-provider.d.ts +15 -0
- package/lib/types/skill-provider.d.ts.map +1 -0
- package/lib/types/workspace-request-trust.d.ts +13 -0
- package/lib/types/workspace-request-trust.d.ts.map +1 -0
- package/lib/types/workspace-route.d.ts +10 -0
- package/lib/types/workspace-route.d.ts.map +1 -0
- package/lib/types/workspace-security.d.ts +9 -0
- package/lib/types/workspace-security.d.ts.map +1 -0
- package/package.json +107 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stdlib-only production adapters for supported media providers.
|
|
3
|
+
|
|
4
|
+
The public ``compile_*`` functions are deterministic and perform no I/O. The
|
|
5
|
+
CLI reads the confirmed production job from stdin and writes only the adapter
|
|
6
|
+
contract JSON to stdout after a provider result has been saved to a temporary
|
|
7
|
+
regular file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import base64
|
|
14
|
+
import binascii
|
|
15
|
+
import json
|
|
16
|
+
import mimetypes
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import secrets
|
|
20
|
+
import stat
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Collection, Mapping, Sequence
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
OPENAI_MODEL = "gpt-image-2"
|
|
31
|
+
MINIMAX_MUSIC_MODEL = "music-3.0"
|
|
32
|
+
OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
33
|
+
SEEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
|
|
34
|
+
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
|
|
35
|
+
MAX_JSON_RESPONSE = 128 * 1024 * 1024
|
|
36
|
+
MAX_OUTPUT_BYTES = 512 * 1024 * 1024
|
|
37
|
+
MAX_REFERENCE_BYTES = 50 * 1024 * 1024
|
|
38
|
+
MAX_MULTIPART_BYTES = 200 * 1024 * 1024
|
|
39
|
+
MAX_ERROR_BODY_BYTES = 64 * 1024
|
|
40
|
+
TERMINAL_FAILURES = {"failed", "cancelled", "canceled", "timeout", "expired"}
|
|
41
|
+
SEEDANCE_RATIOS = {"adaptive", "1:1", "3:4", "4:3", "9:16", "16:9", "21:9"}
|
|
42
|
+
GPT_IMAGE_MIN_PIXELS = 655_360
|
|
43
|
+
GPT_IMAGE_MAX_PIXELS = 8_294_400
|
|
44
|
+
MINIMUM_PYTHON = (3, 9)
|
|
45
|
+
if sys.version_info < MINIMUM_PYTHON:
|
|
46
|
+
raise SystemExit("provider_adapters.py requires Python 3.9 or newer")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AdapterFailure(RuntimeError):
|
|
50
|
+
"""A safe-to-report adapter failure without provider response contents."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
message: str,
|
|
55
|
+
*,
|
|
56
|
+
category: str = "provider_response",
|
|
57
|
+
code: str = "adapter_failure",
|
|
58
|
+
http_status: int | None = None,
|
|
59
|
+
request_id: str | None = None,
|
|
60
|
+
retryable: bool = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.category = category
|
|
64
|
+
self.code = _safe_token(code) or "adapter_failure"
|
|
65
|
+
self.http_status = http_status
|
|
66
|
+
self.request_id = _safe_token(request_id)
|
|
67
|
+
self.retryable = retryable
|
|
68
|
+
|
|
69
|
+
def public(self, provider: str) -> dict[str, Any]:
|
|
70
|
+
result: dict[str, Any] = {
|
|
71
|
+
"provider": provider,
|
|
72
|
+
"category": self.category,
|
|
73
|
+
"code": self.code,
|
|
74
|
+
"retryable": self.retryable,
|
|
75
|
+
}
|
|
76
|
+
if self.http_status is not None:
|
|
77
|
+
result["http_status"] = self.http_status
|
|
78
|
+
if self.request_id is not None:
|
|
79
|
+
result["request_id"] = self.request_id
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _safe_token(value: object) -> str | None:
|
|
84
|
+
if isinstance(value, str) and re.fullmatch(
|
|
85
|
+
r"[A-Za-z0-9][A-Za-z0-9._:-]{0,199}", value
|
|
86
|
+
):
|
|
87
|
+
return value
|
|
88
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
89
|
+
return str(value)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _request_id(headers: Mapping[str, Any]) -> str | None:
|
|
94
|
+
folded = {str(key).casefold(): value for key, value in headers.items()}
|
|
95
|
+
for name in ("x-request-id", "x-tt-logid", "x-trace-id", "trace-id"):
|
|
96
|
+
value = _safe_token(folded.get(name))
|
|
97
|
+
if value is not None:
|
|
98
|
+
return value
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _provider_code(document: object) -> str | None:
|
|
103
|
+
if not isinstance(document, Mapping):
|
|
104
|
+
return None
|
|
105
|
+
error = document.get("error")
|
|
106
|
+
if isinstance(error, Mapping):
|
|
107
|
+
value = _safe_token(error.get("code"))
|
|
108
|
+
if value is not None:
|
|
109
|
+
return value
|
|
110
|
+
base_resp = document.get("base_resp")
|
|
111
|
+
if isinstance(base_resp, Mapping):
|
|
112
|
+
value = _safe_token(base_resp.get("status_code"))
|
|
113
|
+
if value not in {None, "0"}:
|
|
114
|
+
return value
|
|
115
|
+
return _safe_token(document.get("code"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _http_failure(provider: str, error: urllib.error.HTTPError) -> AdapterFailure:
|
|
119
|
+
status = error.code
|
|
120
|
+
try:
|
|
121
|
+
raw = error.read(MAX_ERROR_BODY_BYTES + 1)
|
|
122
|
+
document = json.loads(raw) if len(raw) <= MAX_ERROR_BODY_BYTES else None
|
|
123
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
124
|
+
document = None
|
|
125
|
+
if status == 401:
|
|
126
|
+
category = "authentication"
|
|
127
|
+
elif status == 403:
|
|
128
|
+
category = "permission"
|
|
129
|
+
elif status == 429:
|
|
130
|
+
category = "rate_limit"
|
|
131
|
+
elif 500 <= status <= 599:
|
|
132
|
+
category = "server"
|
|
133
|
+
elif 400 <= status <= 499:
|
|
134
|
+
category = "invalid_request"
|
|
135
|
+
else:
|
|
136
|
+
category = "provider_response"
|
|
137
|
+
return AdapterFailure(
|
|
138
|
+
f"{provider} HTTP request failed",
|
|
139
|
+
category=category,
|
|
140
|
+
code=_provider_code(document) or f"http_{status}",
|
|
141
|
+
http_status=status,
|
|
142
|
+
request_id=_request_id(
|
|
143
|
+
dict(error.headers.items()) if error.headers is not None else {}
|
|
144
|
+
),
|
|
145
|
+
retryable=status == 429 or 500 <= status <= 599,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _require_job(job: Mapping[str, Any], modality: str) -> tuple[str, dict[str, Any]]:
|
|
150
|
+
if not isinstance(job, Mapping) or job.get("modality") != modality:
|
|
151
|
+
raise ValueError(f"adapter requires a {modality} job")
|
|
152
|
+
prompt = job.get("prompt")
|
|
153
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
154
|
+
raise ValueError("job prompt must be non-empty")
|
|
155
|
+
parameters = job.get("parameters", {})
|
|
156
|
+
if not isinstance(parameters, Mapping):
|
|
157
|
+
raise ValueError("job parameters must be an object")
|
|
158
|
+
outputs = job.get("outputs")
|
|
159
|
+
if not isinstance(outputs, list) or len(outputs) != 1 or not isinstance(outputs[0], str):
|
|
160
|
+
raise ValueError("provider adapter requires exactly one output")
|
|
161
|
+
return prompt, dict(parameters)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _take(parameters: dict[str, Any], allowed: set[str]) -> dict[str, Any]:
|
|
165
|
+
unknown = set(parameters) - allowed
|
|
166
|
+
if unknown:
|
|
167
|
+
raise ValueError("unsupported provider parameters: " + ", ".join(sorted(unknown)))
|
|
168
|
+
return parameters
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compile_seedance_payload(
|
|
172
|
+
job: Mapping[str, Any],
|
|
173
|
+
*,
|
|
174
|
+
model: str,
|
|
175
|
+
reference_urls: Sequence[str] = (),
|
|
176
|
+
allowed_ratios: Collection[str] | None = None,
|
|
177
|
+
duration_range: tuple[int, int] | None = None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Compile a video production job into the official Seedance task body.
|
|
180
|
+
|
|
181
|
+
``model`` is deliberately mandatory: callers must obtain it from explicit
|
|
182
|
+
runtime configuration rather than assuming any Seedance release.
|
|
183
|
+
"""
|
|
184
|
+
prompt, parameters = _require_job(job, "video")
|
|
185
|
+
if not isinstance(model, str) or not model.strip():
|
|
186
|
+
raise ValueError("Seedance model must be explicitly configured")
|
|
187
|
+
references = job.get("references", [])
|
|
188
|
+
if not isinstance(references, list) or len(reference_urls) != len(references):
|
|
189
|
+
raise ValueError("Seedance reference URLs must match job references")
|
|
190
|
+
if Path(job["outputs"][0]).suffix.casefold() != ".mp4":
|
|
191
|
+
raise ValueError("Seedance adapter requires an MP4 target")
|
|
192
|
+
parameters = _take(
|
|
193
|
+
parameters,
|
|
194
|
+
{"duration", "ratio"},
|
|
195
|
+
)
|
|
196
|
+
duration = parameters.get("duration")
|
|
197
|
+
if duration is not None and (
|
|
198
|
+
not isinstance(duration, int)
|
|
199
|
+
or isinstance(duration, bool)
|
|
200
|
+
or not 1 <= duration <= 15
|
|
201
|
+
):
|
|
202
|
+
raise ValueError("Seedance duration must be an integer from 1 to 15")
|
|
203
|
+
if duration is not None:
|
|
204
|
+
if duration_range is None:
|
|
205
|
+
raise ValueError("Seedance duration needs an explicit model profile")
|
|
206
|
+
minimum, maximum = duration_range
|
|
207
|
+
if not 1 <= minimum <= maximum <= 15 or not minimum <= duration <= maximum:
|
|
208
|
+
raise ValueError("Seedance duration is outside the configured model profile")
|
|
209
|
+
ratio = parameters.get("ratio")
|
|
210
|
+
if ratio is not None and (
|
|
211
|
+
not isinstance(ratio, str) or ratio.strip() not in SEEDANCE_RATIOS
|
|
212
|
+
):
|
|
213
|
+
raise ValueError("Seedance ratio is outside the supported profile")
|
|
214
|
+
if ratio is not None:
|
|
215
|
+
configured_ratios = set(allowed_ratios or ())
|
|
216
|
+
if not configured_ratios:
|
|
217
|
+
raise ValueError("Seedance ratio needs an explicit model profile")
|
|
218
|
+
if not configured_ratios <= SEEDANCE_RATIOS or ratio.strip() not in configured_ratios:
|
|
219
|
+
raise ValueError("Seedance ratio is outside the configured model profile")
|
|
220
|
+
text = prompt
|
|
221
|
+
if ratio is not None:
|
|
222
|
+
text += f" --ratio {ratio.strip()}"
|
|
223
|
+
if duration is not None:
|
|
224
|
+
text += f" --dur {duration}"
|
|
225
|
+
content: list[dict[str, Any]] = [{"type": "text", "text": text}]
|
|
226
|
+
for index, url in enumerate(reference_urls):
|
|
227
|
+
if not isinstance(url, str) or not url:
|
|
228
|
+
raise ValueError("Seedance reference URL must be non-empty")
|
|
229
|
+
suffix = Path(str(references[index])).suffix.casefold()
|
|
230
|
+
if suffix in {".png", ".jpg", ".jpeg", ".webp"}:
|
|
231
|
+
parsed = urllib.parse.urlparse(url)
|
|
232
|
+
if (
|
|
233
|
+
parsed.scheme == "https" and parsed.netloc
|
|
234
|
+
) or (
|
|
235
|
+
parsed.scheme == "asset"
|
|
236
|
+
and parsed.netloc.startswith("asset-")
|
|
237
|
+
and not parsed.path
|
|
238
|
+
):
|
|
239
|
+
pass
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError("Seedance reference URL must be HTTPS or asset://")
|
|
242
|
+
content.append(
|
|
243
|
+
{
|
|
244
|
+
"type": "image_url",
|
|
245
|
+
"image_url": {"url": url},
|
|
246
|
+
"role": "reference_image",
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
raise ValueError("Seedance adapter accepts image references only")
|
|
251
|
+
return {"model": model.strip(), "content": content}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _seedance_runtime_profile(
|
|
255
|
+
) -> tuple[frozenset[str] | None, tuple[int, int] | None]:
|
|
256
|
+
raw_ratios = os.environ.get("SEEDANCE_ALLOWED_RATIOS")
|
|
257
|
+
allowed_ratios = (
|
|
258
|
+
frozenset(item.strip() for item in raw_ratios.split(",") if item.strip())
|
|
259
|
+
if raw_ratios is not None
|
|
260
|
+
else None
|
|
261
|
+
)
|
|
262
|
+
minimum_raw = os.environ.get("SEEDANCE_MIN_DURATION")
|
|
263
|
+
maximum_raw = os.environ.get("SEEDANCE_MAX_DURATION")
|
|
264
|
+
if (minimum_raw is None) != (maximum_raw is None):
|
|
265
|
+
raise AdapterFailure(
|
|
266
|
+
"Seedance duration profile is incomplete",
|
|
267
|
+
category="configuration",
|
|
268
|
+
code="invalid_model_profile",
|
|
269
|
+
)
|
|
270
|
+
try:
|
|
271
|
+
duration_range = (
|
|
272
|
+
(int(minimum_raw), int(maximum_raw))
|
|
273
|
+
if minimum_raw is not None and maximum_raw is not None
|
|
274
|
+
else None
|
|
275
|
+
)
|
|
276
|
+
except ValueError as exc:
|
|
277
|
+
raise AdapterFailure(
|
|
278
|
+
"Seedance duration profile is invalid",
|
|
279
|
+
category="configuration",
|
|
280
|
+
code="invalid_model_profile",
|
|
281
|
+
) from exc
|
|
282
|
+
if allowed_ratios is not None and (
|
|
283
|
+
not allowed_ratios or not allowed_ratios <= SEEDANCE_RATIOS
|
|
284
|
+
):
|
|
285
|
+
raise AdapterFailure(
|
|
286
|
+
"Seedance ratio profile is invalid",
|
|
287
|
+
category="configuration",
|
|
288
|
+
code="invalid_model_profile",
|
|
289
|
+
)
|
|
290
|
+
if duration_range is not None and not (
|
|
291
|
+
1 <= duration_range[0] <= duration_range[1] <= 15
|
|
292
|
+
):
|
|
293
|
+
raise AdapterFailure(
|
|
294
|
+
"Seedance duration profile is invalid",
|
|
295
|
+
category="configuration",
|
|
296
|
+
code="invalid_model_profile",
|
|
297
|
+
)
|
|
298
|
+
return allowed_ratios, duration_range
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def compile_gpt_image_2_payload(job: Mapping[str, Any]) -> dict[str, Any]:
|
|
302
|
+
"""Compile an image job into GPT Image 2 generation/edit fields."""
|
|
303
|
+
prompt, parameters = _require_job(job, "image")
|
|
304
|
+
parameters = _take(
|
|
305
|
+
parameters,
|
|
306
|
+
{"width", "height", "size", "quality", "background", "moderation"},
|
|
307
|
+
)
|
|
308
|
+
if len(prompt) > 32000:
|
|
309
|
+
raise ValueError("GPT Image prompt exceeds the provider limit")
|
|
310
|
+
width = parameters.pop("width", None)
|
|
311
|
+
height = parameters.pop("height", None)
|
|
312
|
+
if (width is None) != (height is None) or (width is not None and "size" in parameters):
|
|
313
|
+
raise ValueError("use either both width/height or size")
|
|
314
|
+
if width is not None:
|
|
315
|
+
if not isinstance(width, int) or not isinstance(height, int) or width <= 0 or height <= 0:
|
|
316
|
+
raise ValueError("image dimensions must be positive integers")
|
|
317
|
+
if width % 16 or height % 16 or not (1 / 3 <= width / height <= 3):
|
|
318
|
+
raise ValueError("GPT Image dimensions must be divisible by 16 with ratio between 1:3 and 3:1")
|
|
319
|
+
parameters["size"] = f"{width}x{height}"
|
|
320
|
+
size = parameters.get("size")
|
|
321
|
+
if size != "auto" and size is not None:
|
|
322
|
+
if not isinstance(size, str) or "x" not in size:
|
|
323
|
+
raise ValueError("GPT Image size is invalid")
|
|
324
|
+
try:
|
|
325
|
+
parsed_width, parsed_height = (int(part) for part in size.split("x", 1))
|
|
326
|
+
except ValueError as exc:
|
|
327
|
+
raise ValueError("GPT Image size is invalid") from exc
|
|
328
|
+
if (
|
|
329
|
+
parsed_width <= 0
|
|
330
|
+
or parsed_height <= 0
|
|
331
|
+
or parsed_width % 16
|
|
332
|
+
or parsed_height % 16
|
|
333
|
+
or not (1 / 3 <= parsed_width / parsed_height <= 3)
|
|
334
|
+
or parsed_width > 3840
|
|
335
|
+
or parsed_height > 3840
|
|
336
|
+
or not (
|
|
337
|
+
GPT_IMAGE_MIN_PIXELS
|
|
338
|
+
<= parsed_width * parsed_height
|
|
339
|
+
<= GPT_IMAGE_MAX_PIXELS
|
|
340
|
+
)
|
|
341
|
+
):
|
|
342
|
+
raise ValueError("GPT Image size violates official dimension constraints")
|
|
343
|
+
if parameters.get("background") == "transparent":
|
|
344
|
+
raise ValueError("GPT Image 2 transparent background is unsupported")
|
|
345
|
+
if "background" in parameters and parameters["background"] not in {"auto", "opaque"}:
|
|
346
|
+
raise ValueError("GPT Image background is invalid")
|
|
347
|
+
if "quality" in parameters and parameters["quality"] not in {"auto", "low", "medium", "high"}:
|
|
348
|
+
raise ValueError("GPT Image quality is invalid")
|
|
349
|
+
if "moderation" in parameters and parameters["moderation"] not in {"auto", "low"}:
|
|
350
|
+
raise ValueError("GPT Image moderation is invalid")
|
|
351
|
+
suffix = Path(job["outputs"][0]).suffix.casefold()
|
|
352
|
+
formats = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".webp": "webp"}
|
|
353
|
+
if suffix not in formats:
|
|
354
|
+
raise ValueError("unsupported GPT Image output extension")
|
|
355
|
+
references = job.get("references", [])
|
|
356
|
+
if not isinstance(references, list) or len(references) > 16:
|
|
357
|
+
raise ValueError("GPT Image accepts at most sixteen references")
|
|
358
|
+
if any(
|
|
359
|
+
not isinstance(reference, str)
|
|
360
|
+
or Path(reference).suffix.casefold() not in {".png", ".jpg", ".jpeg", ".webp"}
|
|
361
|
+
for reference in references
|
|
362
|
+
):
|
|
363
|
+
raise ValueError("GPT Image references must be supported image files")
|
|
364
|
+
return {
|
|
365
|
+
"model": OPENAI_MODEL,
|
|
366
|
+
"prompt": prompt,
|
|
367
|
+
"n": 1,
|
|
368
|
+
"output_format": formats[suffix],
|
|
369
|
+
**parameters,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def compile_minimax_music_payload(job: Mapping[str, Any]) -> dict[str, Any]:
|
|
374
|
+
"""Compile an audio job into the official MiniMax Music 3.0 JSON body."""
|
|
375
|
+
prompt, parameters = _require_job(job, "music")
|
|
376
|
+
parameters = _take(
|
|
377
|
+
parameters,
|
|
378
|
+
{"lyrics", "sample_rate", "bitrate", "format", "lyrics_optimizer", "is_instrumental"},
|
|
379
|
+
)
|
|
380
|
+
target_format = Path(job["outputs"][0]).suffix.casefold().lstrip(".")
|
|
381
|
+
requested_format = parameters.pop("format", target_format)
|
|
382
|
+
if requested_format != target_format or requested_format not in {"mp3", "wav"}:
|
|
383
|
+
raise ValueError("MiniMax output format must match a supported target extension")
|
|
384
|
+
lyrics = parameters.pop("lyrics", None)
|
|
385
|
+
instrumental = parameters.pop("is_instrumental", False)
|
|
386
|
+
optimizer = parameters.pop("lyrics_optimizer", False)
|
|
387
|
+
if not isinstance(instrumental, bool) or not isinstance(optimizer, bool):
|
|
388
|
+
raise ValueError("MiniMax boolean parameters must be booleans")
|
|
389
|
+
if optimizer:
|
|
390
|
+
raise ValueError("MiniMax lyrics_optimizer is forbidden for confirmed production")
|
|
391
|
+
if instrumental and lyrics not in {None, ""}:
|
|
392
|
+
raise ValueError("MiniMax instrumental music must not carry lyrics")
|
|
393
|
+
if not instrumental and (not isinstance(lyrics, str) or not lyrics.strip()):
|
|
394
|
+
raise ValueError("MiniMax vocal music requires confirmed lyrics")
|
|
395
|
+
if len(prompt) > 2000 or (isinstance(lyrics, str) and len(lyrics) > 3500):
|
|
396
|
+
raise ValueError("MiniMax prompt or lyrics exceeds the provider limit")
|
|
397
|
+
sample_rate = parameters.pop("sample_rate", 44100)
|
|
398
|
+
bitrate = parameters.pop("bitrate", 256000)
|
|
399
|
+
if sample_rate not in {16000, 24000, 32000, 44100}:
|
|
400
|
+
raise ValueError("unsupported MiniMax sample rate")
|
|
401
|
+
if bitrate not in {32000, 64000, 128000, 256000}:
|
|
402
|
+
raise ValueError("unsupported MiniMax bitrate")
|
|
403
|
+
body: dict[str, Any] = {
|
|
404
|
+
"model": MINIMAX_MUSIC_MODEL,
|
|
405
|
+
"prompt": prompt,
|
|
406
|
+
"stream": False,
|
|
407
|
+
"output_format": "hex",
|
|
408
|
+
"audio_setting": {
|
|
409
|
+
"sample_rate": sample_rate,
|
|
410
|
+
"bitrate": bitrate,
|
|
411
|
+
"format": requested_format,
|
|
412
|
+
},
|
|
413
|
+
"lyrics_optimizer": False,
|
|
414
|
+
"is_instrumental": instrumental,
|
|
415
|
+
}
|
|
416
|
+
if lyrics is not None:
|
|
417
|
+
if not isinstance(lyrics, str):
|
|
418
|
+
raise ValueError("MiniMax lyrics must be a string")
|
|
419
|
+
body["lyrics"] = lyrics
|
|
420
|
+
return body
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _base_url(env_name: str, default: str) -> str:
|
|
424
|
+
value = os.environ.get(env_name, default).rstrip("/")
|
|
425
|
+
parsed = urllib.parse.urlparse(value)
|
|
426
|
+
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password:
|
|
427
|
+
raise AdapterFailure(
|
|
428
|
+
"provider base URL is invalid",
|
|
429
|
+
category="configuration",
|
|
430
|
+
code="invalid_base_url",
|
|
431
|
+
)
|
|
432
|
+
return value
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _credential(name: str) -> str:
|
|
436
|
+
value = os.environ.get(name)
|
|
437
|
+
if not value:
|
|
438
|
+
raise AdapterFailure(
|
|
439
|
+
"provider credential is not configured",
|
|
440
|
+
category="configuration",
|
|
441
|
+
code="missing_credential",
|
|
442
|
+
)
|
|
443
|
+
return value
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _request_json(
|
|
447
|
+
url: str,
|
|
448
|
+
*,
|
|
449
|
+
provider: str,
|
|
450
|
+
method: str = "POST",
|
|
451
|
+
body: Mapping[str, Any] | None = None,
|
|
452
|
+
token: str,
|
|
453
|
+
) -> tuple[dict[str, Any], Mapping[str, str]]:
|
|
454
|
+
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
455
|
+
request = urllib.request.Request(url, data=data, method=method)
|
|
456
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
457
|
+
if body is not None:
|
|
458
|
+
request.add_header("Content-Type", "application/json")
|
|
459
|
+
try:
|
|
460
|
+
with urllib.request.urlopen(request, timeout=180) as response:
|
|
461
|
+
raw = response.read(MAX_JSON_RESPONSE + 1)
|
|
462
|
+
headers = dict(response.headers.items())
|
|
463
|
+
except urllib.error.HTTPError as exc:
|
|
464
|
+
raise _http_failure(provider, exc) from exc
|
|
465
|
+
except TimeoutError as exc:
|
|
466
|
+
raise AdapterFailure(
|
|
467
|
+
f"{provider} HTTP request timed out",
|
|
468
|
+
category="timeout",
|
|
469
|
+
code="request_timeout",
|
|
470
|
+
retryable=True,
|
|
471
|
+
) from exc
|
|
472
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
473
|
+
raise AdapterFailure(
|
|
474
|
+
f"{provider} HTTP request failed",
|
|
475
|
+
category="network",
|
|
476
|
+
code="network_error",
|
|
477
|
+
retryable=True,
|
|
478
|
+
) from exc
|
|
479
|
+
if len(raw) > MAX_JSON_RESPONSE:
|
|
480
|
+
raise AdapterFailure(
|
|
481
|
+
f"{provider} response is too large", code="response_too_large"
|
|
482
|
+
)
|
|
483
|
+
try:
|
|
484
|
+
document = json.loads(raw)
|
|
485
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
486
|
+
raise AdapterFailure(
|
|
487
|
+
f"{provider} returned invalid JSON", code="invalid_json"
|
|
488
|
+
) from exc
|
|
489
|
+
if not isinstance(document, dict):
|
|
490
|
+
raise AdapterFailure(
|
|
491
|
+
f"{provider} returned an invalid response", code="invalid_response"
|
|
492
|
+
)
|
|
493
|
+
return document, headers
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _read_reference(path: Path) -> bytes:
|
|
497
|
+
before = path.lstat()
|
|
498
|
+
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
|
|
499
|
+
raise ValueError("reference is not a regular file")
|
|
500
|
+
if before.st_size > MAX_REFERENCE_BYTES:
|
|
501
|
+
raise ValueError("reference exceeds the provider input size limit")
|
|
502
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
503
|
+
descriptor = os.open(path, flags)
|
|
504
|
+
try:
|
|
505
|
+
opened = os.fstat(descriptor)
|
|
506
|
+
if (
|
|
507
|
+
not stat.S_ISREG(opened.st_mode)
|
|
508
|
+
or (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino)
|
|
509
|
+
):
|
|
510
|
+
raise ValueError("reference changed while opening")
|
|
511
|
+
chunks: list[bytes] = []
|
|
512
|
+
size = 0
|
|
513
|
+
while chunk := os.read(descriptor, 1024 * 1024):
|
|
514
|
+
size += len(chunk)
|
|
515
|
+
if size > MAX_REFERENCE_BYTES:
|
|
516
|
+
raise ValueError("reference exceeds the provider input size limit")
|
|
517
|
+
chunks.append(chunk)
|
|
518
|
+
return b"".join(chunks)
|
|
519
|
+
finally:
|
|
520
|
+
os.close(descriptor)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _multipart(fields: Mapping[str, Any], paths: Sequence[Path]) -> tuple[bytes, str]:
|
|
524
|
+
boundary = "short-drama-" + secrets.token_hex(16)
|
|
525
|
+
chunks: list[bytes] = []
|
|
526
|
+
for name, value in fields.items():
|
|
527
|
+
chunks.extend(
|
|
528
|
+
[
|
|
529
|
+
f"--{boundary}\r\n".encode(),
|
|
530
|
+
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(),
|
|
531
|
+
str(value).lower().encode() if isinstance(value, bool) else str(value).encode("utf-8"),
|
|
532
|
+
b"\r\n",
|
|
533
|
+
]
|
|
534
|
+
)
|
|
535
|
+
for path in paths:
|
|
536
|
+
content = _read_reference(path)
|
|
537
|
+
media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
538
|
+
chunks.extend(
|
|
539
|
+
[
|
|
540
|
+
f"--{boundary}\r\n".encode(),
|
|
541
|
+
f'Content-Disposition: form-data; name="image[]"; filename="{path.name}"\r\n'.encode(),
|
|
542
|
+
f"Content-Type: {media_type}\r\n\r\n".encode(),
|
|
543
|
+
content,
|
|
544
|
+
b"\r\n",
|
|
545
|
+
]
|
|
546
|
+
)
|
|
547
|
+
chunks.append(f"--{boundary}--\r\n".encode())
|
|
548
|
+
encoded = b"".join(chunks)
|
|
549
|
+
if len(encoded) > MAX_MULTIPART_BYTES:
|
|
550
|
+
raise ValueError("multipart provider input exceeds the total size limit")
|
|
551
|
+
return encoded, f"multipart/form-data; boundary={boundary}"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _reference_paths(job: Mapping[str, Any]) -> list[Path]:
|
|
555
|
+
raw_root = job.get("project_root")
|
|
556
|
+
if not isinstance(raw_root, str) or not Path(raw_root).is_absolute():
|
|
557
|
+
raise ValueError("project_root is invalid")
|
|
558
|
+
root = Path(raw_root).resolve()
|
|
559
|
+
if not root.is_dir():
|
|
560
|
+
raise ValueError("project_root is invalid")
|
|
561
|
+
result: list[Path] = []
|
|
562
|
+
for reference in job.get("references", []):
|
|
563
|
+
if not isinstance(reference, str):
|
|
564
|
+
raise ValueError("reference path is invalid")
|
|
565
|
+
path = (root / reference).resolve()
|
|
566
|
+
try:
|
|
567
|
+
path.relative_to(root)
|
|
568
|
+
except ValueError as exc:
|
|
569
|
+
raise ValueError("reference escapes project root") from exc
|
|
570
|
+
if not path.is_file() or path.is_symlink():
|
|
571
|
+
raise ValueError("reference is not a regular file")
|
|
572
|
+
result.append(path)
|
|
573
|
+
return result
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _validate_media_content(target: str, content: bytes) -> None:
|
|
577
|
+
suffix = Path(target).suffix.casefold()
|
|
578
|
+
signatures = {
|
|
579
|
+
".png": content.startswith(b"\x89PNG\r\n\x1a\n"),
|
|
580
|
+
".jpg": content.startswith(b"\xff\xd8\xff"),
|
|
581
|
+
".jpeg": content.startswith(b"\xff\xd8\xff"),
|
|
582
|
+
".webp": len(content) >= 12
|
|
583
|
+
and content.startswith(b"RIFF")
|
|
584
|
+
and content[8:12] == b"WEBP",
|
|
585
|
+
".mp3": content.startswith(b"ID3")
|
|
586
|
+
or (
|
|
587
|
+
len(content) >= 2
|
|
588
|
+
and content[0] == 0xFF
|
|
589
|
+
and content[1] & 0xE0 == 0xE0
|
|
590
|
+
),
|
|
591
|
+
".wav": len(content) >= 12
|
|
592
|
+
and content.startswith(b"RIFF")
|
|
593
|
+
and content[8:12] == b"WAVE",
|
|
594
|
+
".mp4": len(content) >= 12 and content[4:8] == b"ftyp",
|
|
595
|
+
}
|
|
596
|
+
if not signatures.get(suffix, False):
|
|
597
|
+
raise AdapterFailure("provider output does not match the target media type")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _output_root(job: Mapping[str, Any]) -> Path:
|
|
601
|
+
raw = job.get("output_root")
|
|
602
|
+
if not isinstance(raw, str):
|
|
603
|
+
raise ValueError("output_root is invalid")
|
|
604
|
+
root = Path(raw)
|
|
605
|
+
if not root.is_absolute():
|
|
606
|
+
raise ValueError("output_root is invalid")
|
|
607
|
+
try:
|
|
608
|
+
details = root.lstat()
|
|
609
|
+
except OSError as exc:
|
|
610
|
+
raise ValueError("output_root is missing") from exc
|
|
611
|
+
if stat.S_ISLNK(details.st_mode) or not stat.S_ISDIR(details.st_mode):
|
|
612
|
+
raise ValueError("output_root is unsafe")
|
|
613
|
+
return root
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _temporary_output(
|
|
617
|
+
job: Mapping[str, Any], target: str, content: bytes
|
|
618
|
+
) -> Path:
|
|
619
|
+
if not content or len(content) > MAX_OUTPUT_BYTES:
|
|
620
|
+
raise AdapterFailure("provider output is too large")
|
|
621
|
+
_validate_media_content(target, content)
|
|
622
|
+
path = _output_root(job) / ("result" + Path(target).suffix.casefold())
|
|
623
|
+
with path.open("xb") as handle:
|
|
624
|
+
handle.write(content)
|
|
625
|
+
return path
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _download(
|
|
629
|
+
job: Mapping[str, Any],
|
|
630
|
+
url: str,
|
|
631
|
+
target: str,
|
|
632
|
+
*,
|
|
633
|
+
provider: str = "seedance",
|
|
634
|
+
) -> Path:
|
|
635
|
+
parsed = urllib.parse.urlparse(url)
|
|
636
|
+
if parsed.scheme != "https" or not parsed.netloc:
|
|
637
|
+
raise AdapterFailure("provider output URL is invalid")
|
|
638
|
+
path = _output_root(job) / ("result" + Path(target).suffix.casefold())
|
|
639
|
+
size = 0
|
|
640
|
+
try:
|
|
641
|
+
with urllib.request.urlopen(url, timeout=180) as response, path.open("xb") as handle:
|
|
642
|
+
while chunk := response.read(1024 * 1024):
|
|
643
|
+
size += len(chunk)
|
|
644
|
+
if size > MAX_OUTPUT_BYTES:
|
|
645
|
+
raise AdapterFailure("provider output is too large")
|
|
646
|
+
handle.write(chunk)
|
|
647
|
+
except AdapterFailure:
|
|
648
|
+
path.unlink(missing_ok=True)
|
|
649
|
+
raise
|
|
650
|
+
except urllib.error.HTTPError as exc:
|
|
651
|
+
path.unlink(missing_ok=True)
|
|
652
|
+
raise _http_failure(provider, exc) from exc
|
|
653
|
+
except TimeoutError as exc:
|
|
654
|
+
path.unlink(missing_ok=True)
|
|
655
|
+
raise AdapterFailure(
|
|
656
|
+
"provider output download timed out",
|
|
657
|
+
category="timeout",
|
|
658
|
+
code="download_timeout",
|
|
659
|
+
retryable=True,
|
|
660
|
+
) from exc
|
|
661
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
662
|
+
path.unlink(missing_ok=True)
|
|
663
|
+
raise AdapterFailure(
|
|
664
|
+
"provider output download failed",
|
|
665
|
+
category="network",
|
|
666
|
+
code="download_failed",
|
|
667
|
+
retryable=True,
|
|
668
|
+
) from exc
|
|
669
|
+
if size == 0:
|
|
670
|
+
raise AdapterFailure("provider output is empty")
|
|
671
|
+
with path.open("rb") as handle:
|
|
672
|
+
_validate_media_content(target, handle.read(16))
|
|
673
|
+
return path
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _run_seedance(job: Mapping[str, Any]) -> tuple[Path, str]:
|
|
677
|
+
token = _credential("ARK_API_KEY")
|
|
678
|
+
model = os.environ.get("SEEDANCE_MODEL", "")
|
|
679
|
+
references = _reference_paths(job)
|
|
680
|
+
if references:
|
|
681
|
+
raise AdapterFailure(
|
|
682
|
+
"Seedance local references require an external trusted HTTPS upload adapter"
|
|
683
|
+
)
|
|
684
|
+
allowed_ratios, duration_range = _seedance_runtime_profile()
|
|
685
|
+
body = compile_seedance_payload(
|
|
686
|
+
job,
|
|
687
|
+
model=model,
|
|
688
|
+
allowed_ratios=allowed_ratios,
|
|
689
|
+
duration_range=duration_range,
|
|
690
|
+
)
|
|
691
|
+
base = _base_url("SEEDANCE_BASE_URL", SEEDANCE_BASE_URL)
|
|
692
|
+
created, _ = _request_json(
|
|
693
|
+
f"{base}/contents/generations/tasks",
|
|
694
|
+
provider="seedance",
|
|
695
|
+
body=body,
|
|
696
|
+
token=token,
|
|
697
|
+
)
|
|
698
|
+
task_id = created.get("id")
|
|
699
|
+
if not isinstance(task_id, str) or not task_id:
|
|
700
|
+
raise AdapterFailure(
|
|
701
|
+
"Seedance did not return a task id", code="missing_task_id"
|
|
702
|
+
)
|
|
703
|
+
try:
|
|
704
|
+
interval = float(os.environ.get("SEEDANCE_POLL_INTERVAL", "5"))
|
|
705
|
+
deadline = time.monotonic() + float(os.environ.get("SEEDANCE_TIMEOUT_SECONDS", "1800"))
|
|
706
|
+
except ValueError as exc:
|
|
707
|
+
raise AdapterFailure("Seedance polling configuration is invalid") from exc
|
|
708
|
+
if interval <= 0 or deadline <= time.monotonic():
|
|
709
|
+
raise AdapterFailure("Seedance polling configuration is invalid")
|
|
710
|
+
while time.monotonic() < deadline:
|
|
711
|
+
status_doc, _ = _request_json(
|
|
712
|
+
f"{base}/contents/generations/tasks/{urllib.parse.quote(task_id, safe='')}",
|
|
713
|
+
provider="seedance",
|
|
714
|
+
method="GET",
|
|
715
|
+
token=token,
|
|
716
|
+
)
|
|
717
|
+
status = status_doc.get("status")
|
|
718
|
+
if status == "succeeded":
|
|
719
|
+
content = status_doc.get("content")
|
|
720
|
+
url = content.get("video_url") if isinstance(content, Mapping) else None
|
|
721
|
+
if not isinstance(url, str):
|
|
722
|
+
raise AdapterFailure(
|
|
723
|
+
"Seedance succeeded without a video URL",
|
|
724
|
+
code="missing_video_url",
|
|
725
|
+
request_id=task_id,
|
|
726
|
+
)
|
|
727
|
+
return _download(
|
|
728
|
+
job,
|
|
729
|
+
url,
|
|
730
|
+
job["outputs"][0],
|
|
731
|
+
provider="seedance",
|
|
732
|
+
), task_id
|
|
733
|
+
if isinstance(status, str) and status.casefold() in TERMINAL_FAILURES:
|
|
734
|
+
raise AdapterFailure(
|
|
735
|
+
"Seedance task failed",
|
|
736
|
+
code="task_" + status.casefold(),
|
|
737
|
+
request_id=task_id,
|
|
738
|
+
)
|
|
739
|
+
if status not in {"queued", "in_progress", "running", "processing", "pending"}:
|
|
740
|
+
raise AdapterFailure(
|
|
741
|
+
"Seedance returned an unknown task status",
|
|
742
|
+
code="unknown_task_status",
|
|
743
|
+
request_id=task_id,
|
|
744
|
+
)
|
|
745
|
+
time.sleep(interval)
|
|
746
|
+
raise AdapterFailure(
|
|
747
|
+
"Seedance task polling timed out",
|
|
748
|
+
category="timeout",
|
|
749
|
+
code="task_poll_timeout",
|
|
750
|
+
request_id=task_id,
|
|
751
|
+
retryable=True,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _run_openai(job: Mapping[str, Any]) -> tuple[Path, str | None]:
|
|
756
|
+
token = _credential("OPENAI_API_KEY")
|
|
757
|
+
body = compile_gpt_image_2_payload(job)
|
|
758
|
+
base = _base_url("OPENAI_BASE_URL", OPENAI_BASE_URL)
|
|
759
|
+
references = _reference_paths(job)
|
|
760
|
+
if references:
|
|
761
|
+
encoded, content_type = _multipart(body, references)
|
|
762
|
+
request = urllib.request.Request(f"{base}/images/edits", data=encoded, method="POST")
|
|
763
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
764
|
+
request.add_header("Content-Type", content_type)
|
|
765
|
+
try:
|
|
766
|
+
with urllib.request.urlopen(request, timeout=300) as response:
|
|
767
|
+
raw = response.read(MAX_JSON_RESPONSE + 1)
|
|
768
|
+
request_id = response.headers.get("x-request-id")
|
|
769
|
+
except urllib.error.HTTPError as exc:
|
|
770
|
+
raise _http_failure("gpt-image-2", exc) from exc
|
|
771
|
+
except TimeoutError as exc:
|
|
772
|
+
raise AdapterFailure(
|
|
773
|
+
"OpenAI HTTP request timed out",
|
|
774
|
+
category="timeout",
|
|
775
|
+
code="request_timeout",
|
|
776
|
+
retryable=True,
|
|
777
|
+
) from exc
|
|
778
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
779
|
+
raise AdapterFailure(
|
|
780
|
+
"OpenAI HTTP request failed",
|
|
781
|
+
category="network",
|
|
782
|
+
code="network_error",
|
|
783
|
+
retryable=True,
|
|
784
|
+
) from exc
|
|
785
|
+
if len(raw) > MAX_JSON_RESPONSE:
|
|
786
|
+
raise AdapterFailure(
|
|
787
|
+
"OpenAI response is too large",
|
|
788
|
+
code="response_too_large",
|
|
789
|
+
request_id=request_id,
|
|
790
|
+
)
|
|
791
|
+
try:
|
|
792
|
+
result = json.loads(raw)
|
|
793
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
794
|
+
raise AdapterFailure(
|
|
795
|
+
"OpenAI returned invalid JSON",
|
|
796
|
+
code="invalid_json",
|
|
797
|
+
request_id=request_id,
|
|
798
|
+
) from exc
|
|
799
|
+
else:
|
|
800
|
+
result, headers = _request_json(
|
|
801
|
+
f"{base}/images/generations",
|
|
802
|
+
provider="gpt-image-2",
|
|
803
|
+
body=body,
|
|
804
|
+
token=token,
|
|
805
|
+
)
|
|
806
|
+
request_id = headers.get("x-request-id")
|
|
807
|
+
data = result.get("data") if isinstance(result, Mapping) else None
|
|
808
|
+
image = data[0].get("b64_json") if isinstance(data, list) and len(data) == 1 and isinstance(data[0], Mapping) else None
|
|
809
|
+
if not isinstance(image, str):
|
|
810
|
+
raise AdapterFailure(
|
|
811
|
+
"OpenAI did not return exactly one image",
|
|
812
|
+
code="missing_image",
|
|
813
|
+
request_id=request_id,
|
|
814
|
+
)
|
|
815
|
+
try:
|
|
816
|
+
content = base64.b64decode(image, validate=True)
|
|
817
|
+
except (ValueError, binascii.Error) as exc:
|
|
818
|
+
raise AdapterFailure(
|
|
819
|
+
"OpenAI returned invalid image data",
|
|
820
|
+
code="invalid_image_data",
|
|
821
|
+
request_id=request_id,
|
|
822
|
+
) from exc
|
|
823
|
+
return _temporary_output(job, job["outputs"][0], content), request_id
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _run_minimax(job: Mapping[str, Any]) -> tuple[Path, str | None]:
|
|
827
|
+
token = _credential("MINIMAX_API_KEY")
|
|
828
|
+
body = compile_minimax_music_payload(job)
|
|
829
|
+
base = _base_url("MINIMAX_BASE_URL", MINIMAX_BASE_URL)
|
|
830
|
+
result, _ = _request_json(
|
|
831
|
+
f"{base}/music_generation",
|
|
832
|
+
provider="minimax-music",
|
|
833
|
+
body=body,
|
|
834
|
+
token=token,
|
|
835
|
+
)
|
|
836
|
+
base_resp = result.get("base_resp")
|
|
837
|
+
if not isinstance(base_resp, Mapping) or base_resp.get("status_code") != 0:
|
|
838
|
+
raise AdapterFailure(
|
|
839
|
+
"MiniMax music generation failed",
|
|
840
|
+
code=_provider_code(result) or "generation_failed",
|
|
841
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
842
|
+
)
|
|
843
|
+
data = result.get("data")
|
|
844
|
+
audio = data.get("audio") if isinstance(data, Mapping) else None
|
|
845
|
+
if not isinstance(data, Mapping) or data.get("status") != 2 or not isinstance(audio, str):
|
|
846
|
+
raise AdapterFailure(
|
|
847
|
+
"MiniMax did not return audio data",
|
|
848
|
+
code="missing_audio",
|
|
849
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
850
|
+
)
|
|
851
|
+
try:
|
|
852
|
+
content = bytes.fromhex(audio)
|
|
853
|
+
except ValueError as exc:
|
|
854
|
+
raise AdapterFailure("MiniMax returned invalid audio data") from exc
|
|
855
|
+
if not content:
|
|
856
|
+
raise AdapterFailure("MiniMax returned empty audio data")
|
|
857
|
+
trace_id = result.get("trace_id")
|
|
858
|
+
return (
|
|
859
|
+
_temporary_output(job, job["outputs"][0], content),
|
|
860
|
+
trace_id if isinstance(trace_id, str) else None,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _selftest() -> None:
|
|
865
|
+
image = {
|
|
866
|
+
"modality": "image", "prompt": "portrait", "references": [],
|
|
867
|
+
"outputs": ["制作成果/a.png"], "parameters": {"width": 1024, "height": 1536},
|
|
868
|
+
}
|
|
869
|
+
video = {
|
|
870
|
+
"modality": "video", "prompt": "slow push in", "references": [],
|
|
871
|
+
"outputs": ["制作成果/a.mp4"], "parameters": {"duration": 5, "ratio": "9:16"},
|
|
872
|
+
}
|
|
873
|
+
music = {
|
|
874
|
+
"modality": "music", "prompt": "cinematic", "references": [],
|
|
875
|
+
"outputs": ["制作成果/a.mp3"], "parameters": {"lyrics": "[Verse]\nHello"},
|
|
876
|
+
}
|
|
877
|
+
if compile_gpt_image_2_payload(image)["model"] != OPENAI_MODEL:
|
|
878
|
+
raise RuntimeError("GPT Image 2 self-test failed")
|
|
879
|
+
seedance = compile_seedance_payload(
|
|
880
|
+
video,
|
|
881
|
+
model="configured-model",
|
|
882
|
+
allowed_ratios={"9:16"},
|
|
883
|
+
duration_range=(5, 10),
|
|
884
|
+
)
|
|
885
|
+
if seedance["model"] != "configured-model":
|
|
886
|
+
raise RuntimeError("Seedance model self-test failed")
|
|
887
|
+
if not seedance["content"][0]["text"].endswith("--ratio 9:16 --dur 5"):
|
|
888
|
+
raise RuntimeError("Seedance prompt switch self-test failed")
|
|
889
|
+
compiled_music = compile_minimax_music_payload(music)
|
|
890
|
+
if compiled_music["model"] != MINIMAX_MUSIC_MODEL:
|
|
891
|
+
raise RuntimeError("MiniMax model self-test failed")
|
|
892
|
+
if compiled_music["output_format"] != "hex":
|
|
893
|
+
raise RuntimeError("MiniMax output self-test failed")
|
|
894
|
+
for invalid_image in (
|
|
895
|
+
{**image, "parameters": {"background": "transparent"}},
|
|
896
|
+
{**image, "parameters": {"input_fidelity": "high"}},
|
|
897
|
+
{**image, "parameters": {"size": "1023x1024"}},
|
|
898
|
+
):
|
|
899
|
+
try:
|
|
900
|
+
compile_gpt_image_2_payload(invalid_image)
|
|
901
|
+
except ValueError:
|
|
902
|
+
pass
|
|
903
|
+
else:
|
|
904
|
+
raise AssertionError("invalid GPT Image payload was accepted")
|
|
905
|
+
invalid_reference = {**video, "references": ["clip.mp4"]}
|
|
906
|
+
try:
|
|
907
|
+
compile_seedance_payload(
|
|
908
|
+
invalid_reference,
|
|
909
|
+
model="configured-model",
|
|
910
|
+
reference_urls=["asset://asset-example-clip"],
|
|
911
|
+
)
|
|
912
|
+
except ValueError:
|
|
913
|
+
pass
|
|
914
|
+
else:
|
|
915
|
+
raise AssertionError("non-image Seedance reference was accepted")
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def main() -> int:
|
|
919
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
920
|
+
parser.add_argument("provider", nargs="?", choices=("seedance", "gpt-image-2", "minimax-music"))
|
|
921
|
+
parser.add_argument("--selftest", action="store_true")
|
|
922
|
+
args = parser.parse_args()
|
|
923
|
+
if args.selftest:
|
|
924
|
+
_selftest()
|
|
925
|
+
return 0
|
|
926
|
+
if args.provider is None:
|
|
927
|
+
parser.error("provider is required unless --selftest is used")
|
|
928
|
+
try:
|
|
929
|
+
job = json.load(sys.stdin.buffer)
|
|
930
|
+
if not isinstance(job, Mapping):
|
|
931
|
+
raise ValueError("adapter input must be an object")
|
|
932
|
+
runners = {"seedance": _run_seedance, "gpt-image-2": _run_openai, "minimax-music": _run_minimax}
|
|
933
|
+
path, provider_job_id = runners[args.provider](job)
|
|
934
|
+
response: dict[str, Any] = {
|
|
935
|
+
"outputs": [{"target": job["outputs"][0], "source": str(path)}]
|
|
936
|
+
}
|
|
937
|
+
if provider_job_id:
|
|
938
|
+
response["provider_job_id"] = provider_job_id
|
|
939
|
+
json.dump(response, sys.stdout, ensure_ascii=True)
|
|
940
|
+
return 0
|
|
941
|
+
except AdapterFailure as exc:
|
|
942
|
+
# Provider bodies and credentials are intentionally never reflected.
|
|
943
|
+
json.dump({"error": exc.public(args.provider)}, sys.stdout, ensure_ascii=True)
|
|
944
|
+
print("provider adapter failed safely", file=sys.stderr)
|
|
945
|
+
return 1
|
|
946
|
+
except (ValueError, KeyError, TypeError, json.JSONDecodeError):
|
|
947
|
+
json.dump(
|
|
948
|
+
{
|
|
949
|
+
"error": {
|
|
950
|
+
"provider": args.provider,
|
|
951
|
+
"category": "invalid_request",
|
|
952
|
+
"code": "invalid_job",
|
|
953
|
+
"retryable": False,
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
sys.stdout,
|
|
957
|
+
ensure_ascii=True,
|
|
958
|
+
)
|
|
959
|
+
print("provider adapter failed safely", file=sys.stderr)
|
|
960
|
+
return 1
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
if __name__ == "__main__":
|
|
964
|
+
raise SystemExit(main())
|