@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.
- package/README.md +14 -14
- package/lib/client.js +147 -96
- package/lib/client.js.map +3 -3
- 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 +18 -14
- package/lib/index.js.map +2 -2
- 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/lib/types/client/game-preview-state.d.ts +34 -0
- package/lib/types/client/game-preview-state.d.ts.map +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/video-studio.d.ts +24 -0
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/host-python.d.ts +10 -3
- package/lib/types/host-python.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts +6 -0
- package/lib/types/workspace-route.d.ts.map +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,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())
|