@remixmate/cli 0.13.4 → 0.13.6

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,19 +1,19 @@
1
- var w=Object.defineProperty;var l=(e,o)=>w(e,"name",{value:o,configurable:!0});import{mmPost as m,resolveHttpContext as v,SkillError as c}from"../http.js";import{getCapabilities as _}from"../capabilities.js";import{emitProgress as y}from"../progress.js";import{isTrue as u,toNumber as $}from"./shared.js";const b=[{id:"Chinese (Mandarin)_Male_Announcer",language:"zh",name:"Mandarin Male Announcer (legacy default)"},{id:"english-male-friendly-01",language:"en",name:"Friendly Male (English, TBD)"}];async function k(e){const t=(await m(e,"/voice/page",{current:1,pageSize:100},{timeoutMs:3e4})).list??[];if(t.length===0){process.stdout.write(`⚠️ No voices available
1
+ var w=Object.defineProperty;var d=(e,i)=>w(e,"name",{value:i,configurable:!0});import{mmPost as m,resolveHttpContext as v,SkillError as c}from"../http.js";import{getCapabilities as y}from"../capabilities.js";import{emitProgress as x}from"../progress.js";import{isTrue as u,toNumber as _}from"./shared.js";const b=[{id:"Chinese (Mandarin)_Male_Announcer",language:"zh",name:"Mandarin Male Announcer (legacy default)"},{id:"english-male-friendly-01",language:"en",name:"Friendly Male (English, TBD)"}];async function $(e){const o=(await m(e,"/voice/page",{current:1,pageSize:100},{timeoutMs:3e4})).list??[];if(o.length===0){process.stdout.write(`⚠️ No voices available
2
2
  `);return}process.stdout.write(`
3
3
  ${"Voice ID".padEnd(30)} Name
4
4
  `),process.stdout.write(`${"-".repeat(30)} ${"-".repeat(20)}
5
- `);for(const s of t)process.stdout.write(`${(s.uniqId??"").padEnd(30)} ${s.name??""}
5
+ `);for(const s of o)process.stdout.write(`${(s.uniqId??"").padEnd(30)} ${s.name??""}
6
6
  `);process.stdout.write(`
7
- ${t.length} voice(s) available
7
+ ${o.length} voice(s) available
8
8
  `),process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis
9
- `)}l(k,"listVoicesRemote");function x(){process.stderr.write(`⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. Drop --local to query the live catalog.
9
+ `)}d($,"listVoicesRemote");function k(){process.stderr.write(`⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. Drop --local to query the live catalog.
10
10
  `);for(const e of b)process.stdout.write(`${e.id} ${e.language} ${e.name}
11
- `)}l(x,"listVoicesLocal");async function E(e,o){if(u(e.list_voices)){if(u(e.local))return x();const a=await v(o.skillName,{apiBaseUrl:e.api_base_url,privateToken:e.priv_token,preflightToken:o.auth?.token});return k(a)}const t=e.text;if(!t||typeof t!="string"||!t.trim())throw new c("❌ Please provide --text to specify the text to synthesize");const s=e.api_base_url,i=(await _(s)).voice,p=e.voice_id??i.defaultVoiceId,r=$(e.speed,"speed")??1;if(r<i.speedMin||r>i.speedMax)throw new c(`❌ speed must be in the range ${i.speedMin}~${i.speedMax}`);const g=e.language_boost?.trim()||void 0,h=await v(o.skillName,{apiBaseUrl:s,privateToken:e.priv_token,preflightToken:o.auth?.token});y({phase:"gen-voice:request",voiceId:p,speed:r});const n=await m(h,"/tool/tts",{provider:i.provider,text:t,voiceSetting:{voiceId:p,speed:r},outputFormat:"url",...g?{languageBoost:g}:{}},{timeoutMs:12e4}),d=n.audio;if(!d)throw new c(`❌ API response did not contain an audio URL: ${JSON.stringify(n)}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:d,audio_length_ms:n.extraInfo?.audioLength??null,subtitles:(n.subtitles??[]).map(a=>({text:a.text??"",timeBegin:a.timeBegin??0,timeEnd:a.timeEnd??0}))})+`
12
- `);return}const f=n.extraInfo?.audioLength;typeof f=="number"&&process.stdout.write(`
11
+ `)}d(k,"listVoicesLocal");async function L(e,i){if(u(e.list_voices)){if(u(e.local))return k();const t=await v(i.skillName,{apiBaseUrl:e.api_base_url,privateToken:e.priv_token,preflightToken:i.auth?.token});return $(t)}const o=e.text;if(!o||typeof o!="string"||!o.trim())throw new c("❌ Please provide --text to specify the text to synthesize");const s=e.api_base_url,n=(await y(s)).voice,p=e.voice_id??n.defaultVoiceId,r=_(e.speed,"speed")??1;if(r<n.speedMin||r>n.speedMax)throw new c(`❌ speed must be in the range ${n.speedMin}~${n.speedMax}`);const g=e.language_boost?.trim()||void 0,h=await v(i.skillName,{apiBaseUrl:s,privateToken:e.priv_token,preflightToken:i.auth?.token});x({phase:"gen-voice:request",voiceId:p,speed:r});const a=await m(h,"/tool/tts",{provider:n.provider,text:o,voiceSetting:{voiceId:p,speed:r},outputFormat:"url",...g?{languageBoost:g}:{}},{timeoutMs:12e4}),l=a.audio;if(!l)throw new c(`❌ API response did not contain an audio URL: ${JSON.stringify(a)}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:l,audio_length_ms:a.extraInfo?.audioLength??null,subtitles:(a.subtitles??[]).map(t=>({text:t.text??"",timeBegin:t.timeBegin??0,timeEnd:t.timeEnd??0,...typeof t.textBegin=="number"&&typeof t.textEnd=="number"?{textBegin:t.textBegin,textEnd:t.textEnd}:{}}))})+`
12
+ `);return}const f=a.extraInfo?.audioLength;typeof f=="number"&&process.stdout.write(`
13
13
  🕐 Audio length: ${(f/1e3).toFixed(1)} s
14
14
  `),process.stdout.write(`
15
15
  🔗 Audio URL (plays directly, no download needed):
16
- ${d}
16
+ ${l}
17
17
  `),process.stdout.write(`
18
18
  🎉 TTS synthesis done!
19
- `)}l(E,"genVoice");export{E as genVoice};
19
+ `)}d(L,"genVoice");export{L as genVoice};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.13.4",
4
- "generatedAt": "2026-10-26T01:31:08.439Z",
3
+ "version": "0.13.6",
4
+ "generatedAt": "2026-10-30T07:14:13.248Z",
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.4",
3
+ "version": "0.13.6",
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",
@@ -32,6 +32,8 @@ Turns a user-supplied **topic** into a **Video DSL v1alpha1** JSON describing wh
32
32
 
33
33
  - **Video DSL**: a declarative description language for videos — describes the final structure, not the orchestration calls.
34
34
  - **AssetRef**: an asset reference declaring which images / audio / video clips are needed; the downstream Render Skill is responsible for producing them.
35
+
36
+ Templates may declare `capabilities.mediaBinding.sceneMode=per-item`: the returned skeleton then contains one scene per input media item, with durations allocated from the complete sequence's existing timing model. Keep those scenes, media references and timing values; fill each scene's narration separately. Captions describe the visuals and are not automatically narration. Continuous TTS can synthesize adjacent scenes together and cut the result back into their separate audio assets; it does not merge the script's scenes.
35
37
  - **Scene**: the unit cell of the video timeline.
36
38
  - **Image `payload.model`**: must match the `gen-image` skill and the backend `ModelGenImgDTO`. **Only** values from the LiteLLM `model` allowlist below may be used. When editing narration or prompts you **must not** rewrite this field.
37
39
 
@@ -1129,8 +1129,8 @@ def _sequence_floor_frames(sequence: dict, preset_timing: dict, text: str, fps:
1129
1129
  return math.floor(min_sec * fps + 0.5)
1130
1130
 
1131
1131
 
1132
- def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps: int = 30,
1133
- headline: str = "") -> float:
1132
+ def _fit_images_hold_frames(custom_payload: dict, model: dict | None, fps: int = 30,
1133
+ headline: str = "") -> list[int]:
1134
1134
 
1135
1135
 
1136
1136
 
@@ -1183,8 +1183,46 @@ def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps:
1183
1183
  if rate is None:
1184
1184
  raise ValueError(f"fit-images with a floor requires durationModel.{key}.floorMinVideoRate")
1185
1185
  floor_frames = min(floor_frames, sum(math.floor(f / rate) for f in frames))
1186
- natural = max(natural, floor_frames)
1187
- return natural / fps
1186
+
1187
+
1188
+
1189
+ target = max(natural, floor_frames)
1190
+ original = list(frames)
1191
+ images = [item.get("type") != "video" for item in sequence["items"]]
1192
+
1193
+ def grow(eligible, caps):
1194
+ need = target - sum(frames)
1195
+ room = [max(0, caps[i] - frames[i]) if eligible[i] else 0 for i in range(len(frames))]
1196
+ capacity = sum(room)
1197
+ if need > 0 and capacity > 0:
1198
+ take = min(need, capacity)
1199
+ for i, r in enumerate(room):
1200
+ frames[i] += math.floor(r * take / capacity)
1201
+
1202
+ def uncapped(eligible):
1203
+ need = target - sum(frames)
1204
+ base = sum(f for i, f in enumerate(frames) if eligible[i])
1205
+ if need > 0 and base > 0:
1206
+ for i, f in enumerate(list(frames)):
1207
+ if eligible[i]:
1208
+ frames[i] += math.floor(f * need / base)
1209
+
1210
+ videos = [not image for image in images]
1211
+ grow(images, [math.floor(f * 2.5 + 0.5) for f in original])
1212
+ if any(videos):
1213
+ rate = timing.get("floorMinVideoRate")
1214
+ if not rate:
1215
+ raise ValueError(f"fit-images with a floor requires durationModel.{key}.floorMinVideoRate")
1216
+ grow(videos, [math.floor(f / rate) for f in original])
1217
+ uncapped(images)
1218
+ uncapped(videos)
1219
+ frames[-1] += target - sum(frames)
1220
+ return frames
1221
+
1222
+
1223
+ def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps: int = 30,
1224
+ headline: str = "") -> float:
1225
+ return sum(_fit_images_hold_frames(custom_payload, model, fps, headline)) / fps
1188
1226
 
1189
1227
 
1190
1228
  def _probe_sequence_video_duration(url: str) -> float | None:
@@ -1535,6 +1573,71 @@ def _build_carousel_caption_dsl(
1535
1573
  for path in narration_locks:
1536
1574
  _lock_scene_path(dsl, path)
1537
1575
 
1576
+ scene_mode = binding.get("sceneMode", "single")
1577
+ if scene_mode not in ("single", "per-item"):
1578
+ raise ValueError("mediaBinding.sceneMode must be single or per-item")
1579
+ if scene_mode == "per-item":
1580
+ if duration_strategy != "fit-images":
1581
+ raise ValueError("mediaBinding.sceneMode=per-item requires durationStrategy=fit-images")
1582
+ holds = _fit_images_hold_frames(custom_payload, duration_model, headline=headline)
1583
+ dsl["assets"] = [a for a in assets if a["source"] != "gen-voice"]
1584
+ dsl["scenes"] = []
1585
+
1586
+
1587
+ scene_defaults = [(path.split("."), value) for path, value in binding.get("sceneDefaults", {}).items()]
1588
+ for index, (item, frames) in enumerate(zip(container[items_parts[-1]], holds)):
1589
+ current = deepcopy(scene)
1590
+ current["id"] = f"scene-{index + 1:02d}"
1591
+ current["purpose"] = "opening" if index == 0 else "point"
1592
+ current["duration"] = frames / 30
1593
+ payload = current["customPayload"]
1594
+ _write_payload(payload, scene_defaults)
1595
+ _write_payload(payload, payload_writes or [])
1596
+ pinned = {**item, "holdSec": frames / 30}
1597
+ hold_schema = (_schema_at(template_config, items_parts).get("items", {}).get("properties", {}).get("holdSec", {}))
1598
+ if not hold_schema.get("minimum", 0) <= frames / 30 <= hold_schema.get("maximum", float("inf")):
1599
+
1600
+
1601
+
1602
+ pinned.pop("holdSec", None)
1603
+ _write_payload(payload, [(items_parts, [pinned])])
1604
+ default_hold_schema = _schema_at(template_config, container_parts + ["defaultHoldSec"])
1605
+ if default_hold_schema and default_hold_schema.get("minimum", 0) <= frames / 30 <= default_hold_schema.get("maximum", float("inf")):
1606
+ _write_payload(payload, [(container_parts + ["defaultHoldSec"], frames / 30)])
1607
+
1608
+
1609
+ if _schema_at(template_config, container_parts + ["minSec"]):
1610
+ minimum = _schema_at(template_config, container_parts + ["minSec"]).get("minimum", 0)
1611
+ if frames / 30 >= minimum:
1612
+ _write_payload(payload, [(container_parts + ["minSec"], frames / 30)])
1613
+ else:
1614
+ _get_path(payload, container_parts).pop("minSec", None)
1615
+ if narration_enabled:
1616
+ if "minDurationSec" in payload:
1617
+ payload["minDurationSec"] = frames / 30
1618
+ ref = f"narration-{current['id']}"
1619
+ current["audio"]["narration"].update(
1620
+ assetRef=ref,
1621
+ text=_localize_label(
1622
+ output_language, "narration_opening", label="", topic=_clip(topic, _PLACEHOLDER_TOPIC_CHARS),
1623
+ duration=frames / 30,
1624
+ budget=_budget_suffix(frames / 30, output_language, narration_speed, None),
1625
+ ),
1626
+ )
1627
+ dsl["assets"].append({"assetId": ref, "type": "audio", "source": "gen-voice",
1628
+ "status": "planned", "payload": {"voiceId": resolved_voice_id}})
1629
+ dsl["scenes"].append(current)
1630
+
1631
+
1632
+ if container_parts:
1633
+ dsl["meta"]["mergeScenePaths"] = ["customPayload." + ".".join(container_parts)]
1634
+ if _schema_at(template_config, container_parts + ["defaultHoldSec"]):
1635
+ _lock_scene_path(dsl, "customPayload." + ".".join(container_parts + ["defaultHoldSec"]))
1636
+ for parts, _ in scene_defaults:
1637
+ _lock_scene_path(dsl, "customPayload." + ".".join(parts))
1638
+ if _schema_at(template_config, container_parts + ["minSec"]):
1639
+ _lock_scene_path(dsl, "customPayload." + ".".join(container_parts + ["minSec"]))
1640
+
1538
1641
  return dsl
1539
1642
 
1540
1643
 
@@ -1918,6 +2021,13 @@ def _apply_scene_payload(
1918
2021
  raise ValueError("--payload / --motion need --template-id: the allowed keys come from the template.")
1919
2022
 
1920
2023
  writes = _parse_payload_specs(template_config, payload_specs)
2024
+ binding = _resolve_media_binding(template_config)
2025
+ if binding.get("sceneMode") == "per-item":
2026
+
2027
+
2028
+ item_path = binding["itemsPath"].split(".")
2029
+ allocated = (item_path, item_path[:-1] + ["minSec"])
2030
+ writes = [(parts, value) for parts, value in writes if parts not in allocated]
1921
2031
  if motion:
1922
2032
  allowed = ("auto", "static", "kenburns-in", "kenburns-out", "pan-left", "pan-right", "float")
1923
2033
  if motion not in allowed:
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V23",
5
+ "version": "V24",
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
  }
@@ -112,6 +112,8 @@ This skill is the **first half** of a two-phase user-confirmation flow. After it
112
112
 
113
113
  Read the `job_id` from the stdout line `📦 render job jobId: N` and remember it; asset URLs come from the `🔊 TTS audio:` and `🖼 Image assets:` sections. Show images inline (`![name](https://...)`) and audio as links (`[🔊 listen](http://cdn.../x.mp3)`) so the user can actually review them. The recommended summary format:
114
114
 
115
+ When `__scene_assets__` is present, the web UI renders the full scene preview from that structured timeline: all visuals, scene times and each scene's audio slice. Give a brief overview/status and request confirmation; do not duplicate the preview or copy URLs into prose. For CLI clients without this UI, render every referenced visual and the corresponding audio per scene from the marker. With older outputs, use the fallback table below, including **all** visuals in each scene rather than one representative image. Scene count is independent of the number of TTS synthesis calls. Visual captions must not automatically become narration.
116
+
115
117
  ```markdown
116
118
  ## Render confirmation
117
119
 
@@ -2,6 +2,6 @@
2
2
  "skillName": "prepare-video-assets",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "476",
5
- "version": "V7",
5
+ "version": "V8",
6
6
  "skillDescription": "Asset-preparation skill: resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a `job_id` for the subsequent `render_video` call.\n\nUse this skill as soon as the user mentions any of these intents:\n- Generate / prepare video assets, resolve assets, render-ready\n- \"Make me a video about X\" (the agent calls gen_script → prepare_video_assets → render_video)\n- Regenerate one asset (image / audio) for a specific scene\n\nNext step: after the user confirms the resolved assets, call `render_video` with the `job_id` returned by this skill.\n\n⚠️ Stop-and-confirm gate: this skill runs only after the user has confirmed the script, and after it returns you must show the resolved assets and wait for the user's explicit confirmation. Never call `render_video` in the same turn."
7
7
  }