@remixmate/cli 0.13.6 → 0.13.7

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.13.6",
4
- "generatedAt": "2026-10-30T07:14:13.248Z",
3
+ "version": "0.13.7",
4
+ "generatedAt": "2026-10-30T09:41:49.930Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.13.6",
3
+ "version": "0.13.7",
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",
@@ -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 image-to-video replicate cards, map **转场效果 / Transition effect** to both `--payload sequence.defaultEffect=<id>` and `--payload sequence.overrideItemEffects=true`. Use the id in parentheses (`fade`, `wipe-left`, `slide-left`, `none`), preserving the chosen preset and all typography. **跟随样片 / Follow example (`follow`)** omits both overrides; `follow` is not a valid effect. Map **转场时长 / Transition duration** to `--payload sequence.transitionMs=<milliseconds>` (e.g. `0.6 秒(600)` → `600`); omit when following the sample or using `none`. 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:
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V24",
5
+ "version": "V25",
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
- lines = asset.get("narrationLines")
2007
- if not isinstance(lines, list) or len(lines) != line_count:
2008
- lines = [s.get("text") for s in old_subs] if len(old_subs) == line_count else []
2009
- if len(lines) != line_count or any(not isinstance(v, str) or not v.strip() for v in lines):
2010
- method = "authored-lines-unavailable"
2011
- message = "Authored narration lines unavailable; cannot restore per-line timing"
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
- asset["narrationLines"] = list(lines)
2015
- subtitles, method = align_narration_lines(
2016
- lines, subtitles, asset["duration"], asset.get("speechSpanMs"),
2017
- )
2018
- message = f"Preserved {line_count} narration lines using {method} timing"
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": "V42",
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": "V30",
5
+ "version": "V31",
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
  }
@@ -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
- if not text or not text.strip():
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: