@remixmate/cli 0.13.6 → 0.13.8
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/dist/manifest.json +3 -3
- package/package.json +1 -1
- package/skills/gen-script/SKILL.md +3 -1
- package/skills/gen-script/scripts/gen_script.py +25 -2
- package/skills/gen-script/skill.json +1 -1
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/_narration_alignment.py +40 -0
- package/skills/render-video/scripts/render_video.py +28 -15
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +40 -0
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +8 -4
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"version": "0.13.8",
|
|
4
|
+
"generatedAt": "2026-10-31T00:45:42.523Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -614,7 +614,7 @@
|
|
|
614
614
|
"on",
|
|
615
615
|
"off"
|
|
616
616
|
],
|
|
617
|
-
"description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates
|
|
617
|
+
"description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates declaring narration optional default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
|
|
618
618
|
},
|
|
619
619
|
"background_asset": {
|
|
620
620
|
"type": "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -491,7 +491,7 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
491
491
|
| `--payload KEY=VALUE` | Template-specific card choice, repeatable. `KEY` is a dotted path in `customPayloadSchema` (or `capabilities.frameLevelKeys`), e.g. `<field>=<value>`, `<object>.<field>=<value>`. Value is converted to the declared type (true/false, numbers) and checked against enum / range; undeclared keys are rejected. Written to every scene with sibling keys kept, applied before fit-caption duration estimation. | template default |
|
|
492
492
|
| `--ending-payload KEY=VALUE` | Same as `--payload`, but written only to the ending scene, e.g. `<object>.<field>=<closing line>`. On countdown templates it also appends an ending scene after #1 (it carries the #1 index; fill its narration and the item field `capabilities.countdown.itemProp` names). Errors when the script has no ending scene. | none |
|
|
493
493
|
| `--avatar-id` | Digital-human avatar id (`dh_xxx`) for templates declaring `capabilities.digitalHuman`. Adds asset `asset-avatar` (`type: avatar`, `source: gen-digital-human`, `payload.avatarId`) and sets `visuals.avatar.assetRef` on every scene whose slot reads it — one continuous presenter driven by the narration. Placement: template default, or `--payload digitalHuman.<field>=…` (a photo-only presenter is `--payload digitalHuman.posterUrl=<url>` without this flag). | none |
|
|
494
|
-
| `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. `auto` removes the motion from every scene so the template picks
|
|
494
|
+
| `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. `auto` removes the motion from every scene so the template picks its own motion; nothing is locked then. | skeleton alternates in / out |
|
|
495
495
|
| `--font-id` | Font library key (`font.uniq_id`), written to `global.font.fontId`; ab-render resolves and installs the real font at render time. Pass it whenever the user picked a font — the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)`; pass the id in the parentheses. Never pass a CSS family or the display name: an unknown key falls back to the template font with no error. | template font |
|
|
496
496
|
| `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
|
|
497
497
|
| `--list-fonts` | Print the live font library (`/font/page`: id, name, lang, category, license) and exit; no script is generated and `--topic` is not needed. The ids it prints are the only valid `--font-id` values — use it when the user asks which fonts exist or names a font without its id. | — |
|
|
@@ -510,6 +510,8 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
510
510
|
|
|
511
511
|
Values written by `--background-preset`, `--background-asset`, `--payload`, `--ending-payload`, `--avatar-id` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload.<object>.<field>`, `animationHints.motion`). When the agent later saves its own `dsl_json` in merge mode, ab-agent restores those paths from this skeleton after merging, so a model-written `customPayload` object cannot wipe a card choice. Do not hand-write these keys in `dsl_json`; pass the flag. Templates that author their own scenes (`meta.authoredScenes`) are not merged: there ab-agent writes each locked value that the skeleton holds on every scene (with one value) onto every submitted scene, and adds the skeleton assets those values reference (`asset-background`, `asset-avatar`) when the submitted `assets` lacks them.
|
|
512
512
|
|
|
513
|
+
For replicate cards exposing transition effects or duration, read the template's `customPayloadSchema` and `llmHint` for the exact field paths, units, allowed values and any paired override switch. Pass each explicit selection through `--payload <declared-path>=<value>`, preserving the chosen preset and typography; following the sample omits the overrides. These payload keys are locked into every generated scene, so do not re-author them in a saved DSL.
|
|
514
|
+
|
|
513
515
|
## DSL generation principles
|
|
514
516
|
|
|
515
517
|
Apply the following principles when producing the DSL:
|
|
@@ -643,6 +643,8 @@ def _template_needs_image(template_config: dict | None) -> bool:
|
|
|
643
643
|
|
|
644
644
|
|
|
645
645
|
|
|
646
|
+
|
|
647
|
+
|
|
646
648
|
|
|
647
649
|
|
|
648
650
|
|
|
@@ -707,6 +709,7 @@ def _resolve_contract(template_config: dict | None) -> dict:
|
|
|
707
709
|
"duration_strategy": duration_strategy,
|
|
708
710
|
"duration_model": caps.get("durationModel"),
|
|
709
711
|
"payload_defaults": payload_defaults,
|
|
712
|
+
"media_binding": caps.get("mediaBinding"),
|
|
710
713
|
"primary_visual_type": primary_visual_type,
|
|
711
714
|
"background_strategy": "shared" if caps.get("backgroundStrategy") == "shared" else "per-scene",
|
|
712
715
|
"default_layout": caps.get("defaultLayout"),
|
|
@@ -1275,6 +1278,8 @@ def _media_option_choice(option_schema: dict, value: str | None, flag: str,
|
|
|
1275
1278
|
|
|
1276
1279
|
|
|
1277
1280
|
|
|
1281
|
+
|
|
1282
|
+
|
|
1278
1283
|
|
|
1279
1284
|
_MEDIA_OPTION_FLAGS = {"preset": "--sequence-preset", "fit": "--sequence-fit"}
|
|
1280
1285
|
_LEGACY_MEDIA_BINDINGS = {
|
|
@@ -2493,7 +2498,7 @@ def _attach_narration(scene: dict, narration_text: str, narration_asset_id: str
|
|
|
2493
2498
|
def _visual_scene(
|
|
2494
2499
|
ctx: _SkeletonContext, idx: int, scene_id: str, plan: dict, visual_asset_id: str,
|
|
2495
2500
|
) -> dict:
|
|
2496
|
-
|
|
2501
|
+
scene = {
|
|
2497
2502
|
"id": scene_id,
|
|
2498
2503
|
"purpose": plan["purpose"],
|
|
2499
2504
|
"duration": plan["duration"],
|
|
@@ -2507,6 +2512,24 @@ def _visual_scene(
|
|
|
2507
2512
|
}
|
|
2508
2513
|
|
|
2509
2514
|
|
|
2515
|
+
|
|
2516
|
+
binding = ctx.contract.get("media_binding")
|
|
2517
|
+
if isinstance(binding, dict) and "assetRefKey" in binding:
|
|
2518
|
+
path = binding.get("itemsPath")
|
|
2519
|
+
ref_key = binding.get("assetRefKey")
|
|
2520
|
+
if not isinstance(path, str) or not all(path.split(".")) or not isinstance(ref_key, str) or not ref_key:
|
|
2521
|
+
raise ValueError("mediaBinding needs non-empty itemsPath and assetRefKey")
|
|
2522
|
+
item = deepcopy(binding.get("item", {}))
|
|
2523
|
+
if not isinstance(item, dict):
|
|
2524
|
+
raise ValueError("mediaBinding.item must be an object")
|
|
2525
|
+
item[ref_key] = visual_asset_id
|
|
2526
|
+
payload = _build_custom_payload(ctx.template_config)
|
|
2527
|
+
_write_payload(payload, [(path.split("."), [item])])
|
|
2528
|
+
scene["customPayload"] = payload
|
|
2529
|
+
del scene["visuals"]
|
|
2530
|
+
return scene
|
|
2531
|
+
|
|
2532
|
+
|
|
2510
2533
|
def _payload_scene(ctx: _SkeletonContext, scene_id: str, plan: dict) -> dict:
|
|
2511
2534
|
|
|
2512
2535
|
|
|
@@ -2979,7 +3002,7 @@ Examples:
|
|
|
2979
3002
|
"--motion", default=None,
|
|
2980
3003
|
help="Camera motion for every scene (animationHints.motion): static / kenburns-in / kenburns-out / "
|
|
2981
3004
|
"pan-left / pan-right / float. Replaces the skeleton's alternating default. `auto` removes the "
|
|
2982
|
-
"motion from every scene so the template picks
|
|
3005
|
+
"motion from every scene so the template picks its own motion.",
|
|
2983
3006
|
)
|
|
2984
3007
|
parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
|
|
2985
3008
|
parser.add_argument("--avatar-id", default=None, help="Digital-human avatar id (dh_xxx) for templates declaring capabilities.digitalHuman: adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef (one continuous presenter driven by the narration). Placement stays the template default unless set with --payload digitalHuman.<field>=...")
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"narration": {
|
|
68
68
|
"type": "string",
|
|
69
69
|
"enum": ["auto", "on", "off"],
|
|
70
|
-
"description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates
|
|
70
|
+
"description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates declaring narration optional default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
|
|
71
71
|
},
|
|
72
72
|
"background_asset": {
|
|
73
73
|
"type": "string",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V26",
|
|
6
6
|
"skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
|
|
7
7
|
}
|
|
@@ -11,6 +11,8 @@ import math
|
|
|
11
11
|
import re
|
|
12
12
|
import unicodedata
|
|
13
13
|
|
|
14
|
+
from video_dsl.runtime.timeline_compiler import assign_sentence_indices, split_narration_sentences
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def _normalized(text: str) -> str:
|
|
16
18
|
text = re.sub(r"<#\s*\d+(?:\.\d+)?\s*#>", "", text)
|
|
@@ -86,3 +88,41 @@ def align_narration_lines(
|
|
|
86
88
|
end = start_ms + (end_ms - start_ms) * offset / total
|
|
87
89
|
result.append({"text": line, "timeBegin": begin, "timeEnd": end})
|
|
88
90
|
return result, "estimated"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def restore_narration_sentences(
|
|
94
|
+
text: str, subtitles: list[dict], duration_ms: float, speech_span_ms=None,
|
|
95
|
+
) -> tuple[list[dict], str]:
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
sentences = split_narration_sentences(text)
|
|
103
|
+
if not subtitles:
|
|
104
|
+
if len(sentences) > 1:
|
|
105
|
+
return align_narration_lines(sentences, [], duration_ms, speech_span_ms)
|
|
106
|
+
return [], "tts-unchanged"
|
|
107
|
+
|
|
108
|
+
result = []
|
|
109
|
+
method = "tts-unchanged"
|
|
110
|
+
for sub in subtitles:
|
|
111
|
+
parts = split_narration_sentences(sub.get("text") or "")
|
|
112
|
+
if len(parts) <= 1:
|
|
113
|
+
result.append(sub)
|
|
114
|
+
continue
|
|
115
|
+
split, timing = align_narration_lines(parts, [sub], duration_ms, speech_span_ms)
|
|
116
|
+
result.extend(split)
|
|
117
|
+
if method != "estimated":
|
|
118
|
+
method = "estimated" if timing == "estimated" else "tts-interpolated"
|
|
119
|
+
|
|
120
|
+
if len(sentences) > 1:
|
|
121
|
+
if _normalized(text) != _normalized("".join(s.get("text") or "" for s in subtitles)):
|
|
122
|
+
return result, "tts-text-mismatch"
|
|
123
|
+
indices = assign_sentence_indices([s.get("text") or "" for s in result])
|
|
124
|
+
if len(set(indices)) != len(sentences):
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
return align_narration_lines(sentences, subtitles, duration_ms, speech_span_ms)
|
|
128
|
+
return result, method
|
|
@@ -880,6 +880,9 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
880
880
|
"retryCount": 0,
|
|
881
881
|
"maxRetries": 3,
|
|
882
882
|
}
|
|
883
|
+
narration_text = (asset.get("payload") or {}).get("text")
|
|
884
|
+
if asset.get("type") == "audio" and isinstance(narration_text, str) and narration_text.strip():
|
|
885
|
+
assets_by_id[asset["assetId"]]["narrationText"] = narration_text
|
|
883
886
|
|
|
884
887
|
|
|
885
888
|
|
|
@@ -1397,6 +1400,8 @@ def _resolve_single_asset(
|
|
|
1397
1400
|
|
|
1398
1401
|
|
|
1399
1402
|
src_payload = asset_with_payload.get("payload", {}) or {}
|
|
1403
|
+
if source == "gen-voice" and isinstance(src_payload.get("text"), str):
|
|
1404
|
+
asset["narrationText"] = src_payload["text"]
|
|
1400
1405
|
if isinstance(src_payload.get("narrationItems"), list):
|
|
1401
1406
|
asset["narrationLineCount"] = len(src_payload["narrationItems"])
|
|
1402
1407
|
asset["narrationIntroLines"] = int(src_payload.get("narrationIntroLines") or 0)
|
|
@@ -1628,6 +1633,8 @@ def resolve_narration_run(
|
|
|
1628
1633
|
if per_scene_subs[k]:
|
|
1629
1634
|
asset["ttsSubtitles"] = per_scene_subs[k]
|
|
1630
1635
|
payload = payloads.get(c["assetId"]) or {}
|
|
1636
|
+
if isinstance(payload.get("text"), str):
|
|
1637
|
+
asset["narrationText"] = payload["text"]
|
|
1631
1638
|
if isinstance(payload.get("narrationItems"), list):
|
|
1632
1639
|
asset["narrationLineCount"] = len(payload["narrationItems"])
|
|
1633
1640
|
asset["narrationIntroLines"] = int(payload.get("narrationIntroLines") or 0)
|
|
@@ -1990,32 +1997,38 @@ def _template_default_tail_pad_frames(tpl: dict, fps: int) -> int | None:
|
|
|
1990
1997
|
|
|
1991
1998
|
def _structured_narration_subtitles(entry: dict, asset: dict, render_plan: dict) -> list[dict]:
|
|
1992
1999
|
|
|
1993
|
-
from _narration_alignment import align_narration_lines
|
|
2000
|
+
from _narration_alignment import align_narration_lines, restore_narration_sentences
|
|
1994
2001
|
|
|
1995
2002
|
subtitles = asset.get("ttsSubtitles") or []
|
|
1996
2003
|
line_count = asset.get("narrationLineCount")
|
|
1997
|
-
if not isinstance(line_count, int) or isinstance(line_count, bool) or line_count <= 0:
|
|
1998
|
-
return subtitles
|
|
1999
|
-
|
|
2000
2004
|
|
|
2001
2005
|
old_subs = entry.get("subtitleSegments") or []
|
|
2002
2006
|
at_sec = entry.get("subtitleAtSec") or []
|
|
2003
2007
|
if len(at_sec) == len(old_subs) and any(isinstance(v, (int, float)) for v in at_sec):
|
|
2004
2008
|
return []
|
|
2005
2009
|
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2010
|
+
if not isinstance(line_count, int) or isinstance(line_count, bool) or line_count <= 0:
|
|
2011
|
+
text = asset.get("narrationText") or ""
|
|
2012
|
+
subtitles, method = restore_narration_sentences(
|
|
2013
|
+
text, subtitles, asset["duration"], asset.get("speechSpanMs"),
|
|
2014
|
+
)
|
|
2015
|
+
if method == "tts-unchanged":
|
|
2016
|
+
return subtitles
|
|
2017
|
+
message = f"Restored narration sentence boundaries using {method} timing"
|
|
2012
2018
|
else:
|
|
2019
|
+
lines = asset.get("narrationLines")
|
|
2020
|
+
if not isinstance(lines, list) or len(lines) != line_count:
|
|
2021
|
+
lines = [s.get("text") for s in old_subs] if len(old_subs) == line_count else []
|
|
2022
|
+
if len(lines) != line_count or any(not isinstance(v, str) or not v.strip() for v in lines):
|
|
2023
|
+
method = "authored-lines-unavailable"
|
|
2024
|
+
message = "Authored narration lines unavailable; cannot restore per-line timing"
|
|
2025
|
+
else:
|
|
2013
2026
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2027
|
+
asset["narrationLines"] = list(lines)
|
|
2028
|
+
subtitles, method = align_narration_lines(
|
|
2029
|
+
lines, subtitles, asset["duration"], asset.get("speechSpanMs"),
|
|
2030
|
+
)
|
|
2031
|
+
message = f"Preserved {line_count} narration lines using {method} timing"
|
|
2019
2032
|
|
|
2020
2033
|
logs = render_plan.setdefault("logs", [])
|
|
2021
2034
|
if not any(l.get("phase") == "narration-align" and l.get("sceneId") == entry.get("sceneId")
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V43",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V32",
|
|
6
6
|
"skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
|
|
7
7
|
}
|
|
@@ -1273,6 +1273,44 @@ def _check_narration_language_strict(dsl: dict) -> list[ValidationError]:
|
|
|
1273
1273
|
_TEXT_LAYER_SKELETON_PREFIXES = ("[骨架待填充]", "[skeleton placeholder]")
|
|
1274
1274
|
|
|
1275
1275
|
|
|
1276
|
+
def _check_presenter_visibility(dsl: dict) -> list[ValidationError]:
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
tpl = _get_template_cfg(_pick_template_id(dsl)) or {}
|
|
1282
|
+
caps = tpl.get("capabilities")
|
|
1283
|
+
caps = caps if isinstance(caps, dict) else {}
|
|
1284
|
+
spec = caps.get("digitalHuman") or {}
|
|
1285
|
+
if not isinstance(spec, dict) or spec.get("backgroundOccludes") is not True:
|
|
1286
|
+
return []
|
|
1287
|
+
presenter_scenes = []
|
|
1288
|
+
for scene in dsl.get("scenes") or []:
|
|
1289
|
+
if not isinstance(scene, dict):
|
|
1290
|
+
continue
|
|
1291
|
+
visuals = scene.get("visuals") or {}
|
|
1292
|
+
if not isinstance(visuals, dict):
|
|
1293
|
+
continue
|
|
1294
|
+
if not isinstance(visuals.get("avatar"), dict):
|
|
1295
|
+
continue
|
|
1296
|
+
if (visuals.get("avatar") or {}).get("assetRef"):
|
|
1297
|
+
presenter_scenes.append(scene)
|
|
1298
|
+
if not presenter_scenes or not all(
|
|
1299
|
+
isinstance(scene["visuals"].get("background"), dict)
|
|
1300
|
+
and scene["visuals"]["background"].get("assetRef")
|
|
1301
|
+
for scene in presenter_scenes
|
|
1302
|
+
):
|
|
1303
|
+
return []
|
|
1304
|
+
binding = caps.get("mediaBinding") or {}
|
|
1305
|
+
path = binding.get("itemsPath") if isinstance(binding, dict) else None
|
|
1306
|
+
hint = f" Use timed media items in customPayload.{path} instead." if path else " Leave presenter scenes without full-scene backgrounds."
|
|
1307
|
+
return [ValidationError(
|
|
1308
|
+
"scenes[].visuals.background.assetRef",
|
|
1309
|
+
"Full-scene backgrounds cover every presenter scene; the presenter would never be visible."
|
|
1310
|
+
+ hint,
|
|
1311
|
+
)]
|
|
1312
|
+
|
|
1313
|
+
|
|
1276
1314
|
def _check_text_layer_rules(dsl: dict) -> list[ValidationError]:
|
|
1277
1315
|
|
|
1278
1316
|
|
|
@@ -1398,6 +1436,7 @@ def validate_integrity(dsl: dict) -> list[ValidationError]:
|
|
|
1398
1436
|
errors += _check_step_narration_alignment(dsl)
|
|
1399
1437
|
errors += _check_slide_cross_field(dsl)
|
|
1400
1438
|
errors += _check_text_layer_rules(dsl)
|
|
1439
|
+
errors += _check_presenter_visibility(dsl)
|
|
1401
1440
|
|
|
1402
1441
|
errors += _check_duration_estimate(dsl)
|
|
1403
1442
|
return errors
|
|
@@ -1435,6 +1474,7 @@ def validate_dsl(dsl: dict) -> list[ValidationError]:
|
|
|
1435
1474
|
errors += _check_step_narration_alignment(dsl)
|
|
1436
1475
|
errors += _check_slide_cross_field(dsl)
|
|
1437
1476
|
errors += _check_text_layer_rules(dsl)
|
|
1477
|
+
errors += _check_presenter_visibility(dsl)
|
|
1438
1478
|
|
|
1439
1479
|
errors += _check_duration_estimate(dsl)
|
|
1440
1480
|
return errors
|
|
@@ -213,15 +213,19 @@ def assign_sentence_indices(raw_texts: list[str]) -> list[int]:
|
|
|
213
213
|
return indices
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def split_narration_sentences(text: str) -> list[str]:
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
return [part.strip(" \t\r") for part in _PRIMARY_BREAK.split(text.strip()) if part.strip()] if text else []
|
|
221
|
+
|
|
222
|
+
|
|
216
223
|
def count_narration_sentences(text: str) -> int:
|
|
217
224
|
|
|
218
225
|
|
|
219
226
|
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
return 0
|
|
223
|
-
chunks = [s for s in _PRIMARY_BREAK.split(text.strip()) if s.strip()]
|
|
224
|
-
return len(chunks)
|
|
228
|
+
return len(split_narration_sentences(text))
|
|
225
229
|
|
|
226
230
|
|
|
227
231
|
def _plain_len(text: str) -> int:
|