@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,443 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Validate standalone image-prompt specs without generating media."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import re
9
- import sys
10
- from pathlib import Path
11
- from typing import Any, NamedTuple
12
-
13
- MINIMUM_PYTHON = (3, 9)
14
- if sys.version_info < MINIMUM_PYTHON:
15
- raise SystemExit("image_prompt_check.py requires Python 3.9 or newer")
16
-
17
- # ---------------------------------------------------------------------------
18
- # REFERENCE RESOLVER -- reference implementation.
19
- #
20
- # Each skill checker carries its own copy of this block. The suite has no shared
21
- # library on purpose: a skill must stay runnable after copying only its own
22
- # directory, so duplicating these few lines across skills is the correct shape.
23
- # Copy the block verbatim; do not import it.
24
- # ---------------------------------------------------------------------------
25
-
26
- SOURCES_RECORD_TYPE = "sources"
27
- SOURCES_SCHEMA_VERSION = "1.0.0"
28
-
29
-
30
- class ResolvedRef(NamedTuple):
31
- """An upstream reference with its snapshot resolved, whichever form it used."""
32
-
33
- owner: str
34
- artifact: str
35
- record_id: str | None
36
- field: str | None
37
- authority: str | None
38
-
39
-
40
- class RefFinding(NamedTuple):
41
- """A structural defect in a reference object."""
42
-
43
- code: str
44
- location: str
45
- detail: str
46
-
47
-
48
- def load_sources(document: Any) -> dict[str, dict[str, Any]]:
49
- """Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
50
-
51
- Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
52
- ``.jsonl`` file, whose declaration lives on the first record.
53
- """
54
- if isinstance(document, list):
55
- document = document[0] if document else None
56
- if not isinstance(document, dict):
57
- return {}
58
- declared = document.get("sources")
59
- if not isinstance(declared, dict):
60
- return {}
61
- return {key: value for key, value in declared.items() if isinstance(value, dict)}
62
-
63
-
64
- def resolve_ref(
65
- ref: Any, sources: dict[str, dict[str, Any]], location: str
66
- ) -> tuple[ResolvedRef | None, RefFinding | None]:
67
- """Resolve a reference object written in either the compact or expanded form."""
68
- if not isinstance(ref, dict):
69
- return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
70
- src = ref.get("src")
71
- if isinstance(src, str):
72
- entry = sources.get(src)
73
- if entry is None:
74
- return None, RefFinding(
75
- "REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
76
- )
77
- owner, artifact = entry.get("owner"), entry.get("artifact")
78
- if not (isinstance(owner, str) and isinstance(artifact, str)):
79
- return None, RefFinding(
80
- "SOURCE_ENTRY_IS_INCOMPLETE", location, f"sources[{src!r}] needs owner/artifact"
81
- )
82
- elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
83
- owner, artifact = ref["owner"], ref["artifact"]
84
- else:
85
- return None, RefFinding(
86
- "REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
87
- )
88
- optional = {
89
- key: ref[key] for key in ("record_id", "field", "authority") if isinstance(ref.get(key), str)
90
- }
91
- return (
92
- ResolvedRef(
93
- owner,
94
- artifact,
95
- optional.get("record_id"),
96
- optional.get("field"),
97
- optional.get("authority"),
98
- ),
99
- None,
100
- )
101
-
102
-
103
- # ---------------------------------------------------------------------------
104
- # END REFERENCE RESOLVER
105
- # ---------------------------------------------------------------------------
106
-
107
- SKILL_ROOT = Path(__file__).resolve().parents[1]
108
- HASH_RE = re.compile(r"[0-9a-f]{64}")
109
- # `common-recipe.md` forbids weight syntax and any one engine's control words: a
110
- # generic prompt that carries provider control syntax has stopped being generic.
111
- # This checks syntax only. Whether the prose leans on generic quality language is
112
- # `IMG-02`, a `craft_default` a creator may override with a reason, so it is a
113
- # reviewer's call citing what the description actually lacks — see
114
- # `common-recipe.md`, which forbids blocking delivery on a fixed word list.
115
- ENGINE_SYNTAX_RE = re.compile(
116
- r"(?:^|[\s,,((])--(?:ar|v|q|niji|style|no|seed|cref|sref)\b"
117
- r"|::-?\d"
118
- # Weight syntax is written (x:1.2); prose writes "(aperture: 1.8)" with a space.
119
- r"|:[01]\.\d\s*[))]",
120
- re.IGNORECASE,
121
- )
122
- PURPOSES = {
123
- "character_sheet",
124
- "location_plate",
125
- "prop_plate",
126
- "look_state_variant",
127
- "edit_delta",
128
- "lookdev_frame",
129
- }
130
- VENDOR_FIELDS = {
131
- "authorization",
132
- "credential",
133
- "credentials",
134
- "model",
135
- "model_id",
136
- "model_name",
137
- "provider",
138
- "provider_id",
139
- "api_key",
140
- "task_id",
141
- "remote_id",
142
- "access_token",
143
- "token",
144
- "secret",
145
- "password",
146
- }
147
- NORMALIZED_VENDOR_FIELDS = {re.sub(r"[^a-z0-9]", "", key) for key in VENDOR_FIELDS}
148
-
149
-
150
- # The source policy -> render treatment mapping from `common-recipe.md`. A
151
- # treatment outside its policy's row is a structural defect: the fix is a
152
- # creator override that changes the accepted policy, never a quiet widening here.
153
- ALLOWED_TREATMENTS = {
154
- "exact_readable": {"readable", "postproduction"},
155
- "graphic_only": {"symbolic"},
156
- "no_readable_text": {"blank", "symbolic"},
157
- "pending_creator_text": {"postproduction"},
158
- }
159
-
160
- # A blanket "no text in the image" instruction, however it is phrased. Matching
161
- # two literals (`no text` / `无文字`) let the reference document's own worked
162
- # counter-example through: `画面中不要任何文字` contains neither.
163
- NO_TEXT_CONSTRAINT = re.compile(
164
- r"(无任何(文字|字|文本)|无文字|不要(任何)?(文字|字|文本)|不出现(任何)?(文字|字)"
165
- r"|没有(任何)?(文字|字)|不含(任何)?(文字|字)|禁止(出现)?(文字|字)"
166
- r"|no\s+(visible\s+)?(text|lettering|writing|words|typography)"
167
- r"|without\s+(any\s+)?text|text[-\s]free)",
168
- re.IGNORECASE,
169
- )
170
-
171
-
172
- class ValidationError(ValueError):
173
- pass
174
-
175
-
176
- def resolve_input(value: str | Path) -> Path:
177
- path = Path(value).expanduser()
178
- if path.exists() or path.is_absolute():
179
- return path
180
- return SKILL_ROOT / path
181
-
182
-
183
- def load_jsonl(value: str | Path) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]:
184
- """Return the file's ``sources`` declaration and its prompt-spec records.
185
-
186
- A leading ``{"record_type": "sources", ...}`` header declares the upstream
187
- snapshots the specs point at; it is not a spec and never counts as one.
188
- """
189
- path = resolve_input(value)
190
- records: list[dict[str, Any]] = []
191
- for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
192
- if not line.strip():
193
- continue
194
- try:
195
- record = json.loads(line)
196
- except json.JSONDecodeError as exc:
197
- raise ValidationError(f"{path}:{number}: invalid JSON") from exc
198
- if not isinstance(record, dict):
199
- raise ValidationError(f"{path}:{number}: each record must be an object")
200
- records.append(record)
201
- sources: dict[str, dict[str, Any]] = {}
202
- if records and records[0].get("record_type") == SOURCES_RECORD_TYPE:
203
- header = records.pop(0)
204
- if not isinstance(header.get("sources"), dict):
205
- raise ValidationError(f"{path}: the sources header must declare a sources object")
206
- sources = load_sources(header)
207
- if not records:
208
- raise ValidationError(f"{path}: no prompt specs")
209
- return sources, records
210
-
211
-
212
- def text(record: dict[str, Any], key: str, label: str) -> str:
213
- value = record.get(key)
214
- if not isinstance(value, str) or not value.strip():
215
- raise ValidationError(f"{label}: {key} must be non-empty text")
216
- return value
217
-
218
-
219
- def nonempty_list(record: dict[str, Any], key: str, label: str) -> list[Any]:
220
- value = record.get(key)
221
- if not isinstance(value, list) or not value:
222
- raise ValidationError(f"{label}: {key} must be a non-empty list")
223
- return value
224
-
225
-
226
- def validate_ref(
227
- value: Any,
228
- sources: dict[str, dict[str, Any]],
229
- label: str,
230
- *,
231
- field_allowed: bool = True,
232
- ) -> None:
233
- if not isinstance(value, dict):
234
- raise ValidationError(f"{label}: reference must be an object")
235
- resolved, finding = resolve_ref(value, sources, label)
236
- if finding is not None:
237
- raise ValidationError(f"{label}: {finding.code}: {finding.detail}")
238
- if resolved is None:
239
- raise ValidationError(f"{label}: reference could not be resolved")
240
- if not resolved.owner.strip() or not resolved.artifact.strip():
241
- raise ValidationError(f"{label}: owner and artifact must be non-empty text")
242
- if "record_id" not in value and (not field_allowed or "field" not in value):
243
- raise ValidationError(f"{label}: record_id or field is required")
244
-
245
-
246
- def validate_reference_bindings(
247
- record: dict[str, Any], sources: dict[str, dict[str, Any]], label: str
248
- ) -> None:
249
- bindings = record.get("reference_bindings", [])
250
- if not isinstance(bindings, list):
251
- raise ValidationError(f"{label}: reference_bindings must be a list")
252
- slots: set[str] = set()
253
- orders: set[int] = set()
254
- for index, binding in enumerate(bindings, 1):
255
- item = f"{label}.reference_bindings[{index}]"
256
- if not isinstance(binding, dict):
257
- raise ValidationError(f"{item}: binding must be an object")
258
- slot = text(binding, "slot_id", item)
259
- order = binding.get("order")
260
- if slot in slots:
261
- raise ValidationError(f"{item}: duplicate slot_id {slot}")
262
- if not isinstance(order, int) or order < 1 or order in orders:
263
- raise ValidationError(f"{item}: order must be a unique positive integer")
264
- slots.add(slot)
265
- orders.add(order)
266
- validate_ref(binding.get("artifact_ref"), sources, f"{item}.artifact_ref")
267
- text(binding, "role", item)
268
- nonempty_list(binding, "may_control", item)
269
- nonempty_list(binding, "must_not_control", item)
270
- admission = binding.get("admission_status")
271
- if admission not in {"unverified", "creator_described", "visually_inspected"}:
272
- raise ValidationError(f"{item}: invalid admission_status")
273
- if admission == "unverified" and not binding.get("unresolved_risks"):
274
- raise ValidationError(f"{item}: unverified references need unresolved_risks")
275
-
276
-
277
- def vendor_field_paths(value: object, prefix: str = "") -> list[str]:
278
- leaked: list[str] = []
279
- if isinstance(value, dict):
280
- for key, child in value.items():
281
- name = str(key)
282
- path = f"{prefix}.{name}" if prefix else name
283
- normalized = re.sub(r"[^a-z0-9]", "", name.casefold())
284
- if normalized in NORMALIZED_VENDOR_FIELDS:
285
- leaked.append(path)
286
- leaked.extend(vendor_field_paths(child, path))
287
- elif isinstance(value, list):
288
- for index, child in enumerate(value):
289
- leaked.extend(vendor_field_paths(child, f"{prefix}[{index}]"))
290
- return leaked
291
-
292
-
293
- def validate_asset_spec(
294
- record: dict[str, Any], sources: dict[str, dict[str, Any]], label: str
295
- ) -> None:
296
- binding = record.get("asset_binding")
297
- if not isinstance(binding, dict):
298
- raise ValidationError(f"{label}: asset_binding must be an object")
299
- validate_ref(binding.get("identity_ref"), sources, f"{label}.asset_binding.identity_ref")
300
- validate_ref(binding.get("variant_ref"), sources, f"{label}.asset_binding.variant_ref")
301
- for index, ref in enumerate(nonempty_list(record, "source_refs", label), 1):
302
- validate_ref(ref, sources, f"{label}.source_refs[{index}]")
303
- nonempty_list(record, "identity_or_form_anchors", label)
304
-
305
- if record["purpose"] == "edit_delta":
306
- edit = record.get("edit")
307
- if not isinstance(edit, dict):
308
- raise ValidationError(f"{label}: edit_delta requires edit")
309
- validate_ref(edit.get("target_ref"), sources, f"{label}.edit.target_ref")
310
- nonempty_list(edit, "changes", f"{label}.edit")
311
- nonempty_list(edit, "preserve", f"{label}.edit")
312
- text(edit, "continuity_impact", f"{label}.edit")
313
-
314
- handling = record.get("text_handling")
315
- if isinstance(handling, dict):
316
- if "source_policy_ref" in handling:
317
- validate_ref(
318
- handling.get("source_policy_ref"),
319
- sources,
320
- f"{label}.text_handling.source_policy_ref",
321
- )
322
- treatment = handling.get("render_treatment")
323
- if not isinstance(treatment, dict):
324
- raise ValidationError(f"{label}: text_handling.render_treatment is required")
325
- # `common-recipe.md` gives the source policy -> render treatment mapping
326
- # as a structural check. Nothing implemented it, so a prop whose accepted
327
- # policy is `no_readable_text` could ask for readable lettering and come
328
- # back `valid` -- inventing a name or a number the creator never accepted.
329
- source_mode = handling.get("source_mode")
330
- allowed = (
331
- ALLOWED_TREATMENTS.get(source_mode)
332
- if isinstance(source_mode, str)
333
- else None
334
- )
335
- mode = treatment.get("mode")
336
- if allowed is not None and mode not in allowed:
337
- raise ValidationError(
338
- f"{label}: text policy {source_mode!r} allows "
339
- f"{' or '.join(sorted(allowed))}, not {mode!r}; a creator "
340
- f"override must change the policy rather than the treatment"
341
- )
342
- if source_mode == "exact_readable" and mode == "readable":
343
- text(treatment, "exact_text", f"{label}.text_handling.render_treatment")
344
- negatives = " ".join(str(item) for item in record.get("negative_constraints", []))
345
- if NO_TEXT_CONSTRAINT.search(negatives):
346
- raise ValidationError(f"{label}: readable text conflicts with a no-text constraint")
347
-
348
-
349
- def validate_lookdev_spec(
350
- record: dict[str, Any], sources: dict[str, dict[str, Any]], label: str
351
- ) -> None:
352
- validate_ref(record.get("direction_ref"), sources, f"{label}.direction_ref")
353
- validate_ref(record.get("production_profile_ref"), sources, f"{label}.production_profile_ref")
354
- subjects = nonempty_list(record, "subject_bindings", label)
355
- for index, subject in enumerate(subjects, 1):
356
- if not isinstance(subject, dict):
357
- raise ValidationError(f"{label}.subject_bindings[{index}]: must be an object")
358
- item = f"{label}.subject_bindings[{index}]"
359
- validate_ref(subject.get("identity_ref"), sources, f"{item}.identity_ref")
360
- if "variant_ref" in subject:
361
- validate_ref(subject.get("variant_ref"), sources, f"{item}.variant_ref")
362
- text(subject, "role", item)
363
- text(record, "test_question", label)
364
- nonempty_list(record, "stable_visual_rules", label)
365
- if record.get("lookdev_axis") == "high_pressure_scene":
366
- refs = nonempty_list(record, "story_context_refs", label)
367
- for index, ref in enumerate(refs, 1):
368
- validate_ref(ref, sources, f"{label}.story_context_refs[{index}]")
369
-
370
-
371
- def validate_records(
372
- records: list[dict[str, Any]], sources: dict[str, dict[str, Any]] | None = None
373
- ) -> dict[str, Any]:
374
- sources = sources or {}
375
- identifiers: set[str] = set()
376
- for index, record in enumerate(records, 1):
377
- label = f"spec[{index}]"
378
- spec_id = text(record, "spec_id", label)
379
- if spec_id in identifiers:
380
- raise ValidationError(f"{label}: duplicate spec_id {spec_id}")
381
- identifiers.add(spec_id)
382
- purpose = record.get("purpose")
383
- if purpose not in PURPOSES:
384
- raise ValidationError(
385
- f"{label}: invalid purpose {purpose!r}; "
386
- f"use one of {', '.join(sorted(PURPOSES))}"
387
- )
388
- if record.get("status") not in {"candidate", "accepted"}:
389
- raise ValidationError(f"{label}: status must be candidate or accepted")
390
- leaked = sorted(vendor_field_paths(record))
391
- if leaked:
392
- raise ValidationError(f"{label}: provider execution fields are forbidden: {', '.join(leaked)}")
393
- prompt = text(record, "generic_prompt", label)
394
- if HASH_RE.search(prompt) or "<sha256>" in prompt:
395
- raise ValidationError(f"{label}: generic_prompt leaks internal hashes")
396
- engine_syntax = ENGINE_SYNTAX_RE.search(prompt)
397
- if engine_syntax:
398
- raise ValidationError(
399
- f"{label}: generic_prompt carries engine-specific syntax "
400
- f"{engine_syntax.group(0).strip()!r}; keep it in a provider adapter"
401
- )
402
- validate_reference_bindings(record, sources, label)
403
- if purpose == "lookdev_frame":
404
- validate_lookdev_spec(record, sources, label)
405
- else:
406
- validate_asset_spec(record, sources, label)
407
- return {
408
- "status": "valid",
409
- "specs": len(records),
410
- "sources": len(sources),
411
- "checks": [
412
- "unique_ids",
413
- "accepted_bindings",
414
- # Named for what it does: every reference names a snapshot this
415
- # file declares. Whether that record exists in the target artifact
416
- # is not knowable here -- the checker is handed this file only.
417
- "source_declaration",
418
- "reference_slots",
419
- "prompt_hygiene",
420
- ],
421
- }
422
-
423
-
424
- def validate_file(path: str | Path) -> dict[str, Any]:
425
- sources, records = load_jsonl(path)
426
- return validate_records(records, sources)
427
-
428
-
429
- def main() -> int:
430
- parser = argparse.ArgumentParser(description=__doc__)
431
- parser.add_argument("specs")
432
- args = parser.parse_args()
433
- try:
434
- result = validate_file(args.specs)
435
- except (OSError, ValidationError) as exc:
436
- print(f"image prompt check failed: {exc}", file=sys.stderr)
437
- return 2
438
- print(json.dumps(result, ensure_ascii=True, indent=2))
439
- return 0
440
-
441
-
442
- if __name__ == "__main__":
443
- raise SystemExit(main())
@@ -1,95 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Offline self-test for the standalone image-prompt checker."""
3
-
4
- from __future__ import annotations
5
-
6
- import copy
7
- import sys
8
- from typing import Any
9
-
10
- from image_prompt_check import SKILL_ROOT, ValidationError, load_jsonl, validate_records
11
-
12
- MINIMUM_PYTHON = (3, 9)
13
- if sys.version_info < MINIMUM_PYTHON:
14
- raise SystemExit("selftest.py requires Python 3.9 or newer")
15
-
16
-
17
- def require(condition: bool, message: str) -> None:
18
- if not condition:
19
- raise AssertionError(message)
20
-
21
-
22
- def fail(records: list[dict[str, Any]], sources: dict[str, dict[str, Any]], marker: str) -> None:
23
- try:
24
- validate_records(records, sources)
25
- except ValidationError as exc:
26
- require(marker in str(exc), f"expected {marker!r}, got {exc!s}")
27
- else:
28
- raise AssertionError(f"expected failure containing {marker!r}")
29
-
30
-
31
- def expanded(ref: dict[str, Any], sources: dict[str, dict[str, Any]]) -> dict[str, Any]:
32
- entry = sources[ref["src"]]
33
- inline = {key: entry[key] for key in ("owner", "artifact")}
34
- inline.update({key: value for key, value in ref.items() if key != "src"})
35
- return inline
36
-
37
-
38
- def main() -> int:
39
- sources, records = load_jsonl(SKILL_ROOT / "examples/minimal-image-prompt-specs.jsonl")
40
- require(validate_records(records, sources)["specs"] == 1, "valid fixture count")
41
- require(len(sources) == 3, "fixture declares its upstream snapshots once")
42
-
43
- duplicate = [records[0], copy.deepcopy(records[0])]
44
- fail(duplicate, sources, "duplicate spec_id")
45
-
46
- bad_order = copy.deepcopy(records)
47
- bad_order[0]["reference_bindings"].append(copy.deepcopy(bad_order[0]["reference_bindings"][0]))
48
- fail(bad_order, sources, "duplicate slot_id")
49
-
50
- leaked = copy.deepcopy(records)
51
- leaked[0]["provider"] = "example"
52
- fail(leaked, sources, "provider execution fields")
53
-
54
- # A/B Round 2: the suite forbids provider control syntax in a generic prompt,
55
- # but nothing enforced it — the only prompt check was for leaked hashes.
56
- # Generic quality language is `IMG-02`, a `craft_default`; it is not checked
57
- # here, because a validator cannot block delivery on a fixed word list.
58
- for syntax in ("--ar 9:16", "(red coat:1.2)", "cat::2"):
59
- engine_syntax = copy.deepcopy(records)
60
- engine_syntax[0]["generic_prompt"] += f" {syntax}"
61
- fail(engine_syntax, sources, "engine-specific syntax")
62
-
63
- nested_provider = copy.deepcopy(records)
64
- nested_provider[0]["reference_bindings"][0]["provider"] = "example"
65
- fail(nested_provider, sources, "reference_bindings[0].provider")
66
-
67
- nested_secret = copy.deepcopy(records)
68
- nested_secret[0]["asset_binding"]["credentials"] = {"token": "not-safe"}
69
- fail(nested_secret, sources, "asset_binding.credentials")
70
-
71
- undeclared = copy.deepcopy(records)
72
- undeclared[0]["asset_binding"]["identity_ref"]["src"] = "no-such-source"
73
- fail(undeclared, sources, "REF_SRC_IS_NOT_DECLARED")
74
-
75
- unbound = copy.deepcopy(records)
76
- del unbound[0]["source_refs"][0]["src"]
77
- fail(unbound, sources, "REF_HAS_NO_UPSTREAM_BINDING")
78
-
79
- # Projects released before the sources declaration write the snapshot inline on
80
- # every reference; both forms resolve to the same upstream binding.
81
- inline = copy.deepcopy(records)
82
- binding = inline[0]["asset_binding"]
83
- binding["identity_ref"] = expanded(binding["identity_ref"], sources)
84
- binding["variant_ref"] = expanded(binding["variant_ref"], sources)
85
- inline[0]["source_refs"] = [expanded(ref, sources) for ref in inline[0]["source_refs"]]
86
- for slot in inline[0]["reference_bindings"]:
87
- slot["artifact_ref"] = expanded(slot["artifact_ref"], sources)
88
- require(validate_records(inline, {})["specs"] == 1, "inline snapshots resolve without sources")
89
-
90
- print("10 self-tests passed")
91
- return 0
92
-
93
-
94
- if __name__ == "__main__":
95
- raise SystemExit(main())
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP001/screenplay-index.jsonl"},"shots":{"owner":"short-drama-storyboard","artifact":"剧集/EP001/storyboard/shots.jsonl"},"screenplay":{"owner":"short-drama-write","artifact":"剧集/EP001/screenplay.md"}}}
2
- {"finding_id":"FIND-EXAMPLE","diagnostic_code":"SCR_SCENE_NO_TURN","knowhow_rule_id":"SCR-01","scope":"story_script","classification":"reviewed_invariant","enforcer":"reviewer","evidence":"Bounded explanation of the conflict","evidence_refs":[{"role":"source","src":"screenplay-index","record_id":"BLK-EP001-SC001-A01"},{"role":"conflicting_consumer","src":"shots","record_id":"SHOT-001","field":"/purpose"}],"impact":"Audience or production consequence","disposition":"keep | post_production | targeted_edit | resubmit | rewrite | not_applicable","disposition_rationale":"Why this disposition: is the defect text-controllable, and has it recurred? Use not_applicable outside project calibration.","required_change":"Outcome required from the owner, or empty when the disposition calls for no change","owner_skill":"short-drama-write","severity":"error","status":"open","target_ref":{"src":"screenplay","record_id":"EP001-SC001"}}
@@ -1,42 +0,0 @@
1
- {
2
- "sources": {
3
- "motion-specs-alternate": {
4
- "owner": "short-drama-video-prompts",
5
- "artifact": "剧集/EP001/storyboard/motion-specs.jsonl"
6
- },
7
- "motion-specs-master": {
8
- "owner": "short-drama-video-prompts",
9
- "artifact": "剧集/EP001/storyboard/motion-specs.jsonl"
10
- },
11
- "shots": {
12
- "owner": "short-drama-storyboard",
13
- "artifact": "剧集/EP001/storyboard/shots.jsonl"
14
- }
15
- },
16
- "example_only": true,
17
- "decision_id": "SUPERSESSION-EXAMPLE",
18
- "alternate_ref": {
19
- "src": "motion-specs-alternate",
20
- "record_id": "MOTION-ALTERNATE",
21
- "authority": "candidate"
22
- },
23
- "master_ref": {
24
- "src": "motion-specs-master",
25
- "record_id": "MOTION-MASTER",
26
- "authority": "accepted"
27
- },
28
- "checked_obligations": [
29
- {
30
- "source_ref": {
31
- "src": "shots",
32
- "record_id": "SHOT-EXAMPLE",
33
- "field": "/end_boundary",
34
- "authority": "accepted"
35
- },
36
- "result": "covered | missing | changed",
37
- "evidence": "<指出 alternate 中的对应字段>"
38
- }
39
- ],
40
- "decision": "approve_supersession | reject_supersession",
41
- "rationale": "<覆盖、对白、揭示、连续性和项目要求的复核结论>"
42
- }
@@ -1,32 +0,0 @@
1
- {
2
- "sources": {
3
- "screenplay": {
4
- "owner": "short-drama-write",
5
- "artifact": "剧集/EP001/screenplay.md"
6
- },
7
- "findings": {
8
- "owner": "short-drama-review",
9
- "artifact": "审查/findings.jsonl"
10
- }
11
- },
12
- "review_id": "REVIEW-EXAMPLE",
13
- "scope": [
14
- "story_script"
15
- ],
16
- "reviewed_artifacts": [
17
- {
18
- "src": "screenplay"
19
- }
20
- ],
21
- "findings_ref": {
22
- "src": "findings"
23
- },
24
- "review_method": "uninvolved_reviewer | self_check",
25
- "reviewer": "<honest reviewer label>",
26
- "structural_validation": "not_run",
27
- "verdict": "PROVISIONAL",
28
- "blocking_findings": [],
29
- "open_blocker_count": 0,
30
- "notes": [],
31
- "supersession_decisions": []
32
- }
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"screenplay-index.jsonl"},"screenplay":{"owner":"short-drama-write","artifact":"screenplay.md"}}}
2
- {"finding_id":"FIND-SC001-TURN","diagnostic_code":"SCR_SCENE_NO_TURN","knowhow_rule_id":"SCR-01","scope":"story_script","classification":"reviewed_invariant","enforcer":"reviewer","evidence":"SC001 ends with the same decision and relationship state with which it began.","evidence_refs":[{"role":"source","src":"screenplay-index","record_id":"SC001-A01"},{"role":"source","src":"screenplay-index","record_id":"SC001-A04"}],"impact":"The scene consumes time without changing the audience's understanding or the characters' options.","disposition":"targeted_edit","disposition_rationale":"The defect is local to the written scene turn and has not recurred elsewhere.","required_change":"End SC001 with a visible choice or consequence while preserving the accepted entrance facts.","owner_skill":"short-drama-write","severity":"error","status":"open","target_ref":{"src":"screenplay","record_id":"SC001"}}
@@ -1,34 +0,0 @@
1
- {
2
- "sources": {
3
- "screenplay": {
4
- "owner": "short-drama-write",
5
- "artifact": "screenplay.md"
6
- },
7
- "findings": {
8
- "owner": "short-drama-review",
9
- "artifact": "minimal-findings.jsonl"
10
- }
11
- },
12
- "review_id": "REVIEW-SC001",
13
- "scope": [
14
- "story_script"
15
- ],
16
- "reviewed_artifacts": [
17
- {
18
- "src": "screenplay"
19
- }
20
- ],
21
- "findings_ref": {
22
- "src": "findings"
23
- },
24
- "review_method": "self_check",
25
- "reviewer": "standalone example self-check",
26
- "structural_validation": "passed",
27
- "verdict": "REVISE",
28
- "blocking_findings": [
29
- "FIND-SC001-TURN"
30
- ],
31
- "open_blocker_count": 1,
32
- "notes": [],
33
- "supersession_decisions": []
34
- }