@oh-story/dsh 0.1.18 → 0.1.19
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/drama/manifest.json +77 -247
- package/lib/drama/skills/short-drama/SKILL.md +9 -1
- package/lib/drama/skills/short-drama/references/creator-documents.md +51 -23
- package/lib/drama/skills/short-drama/references/reference-roles.md +1 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +442 -129
- package/lib/drama/skills/short-drama/scripts/project_tool.py +54 -1
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -6
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +9 -3
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +3 -3
- package/lib/drama/skills/short-drama-assets/references/location-and-view.md +4 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +2 -2
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +19 -4
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +1 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +1 -1
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +28 -6
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +1 -5
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +0 -4
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +9 -9
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +43 -13
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +5 -5
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +5 -5
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +27 -3
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +6 -8
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +16 -1
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +23 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +2 -2
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -2
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +7 -3
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +1 -1
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +94 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +13 -4
- package/lib/drama/skills/short-drama-write/scripts/selftest.py +3 -77
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/novel-to-game/README.md +20 -34
- package/lib/novel-to-game/README_ZH.md +20 -34
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +2 -6
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +33 -39
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +2 -2
- package/lib/novel-to-game/manifest.json +46 -46
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +1 -1
- package/lib/novel-to-game/skills/game-build/references/baked-sprite-workflow.md +2 -3
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +6 -7
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +2 -2
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +1 -4
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +1 -3
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +1 -3
- package/lib/novel-to-game/skills/game-concept/SKILL.md +1 -1
- package/lib/novel-to-game/skills/game-qa/SKILL.md +3 -4
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +1 -1
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +3 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +13 -14
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +1 -2
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +2 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +0 -8
- package/lib/oh-story/manifest.json +49 -49
- package/lib/oh-story/skills/story/SKILL.md +2 -2
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/SKILL.md +5 -5
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-tracking.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +2 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -2
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-review/SKILL.md +8 -7
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +7 -7
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -2
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-setup/SKILL.md +6 -6
- package/lib/oh-story/skills/story-short-write/SKILL.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +1 -1
- package/package.json +21 -21
- package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +0 -4
- package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +0 -5
- package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +0 -5
- package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +0 -2
- package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +0 -2
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +0 -267
- package/lib/drama/skills/short-drama-assets/scripts/selftest.py +0 -89
- package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +0 -2
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +0 -443
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +0 -95
- package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +0 -2
- package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +0 -42
- package/lib/drama/skills/short-drama-review/assets/verdict-template.json +0 -32
- package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +0 -2
- package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +0 -34
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +0 -325
- package/lib/drama/skills/short-drama-review/scripts/selftest.py +0 -87
- package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +0 -12
- package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +0 -191
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +0 -824
- package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +0 -21
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +0 -38
- package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +0 -2
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +0 -329
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +0 -451
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +0 -327
- package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +0 -82
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +0 -416
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Prove a voice record sheet is still a projection of the screenplay.
|
|
3
|
-
|
|
4
|
-
The sheet exists to be carried into a recording session, which is exactly the
|
|
5
|
-
moment nobody can check it against the script. A line edited in the sheet, or a
|
|
6
|
-
script revised after the sheet was built, both read as a perfectly ordinary
|
|
7
|
-
sheet — so the comparison has to be mechanical: resolve each line's block in
|
|
8
|
-
the derived index, slice those exact bytes out of the screenplay, and compare.
|
|
9
|
-
|
|
10
|
-
The script reads accepted creator files and writes nothing.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import argparse
|
|
16
|
-
import json
|
|
17
|
-
import re
|
|
18
|
-
import sys
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
from collections.abc import Mapping
|
|
21
|
-
from typing import Any, NamedTuple
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Creators run these scripts on whatever interpreter their machine provides, so
|
|
25
|
-
# an unsupported version must say so instead of failing inside an import.
|
|
26
|
-
MINIMUM_PYTHON = (3, 9)
|
|
27
|
-
if sys.version_info < MINIMUM_PYTHON:
|
|
28
|
-
raise SystemExit(
|
|
29
|
-
"short-drama needs Python {}.{} or newer; this interpreter is {}.{}".format(
|
|
30
|
-
*MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor
|
|
31
|
-
)
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
# REFERENCE RESOLVER -- reference implementation.
|
|
36
|
-
#
|
|
37
|
-
# Each skill checker carries its own copy of this block. The suite has no shared
|
|
38
|
-
# library on purpose: a skill must stay runnable after copying only its own
|
|
39
|
-
# directory, so duplicating these few lines across skills is the correct shape.
|
|
40
|
-
# Copy the block verbatim; do not import it.
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
SOURCES_RECORD_TYPE = "sources"
|
|
44
|
-
SOURCES_SCHEMA_VERSION = "1.0.0"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class ResolvedRef(NamedTuple):
|
|
48
|
-
"""An upstream reference with its snapshot resolved, whichever form it used."""
|
|
49
|
-
|
|
50
|
-
owner: str
|
|
51
|
-
artifact: str
|
|
52
|
-
record_id: str | None
|
|
53
|
-
field: str | None
|
|
54
|
-
authority: str | None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class RefFinding(NamedTuple):
|
|
58
|
-
"""A structural defect in a reference object."""
|
|
59
|
-
|
|
60
|
-
code: str
|
|
61
|
-
location: str
|
|
62
|
-
detail: str
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def load_sources(document: Any) -> dict[str, dict[str, Any]]:
|
|
66
|
-
"""Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
|
|
67
|
-
|
|
68
|
-
Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
|
|
69
|
-
``.jsonl`` file, whose declaration lives on the first record.
|
|
70
|
-
"""
|
|
71
|
-
if isinstance(document, list):
|
|
72
|
-
document = document[0] if document else None
|
|
73
|
-
if not isinstance(document, dict):
|
|
74
|
-
return {}
|
|
75
|
-
declared = document.get("sources")
|
|
76
|
-
if not isinstance(declared, dict):
|
|
77
|
-
return {}
|
|
78
|
-
return {key: value for key, value in declared.items() if isinstance(value, dict)}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def resolve_ref(
|
|
82
|
-
ref: Any, sources: dict[str, dict[str, Any]], location: str
|
|
83
|
-
) -> tuple[ResolvedRef | None, RefFinding | None]:
|
|
84
|
-
"""Resolve a reference object written in either the compact or expanded form."""
|
|
85
|
-
if not isinstance(ref, dict):
|
|
86
|
-
return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
|
|
87
|
-
src = ref.get("src")
|
|
88
|
-
if isinstance(src, str):
|
|
89
|
-
entry = sources.get(src)
|
|
90
|
-
if entry is None:
|
|
91
|
-
return None, RefFinding(
|
|
92
|
-
"REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
|
|
93
|
-
)
|
|
94
|
-
owner, artifact = entry.get("owner"), entry.get("artifact")
|
|
95
|
-
if not (isinstance(owner, str) and isinstance(artifact, str)):
|
|
96
|
-
return None, RefFinding(
|
|
97
|
-
"SOURCE_ENTRY_IS_INCOMPLETE",
|
|
98
|
-
location,
|
|
99
|
-
f"sources[{src!r}] needs owner/artifact",
|
|
100
|
-
)
|
|
101
|
-
elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
|
|
102
|
-
owner, artifact = ref["owner"], ref["artifact"]
|
|
103
|
-
else:
|
|
104
|
-
return None, RefFinding(
|
|
105
|
-
"REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
|
|
106
|
-
)
|
|
107
|
-
optional = {
|
|
108
|
-
key: ref[key]
|
|
109
|
-
for key in ("record_id", "field", "authority")
|
|
110
|
-
if isinstance(ref.get(key), str)
|
|
111
|
-
}
|
|
112
|
-
return (
|
|
113
|
-
ResolvedRef(
|
|
114
|
-
owner,
|
|
115
|
-
artifact,
|
|
116
|
-
optional.get("record_id"),
|
|
117
|
-
optional.get("field"),
|
|
118
|
-
optional.get("authority"),
|
|
119
|
-
),
|
|
120
|
-
None,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# ---------------------------------------------------------------------------
|
|
125
|
-
# END REFERENCE RESOLVER
|
|
126
|
-
# ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
SCHEMA_VERSION = "1.0.0"
|
|
129
|
-
CHANNELS = {"sync", "dubbed", "VO", "OS"}
|
|
130
|
-
# `[VO]` and `[OS]` are spoken lines that happen to be delivered off-camera, and
|
|
131
|
-
# the index already records them with a `tag` and a `speaker`. Projecting only
|
|
132
|
-
# `dialogue` blocks made the VO and OS channels above unreachable, so an episode
|
|
133
|
-
# that carries its interiority in voice-over reported a clean sheet while the
|
|
134
|
-
# recording list was missing every one of those lines.
|
|
135
|
-
VOICED_TAGS = {"VO", "OS"}
|
|
136
|
-
# `[VO] 角色:台词` — the tag is stripped before the line grammar is applied.
|
|
137
|
-
VOICE_TAG_PREFIX = re.compile(r"^\[(?:VO|OS)\]\s*")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _is_voiced(block: Mapping[str, Any]) -> bool:
|
|
141
|
-
kind = block.get("kind")
|
|
142
|
-
if kind == "dialogue":
|
|
143
|
-
return True
|
|
144
|
-
return kind == "production_tag" and block.get("tag") in VOICED_TAGS
|
|
145
|
-
# The resolver speaks the suite-wide reference vocabulary; this checker reports
|
|
146
|
-
# in its own.
|
|
147
|
-
REF_FINDING_CODES = {
|
|
148
|
-
"REF_IS_NOT_AN_OBJECT": "VOICE_SOURCE_REF_MISSING",
|
|
149
|
-
"REF_HAS_NO_UPSTREAM_BINDING": "VOICE_SOURCE_REF_MISSING",
|
|
150
|
-
"REF_SRC_IS_NOT_DECLARED": "VOICE_SOURCE_REF_UNDECLARED",
|
|
151
|
-
"SOURCE_ENTRY_IS_INCOMPLETE": "VOICE_SOURCE_REF_UNDECLARED",
|
|
152
|
-
}
|
|
153
|
-
# `角色(可表演提示):台词` — the cue is optional and never part of the line.
|
|
154
|
-
DIALOGUE = re.compile(
|
|
155
|
-
r"^(?P<speaker>[^((::]+)(?:[((](?P<cue>[^))]*)[))])?\s*[::]\s*(?P<line>.*)$",
|
|
156
|
-
re.DOTALL,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class CheckError(ValueError):
|
|
161
|
-
"""The inputs cannot be checked at all, as opposed to failing a check."""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _load_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
165
|
-
records: list[dict[str, Any]] = []
|
|
166
|
-
try:
|
|
167
|
-
text = path.read_text(encoding="utf-8")
|
|
168
|
-
except (OSError, UnicodeError) as error:
|
|
169
|
-
raise CheckError(f"unreadable JSONL: {path}") from error
|
|
170
|
-
for number, line in enumerate(text.splitlines(), start=1):
|
|
171
|
-
if not line.strip():
|
|
172
|
-
continue
|
|
173
|
-
try:
|
|
174
|
-
record = json.loads(line)
|
|
175
|
-
except json.JSONDecodeError as error:
|
|
176
|
-
raise CheckError(f"invalid JSONL at {path.name}:{number}") from error
|
|
177
|
-
if not isinstance(record, dict):
|
|
178
|
-
raise CheckError(f"JSONL needs one object per line: {path.name}:{number}")
|
|
179
|
-
records.append(record)
|
|
180
|
-
return records
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def _finding(code: str, message: str, **detail: Any) -> dict[str, Any]:
|
|
184
|
-
return {"code": code, "message": message, **detail}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _blocks_by_id(index: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
188
|
-
return {
|
|
189
|
-
record["block_id"]: record
|
|
190
|
-
for record in index
|
|
191
|
-
if record.get("record_type") == "block"
|
|
192
|
-
and isinstance(record.get("block_id"), str)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def check(
|
|
197
|
-
sheet: list[dict[str, Any]],
|
|
198
|
-
index: list[dict[str, Any]],
|
|
199
|
-
screenplay: bytes,
|
|
200
|
-
) -> dict[str, Any]:
|
|
201
|
-
findings: list[dict[str, Any]] = []
|
|
202
|
-
blocks = _blocks_by_id(index)
|
|
203
|
-
seen: set[str] = set()
|
|
204
|
-
# The first record declares the upstream snapshots this sheet references; the
|
|
205
|
-
# rest are voice lines.
|
|
206
|
-
sources = load_sources(sheet)
|
|
207
|
-
lines = [record for record in sheet if record.get("record_type") != SOURCES_RECORD_TYPE]
|
|
208
|
-
|
|
209
|
-
for record in lines:
|
|
210
|
-
line_id = record.get("line_id")
|
|
211
|
-
if not isinstance(line_id, str):
|
|
212
|
-
findings.append(_finding("VOICE_LINE_HAS_NO_ID", "a line record has no id"))
|
|
213
|
-
continue
|
|
214
|
-
if line_id in seen:
|
|
215
|
-
findings.append(
|
|
216
|
-
_finding("VOICE_LINE_ID_REPEATS", "line_id must be unique", line_id=line_id)
|
|
217
|
-
)
|
|
218
|
-
continue
|
|
219
|
-
seen.add(line_id)
|
|
220
|
-
|
|
221
|
-
channel = record.get("channel")
|
|
222
|
-
if channel not in CHANNELS:
|
|
223
|
-
findings.append(
|
|
224
|
-
_finding(
|
|
225
|
-
"VOICE_CHANNEL_INVALID",
|
|
226
|
-
"channel must be sync, dubbed, VO, or OS",
|
|
227
|
-
line_id=line_id,
|
|
228
|
-
channel=channel,
|
|
229
|
-
)
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
resolved, defect = resolve_ref(record.get("source_ref"), sources, line_id)
|
|
233
|
-
if defect is not None:
|
|
234
|
-
findings.append(
|
|
235
|
-
_finding(
|
|
236
|
-
REF_FINDING_CODES[defect.code],
|
|
237
|
-
f"source_ref does not name an upstream snapshot: {defect.detail}",
|
|
238
|
-
line_id=line_id,
|
|
239
|
-
)
|
|
240
|
-
)
|
|
241
|
-
continue
|
|
242
|
-
if resolved is None or resolved.record_id is None:
|
|
243
|
-
findings.append(
|
|
244
|
-
_finding(
|
|
245
|
-
"VOICE_SOURCE_REF_MISSING",
|
|
246
|
-
"a line must bind the screenplay block it projects",
|
|
247
|
-
line_id=line_id,
|
|
248
|
-
)
|
|
249
|
-
)
|
|
250
|
-
continue
|
|
251
|
-
record_id = resolved.record_id
|
|
252
|
-
block = blocks.get(record_id)
|
|
253
|
-
if block is None:
|
|
254
|
-
findings.append(
|
|
255
|
-
_finding(
|
|
256
|
-
"VOICE_SOURCE_REF_UNRESOLVABLE",
|
|
257
|
-
"source_ref names a block that is not in the index",
|
|
258
|
-
line_id=line_id,
|
|
259
|
-
record_id=record_id,
|
|
260
|
-
)
|
|
261
|
-
)
|
|
262
|
-
continue
|
|
263
|
-
if not _is_voiced(block):
|
|
264
|
-
findings.append(
|
|
265
|
-
_finding(
|
|
266
|
-
"VOICE_SOURCE_IS_NOT_DIALOGUE",
|
|
267
|
-
"a voice line must project a spoken block: dialogue, [VO] or [OS]",
|
|
268
|
-
line_id=line_id,
|
|
269
|
-
record_id=record_id,
|
|
270
|
-
kind=block.get("kind"),
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
continue
|
|
274
|
-
|
|
275
|
-
start, end = block.get("byte_start"), block.get("byte_end")
|
|
276
|
-
if (
|
|
277
|
-
not isinstance(start, int)
|
|
278
|
-
or not isinstance(end, int)
|
|
279
|
-
or not 0 <= start < end <= len(screenplay)
|
|
280
|
-
):
|
|
281
|
-
findings.append(
|
|
282
|
-
_finding(
|
|
283
|
-
"VOICE_BLOCK_SPAN_INVALID",
|
|
284
|
-
"the indexed block span does not fit this screenplay",
|
|
285
|
-
line_id=line_id,
|
|
286
|
-
record_id=record_id,
|
|
287
|
-
)
|
|
288
|
-
)
|
|
289
|
-
continue
|
|
290
|
-
# The channel decides how the line is booked, and off-camera and
|
|
291
|
-
# on-camera carry completely different room for change. It was checked
|
|
292
|
-
# against the enum and never against the block it projects, so a [VO]
|
|
293
|
-
# line could be booked `sync` and go to the booth under lip-sync
|
|
294
|
-
# constraints. `_is_voiced` already reads the block's tag; this compares.
|
|
295
|
-
expected = block.get("tag") if block.get("kind") == "production_tag" else "on_camera"
|
|
296
|
-
if expected in VOICED_TAGS and channel not in {expected, "dubbed"}:
|
|
297
|
-
findings.append(
|
|
298
|
-
_finding(
|
|
299
|
-
"VOICE_CHANNEL_DISAGREES_WITH_BLOCK",
|
|
300
|
-
"an off-camera block must be booked on its own channel",
|
|
301
|
-
line_id=line_id,
|
|
302
|
-
record_id=record_id,
|
|
303
|
-
channel=channel,
|
|
304
|
-
tag=expected,
|
|
305
|
-
)
|
|
306
|
-
)
|
|
307
|
-
elif expected == "on_camera" and channel in VOICED_TAGS:
|
|
308
|
-
findings.append(
|
|
309
|
-
_finding(
|
|
310
|
-
"VOICE_CHANNEL_DISAGREES_WITH_BLOCK",
|
|
311
|
-
"an on-camera dialogue block must not be booked as VO or OS",
|
|
312
|
-
line_id=line_id,
|
|
313
|
-
record_id=record_id,
|
|
314
|
-
channel=channel,
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
raw = screenplay[start:end]
|
|
319
|
-
try:
|
|
320
|
-
decoded = raw.decode("utf-8")
|
|
321
|
-
except UnicodeDecodeError:
|
|
322
|
-
# A span that begins or ends mid-character is a stale offset. It was
|
|
323
|
-
# crashing the whole check instead of reporting the one line.
|
|
324
|
-
findings.append(
|
|
325
|
-
_finding(
|
|
326
|
-
"VOICE_BLOCK_SPAN_INVALID",
|
|
327
|
-
"the indexed block span does not start and end on characters",
|
|
328
|
-
line_id=line_id,
|
|
329
|
-
record_id=record_id,
|
|
330
|
-
)
|
|
331
|
-
)
|
|
332
|
-
continue
|
|
333
|
-
body = VOICE_TAG_PREFIX.sub("", decoded.strip())
|
|
334
|
-
match = DIALOGUE.match(body)
|
|
335
|
-
if match is None:
|
|
336
|
-
findings.append(
|
|
337
|
-
_finding(
|
|
338
|
-
"VOICE_BLOCK_IS_UNPARSEABLE",
|
|
339
|
-
"the dialogue block does not use the documented line grammar",
|
|
340
|
-
line_id=line_id,
|
|
341
|
-
record_id=record_id,
|
|
342
|
-
)
|
|
343
|
-
)
|
|
344
|
-
continue
|
|
345
|
-
if record.get("line_text") != match.group("line"):
|
|
346
|
-
findings.append(
|
|
347
|
-
_finding(
|
|
348
|
-
"VOICE_LINE_TEXT_DIVERGED",
|
|
349
|
-
"line_text is not the screenplay wording; change the screenplay",
|
|
350
|
-
line_id=line_id,
|
|
351
|
-
record_id=record_id,
|
|
352
|
-
)
|
|
353
|
-
)
|
|
354
|
-
indexed_speaker = block.get("speaker")
|
|
355
|
-
if isinstance(indexed_speaker, str) and record.get("speaker_display") not in (
|
|
356
|
-
None,
|
|
357
|
-
indexed_speaker,
|
|
358
|
-
):
|
|
359
|
-
findings.append(
|
|
360
|
-
_finding(
|
|
361
|
-
"VOICE_SPEAKER_DIVERGED",
|
|
362
|
-
"speaker_display disagrees with the indexed speaker",
|
|
363
|
-
line_id=line_id,
|
|
364
|
-
record_id=record_id,
|
|
365
|
-
)
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
dialogue_blocks = {
|
|
369
|
-
block_id for block_id, block in blocks.items() if _is_voiced(block)
|
|
370
|
-
}
|
|
371
|
-
projected: set[str] = set()
|
|
372
|
-
for record in lines:
|
|
373
|
-
covered, _defect = resolve_ref(record.get("source_ref"), sources, "")
|
|
374
|
-
if covered is not None and covered.record_id is not None:
|
|
375
|
-
projected.add(covered.record_id)
|
|
376
|
-
# Reported, never a finding: a sheet may legitimately cover one actor or one
|
|
377
|
-
# scene, so an incomplete sheet is a scope decision, not a defect.
|
|
378
|
-
uncovered = sorted(dialogue_blocks - projected)
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
"schema_version": SCHEMA_VERSION,
|
|
382
|
-
"lines": len(lines),
|
|
383
|
-
"dialogue_blocks": len(dialogue_blocks),
|
|
384
|
-
"uncovered_dialogue_blocks": uncovered,
|
|
385
|
-
"findings": findings,
|
|
386
|
-
"status": "pass" if not findings else "fail",
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
391
|
-
parser = argparse.ArgumentParser(
|
|
392
|
-
description="Check a voice record sheet against its screenplay and index."
|
|
393
|
-
)
|
|
394
|
-
parser.add_argument("sheet", type=Path, help="the voice record sheet JSONL")
|
|
395
|
-
parser.add_argument("--index", type=Path, required=True)
|
|
396
|
-
parser.add_argument("--screenplay", type=Path, required=True)
|
|
397
|
-
return parser
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def main(argv: list[str] | None = None) -> int:
|
|
401
|
-
args = build_parser().parse_args(argv)
|
|
402
|
-
try:
|
|
403
|
-
result = check(
|
|
404
|
-
_load_jsonl(args.sheet),
|
|
405
|
-
_load_jsonl(args.index),
|
|
406
|
-
args.screenplay.read_bytes(),
|
|
407
|
-
)
|
|
408
|
-
except (CheckError, OSError) as error:
|
|
409
|
-
print(f"{type(error).__name__}: {error}", file=sys.stderr)
|
|
410
|
-
return 2
|
|
411
|
-
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
|
412
|
-
return 0 if result["status"] == "pass" else 1
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if __name__ == "__main__":
|
|
416
|
-
raise SystemExit(main())
|