@oh-story/dsh 0.1.9 → 0.1.10
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 +12 -12
- package/lib/client.js +204 -72
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +84 -74
- package/lib/drama/skills/short-drama/SKILL.md +2 -2
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-edit/SKILL.md +27 -29
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +2 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +24 -67
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +27 -56
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +37 -79
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +39 -92
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +12 -16
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +126 -11
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +64 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -3
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +9 -35
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +10 -6
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +58 -21
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +10 -9
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +12 -38
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +4 -7
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -22
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -19
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +2 -2
- package/lib/drama/skills/short-drama-write/SKILL.md +2 -4
- package/lib/index.js +289 -91
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
- package/lib/oh-story/manifest.json +230 -185
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/consistency-checker.md +4 -4
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +5 -5
- package/lib/oh-story/roles/story-explorer.md +11 -11
- package/lib/oh-story/skills/story/SKILL.md +13 -7
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +40 -53
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-import/SKILL.md +77 -81
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +38 -29
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +25 -20
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +4 -13
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/project-files.md +9 -7
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +1 -2
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +86 -115
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -120
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +66 -68
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +139 -18
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +5 -3
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-review/SKILL.md +68 -85
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-review/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-review/references/review-quality.md +4 -10
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +50 -42
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
- package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +6 -147
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +2 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +3 -2
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +16 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +52 -0
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
|
@@ -91,6 +91,10 @@ PICTURE_LIMITS = {
|
|
|
91
91
|
"warmth": (-30.0, 30.0),
|
|
92
92
|
}
|
|
93
93
|
TOLERANCE = 0.005
|
|
94
|
+
# ffmpeg's `noise` strength runs to 100, which is snow, not grain. The useful
|
|
95
|
+
# band for a finished film is single digits; the cap keeps a typo from shipping
|
|
96
|
+
# a broken-signal look that measures perfectly fine.
|
|
97
|
+
GRAIN_LIMIT = 20.0
|
|
94
98
|
|
|
95
99
|
|
|
96
100
|
class EditError(Exception):
|
|
@@ -116,6 +120,7 @@ class Delivery(NamedTuple):
|
|
|
116
120
|
burn_subtitles: bool
|
|
117
121
|
frame_size: Optional[tuple[int, int]]
|
|
118
122
|
fps: Optional[float]
|
|
123
|
+
grain: Optional[float]
|
|
119
124
|
|
|
120
125
|
|
|
121
126
|
def _seconds(raw: str, *, field: str, line: int) -> float:
|
|
@@ -131,6 +136,7 @@ def _parse_delivery(lines: Sequence[str]) -> Delivery:
|
|
|
131
136
|
burn = True
|
|
132
137
|
frame_size: Optional[tuple[int, int]] = None
|
|
133
138
|
fps: Optional[float] = None
|
|
139
|
+
grain: Optional[float] = None
|
|
134
140
|
for raw in lines:
|
|
135
141
|
match = FIELD.match(raw)
|
|
136
142
|
if not match:
|
|
@@ -146,6 +152,23 @@ def _parse_delivery(lines: Sequence[str]) -> Delivery:
|
|
|
146
152
|
loudness = float(found.group(0))
|
|
147
153
|
elif name == "字幕":
|
|
148
154
|
burn = "无" != value.strip() and "不烧" not in value
|
|
155
|
+
elif name == "颗粒":
|
|
156
|
+
# Grain belongs to the delivery spec rather than to a cut, for the
|
|
157
|
+
# same reason loudness does: applied per cut it would become one
|
|
158
|
+
# more thing that differs between segments, which is the defect it
|
|
159
|
+
# is here to cover.
|
|
160
|
+
if value.strip() in {"无", "不加"}:
|
|
161
|
+
grain = None
|
|
162
|
+
else:
|
|
163
|
+
found = re.search(r"[0-9]+(?:\.[0-9]+)?", value)
|
|
164
|
+
if found:
|
|
165
|
+
amount = float(found.group(0))
|
|
166
|
+
if not 0.0 <= amount <= GRAIN_LIMIT:
|
|
167
|
+
raise EditError(
|
|
168
|
+
f"{CUT_LIST_NAME}: 颗粒 {amount} 超出 0–{GRAIN_LIMIT:g};"
|
|
169
|
+
"这一档以上不再像胶片,像信号故障"
|
|
170
|
+
)
|
|
171
|
+
grain = amount or None
|
|
149
172
|
elif name == "画幅与帧率":
|
|
150
173
|
size = re.search(r"([0-9]{2,5})\s*[×x*]\s*([0-9]{2,5})", value)
|
|
151
174
|
if size:
|
|
@@ -153,7 +176,7 @@ def _parse_delivery(lines: Sequence[str]) -> Delivery:
|
|
|
153
176
|
rate = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*fps", value, re.I)
|
|
154
177
|
if rate:
|
|
155
178
|
fps = float(rate.group(1))
|
|
156
|
-
return Delivery(target, loudness, burn, frame_size, fps)
|
|
179
|
+
return Delivery(target, loudness, burn, frame_size, fps, grain)
|
|
157
180
|
|
|
158
181
|
|
|
159
182
|
def parse_cut_list(path: Path) -> tuple[Delivery, list[Cut], list[str]]:
|
|
@@ -371,8 +394,31 @@ def probe_stream(media: Path) -> dict[str, Any]:
|
|
|
371
394
|
}
|
|
372
395
|
|
|
373
396
|
|
|
397
|
+
def _unaccounted_shots(known: set[str], cuts: Sequence[Cut], unused: Sequence[str]) -> list[str]:
|
|
398
|
+
"""Require each source to be used or explicitly omitted with a reason."""
|
|
399
|
+
|
|
400
|
+
used = {cut.motion for cut in cuts}
|
|
401
|
+
excused = set()
|
|
402
|
+
for note in unused:
|
|
403
|
+
match = re.fullmatch(r"(MOTION-[\w-]+)\s*[((]理由[::]\s*(.+?)[))]", note.strip())
|
|
404
|
+
if match and match.group(2).strip():
|
|
405
|
+
excused.add(match.group(1))
|
|
406
|
+
missing = sorted(known - used - excused)
|
|
407
|
+
if not missing:
|
|
408
|
+
return []
|
|
409
|
+
return [
|
|
410
|
+
f"{CUT_LIST_NAME}: 以下镜头未采用,且缺少「未采用镜头」及理由:"
|
|
411
|
+
+ "、".join(missing)
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
|
|
374
415
|
def check_cuts(
|
|
375
|
-
episode: Path,
|
|
416
|
+
episode: Path,
|
|
417
|
+
cuts: Sequence[Cut],
|
|
418
|
+
project_root: Path,
|
|
419
|
+
*,
|
|
420
|
+
probe: bool,
|
|
421
|
+
unused: Sequence[str] = (),
|
|
376
422
|
) -> list[str]:
|
|
377
423
|
"""Every mechanical cross-check the cut list can be held to. Returns findings."""
|
|
378
424
|
|
|
@@ -403,6 +449,7 @@ def check_cuts(
|
|
|
403
449
|
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的来源 "
|
|
404
450
|
f"{cut.motion} 不在《{MOTION_DOCUMENT}》中"
|
|
405
451
|
)
|
|
452
|
+
findings.extend(_unaccounted_shots(known, cuts, unused))
|
|
406
453
|
else:
|
|
407
454
|
findings.append(f"没有 {motion_path},来源 MOTION 无法核对")
|
|
408
455
|
|
|
@@ -411,6 +458,7 @@ def check_cuts(
|
|
|
411
458
|
if screenplay_path.is_file():
|
|
412
459
|
screenplay = screenplay_path.read_text(encoding="utf-8")
|
|
413
460
|
|
|
461
|
+
media_format: Optional[tuple[Any, Any, float]] = None
|
|
414
462
|
for cut in cuts:
|
|
415
463
|
span = cut.end - cut.start
|
|
416
464
|
if cut.start < 0:
|
|
@@ -430,7 +478,17 @@ def check_cuts(
|
|
|
430
478
|
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的素材不存在: {cut.media}"
|
|
431
479
|
)
|
|
432
480
|
elif probe:
|
|
433
|
-
|
|
481
|
+
stream = probe_stream(media)
|
|
482
|
+
available = stream["duration"]
|
|
483
|
+
current_format = (stream["width"], stream["height"], stream["fps"])
|
|
484
|
+
if media_format is None:
|
|
485
|
+
media_format = current_format
|
|
486
|
+
elif current_format != media_format:
|
|
487
|
+
findings.append(
|
|
488
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的画幅或帧率 "
|
|
489
|
+
f"{current_format} 与首段 {media_format} 不一致;"
|
|
490
|
+
"先在外部统一素材规格,再更新来源路径与入出点"
|
|
491
|
+
)
|
|
434
492
|
if cut.end > available + TOLERANCE:
|
|
435
493
|
findings.append(
|
|
436
494
|
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点 {cut.end:.2f} "
|
|
@@ -598,6 +656,17 @@ def render(
|
|
|
598
656
|
)
|
|
599
657
|
filters.append(f"ass='{escaped}'")
|
|
600
658
|
|
|
659
|
+
# Grain goes on last, over the whole assembled film, so one texture sits
|
|
660
|
+
# across every cut. `t` makes it move frame to frame — static noise reads
|
|
661
|
+
# as dirt on the lens, not as film.
|
|
662
|
+
grain = (
|
|
663
|
+
f"noise=alls={delivery.grain:g}:allf=t+u"
|
|
664
|
+
if delivery.grain is not None
|
|
665
|
+
else None
|
|
666
|
+
)
|
|
667
|
+
if grain is not None:
|
|
668
|
+
filters.append(grain)
|
|
669
|
+
|
|
601
670
|
final = output_root / "成片.mp4"
|
|
602
671
|
command = [ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(joined)]
|
|
603
672
|
if overlay_path is not None:
|
|
@@ -605,9 +674,15 @@ def render(
|
|
|
605
674
|
# to be named: the default one drops it and the overlay arrives as
|
|
606
675
|
# an opaque black rectangle.
|
|
607
676
|
command += ["-c:v", "libvpx", "-i", str(overlay_path)]
|
|
677
|
+
# The overlay branch owns its own chain, so grain is spliced in
|
|
678
|
+
# after the composite rather than left in `filters`, which this
|
|
679
|
+
# branch never reads.
|
|
680
|
+
chain = "[0:v][1:v]overlay=0:0:format=auto"
|
|
681
|
+
if grain is not None:
|
|
682
|
+
chain += f",{grain}"
|
|
608
683
|
command += [
|
|
609
684
|
"-filter_complex",
|
|
610
|
-
"
|
|
685
|
+
f"{chain},format=yuv420p[v]",
|
|
611
686
|
"-map", "[v]", "-map", "0:a",
|
|
612
687
|
"-c:v", "libx264", "-preset", "medium", "-crf", "18",
|
|
613
688
|
]
|
|
@@ -618,7 +693,7 @@ def render(
|
|
|
618
693
|
command += ["-c:v", "copy"]
|
|
619
694
|
if delivery.loudness_lufs is not None:
|
|
620
695
|
command += ["-af", _loudnorm_filter(ffmpeg, joined, delivery.loudness_lufs)]
|
|
621
|
-
command += ["-c:a", "aac", "-b:a", "192k"]
|
|
696
|
+
command += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000"]
|
|
622
697
|
else:
|
|
623
698
|
command += ["-c:a", "copy"]
|
|
624
699
|
command.append(str(final))
|
|
@@ -663,11 +738,10 @@ def _shot_match_filter(cut: Cut) -> str:
|
|
|
663
738
|
|
|
664
739
|
|
|
665
740
|
def _loudnorm_filter(ffmpeg: str, media: Path, target: float) -> str:
|
|
666
|
-
"""
|
|
741
|
+
"""Measure EBU R128 before normalization; verify the encoded output afterward.
|
|
667
742
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
the measurement back, and the second pass is linear.
|
|
743
|
+
FFmpeg can fall back to dynamic processing when linear gain would exceed
|
|
744
|
+
the peak or loudness-range target. Two passes do not guarantee target LUFS.
|
|
671
745
|
"""
|
|
672
746
|
|
|
673
747
|
common = f"I={target}:TP=-1.5:LRA=11"
|
|
@@ -894,6 +968,40 @@ def _timecode(seconds: float) -> str:
|
|
|
894
968
|
return f"{hours:02d}:{minutes:02d}:{whole:02d},{milliseconds:03d}"
|
|
895
969
|
|
|
896
970
|
|
|
971
|
+
def _segment_colour(ffmpeg: str, path: Path) -> Optional[tuple[float, float]]:
|
|
972
|
+
"""Mean luma and blue-red difference of one segment, on a 0-255 scale."""
|
|
973
|
+
|
|
974
|
+
result = subprocess.run(
|
|
975
|
+
[ffmpeg, "-v", "error", "-i", str(path), "-vf", "fps=2,scale=96:-1",
|
|
976
|
+
"-pix_fmt", "rgb24", "-f", "rawvideo", "-"],
|
|
977
|
+
capture_output=True,
|
|
978
|
+
)
|
|
979
|
+
raw = result.stdout
|
|
980
|
+
if result.returncode != 0 or len(raw) < 3:
|
|
981
|
+
return None
|
|
982
|
+
count = len(raw) // 3
|
|
983
|
+
red = sum(raw[i * 3] for i in range(count)) / count
|
|
984
|
+
green = sum(raw[i * 3 + 1] for i in range(count)) / count
|
|
985
|
+
blue = sum(raw[i * 3 + 2] for i in range(count)) / count
|
|
986
|
+
return 0.299 * red + 0.587 * green + 0.114 * blue, blue - red
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _segment_colours(ffmpeg: str, output_root: Path, cuts: Sequence[Cut]) -> list[dict[str, Any]]:
|
|
990
|
+
"""Report current cut segments individually, without imposing a shared grade."""
|
|
991
|
+
|
|
992
|
+
rows: list[dict[str, Any]] = []
|
|
993
|
+
for cut in cuts:
|
|
994
|
+
path = output_root / SEGMENT_DIRECTORY / f"{cut.cut_id}.mp4"
|
|
995
|
+
colour = _segment_colour(ffmpeg, path) if path.is_file() else None
|
|
996
|
+
row: dict[str, Any] = {"分段": path.name}
|
|
997
|
+
if colour is None:
|
|
998
|
+
row["测量"] = "未测(分段缺失或不可读)"
|
|
999
|
+
else:
|
|
1000
|
+
row.update({"平均亮度": round(colour[0], 1), "蓝减红": round(colour[1], 1)})
|
|
1001
|
+
rows.append(row)
|
|
1002
|
+
return rows
|
|
1003
|
+
|
|
1004
|
+
|
|
897
1005
|
def verify(episode: Path, cuts: Sequence[Cut], delivery: Delivery) -> dict[str, Any]:
|
|
898
1006
|
"""Measure the rendered film. Every entry is a number or an honest 未测."""
|
|
899
1007
|
|
|
@@ -931,6 +1039,10 @@ def verify(episode: Path, cuts: Sequence[Cut], delivery: Delivery) -> dict[str,
|
|
|
931
1039
|
else:
|
|
932
1040
|
measurements["实测响度 LUFS"] = "未测(loudnorm 没有返回可解析的测量结果)"
|
|
933
1041
|
measurements["交付响度目标"] = delivery.loudness_lufs
|
|
1042
|
+
measurements["分段色彩观测"] = _segment_colours(ffmpeg, episode / OUTPUT_DIRECTORY, cuts)
|
|
1043
|
+
measurements["画内可读文字"] = (
|
|
1044
|
+
"未测(抽有画内文字的帧,逐字对《剧本.md》的「画面文字」与提示词声明的内容)"
|
|
1045
|
+
)
|
|
934
1046
|
measurements["台词完整性"] = "未测(本工具不做转写;在成片上转写后逐句对《剧本.md》原文)"
|
|
935
1047
|
measurements["边界帧"] = "未测(抽剪辑点前后各一帧目视核对黑场/白场/半渲染帧)"
|
|
936
1048
|
return measurements
|
|
@@ -1004,7 +1116,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
1004
1116
|
delivery, cuts, unused = parse_cut_list(episode / CUT_LIST_NAME)
|
|
1005
1117
|
if arguments.command == "check":
|
|
1006
1118
|
findings = check_cuts(
|
|
1007
|
-
episode, cuts, project_root,
|
|
1119
|
+
episode, cuts, project_root,
|
|
1120
|
+
probe=_which("ffprobe") is not None, unused=unused,
|
|
1008
1121
|
)
|
|
1009
1122
|
payload: dict[str, Any] = {
|
|
1010
1123
|
"段数": len(cuts),
|
|
@@ -1018,7 +1131,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
1018
1131
|
_emit(payload)
|
|
1019
1132
|
return 1 if findings else 0
|
|
1020
1133
|
if arguments.command == "render":
|
|
1021
|
-
findings = check_cuts(
|
|
1134
|
+
findings = check_cuts(
|
|
1135
|
+
episode, cuts, project_root, probe=True, unused=unused
|
|
1136
|
+
)
|
|
1022
1137
|
if findings:
|
|
1023
1138
|
_emit({"findings": findings, "已渲染": False})
|
|
1024
1139
|
return 1
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
16
16
|
|
|
17
17
|
from edit_tool import ( # noqa: E402
|
|
18
|
+
_unaccounted_shots,
|
|
18
19
|
DEFAULT_REMOTION_CONCURRENCY,
|
|
19
20
|
REMOTION_SOURCE,
|
|
20
21
|
REMOTION_SOURCE_FILES,
|
|
@@ -377,6 +378,67 @@ def check_remotion_concurrency_is_capped() -> None:
|
|
|
377
378
|
require("--concurrency=" in source, "render 必须显式传 --concurrency,不能用 Remotion 的默认值")
|
|
378
379
|
|
|
379
380
|
|
|
381
|
+
def check_grain_is_a_delivery_wide_decision() -> None:
|
|
382
|
+
"""Grain is declared once for the film, and refused when it is not grain.
|
|
383
|
+
|
|
384
|
+
Per cut it would become one more thing that differs between segments, which
|
|
385
|
+
is the defect it exists to cover.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
389
|
+
root = Path(scratch)
|
|
390
|
+
episode = build(root, CUT_LIST)
|
|
391
|
+
path = episode / "剪辑单.md"
|
|
392
|
+
baseline = path.read_text(encoding="utf-8")
|
|
393
|
+
|
|
394
|
+
delivery, _, _ = parse_cut_list(path)
|
|
395
|
+
require(delivery.grain is None, "没声明颗粒时应当是 None")
|
|
396
|
+
|
|
397
|
+
anchor = "- 交付响度:"
|
|
398
|
+
require(anchor in baseline, "夹具里应当有交付响度那一行")
|
|
399
|
+
path.write_text(baseline.replace(anchor, "- 颗粒:6\n" + anchor, 1), encoding="utf-8")
|
|
400
|
+
delivery, _, _ = parse_cut_list(path)
|
|
401
|
+
require(delivery.grain == 6.0, f"颗粒没有解析出来: {delivery.grain}")
|
|
402
|
+
|
|
403
|
+
path.write_text(baseline.replace(anchor, "- 颗粒:无\n" + anchor, 1), encoding="utf-8")
|
|
404
|
+
delivery, _, _ = parse_cut_list(path)
|
|
405
|
+
require(delivery.grain is None, "「无」应当解析成不加颗粒")
|
|
406
|
+
|
|
407
|
+
path.write_text(baseline.replace(anchor, "- 颗粒:80\n" + anchor, 1), encoding="utf-8")
|
|
408
|
+
try:
|
|
409
|
+
parse_cut_list(path)
|
|
410
|
+
except EditError as error:
|
|
411
|
+
require("超出" in str(error), f"越界报错没说清: {error}")
|
|
412
|
+
else:
|
|
413
|
+
raise AssertionError("颗粒 80 应当被拒绝")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def check_missing_shots_are_reported() -> None:
|
|
417
|
+
"""A shot absent from the film must be absent on purpose.
|
|
418
|
+
|
|
419
|
+
Absence is invisible in a finished film — nobody watching can tell that an
|
|
420
|
+
episode's opening seven shots were never made. The only place it can be
|
|
421
|
+
caught is here, against the document that lists them.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
known = {f"MOTION-EP006-{i:03d}" for i in range(1, 5)}
|
|
425
|
+
|
|
426
|
+
class Stub:
|
|
427
|
+
def __init__(self, motion): self.motion = motion
|
|
428
|
+
|
|
429
|
+
used = [Stub("MOTION-EP006-003"), Stub("MOTION-EP006-004")]
|
|
430
|
+
findings = _unaccounted_shots(known, used, [])
|
|
431
|
+
require(findings, "少了两镜却没有任何 finding")
|
|
432
|
+
require("001" in findings[0] and "002" in findings[0], f"没点名缺的镜头: {findings}")
|
|
433
|
+
|
|
434
|
+
excused = _unaccounted_shots(
|
|
435
|
+
known, used, ["MOTION-EP006-001(理由:质量不可用)", "MOTION-EP006-002(理由:文件缺失)"]
|
|
436
|
+
)
|
|
437
|
+
require(not excused, f"写进未采用的镜头不该再报: {excused}")
|
|
438
|
+
|
|
439
|
+
require(not _unaccounted_shots(known, [Stub(m) for m in known], []), "全采用时不该报")
|
|
440
|
+
|
|
441
|
+
|
|
380
442
|
def main() -> int:
|
|
381
443
|
with tempfile.TemporaryDirectory() as scratch:
|
|
382
444
|
root = Path(scratch)
|
|
@@ -446,6 +508,8 @@ def main() -> int:
|
|
|
446
508
|
check_ass_escaping()
|
|
447
509
|
check_multi_subtitle()
|
|
448
510
|
check_stale_window()
|
|
511
|
+
check_missing_shots_are_reported()
|
|
512
|
+
check_grain_is_a_delivery_wide_decision()
|
|
449
513
|
check_remotion_sources_all_shipped()
|
|
450
514
|
check_remotion_concurrency_is_capped()
|
|
451
515
|
print("short-drama-edit self-tests passed")
|
|
@@ -20,6 +20,8 @@ license: MIT
|
|
|
20
20
|
|
|
21
21
|
有当前视觉设定即可直接开始;剧本只在提示词需要确认剧情状态时读取。Look Development 是可选分支。
|
|
22
22
|
先确认用途:身份板、造型/状态变体、地点板、道具板、组合 production sheet 或比较风格帧。
|
|
23
|
+
分镜需要生产尾帧时,可把已接受的「收尾关键帧提示词」落实为独立 `IMG-...` 状态图条目:
|
|
24
|
+
说明来源 SHOT 与终点,沿用视觉事实和参考范围,不重新设计动作。实际生成以该 IMG 条目为来源。
|
|
23
25
|
|
|
24
26
|
创作者可读说明跟随项目语言;可复制正文跟随 `short-drama.json#/format/prompt_language`。没有
|
|
25
27
|
`short-drama.json` 时正文默认
|
|
@@ -203,6 +203,17 @@
|
|
|
203
203
|
- **`taste_option`**:写实度、媒介、颗粒、色调和镜头感由视觉风格或创作者决定。
|
|
204
204
|
- 禁止用固定词表或形容词比例阻断交付;描述是否空泛,由审查者指出具体缺了什么。
|
|
205
205
|
|
|
206
|
+
### 光学与表面描述
|
|
207
|
+
|
|
208
|
+
**`craft_default`**:对需要明确透视或写实质感的画面,写明机位、取景距离和可见表面特征。
|
|
209
|
+
焦段可辅助表达,但不能据此承诺模型会精确模拟光学。
|
|
210
|
+
|
|
211
|
+
根据画面尺度选择毛孔、布料折痕或金属使用痕迹;「干净」不必等同于没有任何纹理和折痕。
|
|
212
|
+
不要给每张图添加相同的相机参数、瑕疵清单或排除词。
|
|
213
|
+
|
|
214
|
+
**`taste_option`**:表面是否平滑、是否修饰,以及写实程度由已接受的视觉风格决定。
|
|
215
|
+
结果用对照图检查;像素纹理指标只作辅助,不能代替人物与材质的视觉判断。
|
|
216
|
+
|
|
206
217
|
## 5. 输出、常见问题与检查
|
|
207
218
|
|
|
208
219
|
### 输出
|
|
@@ -16,7 +16,8 @@ license: MIT
|
|
|
16
16
|
取出本次提示词,建立一个有边界的运行 job。creator-first job 的 `source` 必须指向拥有这条提示词的
|
|
17
17
|
当前 Markdown,`source_entry` 必须点名该文档允许的二级标题:`图片提示词.md` 用 `IMG-*`,
|
|
18
18
|
`视频提示词.md` 用 `MOTION-*`,`分镜.md` 用 `SHOT-*`(modality 为 `image`,正文取该镜的
|
|
19
|
-
`###
|
|
19
|
+
`### 冻结关键帧提示词`)。此选择器只提取首帧;尾帧需先由图片提示词阶段建立独立 `IMG-*` 状态图条目,
|
|
20
|
+
再从 `图片提示词.md` 准备 image job。创作者也可以提供已有起始帧——
|
|
20
21
|
在自己的工具里出图再放进项目同样成立,图片始终不进项目时走分镜的 `PLAN-...`;
|
|
21
22
|
产出落在 `剧集/<EP>/制作成果/images/` 后,分镜 owner 才能把它绑成 `用途:起始帧` 的 `REF-...`。存在真实参考图时,
|
|
22
23
|
还必须逐张填写 `reference_bindings` 的槽位、顺序、路径、中文名、用途以及允许/禁止控制范围;
|
|
@@ -126,6 +127,9 @@ adapter 配置必须在项目外,只包含 argv 命令和超时;凭据由 ad
|
|
|
126
127
|
固定高保真引用并校验尺寸、格式与透明背景限制。
|
|
127
128
|
- [MiniMax Music](references/providers/minimax-music.md):使用 `music-3.0` 与 hex 结果,区分主题曲
|
|
128
129
|
和纯配乐,不伪造时长请求字段。
|
|
130
|
+
- [MiniMax 语音](references/providers/minimax-speech.md):`tts` 模态;模型与 `voice_id` 都必须
|
|
131
|
+
由账号与文档显式给出,不内置音色清单。只从预置音色合成,不做声音克隆——克隆是授权问题,
|
|
132
|
+
创作者授权的参考录音按普通创作者输入走 `输入/`。
|
|
129
133
|
- [MiniMax H3 视频](references/providers/minimax-h3-video.md):模型 ID、分辨率集合与时长区间必须
|
|
130
134
|
由账号显式配置;提示词进 `content` 的 text 项,参考图按显式 role 绑定,本地参考在没有可信上传时
|
|
131
135
|
fail closed。该模型与画面同一次生成声音,写法影响见视频提示词技能的目标模型能力档案。
|
|
@@ -99,6 +99,12 @@ Keep this file outside the project:
|
|
|
99
99
|
`command` is an argv array, never a shell string. Timeout is 1–3600 seconds. Do not put credentials in this file;
|
|
100
100
|
let the adapter read its environment or operating-system credential store.
|
|
101
101
|
|
|
102
|
+
## Capability sources
|
|
103
|
+
|
|
104
|
+
Use the provider's current API reference for roles, limits and supported combinations.
|
|
105
|
+
A relay can expose a different subset, reject inputs itself or transform requests.
|
|
106
|
+
Record the tested execution path and distinguish its observations from the native API contract.
|
|
107
|
+
|
|
102
108
|
## Adapter stdin
|
|
103
109
|
|
|
104
110
|
The adapter receives one UTF-8 JSON document as raw stdin bytes. Read the
|
|
@@ -121,26 +127,14 @@ on the machine locale. The document contains the confirmed job plus:
|
|
|
121
127
|
adapter must **not submit anything**; it polls and downloads that existing
|
|
122
128
|
task and returns its outputs as usual.
|
|
123
129
|
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
So: write the handle before the first poll, and treat failing to write it as
|
|
134
|
-
non-fatal — a submitted task must never be failed because its id could not be
|
|
135
|
-
recorded locally. The tool copies the handle onto the attempt record on both the
|
|
136
|
-
success and the failure path, `audit` reports any unfinished attempt that
|
|
137
|
-
carries one as `orphaned_provider_job` with `action: collect_before_retry`, and
|
|
138
|
-
`production_tool.py collect` fetches it.
|
|
139
|
-
|
|
140
|
-
`collect` deliberately sits outside the confirmation gate. That gate exists to
|
|
141
|
-
prevent an unintended charge; collecting spends nothing because the charge
|
|
142
|
-
already happened. Requiring a fresh confirmation would make paying again the
|
|
143
|
-
cheapest way out of an interruption — the exact outcome the gate is for.
|
|
130
|
+
## Recovering a submitted task
|
|
131
|
+
|
|
132
|
+
Record the provider task ID before polling so an interrupted task can be collected without resubmission.
|
|
133
|
+
A local handle-write failure is non-fatal after submission. The tool copies the handle into the attempt
|
|
134
|
+
record on success or failure; `audit` reports unfinished tasks as `orphaned_provider_job` with
|
|
135
|
+
`action: collect_before_retry`.
|
|
136
|
+
|
|
137
|
+
`production_tool.py collect` retrieves the existing task without new submission or confirmation.
|
|
144
138
|
|
|
145
139
|
It may translate provider-neutral parameters into its chosen SDK/API. Optional
|
|
146
140
|
provider adapters under `scripts/` document and implement known translations;
|
|
@@ -74,31 +74,13 @@ structured: base/first-frame/first-last-frame tasks use `integrated_multimodal_d
|
|
|
74
74
|
form. Chinese dialogue remains exact inside `<d>[Chinese] ...</d>`. Full details live in the
|
|
75
75
|
video-prompt skill's `references/minimax-h3.md`.
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- the **speech** channel did not behave the same way in those probes: no run invented dialogue,
|
|
86
|
-
including 8 runs whose body carried a contradictory `No dialogue` exclusion. Treat "an unstated
|
|
87
|
-
vocal channel gets filled" as unverified rather than as a known failure, and write the shot's
|
|
88
|
-
sound intent because the body should say what this shot sounds like — not on the strength of a
|
|
89
|
-
mechanism this suite has not observed.
|
|
90
|
-
- readable on-screen text was **not** produced unmasked in those probes either: across 44 sampled
|
|
91
|
-
frames from 11 clips — including dialogue clips carrying no exclusion sentence, on H3 and on
|
|
92
|
-
Seedance 2.0/2.5 — no burned-in subtitle or caption appeared. The exclusion sentence this suite
|
|
93
|
-
mandates is therefore precautionary **for video** rather than evidence-driven. Keep writing it:
|
|
94
|
-
a one-line exclusion costs nothing, these probes are small enough that they only show the failure
|
|
95
|
-
was not produced here, and generator behaviour changes between versions. What should change is the
|
|
96
|
-
justification — do not defend it as a fix for an observed defect. Note the asymmetry before carrying this reasoning anywhere else:
|
|
97
|
-
the same probe run against `gpt-image-2` found the opposite — an unconstrained still came back
|
|
98
|
-
covered in invented readable text, and the exclusion sentence cleaned it up. The image stages
|
|
99
|
-
keep their text policy on evidence; only the video stage's copy of it is precautionary.
|
|
100
|
-
|
|
101
|
-
Neither is written here as a fixed phrase, and this suite never injects one. The wording belongs to
|
|
102
|
-
the shot and to the project's declared prompt language.
|
|
77
|
+
State the intended sound layers in the prompt. For no non-diegetic music, the
|
|
78
|
+
H3 dialect uses `non_diegetic_music: N/A`; small probes observed unwanted music
|
|
79
|
+
when this layer was omitted. They did not establish that omitted speech or
|
|
80
|
+
negative clauses cause extra dialogue.
|
|
81
|
+
|
|
82
|
+
Follow the project's subtitle and in-scene-text policy. Prompt wording belongs
|
|
83
|
+
to the video-prompt skill and the declared language, not to adapter-generated
|
|
84
|
+
boilerplate.
|
|
103
85
|
|
|
104
86
|
Protocol reference: [MiniMax video generation API](https://platform.minimax.io/docs/api-reference/video-generation-v2-create).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# MiniMax Speech adapter
|
|
2
|
+
|
|
3
|
+
Adapter command:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{"command": ["python3", "/absolute/path/provider_adapters.py", "minimax-speech"], "timeout_seconds": 600}
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Required environment: `MINIMAX_API_KEY`. `MINIMAX_BASE_URL` optionally overrides the default
|
|
10
|
+
`https://api.minimax.io/v1` and must remain HTTPS.
|
|
11
|
+
|
|
12
|
+
The job uses the suite's `tts` modality and must have exactly one output. The production prompt is the
|
|
13
|
+
line to be spoken, verbatim — not a description of it. Supported public parameters are `model`,
|
|
14
|
+
`voice_id`, `emotion`, `speed`, `vol`, `pitch`, `sample_rate`, `bitrate`, and `format`. The requested
|
|
15
|
+
format must match the target extension and be `mp3` or `wav`.
|
|
16
|
+
|
|
17
|
+
`model` and `voice_id` are both required and neither has a default. The model is an account-enabled
|
|
18
|
+
endpoint, exactly as for the video providers. The voice is a creative decision recorded in
|
|
19
|
+
`视觉设定.md` under the character's 声音方向, and it reaches the adapter through the confirmed job.
|
|
20
|
+
|
|
21
|
+
## Voice selection
|
|
22
|
+
|
|
23
|
+
Read the provider's voice listing and confirm availability for the chosen model and account.
|
|
24
|
+
The adapter validates the ID format, not catalogue membership. Record the selected ID in the
|
|
25
|
+
character's 声音方向 and pass it through the confirmed job.
|
|
26
|
+
|
|
27
|
+
This workflow selects preset voices. The adapter does not enrol or clone a voice;
|
|
28
|
+
any supplied recording retains its documented authorization and usage scope.
|
|
29
|
+
|
|
30
|
+
## Request shape
|
|
31
|
+
|
|
32
|
+
The request is non-streaming with `output_format: hex`, `POST /t2a_v2`, and carries
|
|
33
|
+
`voice_setting` (`voice_id`, and any of `emotion`, `speed`, `vol`, `pitch`) alongside `audio_setting`
|
|
34
|
+
(`sample_rate`, `bitrate`, `format`). The adapter validates `base_resp.status_code`, decodes
|
|
35
|
+
`data.audio` as hexadecimal bytes, and writes it to a private temporary file.
|
|
36
|
+
|
|
37
|
+
`emotion` accepts the provider's seven values: `happy`, `sad`, `angry`, `fearful`, `disgusted`,
|
|
38
|
+
`surprised`, `neutral`. Anything outside that set is refused rather than passed through — a rejected
|
|
39
|
+
emotion is a typo caught before it is paid for.
|
|
40
|
+
|
|
41
|
+
Protocol reference: [MiniMax Text to Speech](https://platform.minimax.io/docs/api-reference/speech-t2a-http).
|