@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,21 +0,0 @@
1
- {
2
- "coverage_scope": {
3
- "mode": "pickup | alternate",
4
- "master_motion_id": "MOTION-<master-id>",
5
- "supplements_motion_ids": [
6
- "MOTION-<same-file-record-id>"
7
- ],
8
- "source_obligations": [
9
- {
10
- "kind": "action | reaction | dialogue | reveal | directive | end_boundary",
11
- "source_ref": {
12
- "src": "screenplay-index",
13
- "record_id": "BLK-<EP>-<SC>-<kind><nn>"
14
- },
15
- "disposition": "covered_now | retained_in_master | separate_pickup | requires_storyboard_revision",
16
- "motion_field": "/<current-motion-field-or-none>"
17
- }
18
- ],
19
- "replacement_intent": "does_not_replace_master | requests_supersession"
20
- }
21
- }
@@ -1,2 +0,0 @@
1
- {"record_type": "sources", "schema_version": "1.0.0", "sources": {"shots": {"owner": "short-drama-storyboard", "artifact": "剧集/EP001/storyboard/shots.jsonl"}, "keyframes": {"owner": "short-drama-storyboard", "artifact": "剧集/EP001/storyboard/keyframes.jsonl"}}}
2
- {"example_only": true, "motion_id": "MOTION-EP001-LAST", "status": "candidate", "authority": "candidate", "shot_ref": {"src": "shots", "record_id": "SHOT-EP001-LAST", "authority": "candidate"}, "keyframe_ref": {"src": "keyframes", "record_id": "KEY-EP001-LAST", "authority": "candidate"}, "next_start_locator": {"owner": "short-drama-storyboard", "artifact": "剧集/EP002/storyboard/shots.jsonl", "selector": {"record_id": "SHOT-EP002-FIRST", "field": "/start_boundary"}, "status": "future_until_episode_materializes", "access": "comparison_only"}, "rule": "next_start_locator is not an ArtifactRef and cannot satisfy accepted continuity until resolved"}
@@ -1,38 +0,0 @@
1
- {
2
- "performance_arcs": [
3
- {
4
- "actor_ref": {
5
- "src": "characters",
6
- "record_id": "CHAR-<id>"
7
- },
8
- "trigger_ref": {
9
- "src": "screenplay-index",
10
- "record_id": "BLK-<EP>-<SC>-<kind><nn>"
11
- },
12
- "agenda": "<本镜需要读出的当前策略;无增益时删除>",
13
- "receive": "<角色怎样接收触发;无内在处理时删除>",
14
- "mask": "<确有掩饰时才保留>",
15
- "visible_leak": "<当前景别可读的目光/尾音/手部/距离信号;没有则删除>",
16
- "choice": "<说、做或刻意不做的方向性选择>",
17
- "landing": "<选择怎样落到 accepted end>"
18
- }
19
- ],
20
- "attention_handoffs": [
21
- {
22
- "from_ref": {
23
- "src": "<注意归属方产物的 sources key>",
24
- "record_id": "<exact-attention-owner-record-id>"
25
- },
26
- "trigger_ref": {
27
- "src": "screenplay-index",
28
- "record_id": "BLK-<EP>-<SC>-<kind><nn>"
29
- },
30
- "to_ref": {
31
- "src": "<注意归属方产物的 sources key>",
32
- "record_id": "<exact-attention-owner-record-id>"
33
- },
34
- "signal": "<使注意转移的可见或可听信号>",
35
- "readability": "<当前景别、遮挡和声源为何读得到>"
36
- }
37
- ]
38
- }
@@ -1,2 +0,0 @@
1
- {"record_type": "sources", "schema_version": "1.0.0", "sources": {"screenplay": {"owner": "short-drama-write", "artifact": "剧集/EP001/screenplay.md"}}}
2
- {"music_id": "MUS-EP001-01", "scope": {"episode_id": "EP001", "start_seconds": 0, "end_seconds": 12}, "source_refs": [{"src": "screenplay", "record_id": "EP001-SC001"}], "narrative_function": "在开场压低情绪,只在人物做出决定后抬升,不替代表演。", "prompt": "Instrumental restrained urban drama score, low pulse, sparse piano, muted strings, controlled tension, no triumphant release.", "mode": "instrumental", "lyrics": null, "mix_intent": {"entry": "fade in under room tone after the first visible action", "exit": "resolve before the final spoken line", "duck_under_dialogue": true, "loop": false}, "status": "candidate"}
@@ -1,329 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Reconcile an episode's delivery containers against its shot set (`VID-15`).
3
-
4
- Every container can be correct on its own while the episode is wrong: a shot
5
- packed into two containers bills its seconds twice, and a shot packed into none
6
- disappears although its dialogue, bindings, and keyframe prompts are already
7
- done. Neither error is visible from inside a single container, so the check is
8
- a set comparison at episode scope.
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 sys
18
- from pathlib import Path
19
- from typing import Any
20
-
21
-
22
- # Creators run these scripts on whatever interpreter their machine provides, so
23
- # an unsupported version must say so instead of failing inside an import.
24
- MINIMUM_PYTHON = (3, 9)
25
- if sys.version_info < MINIMUM_PYTHON:
26
- raise SystemExit(
27
- "short-drama needs Python {}.{} or newer; this interpreter is {}.{}".format(
28
- *MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor
29
- )
30
- )
31
-
32
- SCHEMA_VERSION = "1.0.0"
33
- # A .jsonl file opens with a header record declaring the upstream snapshots its
34
- # references name. The header is a declaration, not one of the file's records.
35
- SOURCES_RECORD_TYPE = "sources"
36
-
37
-
38
- class CheckError(ValueError):
39
- """The inputs cannot be checked at all, as opposed to failing a check."""
40
-
41
-
42
- def _load_jsonl(path: Path) -> list[dict[str, Any]]:
43
- records: list[dict[str, Any]] = []
44
- try:
45
- text = path.read_text(encoding="utf-8")
46
- except (OSError, UnicodeError) as error:
47
- raise CheckError(f"unreadable JSONL: {path}") from error
48
- for number, line in enumerate(text.splitlines(), start=1):
49
- if not line.strip():
50
- continue
51
- try:
52
- record = json.loads(line)
53
- except json.JSONDecodeError as error:
54
- raise CheckError(f"invalid JSONL at {path.name}:{number}") from error
55
- if not isinstance(record, dict):
56
- raise CheckError(f"JSONL needs one object per line: {path.name}:{number}")
57
- records.append(record)
58
- if records and records[0].get("record_type") == SOURCES_RECORD_TYPE:
59
- del records[0]
60
- return records
61
-
62
-
63
- def _finding(code: str, message: str, **detail: Any) -> dict[str, Any]:
64
- return {"code": code, "message": message, **detail}
65
-
66
-
67
- def _seconds(value: Any) -> float | None:
68
- if isinstance(value, bool) or not isinstance(value, (int, float)):
69
- return None
70
- return float(value)
71
-
72
-
73
- # The three conclusions `delivery-container.jsonl.md` requires a container to
74
- # state about why its members belong together.
75
- MEMBERSHIP_BASIS_KEYS = (
76
- "source_order_contiguous",
77
- "binding_chain_equal",
78
- "scene_boundary_not_crossed",
79
- )
80
-
81
-
82
- def _member_shot_id(member: Any) -> str | None:
83
- if not isinstance(member, dict):
84
- return None
85
- ref = member.get("shot_ref")
86
- if isinstance(ref, dict) and isinstance(ref.get("record_id"), str):
87
- return ref["record_id"]
88
- return None
89
-
90
-
91
- def reconcile(
92
- containers: list[dict[str, Any]],
93
- shots: list[dict[str, Any]],
94
- ) -> dict[str, Any]:
95
- findings: list[dict[str, Any]] = []
96
- durations = {
97
- shot["shot_id"]: _seconds(shot.get("duration_seconds"))
98
- for shot in shots
99
- if isinstance(shot.get("shot_id"), str)
100
- }
101
- episode_shots = set(durations)
102
-
103
- owner_of: dict[str, str] = {}
104
- packed: set[str] = set()
105
- container_total = 0.0
106
- for container in containers:
107
- container_id = container.get("container_id")
108
- if not isinstance(container_id, str):
109
- findings.append(
110
- _finding("VID15_CONTAINER_HAS_NO_ID", "a container record has no id")
111
- )
112
- continue
113
- members = container.get("members")
114
- if not isinstance(members, list) or not members:
115
- findings.append(
116
- _finding(
117
- "VID15_CONTAINER_HAS_NO_MEMBERS",
118
- "a container carries no members",
119
- container_id=container_id,
120
- )
121
- )
122
- continue
123
- # Checkpoint 1: `order` unique, contiguous and ascending. A delivery
124
- # package is assembled in this order, so a repeated or skipped number is
125
- # an ambiguous cut list, not a cosmetic flaw.
126
- orders = [
127
- member.get("order") for member in members if isinstance(member, dict)
128
- ]
129
- if not all(
130
- isinstance(value, int) and not isinstance(value, bool) for value in orders
131
- ) or orders != list(range(1, len(members) + 1)):
132
- findings.append(
133
- _finding(
134
- "VID15_MEMBER_ORDER_IS_NOT_A_SEQUENCE",
135
- "members must carry order 1..n, ascending and without gaps",
136
- container_id=container_id,
137
- orders=orders,
138
- )
139
- )
140
-
141
- # Checkpoint 7: the three membership_basis conclusions. Each is a claim
142
- # about why these shots belong in one container; leaving one blank makes
143
- # the container's own justification unreadable.
144
- basis = container.get("membership_basis")
145
- if not isinstance(basis, dict):
146
- findings.append(
147
- _finding(
148
- "VID15_MEMBERSHIP_BASIS_MISSING",
149
- "a container must record why its members belong together",
150
- container_id=container_id,
151
- )
152
- )
153
- else:
154
- blank = sorted(
155
- key
156
- for key in MEMBERSHIP_BASIS_KEYS
157
- if not str(basis.get(key) or "").strip()
158
- )
159
- if blank:
160
- findings.append(
161
- _finding(
162
- "VID15_MEMBERSHIP_BASIS_INCOMPLETE",
163
- "every membership_basis conclusion must be stated",
164
- container_id=container_id,
165
- missing=blank,
166
- )
167
- )
168
-
169
- member_total = 0.0
170
- for member in members:
171
- shot_id = _member_shot_id(member)
172
- if shot_id is None:
173
- findings.append(
174
- _finding(
175
- "VID15_MEMBER_HAS_NO_SHOT_REF",
176
- "a member does not name the shot it packs",
177
- container_id=container_id,
178
- )
179
- )
180
- continue
181
- if shot_id not in episode_shots:
182
- findings.append(
183
- _finding(
184
- "VID15_MEMBER_IS_NOT_AN_EPISODE_SHOT",
185
- "a container packs a shot that is not in this episode",
186
- container_id=container_id,
187
- shot_id=shot_id,
188
- )
189
- )
190
- continue
191
- previous = owner_of.get(shot_id)
192
- if previous is not None:
193
- findings.append(
194
- _finding(
195
- "VID15_SHOT_PACKED_TWICE",
196
- "a shot belongs to more than one container",
197
- shot_id=shot_id,
198
- container_ids=sorted({previous, container_id}),
199
- )
200
- )
201
- continue
202
- owner_of[shot_id] = container_id
203
- packed.add(shot_id)
204
- seconds = durations.get(shot_id)
205
- if seconds is None:
206
- findings.append(
207
- _finding(
208
- "VID15_MEMBER_SHOT_HAS_NO_DURATION",
209
- "a packed shot carries no numeric duration",
210
- container_id=container_id,
211
- shot_id=shot_id,
212
- )
213
- )
214
- continue
215
-
216
- # Checkpoint 3: the member's own `accepted_duration` against the
217
- # shot it projects. This field was never read: the total was summed
218
- # straight from `shots.jsonl`, so a member could claim 12.0s for a
219
- # 6.3s shot and pass, and the execution end would be handed the 12.0.
220
- # `motion_timing_check` guards exactly this staleness class; this
221
- # file had no equivalent.
222
- claimed = _seconds(member.get("accepted_duration"))
223
- if claimed is None:
224
- findings.append(
225
- _finding(
226
- "VID15_MEMBER_HAS_NO_ACCEPTED_DURATION",
227
- "a member must project the duration it packs",
228
- container_id=container_id,
229
- shot_id=shot_id,
230
- )
231
- )
232
- elif abs(claimed - seconds) > 1e-6:
233
- findings.append(
234
- _finding(
235
- "VID15_MEMBER_DURATION_IS_STALE",
236
- "a member's accepted_duration does not match its shot",
237
- container_id=container_id,
238
- shot_id=shot_id,
239
- claimed=claimed,
240
- accepted=seconds,
241
- )
242
- )
243
- member_total += seconds
244
- stated = _seconds(container.get("container_duration"))
245
- if stated is None:
246
- findings.append(
247
- _finding(
248
- "VID15_CONTAINER_DURATION_MISSING",
249
- "container_duration must be present and a number of seconds",
250
- container_id=container_id,
251
- )
252
- )
253
- elif abs(stated - member_total) > 1e-6:
254
- findings.append(
255
- _finding(
256
- "VID15_CONTAINER_DURATION_IS_NOT_THE_SUM",
257
- "container_duration does not equal its members' durations",
258
- container_id=container_id,
259
- stated=stated,
260
- computed=round(member_total, 6),
261
- )
262
- )
263
- container_total += member_total
264
-
265
- loose = sorted(episode_shots - packed)
266
- # A shot whose duration is still open is a legal state upstream, so it is
267
- # held out of the arithmetic instead of being reported as an error. Only a
268
- # shot packed into a container must already have one, because the
269
- # container's own duration claim depends on it.
270
- unmeasured = sorted(
271
- shot_id for shot_id in episode_shots if durations.get(shot_id) is None
272
- )
273
- loose_total = sum(
274
- durations[shot_id] or 0.0 for shot_id in loose if durations.get(shot_id) is not None
275
- )
276
-
277
- episode_total = sum(value for value in durations.values() if value is not None)
278
- if abs((container_total + loose_total) - episode_total) > 1e-6:
279
- findings.append(
280
- _finding(
281
- "VID15_EPISODE_TOTAL_DOES_NOT_RECONCILE",
282
- "containers plus loose shots do not add up to the episode total",
283
- packed_seconds=round(container_total, 6),
284
- loose_seconds=round(loose_total, 6),
285
- episode_seconds=round(episode_total, 6),
286
- )
287
- )
288
-
289
- return {
290
- "schema_version": SCHEMA_VERSION,
291
- "containers": len(containers),
292
- "episode_shots": len(episode_shots),
293
- "packed_shots": len(packed),
294
- # Loose shots are legal: containers need not cover everything. They are
295
- # reported so the count is a decision rather than an oversight.
296
- "loose_shots": loose,
297
- # Reported, not a finding: these shots are excluded from every total
298
- # above, so a caller can tell an incomplete episode from a wrong one.
299
- "unmeasured_shots": unmeasured,
300
- "packed_seconds": round(container_total, 6),
301
- "loose_seconds": round(loose_total, 6),
302
- "episode_seconds": round(episode_total, 6),
303
- "findings": findings,
304
- "status": "pass" if not findings else "fail",
305
- }
306
-
307
-
308
- def build_parser() -> argparse.ArgumentParser:
309
- parser = argparse.ArgumentParser(
310
- description="Reconcile delivery containers with the episode's shot set."
311
- )
312
- parser.add_argument("containers", type=Path, help="the episode containers JSONL")
313
- parser.add_argument("--shots", type=Path, required=True)
314
- return parser
315
-
316
-
317
- def main(argv: list[str] | None = None) -> int:
318
- args = build_parser().parse_args(argv)
319
- try:
320
- result = reconcile(_load_jsonl(args.containers), _load_jsonl(args.shots))
321
- except CheckError as error:
322
- print(f"{type(error).__name__}: {error}", file=sys.stderr)
323
- return 2
324
- print(json.dumps(result, ensure_ascii=True, sort_keys=True))
325
- return 0 if result["status"] == "pass" else 1
326
-
327
-
328
- if __name__ == "__main__":
329
- raise SystemExit(main())