@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.
- package/dist/handlers/gen-digital-human.d.ts +10 -5
- package/dist/handlers/gen-digital-human.js +119 -15
- package/dist/manifest.json +43 -8
- package/package.json +1 -1
- package/skills/gen-digital-human/SKILL.md +42 -3
- package/skills/gen-digital-human/skill.json +39 -5
- package/skills/gen-digital-human/version.json +1 -1
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gen-digital-human handler — talking-head video,
|
|
3
|
-
* --list-avatars true
|
|
4
|
-
* --
|
|
5
|
-
*
|
|
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
|
|
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,
|
|
3
|
-
* --list-avatars true
|
|
4
|
-
* --
|
|
5
|
-
*
|
|
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
|
|
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(
|
|
36
|
-
process.stdout.write(`${'-'.repeat(
|
|
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(`${
|
|
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) =>
|
|
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
|
|
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,
|
|
204
|
+
return checkStatus(ctx, generationId, json);
|
|
107
205
|
}
|
|
108
|
-
// ── Mode
|
|
109
|
-
const avatarId =
|
|
110
|
-
if (avatarId
|
|
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 (
|
|
256
|
+
if (json) {
|
|
153
257
|
process.stdout.write(JSON.stringify({ url: videoUrl, generationId }) + '\n');
|
|
154
258
|
return;
|
|
155
259
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
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
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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": "
|
|
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
|
}
|