@oh-story/dsh 0.1.17 → 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.
Files changed (136) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +147 -96
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +77 -247
  5. package/lib/drama/skills/short-drama/SKILL.md +9 -1
  6. package/lib/drama/skills/short-drama/references/creator-documents.md +51 -23
  7. package/lib/drama/skills/short-drama/references/reference-roles.md +1 -1
  8. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +442 -129
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +54 -1
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -6
  11. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +9 -3
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +3 -3
  13. package/lib/drama/skills/short-drama-assets/references/location-and-view.md +4 -0
  14. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +2 -2
  15. package/lib/drama/skills/short-drama-develop/references/episode-design.md +19 -4
  16. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +1 -1
  17. package/lib/drama/skills/short-drama-edit/SKILL.md +1 -1
  18. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +28 -6
  19. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +1 -5
  21. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  22. package/lib/drama/skills/short-drama-produce/SKILL.md +2 -0
  23. package/lib/drama/skills/short-drama-review/SKILL.md +0 -4
  24. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +1 -1
  25. package/lib/drama/skills/short-drama-storyboard/SKILL.md +9 -9
  26. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +43 -13
  27. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +5 -5
  28. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +5 -5
  29. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +27 -3
  30. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -1
  31. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +6 -8
  32. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +16 -1
  33. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +23 -6
  34. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +2 -2
  35. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -1
  36. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -2
  37. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +1 -1
  38. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -1
  39. package/lib/drama/skills/short-drama-write/SKILL.md +7 -3
  40. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +1 -1
  41. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +94 -1
  42. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +13 -4
  43. package/lib/drama/skills/short-drama-write/scripts/selftest.py +3 -77
  44. package/lib/index.js +18 -14
  45. package/lib/index.js.map +2 -2
  46. package/lib/novel-to-game/README.md +20 -34
  47. package/lib/novel-to-game/README_ZH.md +20 -34
  48. package/lib/novel-to-game/VERSION +1 -1
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +2 -6
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +33 -39
  51. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +2 -2
  52. package/lib/novel-to-game/manifest.json +46 -46
  53. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +1 -1
  54. package/lib/novel-to-game/skills/game-build/references/baked-sprite-workflow.md +2 -3
  55. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +6 -7
  56. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +2 -2
  57. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +1 -4
  58. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +1 -3
  59. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +1 -3
  60. package/lib/novel-to-game/skills/game-concept/SKILL.md +1 -1
  61. package/lib/novel-to-game/skills/game-qa/SKILL.md +3 -4
  62. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +1 -1
  63. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +3 -2
  64. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +13 -14
  65. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +1 -2
  66. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +4 -4
  67. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +2 -3
  68. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +0 -8
  69. package/lib/oh-story/manifest.json +49 -49
  70. package/lib/oh-story/skills/story/SKILL.md +2 -2
  71. package/lib/oh-story/skills/story/VERSION +1 -1
  72. package/lib/oh-story/skills/story-deslop/SKILL.md +2 -2
  73. package/lib/oh-story/skills/story-import/SKILL.md +5 -5
  74. package/lib/oh-story/skills/story-import/references/deep-analysis.md +1 -1
  75. package/lib/oh-story/skills/story-import/references/import-report.md +1 -1
  76. package/lib/oh-story/skills/story-import/references/import-tracking.md +1 -1
  77. package/lib/oh-story/skills/story-import/references/outline-reverse.md +1 -1
  78. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +6 -1
  79. package/lib/oh-story/skills/story-long-analyze/SKILL.md +2 -2
  80. package/lib/oh-story/skills/story-long-write/SKILL.md +2 -2
  81. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +3 -3
  82. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -2
  83. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +6 -1
  84. package/lib/oh-story/skills/story-review/SKILL.md +8 -7
  85. package/lib/oh-story/skills/story-review/references/agent-prompts.md +7 -7
  86. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -2
  87. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +6 -1
  88. package/lib/oh-story/skills/story-setup/SKILL.md +6 -6
  89. package/lib/oh-story/skills/story-short-write/SKILL.md +2 -2
  90. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +3 -3
  91. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +2 -2
  92. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +1 -1
  93. package/lib/types/client/game-preview-state.d.ts +34 -0
  94. package/lib/types/client/game-preview-state.d.ts.map +1 -0
  95. package/lib/types/client/index.d.ts.map +1 -1
  96. package/lib/types/client/video-studio.d.ts +24 -0
  97. package/lib/types/client/video-studio.d.ts.map +1 -1
  98. package/lib/types/host-python.d.ts +10 -3
  99. package/lib/types/host-python.d.ts.map +1 -1
  100. package/lib/types/workspace-route.d.ts +6 -0
  101. package/lib/types/workspace-route.d.ts.map +1 -1
  102. package/package.json +21 -21
  103. package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +0 -3
  104. package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +0 -4
  105. package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +0 -5
  106. package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +0 -3
  107. package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +0 -5
  108. package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +0 -3
  109. package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +0 -2
  110. package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +0 -2
  111. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +0 -267
  112. package/lib/drama/skills/short-drama-assets/scripts/selftest.py +0 -89
  113. package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +0 -2
  114. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +0 -443
  115. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +0 -95
  116. package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +0 -2
  117. package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +0 -42
  118. package/lib/drama/skills/short-drama-review/assets/verdict-template.json +0 -32
  119. package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +0 -2
  120. package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +0 -34
  121. package/lib/drama/skills/short-drama-review/scripts/review_check.py +0 -325
  122. package/lib/drama/skills/short-drama-review/scripts/selftest.py +0 -87
  123. package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +0 -2
  124. package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +0 -12
  125. package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +0 -2
  126. package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +0 -191
  127. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +0 -824
  128. package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +0 -21
  129. package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +0 -2
  130. package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +0 -38
  131. package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +0 -2
  132. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +0 -329
  133. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +0 -451
  134. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +0 -327
  135. package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +0 -82
  136. 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())