@kolbo/mcp 1.70.0 → 1.70.2
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 +7 -7
- package/bin/kolbo-mcp.js +14 -5
- package/package.json +3 -2
- package/skill/GENERATED.md +4 -5
- package/skill/SKILL.md +11 -18
- package/skill/VERSION +1 -1
- package/skill/assets/filmmaking/continuity-ledger.template.json +27 -0
- package/skill/assets/filmmaking/generation-log.template.csv +2 -0
- package/skill/assets/filmmaking/production-bible.template.json +97 -0
- package/skill/assets/filmmaking/scene-card.template.json +40 -0
- package/skill/assets/filmmaking/shot-card.template.json +72 -0
- package/skill/references/filmmaking/acting-direction.md +131 -0
- package/skill/references/filmmaking/asset-preproduction.md +97 -0
- package/skill/references/filmmaking/audio-dialogue-music.md +111 -0
- package/skill/references/filmmaking/blocking-continuity.md +125 -0
- package/skill/references/filmmaking/cinematography.md +101 -0
- package/skill/references/filmmaking/physics-action.md +93 -0
- package/skill/references/filmmaking/production-bible.md +108 -0
- package/skill/references/filmmaking/prompt-contracts.md +140 -0
- package/skill/references/filmmaking/routing.md +105 -0
- package/skill/references/filmmaking/scene-engine.md +95 -0
- package/skill/references/filmmaking/validation.md +109 -0
- package/skill/references/filmmaking/workflows.md +80 -0
- package/skill/references/models/gpt-image.md +1 -1
- package/skill/references/models/nano-banana.md +1 -1
- package/skill/references/models/prompt-copilot.md +0 -1
- package/skill/references/models/seedance.md +24 -44
- package/skill/references/models/seedance25.md +3 -3
- package/skill/references/workflows/filmmaking.md +168 -0
- package/skill/scripts/filmmaking/lint_prompt.py +249 -0
- package/skill/scripts/filmmaking/validate_film_package.py +435 -0
- package/src/apps/widgets/generation.js +7 -2
- package/src/install.js +62 -3
- package/src/tools/_shared.js +11 -3
- package/src/tools/generate.js +22 -16
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Filmmaking Router
|
|
2
|
+
|
|
3
|
+
Operate as a filmmaking system, not merely a prompt writer. Preserve project truth across generations while compiling every generation into a self-contained instruction the selected model can execute.
|
|
4
|
+
|
|
5
|
+
## Start here
|
|
6
|
+
|
|
7
|
+
1. Identify the requested production stage and deliverable.
|
|
8
|
+
2. Read only the reference files required by the routing table below.
|
|
9
|
+
3. Preserve or establish the relevant production truth before writing a shot.
|
|
10
|
+
4. Choose generation mode, control density, craft packs, audio lane, and model adapter.
|
|
11
|
+
5. Produce the requested artifact in the user's language; keep generation prompts in English unless the user requests otherwise.
|
|
12
|
+
6. Run the applicable audit. For saved prompt/package artifacts, run the bundled validators.
|
|
13
|
+
|
|
14
|
+
Do not generate media, spend credits, or contact external systems unless the user explicitly asks. Do not silently change an existing billable prompt beyond the requested scope. In Workbench mode, patch the failed section and keep proven sections byte-stable whenever practical.
|
|
15
|
+
|
|
16
|
+
## Route the work
|
|
17
|
+
|
|
18
|
+
Read [routing.md](references/filmmaking/routing.md) for the full decision rules.
|
|
19
|
+
|
|
20
|
+
| Request | Mode | Read |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Premise, outline, screenplay, weak scene | Development | `scene-engine.md`, then `workflows.md` |
|
|
23
|
+
| Character, location, prop, state, voice, or production preparation | Pre-production | `asset-preproduction.md`, `production-bible.md`; add `acting-direction.md` for recurring characters |
|
|
24
|
+
| One generation-ready video prompt | Direction | `prompt-contracts.md`, selected craft references, then the model adapter |
|
|
25
|
+
| Connected dialogue or performance | Direction | `acting-direction.md`, `blocking-continuity.md`, `audio-dialogue-music.md`, model adapter |
|
|
26
|
+
| Music video, dance, singing, or exact song | Direction | `audio-dialogue-music.md`, `cinematography.md`, `blocking-continuity.md`, model adapter |
|
|
27
|
+
| Difficult action, transformation, scale, vehicle, creature, water, or impossible gravity | Direction | `physics-action.md`, `blocking-continuity.md`, `cinematography.md`, model adapter |
|
|
28
|
+
| Broken result or prompt | Audit | `validation.md` plus only the craft/model references implicated by the failure |
|
|
29
|
+
| Revise one failed behavior without losing what worked | Workbench | `validation.md` and the relevant craft reference |
|
|
30
|
+
| Multi-scene, episode, commercial, music video, or feature workflow | Production | `production-bible.md`, `workflows.md`, `validation.md` |
|
|
31
|
+
|
|
32
|
+
For Seedance 2.5, always read [seedance-2-5.md](references/models/seedance25.md) before final compilation. Treat capability numbers as a dated adapter snapshot and verify them against current provider/catalog truth when real money or production delivery depends on them.
|
|
33
|
+
|
|
34
|
+
## Keep two layers separate
|
|
35
|
+
|
|
36
|
+
### Project truth
|
|
37
|
+
|
|
38
|
+
Maintain durable facts outside individual prompts:
|
|
39
|
+
|
|
40
|
+
- story goal, world laws, period, genre, tone, and visual registers;
|
|
41
|
+
- characters, immutable identity anchors, states, wardrobe, injuries, performance engines, and voices;
|
|
42
|
+
- locations, landmark geometry, light direction, axes, and available coverage;
|
|
43
|
+
- props, vehicles, creatures, scale laws, ownership, hand state, damage, and versions;
|
|
44
|
+
- scene and shot cards, continuity state, coverage, generation attempts, and editorial needs.
|
|
45
|
+
|
|
46
|
+
Use the templates in `assets/filmmaking/` when the task benefits from saved project state. Read [production-bible.md](references/filmmaking/production-bible.md) before creating or updating them.
|
|
47
|
+
|
|
48
|
+
### Generation island
|
|
49
|
+
|
|
50
|
+
Compile only what the current generation needs. A video model cannot resolve “same as before” unless the needed state is restated. Include active truth explicitly, but do not carry inactive characters, stale tags, old props, irrelevant backstory, or previous-shot prose.
|
|
51
|
+
|
|
52
|
+
Kolbo Visual DNA is semantic project truth, not merely reference imagery. Read and preserve the saved DNA type and analyzed context: character DNAs own identity/state/performance/voice; environment and scene DNAs own location/geography/light; product DNAs own prop/product identity, scale, material, and state; style DNAs own the visual register. Keep exact tags and never reinterpret one DNA type as another.
|
|
53
|
+
|
|
54
|
+
## Choose control density
|
|
55
|
+
|
|
56
|
+
Never equate sophistication with maximum length.
|
|
57
|
+
|
|
58
|
+
- **Strict** — lock exact blocking, count, timing, dialogue, hand/prop state, axis, scale, or failure-prone physics. Use for continuity-heavy dialogue, expensive hero shots, repeated failures, and exact music synchronization.
|
|
59
|
+
- **Anchored** — dictate non-negotiable story/continuity/physics anchors and allow camera or performance variation inside them. Use for complex spectacle where controlled discovery is valuable.
|
|
60
|
+
- **Exploratory** — protect identity, world, safety, and essential beats while inviting coverage variations. Use for montage, inserts, music-video coverage, and ideation.
|
|
61
|
+
|
|
62
|
+
If the user supplied an exact prompt, preserve its chosen density unless the failure diagnosis proves density itself is the problem.
|
|
63
|
+
|
|
64
|
+
## Select craft packs
|
|
65
|
+
|
|
66
|
+
Load only what the shot needs:
|
|
67
|
+
|
|
68
|
+
- Story causality and scene reversals: [scene-engine.md](references/filmmaking/scene-engine.md)
|
|
69
|
+
- Asset building, versions, and stress tests: [asset-preproduction.md](references/filmmaking/asset-preproduction.md)
|
|
70
|
+
- Character performance, listening, and voice identity: [acting-direction.md](references/filmmaking/acting-direction.md)
|
|
71
|
+
- Geography, axes, eyelines, diagrams, and state continuity: [blocking-continuity.md](references/filmmaking/blocking-continuity.md)
|
|
72
|
+
- Shot size, optics, operator behavior, and visual grammar: [cinematography.md](references/filmmaking/cinematography.md)
|
|
73
|
+
- Action feasibility, mass, materials, transformations, and impossible shots: [physics-action.md](references/filmmaking/physics-action.md)
|
|
74
|
+
- Dialogue, ambience, native audio, source-song performance, and post music: [audio-dialogue-music.md](references/filmmaking/audio-dialogue-music.md)
|
|
75
|
+
|
|
76
|
+
Do not paste every craft pack into every prompt. Translate the selected pack into the shortest observable instructions that preserve the intended result.
|
|
77
|
+
|
|
78
|
+
## Compile a shot
|
|
79
|
+
|
|
80
|
+
Read [prompt-contracts.md](references/filmmaking/prompt-contracts.md) for exact structures.
|
|
81
|
+
|
|
82
|
+
Before writing, establish:
|
|
83
|
+
|
|
84
|
+
1. The dramatic event and the shot's job in the edit.
|
|
85
|
+
2. Active references and exact state variants.
|
|
86
|
+
3. First visible frame and final state.
|
|
87
|
+
4. Geography, axis, screen direction, eyelines, and prop/hand state.
|
|
88
|
+
5. Action feasibility inside the duration.
|
|
89
|
+
6. Acting tasks for performers and listeners.
|
|
90
|
+
7. Camera grammar and control density.
|
|
91
|
+
8. Audio ownership, exact words/lyrics, and whether music is native, source-driven, or reserved for post.
|
|
92
|
+
9. Model capability limits and target generation mode.
|
|
93
|
+
|
|
94
|
+
Prompt-length limits apply to the entire compiled generation prompt as one string, including whitespace, headers, timecodes, dialogue, audio, and locks. Count after compilation. For the current Seedance 2.5 adapter snapshot, the hard ceiling is 30,000 characters; never borrow that number for another model.
|
|
95
|
+
|
|
96
|
+
For a serious controlled shot, prefer this logical order, omitting irrelevant blocks:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
SCENE CONTEXT
|
|
100
|
+
ACTIVE REFERENCES
|
|
101
|
+
LOCATION MAP
|
|
102
|
+
FIRST FRAME AND SPATIAL BLOCKING
|
|
103
|
+
FORMAT MODE
|
|
104
|
+
OPTICS
|
|
105
|
+
CAMERA
|
|
106
|
+
ACTION TIMING / SHOTS
|
|
107
|
+
PHYSICS
|
|
108
|
+
LIGHTING
|
|
109
|
+
ACTING TASKS
|
|
110
|
+
DIALOGUE
|
|
111
|
+
AUDIO / MUSIC
|
|
112
|
+
STYLE
|
|
113
|
+
POSITIVE LOCKS
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For anchored or exploratory work, collapse compatible blocks and protect only non-negotiables. Never manufacture a rigid skeleton when a looser model-native prompt is more likely to succeed.
|
|
117
|
+
|
|
118
|
+
## Preserve continuity
|
|
119
|
+
|
|
120
|
+
Track both kinds:
|
|
121
|
+
|
|
122
|
+
- **Within-generation continuity:** positions, axis, gaze, wardrobe, props, injuries, lighting, motion, audio ownership, and state across internal cuts.
|
|
123
|
+
- **Across-generation continuity:** the exact final state of shot N becomes explicit input truth for shot N+1. Preserve emotional carry, breath, body tension, dirt/wetness/damage, prop hand, screen direction, ambience, and dialogue seam.
|
|
124
|
+
|
|
125
|
+
Never rely on memory alone for a substantial production. Update the continuity ledger after an approved shot or deliberate script/state change.
|
|
126
|
+
|
|
127
|
+
## Workbench revisions
|
|
128
|
+
|
|
129
|
+
When a generation fails:
|
|
130
|
+
|
|
131
|
+
1. Compare intended versus observed result.
|
|
132
|
+
2. Identify one primary failure owner: asset, state, dramatic design, blocking, acting, optics, camera, timing, physics, lighting, audio, model capability, or prompt contradiction.
|
|
133
|
+
3. Change the smallest causal unit.
|
|
134
|
+
4. Preserve every proven line or block.
|
|
135
|
+
5. Log the change and verdict.
|
|
136
|
+
6. After repeated failures, redesign the shot: bake the state into an asset, add a staging/layout reference, reduce actions, split the shot, change the angle, or switch model/mode.
|
|
137
|
+
|
|
138
|
+
Do not keep polishing adjectives when the shot is physically or structurally overconstrained.
|
|
139
|
+
|
|
140
|
+
## Validate
|
|
141
|
+
|
|
142
|
+
Read [validation.md](references/filmmaking/validation.md). At minimum check:
|
|
143
|
+
|
|
144
|
+
- all referenced assets exist and match the intended state;
|
|
145
|
+
- no stale, invented, dangling, or conflicting tags;
|
|
146
|
+
- character and prop counts remain feasible;
|
|
147
|
+
- duration, timecodes, shot count, and dialogue fit;
|
|
148
|
+
- continuous-take and cut instructions do not conflict;
|
|
149
|
+
- one actor owns each spoken/sung line;
|
|
150
|
+
- camera freedom does not contradict strict geography;
|
|
151
|
+
- model limits and prompt budget are respected;
|
|
152
|
+
- final frame state is explicit enough for the next shot;
|
|
153
|
+
- the result is usable without reading hidden reasoning.
|
|
154
|
+
|
|
155
|
+
Run:
|
|
156
|
+
|
|
157
|
+
```powershell
|
|
158
|
+
python scripts/filmmaking/validate_film_package.py <project-folder>
|
|
159
|
+
python scripts/filmmaking/lint_prompt.py <prompt.txt> --shot-card <shot-card.json> --model seedance-2.5
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Fix errors before delivery. Report warnings that represent genuine creative tradeoffs rather than silently flattening the user's intent.
|
|
163
|
+
|
|
164
|
+
## Production workflows
|
|
165
|
+
|
|
166
|
+
Read [workflows.md](references/filmmaking/workflows.md) for single shots, dialogue scenes, music performance, connected sequences, impossible shots, and feature workflows.
|
|
167
|
+
|
|
168
|
+
This workflow is part of the canonical Kolbo skill. The Kolbo Code sync pipeline mirrors it to MCP and plugin consumers; product surfaces may compile the same filmmaking truth through their own model adapters.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint a compiled AI-video prompt against its shot card and model adapter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TAG_RE = re.compile(r"@[A-Za-z][A-Za-z0-9_.:-]*(?:\s+\d+)?")
|
|
14
|
+
SHOT_RE = re.compile(r"(?im)^\s*(?:SHOT|SEGMENT)\s+(\d+)\b")
|
|
15
|
+
RANGE_RE = re.compile(
|
|
16
|
+
r"(?i)(\d+(?:\.\d+)?)\s*s\s*(?:-|–|—|to)\s*(\d+(?:\.\d+)?)\s*s"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Report:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.items: list[dict[str, str]] = []
|
|
23
|
+
|
|
24
|
+
def add(self, level: str, code: str, message: str) -> None:
|
|
25
|
+
self.items.append({"level": level, "code": code, "message": message})
|
|
26
|
+
|
|
27
|
+
def error(self, code: str, message: str) -> None:
|
|
28
|
+
self.add("error", code, message)
|
|
29
|
+
|
|
30
|
+
def warn(self, code: str, message: str) -> None:
|
|
31
|
+
self.add("warning", code, message)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def errors(self) -> int:
|
|
35
|
+
return sum(item["level"] == "error" for item in self.items)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def warnings(self) -> int:
|
|
39
|
+
return sum(item["level"] == "warning" for item in self.items)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_text(path: Path, report: Report) -> str:
|
|
43
|
+
try:
|
|
44
|
+
return path.read_text(encoding="utf-8-sig")
|
|
45
|
+
except OSError as exc:
|
|
46
|
+
report.error("read_failed", f"Could not read prompt: {exc}")
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_card(path: Path | None, report: Report) -> dict[str, Any] | None:
|
|
51
|
+
if path is None:
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
data = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
55
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
56
|
+
report.error("invalid_shot_card", f"Could not load shot card: {exc}")
|
|
57
|
+
return None
|
|
58
|
+
if not isinstance(data, dict):
|
|
59
|
+
report.error("invalid_shot_card", "Shot card must contain a JSON object.")
|
|
60
|
+
return None
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def expected_tags(card: dict[str, Any] | None) -> set[str]:
|
|
65
|
+
if not card:
|
|
66
|
+
return set()
|
|
67
|
+
tags: set[str] = set()
|
|
68
|
+
for asset in card.get("active_assets", []):
|
|
69
|
+
if isinstance(asset, dict) and isinstance(asset.get("tag"), str):
|
|
70
|
+
tags.add(asset["tag"])
|
|
71
|
+
return tags
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_adapter(
|
|
75
|
+
prompt: str, card: dict[str, Any] | None, model: str, report: Report
|
|
76
|
+
) -> None:
|
|
77
|
+
if model != "seedance-2.5":
|
|
78
|
+
return
|
|
79
|
+
if len(prompt) > 30_000:
|
|
80
|
+
report.error(
|
|
81
|
+
"seedance_prompt_length",
|
|
82
|
+
f"Prompt has {len(prompt)} characters; Seedance 2.5 adapter ceiling is 30,000.",
|
|
83
|
+
)
|
|
84
|
+
shot_numbers = [int(value) for value in SHOT_RE.findall(prompt)]
|
|
85
|
+
if len(shot_numbers) > 30:
|
|
86
|
+
report.error(
|
|
87
|
+
"seedance_shot_count",
|
|
88
|
+
f"Prompt contains {len(shot_numbers)} shot/segment headings; adapter ceiling is 30.",
|
|
89
|
+
)
|
|
90
|
+
if card:
|
|
91
|
+
duration = card.get("duration_seconds")
|
|
92
|
+
if isinstance(duration, (int, float)) and not 4 <= duration <= 30:
|
|
93
|
+
report.error(
|
|
94
|
+
"seedance_duration",
|
|
95
|
+
f"Shot-card duration is {duration}s; Seedance 2.5 adapter expects 4-30s.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_contradictions(prompt: str, report: Report) -> None:
|
|
100
|
+
continuous = re.search(
|
|
101
|
+
r"(?i)\b(one|single)\s+(?:unbroken\s+)?continuous\s+take\b|\bno\s+cuts?\b",
|
|
102
|
+
prompt,
|
|
103
|
+
)
|
|
104
|
+
cuts = re.search(r"(?i)\b(hard|match|smash|jump|whip)\s+cut\b|\bhard\s+cuts\b", prompt)
|
|
105
|
+
if continuous and cuts:
|
|
106
|
+
report.error(
|
|
107
|
+
"continuous_take_cut_conflict",
|
|
108
|
+
"Prompt requests a continuous/no-cut take and also specifies an editorial cut.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
no_music = re.search(r"(?i)\b(?:no|without)\s+(?:source\s+)?music\b", prompt)
|
|
112
|
+
positive_music = re.search(
|
|
113
|
+
r"(?i)\b(?:music|score|song)\s+(?:plays|continues|swells|throughout)\b|"
|
|
114
|
+
r"@[Aa]udio\s+\d+\s+(?:plays|continues)",
|
|
115
|
+
prompt,
|
|
116
|
+
)
|
|
117
|
+
if no_music and positive_music:
|
|
118
|
+
report.error(
|
|
119
|
+
"music_policy_conflict",
|
|
120
|
+
"Prompt both forbids music and directs music or a song to play.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
realtime = re.search(r"(?i)\breal[- ]time\b|\bnormal\s+speed\b", prompt)
|
|
124
|
+
slow_motion = re.search(r"(?i)\bslow[- ]motion\b|\bslow[- ]mo\b", prompt)
|
|
125
|
+
if realtime and slow_motion:
|
|
126
|
+
report.warn(
|
|
127
|
+
"speed_policy_conflict",
|
|
128
|
+
"Prompt contains both real-time/normal-speed and slow-motion instructions; scope them explicitly.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_stale_language(prompt: str, report: Report) -> None:
|
|
133
|
+
patterns = [
|
|
134
|
+
r"(?i)\bsame\s+as\s+(?:before|above|previous)\b",
|
|
135
|
+
r"(?i)\bas\s+(?:before|above)\b",
|
|
136
|
+
r"(?i)\bcontinues?\s+from\s+(?:the\s+)?previous\s+shot\b",
|
|
137
|
+
r"(?i)\bunchanged\s+from\s+(?:the\s+)?previous\b",
|
|
138
|
+
]
|
|
139
|
+
if any(re.search(pattern, prompt) for pattern in patterns):
|
|
140
|
+
report.warn(
|
|
141
|
+
"implicit_continuity",
|
|
142
|
+
"Replace stale phrases such as 'same as before' with explicit visible state for this generation island.",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_tags(prompt: str, card: dict[str, Any] | None, report: Report) -> tuple[set[str], set[str]]:
|
|
147
|
+
found = {value.rstrip(".,;:!?") for value in TAG_RE.findall(prompt)}
|
|
148
|
+
expected = expected_tags(card)
|
|
149
|
+
for tag in sorted(expected - found):
|
|
150
|
+
report.error("missing_active_asset", f"Shot-card asset is absent from prompt: {tag}")
|
|
151
|
+
for tag in sorted(found - expected):
|
|
152
|
+
if expected and not re.fullmatch(r"@[Ii]mage\s+\d+|@[Aa]udio\s+\d+|@[Vv]ideo\s+\d+", tag):
|
|
153
|
+
report.warn(
|
|
154
|
+
"unregistered_prompt_asset",
|
|
155
|
+
f"Prompt tag is not listed in shot-card active_assets: {tag}",
|
|
156
|
+
)
|
|
157
|
+
if len(found) > 50:
|
|
158
|
+
report.error(
|
|
159
|
+
"reference_count",
|
|
160
|
+
f"Prompt has {len(found)} distinct references; current Kolbo Seedance contract ceiling is 50.",
|
|
161
|
+
)
|
|
162
|
+
return found, expected
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_timing(prompt: str, card: dict[str, Any] | None, report: Report) -> None:
|
|
166
|
+
duration = card.get("duration_seconds") if card else None
|
|
167
|
+
for start_text, end_text in RANGE_RE.findall(prompt):
|
|
168
|
+
start, end = float(start_text), float(end_text)
|
|
169
|
+
if end <= start:
|
|
170
|
+
report.error("invalid_time_range", f"Prompt time range {start:g}s-{end:g}s is not increasing.")
|
|
171
|
+
if isinstance(duration, (int, float)) and end > duration:
|
|
172
|
+
report.error(
|
|
173
|
+
"timecode_overflow",
|
|
174
|
+
f"Prompt time range ends at {end:g}s, beyond shot-card duration {duration:g}s.",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check_dialogue_card(card: dict[str, Any] | None, report: Report) -> None:
|
|
179
|
+
if not card:
|
|
180
|
+
return
|
|
181
|
+
dialogue = card.get("dialogue", [])
|
|
182
|
+
if not isinstance(dialogue, list):
|
|
183
|
+
report.error("invalid_dialogue", "Shot-card dialogue must be a list.")
|
|
184
|
+
return
|
|
185
|
+
intervals: list[tuple[float, float, int, dict[str, Any]]] = []
|
|
186
|
+
for index, item in enumerate(dialogue):
|
|
187
|
+
if not isinstance(item, dict):
|
|
188
|
+
report.error("invalid_dialogue", f"Dialogue item {index} must be an object.")
|
|
189
|
+
continue
|
|
190
|
+
start, end = item.get("start_seconds"), item.get("end_seconds")
|
|
191
|
+
if not isinstance(start, (int, float)) or not isinstance(end, (int, float)):
|
|
192
|
+
report.error("invalid_dialogue_time", f"Dialogue item {index} needs numeric start/end seconds.")
|
|
193
|
+
continue
|
|
194
|
+
intervals.append((float(start), float(end), index, item))
|
|
195
|
+
intervals.sort()
|
|
196
|
+
for previous, current in zip(intervals, intervals[1:]):
|
|
197
|
+
if current[0] < previous[1] and not (
|
|
198
|
+
previous[3].get("allow_overlap") or current[3].get("allow_overlap")
|
|
199
|
+
):
|
|
200
|
+
report.error(
|
|
201
|
+
"dialogue_overlap",
|
|
202
|
+
f"Dialogue items {previous[2]} and {current[2]} overlap without allow_overlap.",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main() -> int:
|
|
207
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
208
|
+
parser.add_argument("prompt", type=Path, help="Compiled prompt text file")
|
|
209
|
+
parser.add_argument("--shot-card", type=Path, help="Shot card JSON used to compile the prompt")
|
|
210
|
+
parser.add_argument("--model", default="seedance-2.5", help="Model adapter name")
|
|
211
|
+
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
|
212
|
+
args = parser.parse_args()
|
|
213
|
+
|
|
214
|
+
report = Report()
|
|
215
|
+
prompt = load_text(args.prompt, report)
|
|
216
|
+
card = load_card(args.shot_card, report)
|
|
217
|
+
check_adapter(prompt, card, args.model, report)
|
|
218
|
+
check_contradictions(prompt, report)
|
|
219
|
+
check_stale_language(prompt, report)
|
|
220
|
+
found, expected = check_tags(prompt, card, report)
|
|
221
|
+
check_timing(prompt, card, report)
|
|
222
|
+
check_dialogue_card(card, report)
|
|
223
|
+
|
|
224
|
+
result = {
|
|
225
|
+
"prompt": str(args.prompt.resolve()),
|
|
226
|
+
"model": args.model,
|
|
227
|
+
"valid": report.errors == 0,
|
|
228
|
+
"characters": len(prompt),
|
|
229
|
+
"references_found": sorted(found),
|
|
230
|
+
"references_expected": sorted(expected),
|
|
231
|
+
"errors": report.errors,
|
|
232
|
+
"warnings": report.warnings,
|
|
233
|
+
"items": report.items,
|
|
234
|
+
}
|
|
235
|
+
if args.json:
|
|
236
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
237
|
+
else:
|
|
238
|
+
print(f"Prompt: {result['prompt']}")
|
|
239
|
+
print(
|
|
240
|
+
f"Model: {args.model} | {len(prompt)} chars | {len(found)} reference(s)"
|
|
241
|
+
)
|
|
242
|
+
for item in report.items:
|
|
243
|
+
print(f"[{item['level'].upper()}] {item['code']} - {item['message']}")
|
|
244
|
+
print(f"Result: {report.errors} error(s), {report.warnings} warning(s).")
|
|
245
|
+
return 1 if report.errors else 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
raise SystemExit(main())
|