@oh-story/dsh 0.1.18 → 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 +12 -12
- 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 +2 -2
- package/lib/index.js.map +1 -1
- 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/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,5 +0,0 @@
|
|
|
1
|
-
{"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"}}}
|
|
2
|
-
{"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-001","episode_id":"EP003","scene_id":"SC004","asset_kind":"character","surface_form":"顾禾","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A01"},"source_blocks":["BLK-EP003-SC004-A01"],"extracted_projection":{"role":"derived_projection","visible_facts":["外穿橙色旧雨衣","兜帽放下"],"explicit_state":{"look_hint":"rain_shell","wetness":"dry_at_entry"}},"story_function":["enters_location","carries_case"],"production_disposition":"visual_asset_required","proposed_binding":{"character_id":"CHAR-GUHE","look_id":"LOOK-GUHE-RAIN"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-001"},"status":"future_until_decision_exists"},"status":"proposed"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
|
|
3
|
-
{"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-002","episode_id":"EP003","scene_id":"SC004","asset_kind":"location","surface_form":"旧渡站值班室","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-H01"},"source_blocks":["BLK-EP003-SC004-H01","BLK-EP003-SC004-A01"],"extracted_projection":{"role":"derived_projection","visible_facts":["北门通向候船厅","东侧河窗","夜间顶灯亮"],"explicit_state":{"time":"night","light":"ceiling_light_on"}},"story_function":["action_space","later_blackout_setup"],"production_disposition":"visual_asset_required","proposed_binding":{"location_id":"LOC-FERRY-OFFICE","view_id":"VIEW-FERRY-OFFICE-NORTH-NIGHT"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-002"},"status":"future_until_decision_exists"},"status":"proposed"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
|
|
4
|
-
{"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-003","episode_id":"EP003","scene_id":"SC004","asset_kind":"prop","surface_form":"铁皮匣","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"},"source_blocks":["BLK-EP003-SC004-A02"],"extracted_projection":{"role":"derived_projection","visible_facts":["顾禾将匣放上柜台","匣起初闭合"],"explicit_state":{"condition":"closed","holder":"CHAR-GUHE","location":"counter"}},"story_function":["contains_reveal_object","supports_readable_text_reveal"],"production_disposition":"hero_prop_required","proposed_binding":{"prop_id":"PROP-TIN-CASE","state_id":"PSTATE-TIN-CLOSED-BADGE-IN"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-003"},"status":"future_until_decision_exists"},"status":"proposed"},"destination":"剧集/EP003/assets/occurrences.jsonl","continuity_delta_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/continuity.jsonl","selector":{"source_occurrence_id":"OCC-EP003-SC004-003"},"status":"future_until_delta_exists"}}
|
|
5
|
-
{"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-004","episode_id":"EP003","scene_id":"SC004","asset_kind":"character","surface_form":"她","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A05"},"source_blocks":["BLK-EP003-SC004-A05"],"extracted_projection":{"role":"derived_projection","visible_facts":["从北门离开"],"explicit_state":{}},"story_function":["exits_with_unknown_identity"],"production_disposition":"identity_resolution_required","proposed_binding":null,"candidate_bindings":["CHAR-GUHE","CHAR-WEISHU"],"ambiguity":"block 内无唯一先行项,两个已出场角色均可能","reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-004"},"status":"future_until_decision_exists"},"status":"unresolved"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
{"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"cd-prop-001":{"owner":"creator","artifact":"创作者决策/cd-prop-001.json"},"props":{"owner":"short-drama-assets","artifact":"设定集/props.jsonl"},"cd-pstate-001":{"owner":"creator","artifact":"创作者决策/cd-pstate-001.json"}}}
|
|
2
|
-
{"example_only":true,"destination":"设定集/props.jsonl","schema_version":"1.0.0-draft","record_type":"prop","prop_id":"PROP-TIN-CASE","display_name":"黑铁皮匣","identity_anchors":{"scale":"手掌至小臂长度的扁长匣","form":"铰链盖,长方体,右前角凹陷","materials":["黑色旧铁皮"],"function":"保存渡站号牌","permanent_marks":["右前角永久凹陷"]},"text_policy":{"mode":"exact_readable","text":"潮位 7","placement":"匣盖内侧","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A03"}},"source_refs":[{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-prop-001","record_id":"CD-PROP-001"}}}
|
|
3
|
-
{"example_only":true,"destination":"设定集/prop-states.jsonl","schema_version":"1.0.0-draft","record_type":"prop_state","state_id":"PSTATE-TIN-CLOSED-BADGE-IN","prop_ref":{"src":"props","record_id":"PROP-TIN-CASE"},"condition":{"open":"closed","damage":"right_front_dent_only","powered":"not_applicable"},"custody":{"owner":"CHAR-GUHE","holder":"CHAR-GUHE","hand":null,"location":"under_ferry_office_counter"},"contents":["PROP-CERAMIC-BADGE-017"],"text_visibility":"hidden_while_closed","validity":{"from":"EP003/SC004/start","until":"EP003/SC004/BLK-EP003-SC004-A03"},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-pstate-001","record_id":"CD-PSTATE-001"}}}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"record_type":"sources","schema_version":"1.0.0","sources":{"input-script":{"owner":"creator","artifact":"input-script.md"},"decisions":{"owner":"creator","artifact":"decisions.json"}}}
|
|
2
|
-
{"schema_version":"1.0.0-draft","record_type":"character","character_id":"CHAR-LIN","display_name":"林岚","aliases":[],"identity_anchors":["齐下颌黑发","左眉尾浅疤","站立时重心偏右"],"persistent_performance_facts":["回答前先看向出口"],"not_identity":["本场湿外套"],"source_refs":[{"src":"input-script","record_id":"SC001-A01"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"decisions","record_id":"CD-CHAR-LIN"}}}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"record_type":"sources","schema_version":"1.0.0","sources":{"characters":{"owner":"short-drama-assets","artifact":"characters.jsonl"},"input-script":{"owner":"creator","artifact":"input-script.md"},"decisions":{"owner":"creator","artifact":"decisions.json"}}}
|
|
2
|
-
{"schema_version":"1.0.0-draft","record_type":"look","look_id":"LOOK-LIN-RAIN","character_ref":{"src":"characters","record_id":"CHAR-LIN"},"base_look_id":"LOOK-LIN-BASE","differences":{"wardrobe_layers":["深蓝旧雨衣"],"weathering":["肩部雨水加深"]},"cause_ref":{"src":"input-script","record_id":"SC001-A01"},"validity":{"from":"SC001/A01","until":"SC002/end"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"decisions","record_id":"CD-LOOK-LIN-RAIN"}}}
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Validate the structural core of standalone character and look records."""
|
|
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("asset_check.py requires Python 3.9 or newer")
|
|
16
|
-
|
|
17
|
-
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
18
|
-
HASH_RE = re.compile(r"[0-9a-f]{64}")
|
|
19
|
-
ACCEPTANCE_STATUSES = {"accepted", "proposed", "pending_choice"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
# REFERENCE RESOLVER -- reference implementation.
|
|
24
|
-
#
|
|
25
|
-
# Each skill checker carries its own copy of this block. The suite has no shared
|
|
26
|
-
# library on purpose: a skill must stay runnable after copying only its own
|
|
27
|
-
# directory, so duplicating these few lines across skills is the correct shape.
|
|
28
|
-
# Copy the block verbatim; do not import it.
|
|
29
|
-
# ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
SOURCES_RECORD_TYPE = "sources"
|
|
32
|
-
SOURCES_SCHEMA_VERSION = "1.0.0"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ResolvedRef(NamedTuple):
|
|
36
|
-
"""An upstream reference with its snapshot resolved, whichever form it used."""
|
|
37
|
-
|
|
38
|
-
owner: str
|
|
39
|
-
artifact: str
|
|
40
|
-
record_id: str | None
|
|
41
|
-
field: str | None
|
|
42
|
-
authority: str | None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class RefFinding(NamedTuple):
|
|
46
|
-
"""A structural defect in a reference object."""
|
|
47
|
-
|
|
48
|
-
code: str
|
|
49
|
-
location: str
|
|
50
|
-
detail: str
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def load_sources(document: Any) -> dict[str, dict[str, Any]]:
|
|
54
|
-
"""Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
|
|
55
|
-
|
|
56
|
-
Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
|
|
57
|
-
``.jsonl`` file, whose declaration lives on the first record.
|
|
58
|
-
"""
|
|
59
|
-
if isinstance(document, list):
|
|
60
|
-
document = document[0] if document else None
|
|
61
|
-
if not isinstance(document, dict):
|
|
62
|
-
return {}
|
|
63
|
-
declared = document.get("sources")
|
|
64
|
-
if not isinstance(declared, dict):
|
|
65
|
-
return {}
|
|
66
|
-
return {key: value for key, value in declared.items() if isinstance(value, dict)}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def resolve_ref(
|
|
70
|
-
ref: Any, sources: dict[str, dict[str, Any]], location: str
|
|
71
|
-
) -> tuple[ResolvedRef | None, RefFinding | None]:
|
|
72
|
-
"""Resolve a reference object written in either the compact or expanded form."""
|
|
73
|
-
if not isinstance(ref, dict):
|
|
74
|
-
return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
|
|
75
|
-
src = ref.get("src")
|
|
76
|
-
if isinstance(src, str):
|
|
77
|
-
entry = sources.get(src)
|
|
78
|
-
if entry is None:
|
|
79
|
-
return None, RefFinding(
|
|
80
|
-
"REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
|
|
81
|
-
)
|
|
82
|
-
owner, artifact = entry.get("owner"), entry.get("artifact")
|
|
83
|
-
if not (isinstance(owner, str) and isinstance(artifact, str)):
|
|
84
|
-
return None, RefFinding(
|
|
85
|
-
"SOURCE_ENTRY_IS_INCOMPLETE", location, f"sources[{src!r}] needs owner/artifact"
|
|
86
|
-
)
|
|
87
|
-
elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
|
|
88
|
-
owner, artifact = ref["owner"], ref["artifact"]
|
|
89
|
-
else:
|
|
90
|
-
return None, RefFinding(
|
|
91
|
-
"REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
|
|
92
|
-
)
|
|
93
|
-
optional = {
|
|
94
|
-
key: ref[key] for key in ("record_id", "field", "authority") 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
|
-
|
|
113
|
-
class ValidationError(ValueError):
|
|
114
|
-
pass
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class RecordFile(NamedTuple):
|
|
118
|
-
"""One JSONL file: its upstream snapshot declaration and its records."""
|
|
119
|
-
|
|
120
|
-
sources: dict[str, dict[str, Any]]
|
|
121
|
-
records: list[dict[str, Any]]
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def resolve_input(value: str | Path) -> Path:
|
|
125
|
-
path = Path(value).expanduser()
|
|
126
|
-
if path.exists() or path.is_absolute():
|
|
127
|
-
return path
|
|
128
|
-
return SKILL_ROOT / path
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def load_jsonl(value: str | Path) -> RecordFile:
|
|
132
|
-
path = resolve_input(value)
|
|
133
|
-
parsed: list[dict[str, Any]] = []
|
|
134
|
-
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
135
|
-
if not line.strip():
|
|
136
|
-
continue
|
|
137
|
-
try:
|
|
138
|
-
record = json.loads(line)
|
|
139
|
-
except json.JSONDecodeError as exc:
|
|
140
|
-
raise ValidationError(f"{path}:{number}: invalid JSON") from exc
|
|
141
|
-
if not isinstance(record, dict):
|
|
142
|
-
raise ValidationError(f"{path}:{number}: each JSONL record must be an object")
|
|
143
|
-
parsed.append(record)
|
|
144
|
-
sources = load_sources(parsed)
|
|
145
|
-
records = [
|
|
146
|
-
record for record in parsed if record.get("record_type") != SOURCES_RECORD_TYPE
|
|
147
|
-
]
|
|
148
|
-
if not records:
|
|
149
|
-
raise ValidationError(f"{path}: no records")
|
|
150
|
-
return RecordFile(sources, records)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def require_text(record: dict[str, Any], key: str, label: str) -> str:
|
|
154
|
-
value = record.get(key)
|
|
155
|
-
if not isinstance(value, str) or not value.strip():
|
|
156
|
-
raise ValidationError(f"{label}: {key} must be non-empty text")
|
|
157
|
-
return value
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def require_list(record: dict[str, Any], key: str, label: str) -> list[Any]:
|
|
161
|
-
value = record.get(key)
|
|
162
|
-
if not isinstance(value, list) or not value:
|
|
163
|
-
raise ValidationError(f"{label}: {key} must be a non-empty list")
|
|
164
|
-
return value
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def validate_ref(
|
|
168
|
-
value: Any, sources: dict[str, dict[str, Any]], label: str
|
|
169
|
-
) -> ResolvedRef:
|
|
170
|
-
resolved, finding = resolve_ref(value, sources, label)
|
|
171
|
-
if finding is not None:
|
|
172
|
-
raise ValidationError(f"{label}: {finding.code}: {finding.detail}")
|
|
173
|
-
assert resolved is not None
|
|
174
|
-
if not resolved.record_id or not resolved.record_id.strip():
|
|
175
|
-
raise ValidationError(f"{label}: record_id must be non-empty text")
|
|
176
|
-
return resolved
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def validate_acceptance(
|
|
180
|
-
value: Any, sources: dict[str, dict[str, Any]], label: str
|
|
181
|
-
) -> None:
|
|
182
|
-
if not isinstance(value, dict) or value.get("status") not in ACCEPTANCE_STATUSES:
|
|
183
|
-
raise ValidationError(f"{label}: invalid creator_acceptance status")
|
|
184
|
-
decision_ref = value.get("decision_ref")
|
|
185
|
-
if value["status"] == "accepted" or decision_ref is not None:
|
|
186
|
-
resolved = validate_ref(decision_ref, sources, f"{label}.decision_ref")
|
|
187
|
-
# A decision_ref pointing at a block or an asset record binds acceptance to
|
|
188
|
-
# something that never recorded a creator decision.
|
|
189
|
-
if not str(resolved.record_id).startswith("CD-"):
|
|
190
|
-
raise ValidationError(
|
|
191
|
-
f"{label}.decision_ref: record_id must be a creator decision starting with CD-"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def validate_records(characters: RecordFile, looks: RecordFile) -> dict[str, Any]:
|
|
196
|
-
character_ids: set[str] = set()
|
|
197
|
-
for index, record in enumerate(characters.records, 1):
|
|
198
|
-
label = f"character[{index}]"
|
|
199
|
-
character_id = require_text(record, "character_id", label)
|
|
200
|
-
if not character_id.startswith("CHAR-"):
|
|
201
|
-
raise ValidationError(f"{label}: character_id must start with CHAR-")
|
|
202
|
-
if character_id in character_ids:
|
|
203
|
-
raise ValidationError(f"{label}: duplicate character_id {character_id}")
|
|
204
|
-
character_ids.add(character_id)
|
|
205
|
-
require_text(record, "display_name", label)
|
|
206
|
-
anchors = require_list(record, "identity_anchors", label)
|
|
207
|
-
if any(not isinstance(item, str) or not item.strip() for item in anchors):
|
|
208
|
-
raise ValidationError(f"{label}: identity_anchors must contain text")
|
|
209
|
-
for ref_index, ref in enumerate(require_list(record, "source_refs", label), 1):
|
|
210
|
-
validate_ref(ref, characters.sources, f"{label}.source_refs[{ref_index}]")
|
|
211
|
-
validate_acceptance(record.get("creator_acceptance"), characters.sources, label)
|
|
212
|
-
|
|
213
|
-
look_ids: set[str] = set()
|
|
214
|
-
for index, record in enumerate(looks.records, 1):
|
|
215
|
-
label = f"look[{index}]"
|
|
216
|
-
look_id = require_text(record, "look_id", label)
|
|
217
|
-
if not look_id.startswith("LOOK-"):
|
|
218
|
-
raise ValidationError(f"{label}: look_id must start with LOOK-")
|
|
219
|
-
if look_id in look_ids:
|
|
220
|
-
raise ValidationError(f"{label}: duplicate look_id {look_id}")
|
|
221
|
-
look_ids.add(look_id)
|
|
222
|
-
character_ref = validate_ref(
|
|
223
|
-
record.get("character_ref"), looks.sources, f"{label}.character_ref"
|
|
224
|
-
)
|
|
225
|
-
if character_ref.record_id not in character_ids:
|
|
226
|
-
raise ValidationError(
|
|
227
|
-
f"{label}: character_ref does not resolve: {character_ref.record_id}"
|
|
228
|
-
)
|
|
229
|
-
differences = record.get("differences")
|
|
230
|
-
if not isinstance(differences, dict) or not any(differences.values()):
|
|
231
|
-
raise ValidationError(f"{label}: differences must describe an observable change")
|
|
232
|
-
validity = record.get("validity")
|
|
233
|
-
if not isinstance(validity, dict) or not validity.get("from"):
|
|
234
|
-
raise ValidationError(f"{label}: validity.from is required")
|
|
235
|
-
validate_acceptance(record.get("creator_acceptance"), looks.sources, label)
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
"status": "valid",
|
|
239
|
-
"characters": len(characters.records),
|
|
240
|
-
"looks": len(looks.records),
|
|
241
|
-
# `source_declaration` is this file's own `sources` header resolving;
|
|
242
|
-
# `look_binding` is a real cross-record lookup, and it is one only
|
|
243
|
-
# because both files are handed to this checker.
|
|
244
|
-
"checks": ["unique_ids", "acceptance_shape", "source_declaration", "look_binding"],
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def validate_files(characters: str | Path, looks: str | Path) -> dict[str, Any]:
|
|
249
|
-
return validate_records(load_jsonl(characters), load_jsonl(looks))
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def main() -> int:
|
|
253
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
254
|
-
parser.add_argument("--characters", required=True)
|
|
255
|
-
parser.add_argument("--looks", required=True)
|
|
256
|
-
args = parser.parse_args()
|
|
257
|
-
try:
|
|
258
|
-
result = validate_files(args.characters, args.looks)
|
|
259
|
-
except (OSError, ValidationError) as exc:
|
|
260
|
-
print(f"asset check failed: {exc}", file=sys.stderr)
|
|
261
|
-
return 2
|
|
262
|
-
print(json.dumps(result, ensure_ascii=True, indent=2))
|
|
263
|
-
return 0
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if __name__ == "__main__":
|
|
267
|
-
raise SystemExit(main())
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Offline self-test for the standalone asset checker."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import copy
|
|
7
|
-
import sys
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
from asset_check import SKILL_ROOT, RecordFile, 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 expect_failure(characters: RecordFile, looks: RecordFile, marker: str) -> None:
|
|
23
|
-
try:
|
|
24
|
-
validate_records(characters, looks)
|
|
25
|
-
except ValidationError as exc:
|
|
26
|
-
if marker not in str(exc):
|
|
27
|
-
raise AssertionError(f"expected {marker!r}, got {exc!s}") from exc
|
|
28
|
-
else:
|
|
29
|
-
raise AssertionError(f"expected validation failure containing {marker!r}")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def edited(source: RecordFile, index: int) -> tuple[RecordFile, dict]:
|
|
33
|
-
records = copy.deepcopy(source.records)
|
|
34
|
-
return RecordFile(copy.deepcopy(source.sources), records), records[index]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def main() -> int:
|
|
38
|
-
example = Path(SKILL_ROOT, "examples/minimal")
|
|
39
|
-
characters = load_jsonl(example / "characters.jsonl")
|
|
40
|
-
looks = load_jsonl(example / "looks.jsonl")
|
|
41
|
-
result = validate_records(characters, looks)
|
|
42
|
-
require(result["characters"] == 1 and result["looks"] == 1, "valid fixture count")
|
|
43
|
-
|
|
44
|
-
duplicate = copy.deepcopy(characters.records[0])
|
|
45
|
-
expect_failure(
|
|
46
|
-
RecordFile(characters.sources, [*characters.records, duplicate]),
|
|
47
|
-
looks,
|
|
48
|
-
"duplicate character_id",
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
broken_look, record = edited(looks, 0)
|
|
52
|
-
record["character_ref"]["record_id"] = "CHAR-MISSING"
|
|
53
|
-
expect_failure(characters, broken_look, "does not resolve")
|
|
54
|
-
|
|
55
|
-
undeclared, record = edited(looks, 0)
|
|
56
|
-
record["character_ref"]["src"] = "not-declared"
|
|
57
|
-
expect_failure(characters, undeclared, "REF_SRC_IS_NOT_DECLARED")
|
|
58
|
-
|
|
59
|
-
unbound, record = edited(looks, 0)
|
|
60
|
-
record["character_ref"] = {"record_id": "CHAR-LIN"}
|
|
61
|
-
expect_failure(characters, unbound, "REF_HAS_NO_UPSTREAM_BINDING")
|
|
62
|
-
|
|
63
|
-
# A released project may still carry the snapshot inline on the reference.
|
|
64
|
-
inline, record = edited(looks, 0)
|
|
65
|
-
record["character_ref"] = {
|
|
66
|
-
**inline.sources["characters"],
|
|
67
|
-
"record_id": "CHAR-LIN",
|
|
68
|
-
}
|
|
69
|
-
inline.sources.pop("characters")
|
|
70
|
-
require(validate_records(characters, inline)["status"] == "valid", "inline snapshot")
|
|
71
|
-
|
|
72
|
-
candidate, record = edited(characters, 0)
|
|
73
|
-
record["creator_acceptance"] = {"status": "proposed", "decision_ref": None}
|
|
74
|
-
require(validate_records(candidate, looks)["status"] == "valid", "candidate status")
|
|
75
|
-
|
|
76
|
-
invalid, record = edited(characters, 0)
|
|
77
|
-
record["creator_acceptance"] = {"status": "approved", "decision_ref": None}
|
|
78
|
-
expect_failure(invalid, looks, "invalid creator_acceptance status")
|
|
79
|
-
|
|
80
|
-
misbound, record = edited(characters, 0)
|
|
81
|
-
record["creator_acceptance"]["decision_ref"]["record_id"] = "SC001-A01"
|
|
82
|
-
expect_failure(misbound, looks, "must be a creator decision starting with CD-")
|
|
83
|
-
|
|
84
|
-
print("9 self-tests passed")
|
|
85
|
-
return 0
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if __name__ == "__main__":
|
|
89
|
-
raise SystemExit(main())
|
package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"record_type": "sources", "schema_version": "1.0.0", "sources": {"characters": {"owner": "short-drama-assets", "artifact": "characters.jsonl"}, "looks": {"owner": "short-drama-assets", "artifact": "looks.jsonl"}, "composition-reference": {"owner": "creator", "artifact": "composition-reference.png"}}}
|
|
2
|
-
{"spec_id": "IMG-LIN-RAIN", "status": "candidate", "purpose": "character_sheet", "asset_binding": {"identity_ref": {"src": "characters", "record_id": "CHAR-LIN"}, "variant_ref": {"src": "looks", "record_id": "LOOK-LIN-RAIN"}}, "source_refs": [{"src": "characters", "record_id": "CHAR-LIN", "field": "/identity_anchors", "role": "identity_anchor"}], "reference_bindings": [{"slot_id": "REF-LIN-COMPOSITION", "order": 1, "artifact_ref": {"src": "composition-reference", "record_id": "REF-COMP-01"}, "role": "composition", "may_control": ["左右排版与留白"], "must_not_control": ["角色身份、服装、文字"], "admission_status": "creator_described", "reference_observation_ref": null, "unresolved_risks": []}], "intent": {"reuse_job": "保持角色在雨夜造型中的跨镜身份", "audience": "image production"}, "identity_or_form_anchors": ["齐下颌黑发", "左眉尾浅疤", "站立时重心偏右"], "variant_deltas": [{"field": "wardrobe", "observable_change": "深蓝旧雨衣,肩部被雨水加深", "valid_range": "SC001-SC002"}], "composition": {"view": "front and side reference sheet", "framing": "full body", "orientation": "neutral", "scale_relation": "consistent body proportions", "spatial_relations": []}, "appearance": {"materials": ["worn waterproof fabric"], "palette": "deep blue with muted skin tones", "lighting": "soft neutral studio light", "atmosphere": "clean reference presentation"}, "background": {"policy": "clean", "details": "plain light-gray background"}, "constraints": ["one coherent accepted look", "identity anchors remain visible"], "negative_constraints": ["no extra accessories", "no readable text"], "generic_prompt": "Character reference sheet for Lin Lan in one coherent rain look, jaw-length black hair, a shallow scar at the left eyebrow tail, weight resting slightly on the right leg, worn deep-blue raincoat with rain-darkened shoulders, front and side full-body views, neutral studio light, plain light-gray background, no extra accessories, no readable text.", "provenance": "creator_project"}
|