@remixmate/cli 0.13.2 → 0.13.4
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/handlers/gen-digital-human.js +29 -23
- package/dist/manifest.json +8 -3
- package/package.json +1 -1
- package/skills/gen-digital-human/SKILL.md +7 -4
- package/skills/gen-digital-human/skill.json +6 -1
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +49 -3
- package/skills/render-video/scripts/render_video.py +61 -11
- package/skills/render-video/version.json +1 -1
|
@@ -1,33 +1,39 @@
|
|
|
1
|
-
var
|
|
2
|
-
`);return}if(
|
|
3
|
-
|
|
1
|
+
var k=Object.defineProperty;var f=(t,e)=>k(t,"name",{value:e,configurable:!0});import{mmPost as p,pollUntil as y,resolveHttpContext as U,SkillError as i}from"../http.js";import{getCapabilities as b}from"../capabilities.js";import{emitProgress as m}from"../progress.js";import{isTrue as w,toNumber as M}from"./shared.js";const E=["video","image"],S="hifly";function l(t){return t==null?"":String(t).trim()}f(l,"toId");async function N(t,e){const r=w(e.mine),a=!r&&w(e.all),n=!r&&!a,s={current:1,pageSize:100};n?s.featured=!0:s.source=S,r&&(s.mine=!0),e.gender&&(s.gender=String(e.gender)),e.name&&(s.name=String(e.name));const g=await p(t,"/digital-human/avatar/page",s,{timeoutMs:3e4}),o=g.list??[];if(w(e.json_output)){process.stdout.write(JSON.stringify({avatars:o})+`
|
|
2
|
+
`);return}if(o.length===0){process.stdout.write(n?`⚠️ No curated public avatars match. Try --all for the full inventory, or --mine for your own.
|
|
3
|
+
`:`⚠️ No avatars available
|
|
4
|
+
`);return}if(n){process.stdout.write(`
|
|
5
|
+
${"ID".padEnd(24)} ${"Name".padEnd(16)} ${"Gender".padEnd(8)} ${"Ratio".padEnd(6)} ${"Scenes".padEnd(22)} Voice
|
|
6
|
+
`),process.stdout.write(`${"-".repeat(24)} ${"-".repeat(16)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(22)} ${"-".repeat(12)}
|
|
7
|
+
`);for(const d of o)process.stdout.write(`${l(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${(d.tags??[]).join(",").padEnd(22)} ${d.defaultVoiceId||"-"}
|
|
8
|
+
`)}else{process.stdout.write(`
|
|
4
9
|
${"ID".padEnd(24)} ${"Name".padEnd(16)} ${"Source".padEnd(14)} ${"Gender".padEnd(8)} ${"Ratio".padEnd(6)} Clone
|
|
5
10
|
`),process.stdout.write(`${"-".repeat(24)} ${"-".repeat(16)} ${"-".repeat(14)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(8)}
|
|
6
|
-
`);for(const d of
|
|
7
|
-
`)
|
|
8
|
-
${
|
|
9
|
-
`),process.stdout.write(
|
|
10
|
-
|
|
11
|
+
`);for(const d of o)process.stdout.write(`${l(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.source??"").padEnd(14)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${d.cloneStatus||"-"}
|
|
12
|
+
`)}const u=g.pager?.total??o.length;process.stdout.write(`
|
|
13
|
+
${o.length} avatar(s) shown${u>o.length?` of ${u}; narrow with --name`:""}
|
|
14
|
+
`),process.stdout.write(n?`💡 Use --avatar-id <ID> to pick an avatar; pass its Voice as --voice-id for a matching voice
|
|
15
|
+
`:`💡 Use --avatar-id <ID> to pick an avatar for generation
|
|
16
|
+
`)}f(N,"listAvatars");async function I(t,e){return((await p(t,"/digital-human/avatar/page",{current:1,pageSize:100,mine:!0,source:S},{timeoutMs:3e4})).list??[]).find(n=>l(n.id)===e)??{id:e,source:S}}f(I,"fetchAvatarInfo");function O(t,e){const r=(t.source??"").trim()||S;if(!e.includes(r))throw new i(`❌ avatar uses provider "${r}", which is not available (supported: ${e.join("/")}). Pick another avatar from --list-avatars.`);return r}f(O,"resolveSource");async function T(t,e){const r=l(e.clone_url);if(!/^https?:\/\//i.test(r))throw new i("❌ --clone needs --clone-url <public http(s) URL of a video or portrait image> (local files are not uploaded)");const a=l(e.name);if(!a)throw new i("❌ --clone needs --name <avatar name>");const n=l(e.clone_type)||"video";if(!E.includes(n))throw new i(`❌ --clone-type must be one of ${E.join(" / ")}, received: ${n}`);const s={name:a,sourceType:n,fileUrl:r};e.thumbnail_url&&(s.thumbnailUrl=l(e.thumbnail_url)),e.gender&&(s.gender=l(e.gender)),m({phase:"gen-digital-human:clone-request",sourceType:n});const g=await p(t,"/digital-human/avatar/clone",s,{timeoutMs:6e4}),o=l(g.id);if(!o)throw new i(`❌ could not parse the new avatar id; API returned: ${JSON.stringify(g)}`);m({phase:"gen-digital-human:clone-async",avatarId:o});const u=w(e.json_output);if(e.wait!==void 0&&!w(e.wait)){if(u){process.stdout.write(JSON.stringify({avatarId:o,cloneStatus:g.cloneStatus??"cloning"})+`
|
|
11
17
|
`);return}process.stdout.write(`
|
|
12
|
-
🧬 Clone submitted, avatar id: ${
|
|
13
|
-
`),process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${
|
|
14
|
-
`);return}let
|
|
18
|
+
🧬 Clone submitted, avatar id: ${o}
|
|
19
|
+
`),process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${o}
|
|
20
|
+
`);return}let d;try{d=await y(async()=>{const c=await p(t,"/digital-human/avatar/clone-status",{id:o},{timeoutMs:3e4}),h=c.cloneStatus??"unknown";if(m({phase:"gen-digital-human:clone-poll",avatarId:o,status:h}),h==="done")return c;if(h==="failed")throw new i(`❌ avatar clone failed: ${c.cloneError||"unknown error"}`);return null},{intervalMs:1e4,timeoutMs:20*6e4})}catch(c){throw c instanceof i&&/polling timed out/.test(c.message)?new i(`❌ avatar ${o} is still cloning; rerun --clone-status --avatar-id ${o} later`):c}if(u){process.stdout.write(JSON.stringify({avatarId:o,cloneStatus:d.cloneStatus})+`
|
|
15
21
|
`);return}process.stdout.write(`
|
|
16
|
-
✅ Avatar cloned, id: ${
|
|
17
|
-
`),process.stdout.write(`💡 Generate with --avatar-id ${
|
|
18
|
-
`)}
|
|
22
|
+
✅ Avatar cloned, id: ${o}
|
|
23
|
+
`),process.stdout.write(`💡 Generate with --avatar-id ${o} --text "..."
|
|
24
|
+
`)}f(T,"cloneAvatar");async function R(t,e,r){const a=await p(t,"/digital-human/avatar/clone-status",{id:e},{timeoutMs:3e4}),n=a.cloneStatus||"unknown";if(r){process.stdout.write(JSON.stringify({avatarId:e,cloneStatus:n,error:a.cloneError||null})+`
|
|
19
25
|
`);return}process.stdout.write(`
|
|
20
|
-
clone status: ${
|
|
21
|
-
`),
|
|
22
|
-
`)}
|
|
26
|
+
clone status: ${n}
|
|
27
|
+
`),n==="failed"&&process.stdout.write(` failure reason: ${a.cloneError||"unknown error"}
|
|
28
|
+
`)}f(R,"checkCloneStatus");async function C(t,e,r){const a=await p(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),n=a.status??"unknown";if(r){process.stdout.write(JSON.stringify({status:n,url:a.videoUrl??null,generationId:e})+`
|
|
23
29
|
`);return}if(process.stdout.write(`
|
|
24
|
-
task status: ${
|
|
25
|
-
`),
|
|
26
|
-
`),
|
|
27
|
-
`)}}
|
|
30
|
+
task status: ${n}
|
|
31
|
+
`),a.videoUrl&&process.stdout.write(` video URL: ${a.videoUrl}
|
|
32
|
+
`),n==="failed"){const s=a.errorMessage??a.error??a.message??"unknown error";process.stdout.write(` failure reason: ${s}
|
|
33
|
+
`)}}f(C,"checkStatus");const j=30*6e4;async function J(t,e){return await y(async()=>{const a=await p(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),n=a.status??"unknown";if(m({phase:"gen-digital-human:poll",generationId:e,status:n}),n==="completed")return a.videoUrl??"";if(n==="failed"){const s=a.errorMessage??a.error??a.message??"unknown error";throw new i(`❌ video generation failed: ${s}`)}return null},{intervalMs:5e3,timeoutMs:j}).catch(a=>{throw a instanceof i&&/polling timed out/.test(a.message)?new i(`${a.message}; still generating on the server — resume with --check-status --generation-id ${e}`):a})}f(J,"pollVideoStatus");async function x(t,e){const r=await U(e.skillName,{apiBaseUrl:t.api_base_url,privateToken:t.priv_token,preflightToken:e.auth?.token}),a=w(t.json_output);if(w(t.list_avatars))return N(r,t);if(w(t.clone))return T(r,t);if(w(t.clone_status)){const $=l(t.avatar_id);if(!$)throw new i("❌ clone-status mode requires --avatar-id");return R(r,$,a)}if(w(t.check_status)){const $=M(t.generation_id,"generation_id");if($===void 0)throw new i("❌ check-status mode requires --generation-id");return C(r,$,a)}const n=l(t.avatar_id);if(!n)throw new i("❌ provide --avatar-id to pick the avatar (use --list-avatars to see available avatars)");const s=(t.audio_url??"").trim(),g=t.text??"";if(s&&!/^https?:\/\//i.test(s))throw new i(`❌ --audio-url needs a public http(s) URL (local files are not uploaded), received: ${s}`);if(!s&&!g.trim())throw new i("❌ in TTS mode provide --text, or use --audio-url for audio-driven mode");const o=(await b(t.api_base_url)).digitalHuman;m({phase:"gen-digital-human:resolve-avatar",avatarId:n});const u=await I(r,n);if(u.cloneStatus==="cloning")throw new i(`❌ avatar ${n} is still cloning; check with --clone-status --avatar-id ${n}`);if(u.cloneStatus==="failed")throw new i(`❌ avatar ${n} failed to clone (${u.cloneError||"unknown error"}); clone it again`);const d=O(u,o.sources),c={avatarId:n,source:d};s?c.audioUrl=s:(c.text=g,c.voiceId=t.voice_id??o.defaultVoiceId,t.voice_name&&(c.voiceName=String(t.voice_name))),m({phase:"gen-digital-human:request",avatarId:n,source:d,mode:s?"audio":"tts"});const h=await p(r,"/digital-human/video/generate",c,{timeoutMs:6e4}),v=h.generationId;if(!v)throw new i(`❌ could not parse generationId; API returned: ${JSON.stringify(h)}`);m({phase:"gen-digital-human:async",generationId:v});const _=await J(r,v);if(!_)throw new i(`❌ video generated but no URL was returned; rerun --check-status --generation-id ${v} later`);if(a){process.stdout.write(JSON.stringify({url:_,generationId:v})+`
|
|
28
34
|
`);return}process.stdout.write(`
|
|
29
35
|
🔗 Video URL (renders directly, no download needed):
|
|
30
|
-
${
|
|
36
|
+
${_}
|
|
31
37
|
`),process.stdout.write(`
|
|
32
38
|
🎉 Digital-human video generation done!
|
|
33
|
-
`)}
|
|
39
|
+
`)}f(x,"genDigitalHuman");export{x as genDigitalHuman};
|
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.4",
|
|
4
|
+
"generatedAt": "2026-10-26T01:31:08.439Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -195,12 +195,16 @@
|
|
|
195
195
|
"properties": {
|
|
196
196
|
"list_avatars": {
|
|
197
197
|
"type": "boolean",
|
|
198
|
-
"description": "List available digital-human avatars"
|
|
198
|
+
"description": "List available digital-human avatars. By default lists the curated public avatars (with scene tags and a suggested voice); see mine / all for other scopes."
|
|
199
199
|
},
|
|
200
200
|
"mine": {
|
|
201
201
|
"type": "boolean",
|
|
202
202
|
"description": "With list_avatars=true: list the caller's own custom avatars instead of the public catalog. Custom avatars do not appear without this."
|
|
203
203
|
},
|
|
204
|
+
"all": {
|
|
205
|
+
"type": "boolean",
|
|
206
|
+
"description": "With list_avatars=true: list the full public HiFly inventory (500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
|
|
207
|
+
},
|
|
204
208
|
"name": {
|
|
205
209
|
"type": "string",
|
|
206
210
|
"description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
|
|
@@ -294,6 +298,7 @@
|
|
|
294
298
|
"wait",
|
|
295
299
|
"clone_status",
|
|
296
300
|
"mine",
|
|
301
|
+
"all",
|
|
297
302
|
"name",
|
|
298
303
|
"gender",
|
|
299
304
|
"check_status",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
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",
|
|
@@ -25,7 +25,7 @@ Wraps ab-api's digital-human endpoints (the same ones the web studio uses), auth
|
|
|
25
25
|
|
|
26
26
|
| Capability | Description | API |
|
|
27
27
|
|------------|-------------|-----|
|
|
28
|
-
| List avatars |
|
|
28
|
+
| List avatars | Curated public avatars (default), your own with `--mine`, or the full inventory with `--all` | `POST /digital-human/avatar/page` |
|
|
29
29
|
| Text-driven | avatar + text + voice → speech is synthesized, then drives the avatar | `POST /digital-human/video/generate` |
|
|
30
30
|
| Audio-driven | avatar + existing audio URL → talking-head video | `POST /digital-human/video/generate` |
|
|
31
31
|
| Check status | Poll a generation job | `POST /digital-human/video/status` |
|
|
@@ -53,11 +53,14 @@ Generation typically takes 1–5 minutes; the handler polls and emits `__progres
|
|
|
53
53
|
### 1. List avatars
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
remixmate gen-digital-human --list-avatars
|
|
57
|
-
remixmate gen-digital-human --list-avatars --
|
|
58
|
-
remixmate gen-digital-human --list-avatars --
|
|
56
|
+
remixmate gen-digital-human --list-avatars # curated public avatars (default)
|
|
57
|
+
remixmate gen-digital-human --list-avatars --gender female # curated, filtered by gender
|
|
58
|
+
remixmate gen-digital-human --list-avatars --mine # your cloned avatars
|
|
59
|
+
remixmate gen-digital-human --list-avatars --all --name "教师" # full HiFly inventory (500+; always narrow with --name)
|
|
59
60
|
```
|
|
60
61
|
|
|
62
|
+
The default list is the **curated public library**: avatars an admin picked, with a cover, scene tags (`education` / `medical` / `news` / `business` / `culture` / `outdoor` / `lifestyle` / `festival` / `ecommerce`) and usually a suggested voice. Prefer it — pick the avatar whose scenes match the script, and pass its `Voice` column as `--voice-id` unless the user chose a voice. Fall back to `--all` only when nothing curated fits; those entries have no tags or suggested voice.
|
|
63
|
+
|
|
61
64
|
### 2. Generate a video
|
|
62
65
|
|
|
63
66
|
Pick exactly one driver: **text** (+ voice) or **audio**. With `--audio-url`, text and voice are ignored.
|
|
@@ -21,12 +21,16 @@
|
|
|
21
21
|
"properties": {
|
|
22
22
|
"list_avatars": {
|
|
23
23
|
"type": "boolean",
|
|
24
|
-
"description": "List available digital-human avatars"
|
|
24
|
+
"description": "List available digital-human avatars. By default lists the curated public avatars (with scene tags and a suggested voice); see mine / all for other scopes."
|
|
25
25
|
},
|
|
26
26
|
"mine": {
|
|
27
27
|
"type": "boolean",
|
|
28
28
|
"description": "With list_avatars=true: list the caller's own custom avatars instead of the public catalog. Custom avatars do not appear without this."
|
|
29
29
|
},
|
|
30
|
+
"all": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"description": "With list_avatars=true: list the full public HiFly inventory (500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
|
|
33
|
+
},
|
|
30
34
|
"name": {
|
|
31
35
|
"type": "string",
|
|
32
36
|
"description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
|
|
@@ -120,6 +124,7 @@
|
|
|
120
124
|
"wait",
|
|
121
125
|
"clone_status",
|
|
122
126
|
"mine",
|
|
127
|
+
"all",
|
|
123
128
|
"name",
|
|
124
129
|
"gender",
|
|
125
130
|
"check_status",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-digital-human",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "336",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V11",
|
|
6
6
|
"skillDescription": "Digital-human (talking-head) skill backed by HiFly: list avatars, clone a custom avatar from a video or photo, drive an avatar with text (TTS) or an existing audio URL, and check the status of pending jobs.\n\nUse this skill immediately whenever the user asks for any of:\n- Digital human, talking-head video, AI presenter, virtual host\n- \"Make a talking-head video out of this script / this audio file\"\n- List or browse digital-human avatars\n- Clone / create a custom avatar from a video or photo\n\nEven without an explicit \"use AI\", any request that turns text or audio into a synthesized presenter video should route here."
|
|
7
7
|
}
|
|
@@ -1104,7 +1104,38 @@ def _estimate_fit_caption_duration(
|
|
|
1104
1104
|
return max(parts) + duration_model.get("tailPaddingSec", 0)
|
|
1105
1105
|
|
|
1106
1106
|
|
|
1107
|
-
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
_READING_UNIT_RE = re.compile(r"[\u3400-\u9fff\uf900-\ufaff]|[A-Za-z0-9]+")
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _reading_units(text: str) -> int:
|
|
1113
|
+
return len(_READING_UNIT_RE.findall(text or ""))
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _sequence_floor_frames(sequence: dict, preset_timing: dict, text: str, fps: int) -> int:
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
min_sec = sequence.get("minSec")
|
|
1123
|
+
if not (isinstance(min_sec, (int, float)) and min_sec > 0):
|
|
1124
|
+
floor = preset_timing.get("readingFloor")
|
|
1125
|
+
units = _reading_units(text)
|
|
1126
|
+
if not isinstance(floor, dict) or units == 0:
|
|
1127
|
+
return 0
|
|
1128
|
+
min_sec = floor["baseSec"] + floor["secPerUnit"] * units
|
|
1129
|
+
return math.floor(min_sec * fps + 0.5)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps: int = 30,
|
|
1133
|
+
headline: str = "") -> float:
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
|
|
1108
1139
|
|
|
1109
1140
|
|
|
1110
1141
|
|
|
@@ -1138,7 +1169,22 @@ def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps:
|
|
|
1138
1169
|
return max(1, math.floor(item.get("holdSec", item["sourceDurationSec"]) * fps + 0.5))
|
|
1139
1170
|
return max(min_frames, math.floor(item.get("holdSec", base_hold) * fps + 0.5))
|
|
1140
1171
|
|
|
1141
|
-
|
|
1172
|
+
frames = [item_frames(item) for item in sequence["items"]]
|
|
1173
|
+
natural = sum(frames)
|
|
1174
|
+
|
|
1175
|
+
title_override = ((sequence.get("text") or {}).get("title") or {}).get("text")
|
|
1176
|
+
candidates = (title_override, custom_payload.get("title"),
|
|
1177
|
+
(custom_payload.get("templateData") or {}).get("title"), headline)
|
|
1178
|
+
text = next((c for c in candidates if isinstance(c, str) and c.strip()), "")
|
|
1179
|
+
floor_frames = _sequence_floor_frames(sequence, preset_timing, text, fps)
|
|
1180
|
+
if floor_frames > natural:
|
|
1181
|
+
if all(item.get("type") == "video" for item in sequence["items"]):
|
|
1182
|
+
rate = timing.get("floorMinVideoRate")
|
|
1183
|
+
if rate is None:
|
|
1184
|
+
raise ValueError(f"fit-images with a floor requires durationModel.{key}.floorMinVideoRate")
|
|
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
|
|
1142
1188
|
|
|
1143
1189
|
|
|
1144
1190
|
def _probe_sequence_video_duration(url: str) -> float | None:
|
|
@@ -1391,7 +1437,7 @@ def _build_carousel_caption_dsl(
|
|
|
1391
1437
|
|
|
1392
1438
|
effective_duration = duration
|
|
1393
1439
|
if duration_strategy == "fit-images":
|
|
1394
|
-
effective_duration = _estimate_fit_images_duration(custom_payload, duration_model)
|
|
1440
|
+
effective_duration = _estimate_fit_images_duration(custom_payload, duration_model, headline=headline)
|
|
1395
1441
|
if duration_strategy == "fit-caption":
|
|
1396
1442
|
est = _estimate_fit_caption_duration(custom_payload, duration_model, template_id)
|
|
1397
1443
|
if est > 0:
|
|
@@ -1196,6 +1196,11 @@ def resolve_asset_video(asset: dict, private_token: str, timeout: int) -> dict:
|
|
|
1196
1196
|
|
|
1197
1197
|
def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -> dict:
|
|
1198
1198
|
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
|
|
1199
1204
|
payload = asset.get("payload", {})
|
|
1200
1205
|
|
|
1201
1206
|
try:
|
|
@@ -1203,6 +1208,9 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
1203
1208
|
except RuntimeError as e:
|
|
1204
1209
|
return {"status": "failed", "error": str(e)}
|
|
1205
1210
|
|
|
1211
|
+
if asset.get("_dhGenerationId"):
|
|
1212
|
+
return _wait_digital_human_generation(cmd, int(asset["_dhGenerationId"]), private_token, max(timeout, DH_POLL_TIMEOUT_SEC))
|
|
1213
|
+
|
|
1206
1214
|
if payload.get("avatarId"):
|
|
1207
1215
|
cmd.extend(["--avatar-id", str(payload["avatarId"])])
|
|
1208
1216
|
if payload.get("text"):
|
|
@@ -1218,7 +1226,8 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
1218
1226
|
if private_token:
|
|
1219
1227
|
cmd.extend(["--priv-token", private_token])
|
|
1220
1228
|
|
|
1221
|
-
|
|
1229
|
+
|
|
1230
|
+
dh_timeout = max(timeout, DH_POLL_TIMEOUT_SEC + 120)
|
|
1222
1231
|
try:
|
|
1223
1232
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=dh_timeout, stdin=subprocess.DEVNULL)
|
|
1224
1233
|
absorb_child_billing(result.stdout)
|
|
@@ -1226,23 +1235,64 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
1226
1235
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
1227
1236
|
line = line.strip()
|
|
1228
1237
|
if line.startswith("http"):
|
|
1229
|
-
|
|
1238
|
+
return _digital_human_result(line)
|
|
1239
|
+
stderr = result.stderr.strip()
|
|
1240
|
+
resume = _DH_RESUME_RE.search(stderr)
|
|
1241
|
+
if resume:
|
|
1230
1242
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
meta = probe_video_meta(line)
|
|
1234
|
-
if meta.get("width") and meta.get("height"):
|
|
1235
|
-
out["width"], out["height"] = meta["width"], meta["height"]
|
|
1236
|
-
if meta.get("duration_ms"):
|
|
1237
|
-
out["duration_ms"] = meta["duration_ms"]
|
|
1238
|
-
return out
|
|
1239
|
-
return {"status": "failed", "error": result.stderr.strip()[:1000]}
|
|
1243
|
+
asset["_dhGenerationId"] = int(resume.group(1))
|
|
1244
|
+
return {"status": "failed", "error": stderr[:1000]}
|
|
1240
1245
|
except subprocess.TimeoutExpired:
|
|
1241
1246
|
return {"status": "failed", "error": f"Digital human generation timed out after {dh_timeout}s"}
|
|
1242
1247
|
except Exception as e:
|
|
1243
1248
|
return {"status": "failed", "error": str(e)[:1000]}
|
|
1244
1249
|
|
|
1245
1250
|
|
|
1251
|
+
DH_POLL_TIMEOUT_SEC = 30 * 60
|
|
1252
|
+
DH_STATUS_POLL_INTERVAL_SEC = 10
|
|
1253
|
+
|
|
1254
|
+
_DH_RESUME_RE = re.compile(r"--check-status --generation-id (\d+)")
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def _digital_human_result(url: str) -> dict:
|
|
1258
|
+
out = {"status": "generated", "url": url}
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
from _video_probe import probe_video_meta
|
|
1262
|
+
meta = probe_video_meta(url)
|
|
1263
|
+
if meta.get("width") and meta.get("height"):
|
|
1264
|
+
out["width"], out["height"] = meta["width"], meta["height"]
|
|
1265
|
+
if meta.get("duration_ms"):
|
|
1266
|
+
out["duration_ms"] = meta["duration_ms"]
|
|
1267
|
+
return out
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def _wait_digital_human_generation(base_cmd: list, generation_id: int, private_token: str, timeout: int) -> dict:
|
|
1271
|
+
|
|
1272
|
+
cmd = [*base_cmd, "--check-status", "--generation-id", str(generation_id), "--json-output"]
|
|
1273
|
+
if private_token:
|
|
1274
|
+
cmd.extend(["--priv-token", private_token])
|
|
1275
|
+
deadline = time.time() + timeout
|
|
1276
|
+
while time.time() < deadline:
|
|
1277
|
+
try:
|
|
1278
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, stdin=subprocess.DEVNULL)
|
|
1279
|
+
line = (result.stdout.strip().split("\n") or [""])[-1]
|
|
1280
|
+
data = json.loads(line) if line.startswith("{") else {}
|
|
1281
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError):
|
|
1282
|
+
data = {}
|
|
1283
|
+
status = data.get("status")
|
|
1284
|
+
if status == "completed" and data.get("url"):
|
|
1285
|
+
return _digital_human_result(data["url"])
|
|
1286
|
+
if status == "failed":
|
|
1287
|
+
return {"status": "failed", "error": f"digital-human generation {generation_id} failed"}
|
|
1288
|
+
time.sleep(DH_STATUS_POLL_INTERVAL_SEC)
|
|
1289
|
+
return {
|
|
1290
|
+
"status": "failed",
|
|
1291
|
+
"error": f"digital-human generation {generation_id} still not done after {timeout}s; "
|
|
1292
|
+
f"check later with gen-digital-human --check-status --generation-id {generation_id}",
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
|
|
1246
1296
|
ASSET_RESOLVERS = {
|
|
1247
1297
|
"gen-image": resolve_asset_image,
|
|
1248
1298
|
"gen-voice": resolve_asset_audio,
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V40",
|
|
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
|
}
|