@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,327 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Validate provider-neutral, timeline-level short-drama music specs."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import math
9
- import re
10
- import sys
11
- from pathlib import Path
12
- from typing import Any, NamedTuple
13
-
14
- MINIMUM_PYTHON = (3, 9)
15
- if sys.version_info < MINIMUM_PYTHON:
16
- raise SystemExit("music_spec_check.py requires Python 3.9 or newer")
17
-
18
-
19
- # ---------------------------------------------------------------------------
20
- # REFERENCE RESOLVER
21
- #
22
- # Each skill carries its own copy of this block. A skill must stay runnable
23
- # after copying only its own directory, so the suite has no shared library and
24
- # this file imports nothing from outside its own skill.
25
- # ---------------------------------------------------------------------------
26
-
27
- SOURCES_RECORD_TYPE = "sources"
28
- SOURCES_SCHEMA_VERSION = "1.0.0"
29
-
30
-
31
- class ResolvedRef(NamedTuple):
32
- """An upstream reference with its snapshot resolved, whichever form it used."""
33
-
34
- owner: str
35
- artifact: str
36
- record_id: str | None
37
- field: str | None
38
- authority: str | None
39
-
40
-
41
- class RefFinding(NamedTuple):
42
- """A structural defect in a reference object."""
43
-
44
- code: str
45
- location: str
46
- detail: str
47
-
48
-
49
- def load_sources(document: Any) -> dict[str, dict[str, Any]]:
50
- """Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
51
-
52
- Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
53
- ``.jsonl`` file, whose declaration lives on the first record.
54
- """
55
- if isinstance(document, list):
56
- document = document[0] if document else None
57
- if not isinstance(document, dict):
58
- return {}
59
- declared = document.get("sources")
60
- if not isinstance(declared, dict):
61
- return {}
62
- return {key: value for key, value in declared.items() if isinstance(value, dict)}
63
-
64
-
65
- def resolve_ref(
66
- ref: Any, sources: dict[str, dict[str, Any]], location: str
67
- ) -> tuple[ResolvedRef | None, RefFinding | None]:
68
- """Resolve a reference object written in either the compact or expanded form."""
69
- if not isinstance(ref, dict):
70
- return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
71
- src = ref.get("src")
72
- if isinstance(src, str):
73
- entry = sources.get(src)
74
- if entry is None:
75
- return None, RefFinding(
76
- "REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
77
- )
78
- owner, artifact = entry.get("owner"), entry.get("artifact")
79
- if not (isinstance(owner, str) and isinstance(artifact, str)):
80
- return None, RefFinding(
81
- "SOURCE_ENTRY_IS_INCOMPLETE",
82
- location,
83
- f"sources[{src!r}] needs owner/artifact",
84
- )
85
- elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
86
- owner, artifact = ref["owner"], ref["artifact"]
87
- else:
88
- return None, RefFinding(
89
- "REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
90
- )
91
- optional = {
92
- key: ref[key]
93
- for key in ("record_id", "field", "authority")
94
- if isinstance(ref.get(key), str)
95
- }
96
- return (
97
- ResolvedRef(
98
- owner,
99
- artifact,
100
- optional.get("record_id"),
101
- optional.get("field"),
102
- optional.get("authority"),
103
- ),
104
- None,
105
- )
106
-
107
-
108
- # ---------------------------------------------------------------------------
109
- # END REFERENCE RESOLVER
110
- # ---------------------------------------------------------------------------
111
-
112
- SKILL_ROOT = Path(__file__).resolve().parents[1]
113
- HASH_RE = re.compile(r"[0-9a-f]{64}")
114
- VENDOR_FIELDS = {
115
- "provider",
116
- "model",
117
- "endpoint",
118
- "api_key",
119
- "task_id",
120
- "remote_id",
121
- "callback_url",
122
- }
123
- TOP_LEVEL_FIELDS = {
124
- "music_id",
125
- "scope",
126
- "source_refs",
127
- "narrative_function",
128
- "prompt",
129
- "mode",
130
- "lyrics",
131
- "mix_intent",
132
- "status",
133
- }
134
- SCOPE_FIELDS = {"episode_id", "start_seconds", "end_seconds"}
135
- REFERENCE_FIELDS = {"src", "owner", "artifact", "record_id", "field", "authority"}
136
- SOURCES_HEADER_FIELDS = {"record_type", "schema_version", "sources"}
137
- SOURCE_ENTRY_FIELDS = {"owner", "artifact"}
138
- MIX_FIELDS = {"entry", "exit", "duck_under_dialogue", "loop"}
139
- MUSIC_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,79}")
140
- EPISODE_ID_RE = re.compile(r"EP(?:[0-9]{3}|[1-9][0-9]{3,})")
141
-
142
-
143
- class ValidationError(ValueError):
144
- """A music spec cannot be handed to production safely."""
145
-
146
-
147
- def resolve_input(value: str | Path) -> Path:
148
- path = Path(value).expanduser()
149
- if path.exists() or path.is_absolute():
150
- return path
151
- return SKILL_ROOT / path
152
-
153
-
154
- def load_jsonl(value: str | Path) -> list[dict[str, Any]]:
155
- path = resolve_input(value)
156
- records: list[dict[str, Any]] = []
157
- for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
158
- if not line.strip():
159
- continue
160
- try:
161
- record = json.loads(line)
162
- except json.JSONDecodeError as exc:
163
- raise ValidationError(f"{path}:{number}: invalid JSON") from exc
164
- if not isinstance(record, dict):
165
- raise ValidationError(f"{path}:{number}: each record must be an object")
166
- records.append(record)
167
- if not records:
168
- raise ValidationError(f"{path}: no music specs")
169
- return records
170
-
171
-
172
- def _text(value: object, label: str, *, maximum: int = 10_000) -> str:
173
- if not isinstance(value, str) or not value.strip() or len(value) > maximum:
174
- raise ValidationError(f"{label} must be non-empty text up to {maximum} characters")
175
- return value
176
-
177
-
178
- def _number(value: object, label: str) -> float:
179
- if isinstance(value, bool) or not isinstance(value, (int, float)):
180
- raise ValidationError(f"{label} must be a number")
181
- number = float(value)
182
- if not math.isfinite(number):
183
- raise ValidationError(f"{label} must be finite")
184
- return number
185
-
186
-
187
- def _exact_fields(value: dict[str, Any], allowed: set[str], label: str) -> None:
188
- unknown = sorted(set(value) - allowed)
189
- if unknown:
190
- raise ValidationError(f"{label} has unsupported fields: {', '.join(unknown)}")
191
-
192
-
193
- def _reject_vendor_fields(value: object, label: str) -> None:
194
- if isinstance(value, dict):
195
- leaked = sorted(str(key) for key in value if str(key).casefold() in VENDOR_FIELDS)
196
- if leaked:
197
- raise ValidationError(f"{label}: provider execution fields are forbidden: {', '.join(leaked)}")
198
- for key, child in value.items():
199
- _reject_vendor_fields(child, f"{label}.{key}")
200
- elif isinstance(value, list):
201
- for index, child in enumerate(value):
202
- _reject_vendor_fields(child, f"{label}[{index}]")
203
-
204
-
205
- def _validate_ref(value: object, label: str, sources: dict[str, dict[str, Any]]) -> None:
206
- if not isinstance(value, dict):
207
- raise ValidationError(f"{label} must be a reference object")
208
- _exact_fields(value, REFERENCE_FIELDS, label)
209
- resolved, finding = resolve_ref(value, sources, label)
210
- if finding is not None:
211
- raise ValidationError(f"{finding.location}: {finding.code}: {finding.detail}")
212
- if resolved is None:
213
- raise ValidationError(f"{label} could not be resolved")
214
- for key, text in (
215
- ("owner", resolved.owner),
216
- ("artifact", resolved.artifact),
217
- ):
218
- _text(text, f"{label}.{key}")
219
- if not isinstance(value.get("record_id") or value.get("field"), str):
220
- raise ValidationError(f"{label} needs record_id or field")
221
-
222
-
223
- def split_sources_header(
224
- records: list[dict[str, Any]],
225
- ) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]:
226
- """Split a leading ``sources`` header off the file's own records.
227
-
228
- The header declares each upstream snapshot once, so a reference only names
229
- the snapshot key and the record it points at.
230
- """
231
- if not records or records[0].get("record_type") != SOURCES_RECORD_TYPE:
232
- return {}, list(records)
233
- header, *specs = records
234
- _exact_fields(header, SOURCES_HEADER_FIELDS, "sources header")
235
- if header.get("schema_version") != SOURCES_SCHEMA_VERSION:
236
- raise ValidationError(f"sources header schema_version must be {SOURCES_SCHEMA_VERSION}")
237
- declared = header.get("sources")
238
- if not isinstance(declared, dict) or not declared:
239
- raise ValidationError("sources header must declare at least one source")
240
- for key, entry in declared.items():
241
- label = f"sources[{key!r}]"
242
- if not isinstance(entry, dict):
243
- raise ValidationError(f"{label} must be an object")
244
- _exact_fields(entry, SOURCE_ENTRY_FIELDS, label)
245
- for field in sorted(SOURCE_ENTRY_FIELDS):
246
- _text(entry.get(field), f"{label}.{field}")
247
- return load_sources(header), specs
248
-
249
-
250
- def validate_records(records: list[dict[str, Any]]) -> dict[str, Any]:
251
- sources, specs = split_sources_header(records)
252
- if not specs:
253
- raise ValidationError("no music specs")
254
- identifiers: set[str] = set()
255
- for index, record in enumerate(specs, 1):
256
- label = f"music[{index}]"
257
- _reject_vendor_fields(record, label)
258
- _exact_fields(record, TOP_LEVEL_FIELDS, label)
259
- music_id = _text(record.get("music_id"), f"{label}.music_id", maximum=80)
260
- if MUSIC_ID_RE.fullmatch(music_id) is None:
261
- raise ValidationError(f"{label}.music_id must be a portable identifier")
262
- if music_id in identifiers:
263
- raise ValidationError(f"duplicate music_id: {music_id}")
264
- identifiers.add(music_id)
265
-
266
- scope = record.get("scope")
267
- if not isinstance(scope, dict):
268
- raise ValidationError(f"{label}.scope must be an object")
269
- _exact_fields(scope, SCOPE_FIELDS, f"{label}.scope")
270
- episode_id = _text(
271
- scope.get("episode_id"), f"{label}.scope.episode_id", maximum=40
272
- )
273
- if EPISODE_ID_RE.fullmatch(episode_id) is None:
274
- raise ValidationError(f"{label}.scope.episode_id is invalid")
275
- start = _number(scope.get("start_seconds"), f"{label}.scope.start_seconds")
276
- end = _number(scope.get("end_seconds"), f"{label}.scope.end_seconds")
277
- if start < 0 or end <= start:
278
- raise ValidationError(f"{label}.scope must have 0 <= start < end")
279
-
280
- refs = record.get("source_refs")
281
- if not isinstance(refs, list) or not refs:
282
- raise ValidationError(f"{label}.source_refs must be a non-empty list")
283
- for ref_index, ref in enumerate(refs, 1):
284
- _validate_ref(ref, f"{label}.source_refs[{ref_index}]", sources)
285
-
286
- _text(record.get("narrative_function"), f"{label}.narrative_function")
287
- _text(record.get("prompt"), f"{label}.prompt", maximum=2_000)
288
- mode = record.get("mode")
289
- if mode not in {"instrumental", "song"}:
290
- raise ValidationError(f"{label}.mode must be instrumental or song")
291
- lyrics = record.get("lyrics")
292
- if mode == "instrumental" and lyrics not in {None, ""}:
293
- raise ValidationError(f"{label}: instrumental music must not carry lyrics")
294
- if mode == "song":
295
- _text(lyrics, f"{label}.lyrics", maximum=3_500)
296
-
297
- mix = record.get("mix_intent")
298
- if not isinstance(mix, dict):
299
- raise ValidationError(f"{label}.mix_intent must be an object")
300
- _exact_fields(mix, MIX_FIELDS, f"{label}.mix_intent")
301
- if not isinstance(mix.get("duck_under_dialogue"), bool):
302
- raise ValidationError(f"{label}.mix_intent.duck_under_dialogue must be boolean")
303
- if not isinstance(mix.get("loop"), bool):
304
- raise ValidationError(f"{label}.mix_intent.loop must be boolean")
305
- _text(mix.get("entry"), f"{label}.mix_intent.entry")
306
- _text(mix.get("exit"), f"{label}.mix_intent.exit")
307
- if record.get("status") not in {"candidate", "accepted", "revise"}:
308
- raise ValidationError(f"{label}.status must be candidate, accepted, or revise")
309
-
310
- return {"status": "valid", "music_specs": len(specs)}
311
-
312
-
313
- def main(argv: list[str] | None = None) -> int:
314
- parser = argparse.ArgumentParser(description=__doc__)
315
- parser.add_argument("specs")
316
- args = parser.parse_args(argv)
317
- try:
318
- result = validate_records(load_jsonl(args.specs))
319
- except (OSError, UnicodeError, ValidationError) as exc:
320
- print(str(exc), file=sys.stderr)
321
- return 2
322
- print(json.dumps(result, ensure_ascii=True, sort_keys=True))
323
- return 0
324
-
325
-
326
- if __name__ == "__main__":
327
- raise SystemExit(main())
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Offline self-test for standalone video and music prompt tooling."""
3
-
4
- from __future__ import annotations
5
-
6
- import copy
7
- import sys
8
-
9
- from music_spec_check import SKILL_ROOT, ValidationError, load_jsonl, validate_records
10
-
11
- MINIMUM_PYTHON = (3, 9)
12
- if sys.version_info < MINIMUM_PYTHON:
13
- raise SystemExit("selftest.py requires Python 3.9 or newer")
14
-
15
-
16
- def require(condition: bool, message: str) -> None:
17
- if not condition:
18
- raise AssertionError(message)
19
-
20
-
21
- def fail(records: list[dict], marker: str) -> None:
22
- try:
23
- validate_records(records)
24
- except ValidationError as exc:
25
- require(marker in str(exc), f"expected {marker!r}, got {exc!s}")
26
- else:
27
- raise AssertionError(f"expected failure containing {marker!r}")
28
-
29
-
30
- def main() -> int:
31
- records = load_jsonl(SKILL_ROOT / "examples/minimal-music-specs.jsonl")
32
- require(validate_records(records)["music_specs"] == 1, "valid fixture count")
33
- header, spec = records[0], records[1]
34
- require(spec["source_refs"][0]["src"] == "screenplay", "fixture uses a source key")
35
-
36
- # A file that was written before sources declarations existed still carries
37
- # the snapshot on every reference, and still validates.
38
- expanded = copy.deepcopy(spec)
39
- expanded["source_refs"] = [
40
- {**header["sources"]["screenplay"], "record_id": "EP001-SC001"}
41
- ]
42
- require(validate_records([expanded])["music_specs"] == 1, "inline snapshot accepted")
43
-
44
- undeclared = copy.deepcopy(spec)
45
- undeclared["source_refs"] = [{"src": "screenplai", "record_id": "EP001-SC001"}]
46
- fail([header, undeclared], "REF_SRC_IS_NOT_DECLARED")
47
-
48
- unbound = copy.deepcopy(spec)
49
- unbound["source_refs"] = [{"record_id": "EP001-SC001"}]
50
- fail([header, unbound], "REF_HAS_NO_UPSTREAM_BINDING")
51
-
52
- duplicate = [header, spec, copy.deepcopy(spec)]
53
- fail(duplicate, "duplicate music_id")
54
-
55
- leaked = [header, copy.deepcopy(spec)]
56
- leaked[1]["model"] = "example"
57
- fail(leaked, "provider execution fields")
58
-
59
- song_without_lyrics = [header, copy.deepcopy(spec)]
60
- song_without_lyrics[1]["mode"] = "song"
61
- fail(song_without_lyrics, "lyrics")
62
-
63
- invalid_scope = [header, copy.deepcopy(spec)]
64
- invalid_scope[1]["scope"]["end_seconds"] = 0
65
- fail(invalid_scope, "0 <= start < end")
66
-
67
- unsupported = [header, copy.deepcopy(spec)]
68
- unsupported[1]["token"] = "not provider-neutral"
69
- fail(unsupported, "unsupported fields")
70
-
71
- # A source entry now names an upstream artifact and nothing else; a stray
72
- # byte digest is an unsupported field rather than a malformed hash.
73
- stale_source = [copy.deepcopy(header), spec]
74
- stale_source[0]["sources"]["screenplay"]["hash"] = "not-a-sha256"
75
- fail(stale_source, "unsupported fields")
76
-
77
- print("10 self-tests passed")
78
- return 0
79
-
80
-
81
- if __name__ == "__main__":
82
- raise SystemExit(main())