@remixmate/cli 0.9.54 → 0.9.56

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,12 +1,17 @@
1
1
  /**
2
- * gen-digital-human handler — talking-head video, three modes:
3
- * --list-avatars true → POST /digital-human/avatar/page (list)
4
- * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
5
- * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
2
+ * gen-digital-human handler — talking-head video + custom avatars, five modes:
3
+ * --list-avatars true → POST /digital-human/avatar/page (list)
4
+ * --clone --clone-url <url> --name <n> → POST /digital-human/avatar/clone, then poll clone-status
5
+ * --clone-status --avatar-id <id> → POST /digital-human/avatar/clone-status (one-shot)
6
+ * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
7
+ * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
6
8
  *
7
9
  * Ported from `scripts/gen_digital_human_video.py` (python → http) so it shares
8
10
  * auth / base-URL / business-code handling with the other generators via
9
- * http.ts. Endpoints and payloads match the backend DigitalHumanVideoGenerateDTO.
11
+ * http.ts. Endpoints and payloads match the backend DigitalHuman* DTOs.
12
+ *
13
+ * Avatar ids are the backend's opaque uids (`dh_...`); legacy numeric ids are
14
+ * still accepted as strings.
10
15
  */
11
16
  import type { HandlerContext, HandlerInput } from './index.js';
12
17
  export declare function genDigitalHuman(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
@@ -1,17 +1,28 @@
1
1
  /**
2
- * gen-digital-human handler — talking-head video, three modes:
3
- * --list-avatars true → POST /digital-human/avatar/page (list)
4
- * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
5
- * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
2
+ * gen-digital-human handler — talking-head video + custom avatars, five modes:
3
+ * --list-avatars true → POST /digital-human/avatar/page (list)
4
+ * --clone --clone-url <url> --name <n> → POST /digital-human/avatar/clone, then poll clone-status
5
+ * --clone-status --avatar-id <id> → POST /digital-human/avatar/clone-status (one-shot)
6
+ * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
7
+ * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
6
8
  *
7
9
  * Ported from `scripts/gen_digital_human_video.py` (python → http) so it shares
8
10
  * auth / base-URL / business-code handling with the other generators via
9
- * http.ts. Endpoints and payloads match the backend DigitalHumanVideoGenerateDTO.
11
+ * http.ts. Endpoints and payloads match the backend DigitalHuman* DTOs.
12
+ *
13
+ * Avatar ids are the backend's opaque uids (`dh_...`); legacy numeric ids are
14
+ * still accepted as strings.
10
15
  */
11
16
  import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
12
17
  import { getCapabilities } from '../capabilities.js';
13
18
  import { emitProgress } from '../progress.js';
14
19
  import { isTrue, toNumber } from './shared.js';
20
+ const CLONE_TYPES = ['video', 'image'];
21
+ function toId(value) {
22
+ if (value === undefined || value === null)
23
+ return '';
24
+ return String(value).trim();
25
+ }
15
26
  async function listAvatars(ctx, input) {
16
27
  const payload = { current: 1, pageSize: 100 };
17
28
  if (input.source)
@@ -32,10 +43,10 @@ async function listAvatars(ctx, input) {
32
43
  process.stdout.write('⚠️ No avatars available\n');
33
44
  return;
34
45
  }
35
- process.stdout.write(`\n${'ID'.padEnd(8)} ${'Name'.padEnd(16)} ${'Source'.padEnd(10)} ${'Gender'.padEnd(8)} Ratio\n`);
36
- process.stdout.write(`${'-'.repeat(8)} ${'-'.repeat(16)} ${'-'.repeat(10)} ${'-'.repeat(8)} ${'-'.repeat(10)}\n`);
46
+ process.stdout.write(`\n${'ID'.padEnd(24)} ${'Name'.padEnd(16)} ${'Source'.padEnd(14)} ${'Gender'.padEnd(8)} ${'Ratio'.padEnd(6)} Clone\n`);
47
+ process.stdout.write(`${'-'.repeat(24)} ${'-'.repeat(16)} ${'-'.repeat(14)} ${'-'.repeat(8)} ${'-'.repeat(6)} ${'-'.repeat(8)}\n`);
37
48
  for (const a of avatars) {
38
- process.stdout.write(`${String(a.id ?? '').padEnd(8)} ${String(a.name ?? '').padEnd(16)} ${String(a.source ?? '').padEnd(10)} ${String(a.gender ?? '').padEnd(8)} ${a.aspectRatio ?? ''}\n`);
49
+ process.stdout.write(`${toId(a.id).padEnd(24)} ${String(a.name ?? '').padEnd(16)} ${String(a.source ?? '').padEnd(14)} ${String(a.gender ?? '').padEnd(8)} ${String(a.aspectRatio ?? '').padEnd(6)} ${a.cloneStatus || '-'}\n`);
39
50
  }
40
51
  process.stdout.write(`\n${avatars.length} avatar(s) available\n`);
41
52
  process.stdout.write('💡 Use --avatar-id <ID> to pick an avatar for generation\n');
@@ -44,7 +55,7 @@ async function listAvatars(ctx, input) {
44
55
  async function fetchAvatarInfo(ctx, avatarId) {
45
56
  for (const extra of [{}, { mine: true }]) {
46
57
  const data = await mmPost(ctx, '/digital-human/avatar/page', { current: 1, pageSize: 100, ...extra }, { timeoutMs: 30_000 });
47
- const found = (data.list ?? []).find((a) => String(a.id) === String(avatarId));
58
+ const found = (data.list ?? []).find((a) => toId(a.id) === avatarId);
48
59
  if (found)
49
60
  return found;
50
61
  }
@@ -57,6 +68,81 @@ function resolveSource(explicit, avatar, validSources) {
57
68
  }
58
69
  return src;
59
70
  }
71
+ // ── Avatar cloning (HiFly) ─────────────────────────────────────────────────
72
+ async function cloneAvatar(ctx, input) {
73
+ const fileUrl = toId(input.clone_url);
74
+ if (!/^https?:\/\//i.test(fileUrl)) {
75
+ throw new SkillError('❌ --clone needs --clone-url <public http(s) URL of a video or portrait image> (local files are not uploaded)');
76
+ }
77
+ const name = toId(input.name);
78
+ if (!name)
79
+ throw new SkillError('❌ --clone needs --name <avatar name>');
80
+ const sourceType = toId(input.clone_type) || 'video';
81
+ if (!CLONE_TYPES.includes(sourceType)) {
82
+ throw new SkillError(`❌ --clone-type must be one of ${CLONE_TYPES.join(' / ')}, received: ${sourceType}`);
83
+ }
84
+ const payload = { name, sourceType, fileUrl };
85
+ if (input.thumbnail_url)
86
+ payload.thumbnailUrl = toId(input.thumbnail_url);
87
+ if (input.gender)
88
+ payload.gender = toId(input.gender);
89
+ if (input.aspect_ratio)
90
+ payload.aspectRatio = toId(input.aspect_ratio);
91
+ emitProgress({ phase: 'gen-digital-human:clone-request', sourceType });
92
+ const data = await mmPost(ctx, '/digital-human/avatar/clone', payload, { timeoutMs: 60_000 });
93
+ const avatarId = toId(data.id);
94
+ if (!avatarId) {
95
+ throw new SkillError(`❌ could not parse the new avatar id; API returned: ${JSON.stringify(data)}`);
96
+ }
97
+ emitProgress({ phase: 'gen-digital-human:clone-async', avatarId });
98
+ const json = isTrue(input.json_output);
99
+ if (input.wait !== undefined && !isTrue(input.wait)) {
100
+ if (json) {
101
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: data.cloneStatus ?? 'cloning' }) + '\n');
102
+ return;
103
+ }
104
+ process.stdout.write(`\n🧬 Clone submitted, avatar id: ${avatarId}\n`);
105
+ process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${avatarId}\n`);
106
+ return;
107
+ }
108
+ let final;
109
+ try {
110
+ final = await pollUntil(async () => {
111
+ const s = await mmPost(ctx, '/digital-human/avatar/clone-status', { id: avatarId }, { timeoutMs: 30_000 });
112
+ const status = s.cloneStatus ?? 'unknown';
113
+ emitProgress({ phase: 'gen-digital-human:clone-poll', avatarId, status });
114
+ if (status === 'done')
115
+ return s;
116
+ if (status === 'failed')
117
+ throw new SkillError(`❌ avatar clone failed: ${s.cloneError || 'unknown error'}`);
118
+ return null;
119
+ }, { intervalMs: 10_000, timeoutMs: 20 * 60_000 });
120
+ }
121
+ catch (err) {
122
+ if (err instanceof SkillError && /polling timed out/.test(err.message)) {
123
+ throw new SkillError(`❌ avatar ${avatarId} is still cloning; rerun --clone-status --avatar-id ${avatarId} later`);
124
+ }
125
+ throw err;
126
+ }
127
+ if (json) {
128
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: final.cloneStatus }) + '\n');
129
+ return;
130
+ }
131
+ process.stdout.write(`\n✅ Avatar cloned, id: ${avatarId}\n`);
132
+ process.stdout.write(`💡 Generate with --avatar-id ${avatarId} --text "..."\n`);
133
+ }
134
+ async function checkCloneStatus(ctx, avatarId, json) {
135
+ const s = await mmPost(ctx, '/digital-human/avatar/clone-status', { id: avatarId }, { timeoutMs: 30_000 });
136
+ const status = s.cloneStatus || 'unknown';
137
+ if (json) {
138
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: status, error: s.cloneError || null }) + '\n');
139
+ return;
140
+ }
141
+ process.stdout.write(`\nclone status: ${status}\n`);
142
+ if (status === 'failed')
143
+ process.stdout.write(` failure reason: ${s.cloneError || 'unknown error'}\n`);
144
+ }
145
+ // ── Video generation ───────────────────────────────────────────────────────
60
146
  async function checkStatus(ctx, generationId, json) {
61
147
  const data = await mmPost(ctx, '/digital-human/video/status', { generationId }, { timeoutMs: 30_000 });
62
148
  const status = data.status ?? 'unknown';
@@ -93,21 +179,33 @@ export async function genDigitalHuman(input, ctxIn) {
93
179
  privateToken: input.priv_token,
94
180
  preflightToken: ctxIn.auth?.token,
95
181
  });
182
+ const json = isTrue(input.json_output);
96
183
  // ── Mode 1: list avatars ────────────────────────────────────────────────
97
184
  if (isTrue(input.list_avatars)) {
98
185
  return listAvatars(ctx, input);
99
186
  }
100
- // ── Mode 3: check status ────────────────────────────────────────────────
187
+ // ── Mode 2: clone a custom avatar (HiFly) ───────────────────────────────
188
+ if (isTrue(input.clone)) {
189
+ return cloneAvatar(ctx, input);
190
+ }
191
+ // ── Mode 3: clone status ────────────────────────────────────────────────
192
+ if (isTrue(input.clone_status)) {
193
+ const avatarId = toId(input.avatar_id);
194
+ if (!avatarId)
195
+ throw new SkillError('❌ clone-status mode requires --avatar-id');
196
+ return checkCloneStatus(ctx, avatarId, json);
197
+ }
198
+ // ── Mode 4: check generation status ─────────────────────────────────────
101
199
  if (isTrue(input.check_status)) {
102
200
  const generationId = toNumber(input.generation_id, 'generation_id');
103
201
  if (generationId === undefined) {
104
202
  throw new SkillError('❌ check-status mode requires --generation-id');
105
203
  }
106
- return checkStatus(ctx, generationId, isTrue(input.json_output));
204
+ return checkStatus(ctx, generationId, json);
107
205
  }
108
- // ── Mode 2: generate (default) ──────────────────────────────────────────
109
- const avatarId = toNumber(input.avatar_id, 'avatar_id');
110
- if (avatarId === undefined) {
206
+ // ── Mode 5: generate (default) ──────────────────────────────────────────
207
+ const avatarId = toId(input.avatar_id);
208
+ if (!avatarId) {
111
209
  throw new SkillError('❌ provide --avatar-id to pick the avatar (use --list-avatars to see available avatars)');
112
210
  }
113
211
  const audioUrl = (input.audio_url ?? '').trim();
@@ -120,6 +218,12 @@ export async function genDigitalHuman(input, ctxIn) {
120
218
  const dh = (await getCapabilities(input.api_base_url)).digitalHuman;
121
219
  emitProgress({ phase: 'gen-digital-human:resolve-avatar', avatarId });
122
220
  const avatar = await fetchAvatarInfo(ctx, avatarId);
221
+ if (avatar.cloneStatus === 'cloning') {
222
+ throw new SkillError(`❌ avatar ${avatarId} is still cloning; check with --clone-status --avatar-id ${avatarId}`);
223
+ }
224
+ if (avatar.cloneStatus === 'failed') {
225
+ throw new SkillError(`❌ avatar ${avatarId} failed to clone (${avatar.cloneError || 'unknown error'}); clone it again`);
226
+ }
123
227
  const source = resolveSource(input.source, avatar, dh.sources);
124
228
  const payload = { avatarId, source };
125
229
  if (audioUrl) {
@@ -149,7 +253,7 @@ export async function genDigitalHuman(input, ctxIn) {
149
253
  if (!videoUrl) {
150
254
  throw new SkillError(`❌ video generated but no URL was returned; rerun --check-status --generation-id ${generationId} later`);
151
255
  }
152
- if (isTrue(input.json_output)) {
256
+ if (json) {
153
257
  process.stdout.write(JSON.stringify({ url: videoUrl, generationId }) + '\n');
154
258
  return;
155
259
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.54",
4
- "generatedAt": "2026-09-28T11:58:29.708Z",
3
+ "version": "0.9.56",
4
+ "generatedAt": "2026-09-28T23:55:13.296Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -174,7 +174,8 @@
174
174
  "Digital human, talking-head video, AI presenter, virtual host",
175
175
  "Make a talking-head video out of this script / this audio file",
176
176
  "List or browse digital-human avatars",
177
- "Use Jimeng or HiFly to drive an avatar"
177
+ "Use Jimeng or HiFly to drive an avatar",
178
+ "Clone / create a custom avatar from a video or photo (HiFly)"
178
179
  ],
179
180
  "entry": {
180
181
  "type": "http",
@@ -199,7 +200,7 @@
199
200
  },
200
201
  "name": {
201
202
  "type": "string",
202
- "description": "With list_avatars=true: fuzzy-filter avatars by name"
203
+ "description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
203
204
  },
204
205
  "source": {
205
206
  "type": "string",
@@ -207,7 +208,7 @@
207
208
  "jimeng",
208
209
  "hifly"
209
210
  ],
210
- "description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
211
+ "description": "Provider filter / override. hifly avatars (including cloned custom avatars) are video-driven; jimeng is image-driven. Usually inferred from the avatar; pass it explicitly only when the avatar declares no source."
211
212
  },
212
213
  "gender": {
213
214
  "type": "string",
@@ -218,8 +219,36 @@
218
219
  "description": "Filter by gender"
219
220
  },
220
221
  "avatar_id": {
221
- "type": "number",
222
- "description": "Avatar id"
222
+ "type": "string",
223
+ "description": "Avatar id as returned by list_avatars (opaque string like dh_xxx). Also used with clone_status=true."
224
+ },
225
+ "clone": {
226
+ "type": "boolean",
227
+ "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs HiFly credits). Requires clone_url and name. Waits until cloning finishes unless wait=false."
228
+ },
229
+ "clone_url": {
230
+ "type": "string",
231
+ "description": "With clone=true: public http(s) URL of the source. Video: mp4/mov (h264), 5s-30min, 360p-4K, ≤500MB, one clearly visible speaking person. Image: a front-facing portrait."
232
+ },
233
+ "clone_type": {
234
+ "type": "string",
235
+ "enum": [
236
+ "video",
237
+ "image"
238
+ ],
239
+ "description": "With clone=true: source type. Defaults to video."
240
+ },
241
+ "thumbnail_url": {
242
+ "type": "string",
243
+ "description": "With clone=true: optional cover image URL shown in avatar lists (image clones default to the image itself)."
244
+ },
245
+ "wait": {
246
+ "type": "boolean",
247
+ "description": "With clone=true: set false to return right after submission (then poll with clone_status=true). Defaults to waiting up to ~20 minutes."
248
+ },
249
+ "clone_status": {
250
+ "type": "boolean",
251
+ "description": "Clone-status mode: report cloning / done / failed for avatar_id."
223
252
  },
224
253
  "text": {
225
254
  "type": "string",
@@ -255,7 +284,7 @@
255
284
  },
256
285
  "json_output": {
257
286
  "type": "boolean",
258
- "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
287
+ "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }; clone / clone-status: { avatarId, cloneStatus }) instead of human-readable output"
259
288
  }
260
289
  },
261
290
  "required": []
@@ -275,6 +304,12 @@
275
304
  "hidden": [
276
305
  "json_output",
277
306
  "list_avatars",
307
+ "clone",
308
+ "clone_url",
309
+ "clone_type",
310
+ "thumbnail_url",
311
+ "wait",
312
+ "clone_status",
278
313
  "mine",
279
314
  "name",
280
315
  "gender",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.54",
3
+ "version": "0.9.56",
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",
@@ -8,6 +8,7 @@ description: |
8
8
  - "Make a talking-head video out of this script / this audio file"
9
9
  - List or browse digital-human avatars
10
10
  - Use Jimeng or HiFly to drive an avatar
11
+ - Clone / create a custom avatar from a video or photo (HiFly)
11
12
 
12
13
  Even without an explicit "use AI", any request that turns text or audio into a synthesized presenter video should route here.
13
14
  triggers:
@@ -15,6 +16,7 @@ triggers:
15
16
  - "Make a talking-head video out of this script / this audio file"
16
17
  - List or browse digital-human avatars
17
18
  - Use Jimeng or HiFly to drive an avatar
19
+ - Clone / create a custom avatar from a video or photo (HiFly)
18
20
  ---
19
21
 
20
22
  # Digital-Human Skill
@@ -31,6 +33,10 @@ Wraps ab-api's digital-human endpoints (the same ones the web studio uses), auth
31
33
  | TTS lip-sync | avatar + text + voice → talking-head video | `POST /digital-human/video/generate` |
32
34
  | Audio-driven | avatar + existing audio URL → talking-head video | `POST /digital-human/video/generate` |
33
35
  | Check status | Poll the progress of a generation job | `POST /digital-human/video/status` |
36
+ | Clone avatar | Create a custom HiFly avatar from a video (free) or portrait image (costs HiFly credits) | `POST /digital-human/avatar/clone` |
37
+ | Clone status | Report `cloning` / `done` / `failed` for a cloned avatar | `POST /digital-human/avatar/clone-status` |
38
+
39
+ Avatar ids are opaque strings (e.g. `dh_xxx`) — always copy them from `--list-avatars`, never invent or renumber them.
34
40
 
35
41
  **Cross-skill dependency**: voice ids are shared with `gen-voice` (same Minimax catalog). Use `gen-voice --list-voices` to see available voices.
36
42
 
@@ -82,7 +88,7 @@ remixmate gen-digital-human --list-avatars --name "alice"
82
88
 
83
89
  ```bash
84
90
  remixmate gen-digital-human \
85
- --avatar-id 7 \
91
+ --avatar-id dh_xxx \
86
92
  --text "Hi everyone, welcome to the live stream — today I'll introduce a new product." \
87
93
  --voice-id "male-qn-qingse"
88
94
  ```
@@ -91,7 +97,7 @@ remixmate gen-digital-human \
91
97
 
92
98
  ```bash
93
99
  remixmate gen-digital-human \
94
- --avatar-id 7 \
100
+ --avatar-id dh_xxx \
95
101
  --text "Hi class, here's our brand-new course." \
96
102
  --voice-id "female-shaonv" \
97
103
  --aspect-ratio "9:16"
@@ -101,7 +107,7 @@ remixmate gen-digital-human \
101
107
 
102
108
  ```bash
103
109
  remixmate gen-digital-human \
104
- --avatar-id 39 \
110
+ --avatar-id dh_yyy \
105
111
  --source hifly \
106
112
  --audio-url "https://example.com/voice.mp3" \
107
113
  --aspect-ratio "16:9"
@@ -113,6 +119,24 @@ remixmate gen-digital-human \
113
119
  remixmate gen-digital-human --check-status --generation-id 123
114
120
  ```
115
121
 
122
+ ### 4. Clone a custom avatar (HiFly)
123
+
124
+ The source must be a public http(s) URL (local files are not uploaded). Video: mp4/mov (h264), 5 s – 30 min, 360p – 4K, ≤ 500 MB, one person speaking to camera. Cloning takes minutes; the handler waits (up to ~20 min) and prints the new avatar id. The avatar also shows up under 素材库 → 数字人 in the web app.
125
+
126
+ ```bash
127
+ # Video clone (free)
128
+ remixmate gen-digital-human --clone --name "My presenter" --clone-url "https://example.com/me.mp4"
129
+
130
+ # Portrait image clone (costs HiFly credits)
131
+ remixmate gen-digital-human --clone --clone-type image --name "My presenter" --clone-url "https://example.com/me.jpg"
132
+
133
+ # Submit without waiting, then poll
134
+ remixmate gen-digital-human --clone --wait false --name "My presenter" --clone-url "https://example.com/me.mp4"
135
+ remixmate gen-digital-human --clone-status --avatar-id dh_xxx
136
+ ```
137
+
138
+ A cloned avatar is a normal `hifly` avatar: find it with `--list-avatars --mine`, then generate with `--avatar-id <id> --text "..."` (TTS is synthesized with the gen-voice catalog, then drives the avatar) or `--audio-url`. Audio must be 5 s – 30 min.
139
+
116
140
  ## Common CLI flags
117
141
 
118
142
  ### List avatars
@@ -147,6 +171,19 @@ remixmate gen-digital-human --check-status --generation-id 123
147
171
  | `--check-status` | Enter status-check mode | — |
148
172
  | `--generation-id` | Job id (required) | — |
149
173
 
174
+ ### Clone avatar
175
+
176
+ | Flag | Description | Default |
177
+ |------|-------------|---------|
178
+ | `--clone` | Enter clone mode | — |
179
+ | `--name` | New avatar name (required) | — |
180
+ | `--clone-url` | Public URL of the source video / image (required) | — |
181
+ | `--clone-type` | `video` / `image` | `video` |
182
+ | `--thumbnail-url` | Cover image shown in lists | image itself / none |
183
+ | `--gender` | `male` / `female` | none |
184
+ | `--wait` | `false` to return right after submission | wait |
185
+ | `--clone-status` | Report clone status of `--avatar-id` | — |
186
+
150
187
  ## Typical flows
151
188
 
152
189
  ### Flow A — TTS
@@ -196,5 +233,7 @@ generation cost, and the balance is the only warning before a run fails with
196
233
  - **Empty TTS text**: provide non-empty `--text` or use `--audio-url`.
197
234
  - **Invalid voice id**: re-check via `gen-voice --list-voices`.
198
235
  - **Avatar id not found**: re-check via `--list-avatars` (or `--list-avatars --mine`).
236
+ - **Avatar still cloning / clone failed**: poll `--clone-status --avatar-id <id>`; on failure, re-clone with a clearer source (single speaker facing the camera, h264 mp4).
237
+ - **HiFly audio too short**: HiFly needs ≥ 5 s of speech; lengthen the text or audio.
199
238
  - **Cannot infer source**: when the avatar lacks a `source` and `--source` is not set, pass `--source jimeng` or `--source hifly` explicitly.
200
239
  - **Network**: verify connectivity and `MM_API_BASE_URL`.
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "name": {
31
31
  "type": "string",
32
- "description": "With list_avatars=true: fuzzy-filter avatars by name"
32
+ "description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
33
33
  },
34
34
  "source": {
35
35
  "type": "string",
@@ -37,7 +37,7 @@
37
37
  "jimeng",
38
38
  "hifly"
39
39
  ],
40
- "description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
40
+ "description": "Provider filter / override. hifly avatars (including cloned custom avatars) are video-driven; jimeng is image-driven. Usually inferred from the avatar; pass it explicitly only when the avatar declares no source."
41
41
  },
42
42
  "gender": {
43
43
  "type": "string",
@@ -48,8 +48,36 @@
48
48
  "description": "Filter by gender"
49
49
  },
50
50
  "avatar_id": {
51
- "type": "number",
52
- "description": "Avatar id"
51
+ "type": "string",
52
+ "description": "Avatar id as returned by list_avatars (opaque string like dh_xxx). Also used with clone_status=true."
53
+ },
54
+ "clone": {
55
+ "type": "boolean",
56
+ "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs HiFly credits). Requires clone_url and name. Waits until cloning finishes unless wait=false."
57
+ },
58
+ "clone_url": {
59
+ "type": "string",
60
+ "description": "With clone=true: public http(s) URL of the source. Video: mp4/mov (h264), 5s-30min, 360p-4K, ≤500MB, one clearly visible speaking person. Image: a front-facing portrait."
61
+ },
62
+ "clone_type": {
63
+ "type": "string",
64
+ "enum": [
65
+ "video",
66
+ "image"
67
+ ],
68
+ "description": "With clone=true: source type. Defaults to video."
69
+ },
70
+ "thumbnail_url": {
71
+ "type": "string",
72
+ "description": "With clone=true: optional cover image URL shown in avatar lists (image clones default to the image itself)."
73
+ },
74
+ "wait": {
75
+ "type": "boolean",
76
+ "description": "With clone=true: set false to return right after submission (then poll with clone_status=true). Defaults to waiting up to ~20 minutes."
77
+ },
78
+ "clone_status": {
79
+ "type": "boolean",
80
+ "description": "Clone-status mode: report cloning / done / failed for avatar_id."
53
81
  },
54
82
  "text": {
55
83
  "type": "string",
@@ -85,7 +113,7 @@
85
113
  },
86
114
  "json_output": {
87
115
  "type": "boolean",
88
- "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
116
+ "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }; clone / clone-status: { avatarId, cloneStatus }) instead of human-readable output"
89
117
  }
90
118
  },
91
119
  "required": []
@@ -105,6 +133,12 @@
105
133
  "hidden": [
106
134
  "json_output",
107
135
  "list_avatars",
136
+ "clone",
137
+ "clone_url",
138
+ "clone_type",
139
+ "thumbnail_url",
140
+ "wait",
141
+ "clone_status",
108
142
  "mine",
109
143
  "name",
110
144
  "gender",
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-digital-human",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "336",
5
- "version": "V8",
5
+ "version": "V9",
6
6
  "skillDescription": "Digital-human (talking-head) skill: list available avatars, run TTS-based or audio-driven lip-sync, and check the status of pending jobs. Backed by ab-api's digital-human endpoints (Jimeng / HiFly providers).\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- Use Jimeng or HiFly to drive an avatar\n\nEven without an explicit \"use AI\", any request that turns text or audio into a synthesized presenter video should route here."
7
7
  }