@remixmate/cli 0.9.72 → 0.9.74
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/LICENSE +45 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/argv.js +1 -51
- package/dist/auth/auth-lock.js +1 -100
- package/dist/auth/auto-login.js +7 -75
- package/dist/auth/commands.js +40 -290
- package/dist/auth/credential-store.js +1 -143
- package/dist/auth/device-flow-runner.js +13 -110
- package/dist/auth/device-flow.js +1 -132
- package/dist/auth/ensure.js +1 -62
- package/dist/auth/environment.js +1 -69
- package/dist/auth/pending-store.js +1 -45
- package/dist/auth/resolve.js +1 -44
- package/dist/billing.js +3 -76
- package/dist/capabilities.js +1 -75
- package/dist/cli.js +39 -211
- package/dist/diagnostics.js +2 -70
- package/dist/doctor.js +9 -188
- package/dist/errors.js +1 -33
- package/dist/exec.js +4 -47
- package/dist/handlers/gen-digital-human.js +33 -264
- package/dist/handlers/gen-image.js +9 -113
- package/dist/handlers/gen-post.js +15 -192
- package/dist/handlers/gen-video.js +7 -185
- package/dist/handlers/gen-voice.js +19 -133
- package/dist/handlers/index.js +1 -20
- package/dist/handlers/shared.js +1 -66
- package/dist/handlers/template-init.js +1 -21
- package/dist/http.js +3 -155
- package/dist/index.js +1 -8
- package/dist/install.js +40 -448
- package/dist/manifest.json +44 -3
- package/dist/progress.js +2 -15
- package/dist/project/commands.js +34 -156
- package/dist/project/host.js +1 -151
- package/dist/project/resolve.js +1 -65
- package/dist/project/store.js +2 -97
- package/dist/project/take.js +1 -106
- package/dist/project/trace.js +5 -278
- package/dist/registry.js +3 -70
- package/dist/runner.js +7 -225
- package/dist/skill-schema.js +1 -194
- package/dist/template/args.js +1 -69
- package/dist/template/check.js +2 -30
- package/dist/template/commands.js +34 -163
- package/dist/template/constants.js +1 -46
- package/dist/template/init.js +5 -133
- package/dist/template/list.js +1 -46
- package/dist/template/preview.js +7 -318
- package/dist/template/proc.js +1 -43
- package/dist/template/project.js +6 -161
- package/dist/template/publish.js +5 -185
- package/dist/template/render-client.js +2 -79
- package/dist/template/upgrade.js +4 -123
- package/dist/text.js +1 -15
- package/package.json +2 -35
- package/skills/doc-parse/scripts/extract_via_render.py +32 -32
- package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
- package/skills/gen-script/SKILL.md +23 -1
- package/skills/gen-script/scripts/gen_script.py +644 -551
- package/skills/gen-script/skill.json +12 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
- package/skills/render-video/scripts/_chrome_vendor.py +17 -17
- package/skills/render-video/scripts/_digital_human_segments.py +34 -34
- package/skills/render-video/scripts/_video_probe.py +33 -33
- package/skills/render-video/scripts/_vod_polling.py +13 -13
- package/skills/render-video/scripts/remote_renderer_client.py +42 -42
- package/skills/render-video/scripts/render_video.py +639 -639
- package/skills/render-video/scripts/required_props.py +37 -37
- package/skills/render-video/scripts/upload_video.py +35 -35
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +16 -0
- package/skills/template-registry/scripts/countdown_spec.py +92 -92
- package/skills/template-registry/scripts/doc_extract.py +40 -40
- package/skills/template-registry/scripts/list_templates.py +303 -191
- package/skills/template-registry/scripts/log_diagnostics.py +1 -1
- package/skills/template-registry/scripts/match_template.py +87 -87
- package/skills/template-registry/scripts/payload_hydrator.py +89 -89
- package/skills/template-registry/scripts/registry_loader.py +159 -159
- package/skills/template-registry/scripts/render_job_client.py +75 -75
- package/skills/template-registry/scripts/showcase_loader.py +44 -44
- package/skills/template-registry/skill.json +4 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
- package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
- package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
- package/skills/video-parser/scripts/analyze_video.py +173 -173
- package/skills/video-parser/scripts/deconstruct_video.py +91 -91
- package/skills/video-parser/scripts/parse_via_render.py +21 -21
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
- package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
- package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
- package/skills/web-screenshot/scripts/_media_upload.py +33 -33
- package/skills/web-screenshot/scripts/read_page.py +15 -15
- package/skills/web-screenshot/scripts/record.py +26 -26
- package/skills/web-screenshot/scripts/screenshot.py +5 -5
- package/dist/argv.d.ts +0 -15
- package/dist/auth/auth-lock.d.ts +0 -26
- package/dist/auth/auto-login.d.ts +0 -20
- package/dist/auth/commands.d.ts +0 -8
- package/dist/auth/credential-store.d.ts +0 -60
- package/dist/auth/device-flow-runner.d.ts +0 -58
- package/dist/auth/device-flow.d.ts +0 -52
- package/dist/auth/ensure.d.ts +0 -55
- package/dist/auth/environment.d.ts +0 -38
- package/dist/auth/pending-store.d.ts +0 -33
- package/dist/auth/resolve.d.ts +0 -30
- package/dist/billing.d.ts +0 -44
- package/dist/capabilities.d.ts +0 -63
- package/dist/cli.d.ts +0 -13
- package/dist/diagnostics.d.ts +0 -24
- package/dist/doctor.d.ts +0 -22
- package/dist/errors.d.ts +0 -29
- package/dist/exec.d.ts +0 -18
- package/dist/handlers/gen-digital-human.d.ts +0 -17
- package/dist/handlers/gen-image.d.ts +0 -20
- package/dist/handlers/gen-post.d.ts +0 -22
- package/dist/handlers/gen-video.d.ts +0 -19
- package/dist/handlers/gen-voice.d.ts +0 -26
- package/dist/handlers/index.d.ts +0 -20
- package/dist/handlers/shared.d.ts +0 -26
- package/dist/handlers/template-init.d.ts +0 -10
- package/dist/http.d.ts +0 -72
- package/dist/index.d.ts +0 -9
- package/dist/install.d.ts +0 -71
- package/dist/progress.d.ts +0 -23
- package/dist/project/commands.d.ts +0 -19
- package/dist/project/host.d.ts +0 -91
- package/dist/project/resolve.d.ts +0 -43
- package/dist/project/store.d.ts +0 -31
- package/dist/project/take.d.ts +0 -50
- package/dist/project/trace.d.ts +0 -98
- package/dist/registry.d.ts +0 -30
- package/dist/runner.d.ts +0 -39
- package/dist/skill-schema.d.ts +0 -138
- package/dist/template/args.d.ts +0 -25
- package/dist/template/check.d.ts +0 -23
- package/dist/template/commands.d.ts +0 -25
- package/dist/template/constants.d.ts +0 -39
- package/dist/template/init.d.ts +0 -28
- package/dist/template/list.d.ts +0 -23
- package/dist/template/preview.d.ts +0 -60
- package/dist/template/proc.d.ts +0 -26
- package/dist/template/project.d.ts +0 -47
- package/dist/template/publish.d.ts +0 -43
- package/dist/template/render-client.d.ts +0 -39
- package/dist/template/upgrade.d.ts +0 -44
- package/dist/text.d.ts +0 -8
|
@@ -1,192 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* - --instruction is the user's "how should it read" request. With a draft
|
|
17
|
-
* (--title / --description / --tag) it rewrites that caption — single platform
|
|
18
|
-
* only, since the draft belongs to one platform. Without a draft it is an extra
|
|
19
|
-
* requirement applied to a fresh generation on every requested platform.
|
|
20
|
-
*/
|
|
21
|
-
import { EXIT, mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
22
|
-
import { emitProgress } from '../progress.js';
|
|
23
|
-
import { isTrue, toNumber, toStringArray } from './shared.js';
|
|
24
|
-
/** Platforms the web panel enables by default; `all` expands to these. */
|
|
25
|
-
const DEFAULT_PLATFORMS = ['douyin', 'xiaohongshu', 'tiktok'];
|
|
26
|
-
/** Friendly aliases → backend platform codes. Unknown values pass through for ab-api to validate. */
|
|
27
|
-
const PLATFORM_ALIASES = {
|
|
28
|
-
dy: 'douyin',
|
|
29
|
-
抖音: 'douyin',
|
|
30
|
-
xhs: 'xiaohongshu',
|
|
31
|
-
rednote: 'xiaohongshu',
|
|
32
|
-
小红书: 'xiaohongshu',
|
|
33
|
-
tt: 'tiktok',
|
|
34
|
-
};
|
|
35
|
-
/** Display names for human-readable output only. */
|
|
36
|
-
const PLATFORM_LABELS = {
|
|
37
|
-
douyin: '抖音 Douyin',
|
|
38
|
-
xiaohongshu: '小红书 Xiaohongshu',
|
|
39
|
-
tiktok: 'TikTok',
|
|
40
|
-
};
|
|
41
|
-
/** Backend waits on a non-streaming LLM call (80s server-side cap); leave headroom. */
|
|
42
|
-
const GENERATE_TIMEOUT_MS = 90_000;
|
|
43
|
-
function resolvePlatforms(value) {
|
|
44
|
-
const raw = toStringArray(value)
|
|
45
|
-
.flatMap((v) => v.split(/[,,\s]+/))
|
|
46
|
-
.map((v) => v.trim())
|
|
47
|
-
.filter(Boolean);
|
|
48
|
-
if (raw.length === 0)
|
|
49
|
-
return [...DEFAULT_PLATFORMS];
|
|
50
|
-
const out = [];
|
|
51
|
-
for (const item of raw) {
|
|
52
|
-
const key = item.toLowerCase();
|
|
53
|
-
// 「全部」: the web param card writes option labels into the message, and the agent
|
|
54
|
-
// may forward the label verbatim.
|
|
55
|
-
const codes = key === 'all' || item === '全部' ? [...DEFAULT_PLATFORMS] : [PLATFORM_ALIASES[item] ?? PLATFORM_ALIASES[key] ?? key];
|
|
56
|
-
for (const code of codes)
|
|
57
|
-
if (!out.includes(code))
|
|
58
|
-
out.push(code);
|
|
59
|
-
}
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
62
|
-
function optString(value) {
|
|
63
|
-
if (typeof value !== 'string')
|
|
64
|
-
return undefined;
|
|
65
|
-
const trimmed = value.trim();
|
|
66
|
-
return trimmed || undefined;
|
|
67
|
-
}
|
|
68
|
-
/** Tags may arrive as `--tag a --tag b`, `--tag "a,b"`, or with a leading `#`. */
|
|
69
|
-
function normalizeTags(value) {
|
|
70
|
-
return toStringArray(value)
|
|
71
|
-
.flatMap((v) => v.split(/[,,]+/))
|
|
72
|
-
.map((v) => v.trim().replace(/^#+/, ''))
|
|
73
|
-
.filter(Boolean);
|
|
74
|
-
}
|
|
75
|
-
async function generateOne(ctx, platform, base) {
|
|
76
|
-
emitProgress({ phase: 'gen-post:request', platform });
|
|
77
|
-
try {
|
|
78
|
-
const caption = await mmPost(ctx, '/publish/caption/generate', { ...base, platform }, { timeoutMs: GENERATE_TIMEOUT_MS });
|
|
79
|
-
if (!caption || (!caption.title && !caption.description)) {
|
|
80
|
-
return { platform, error: 'backend returned an empty caption' };
|
|
81
|
-
}
|
|
82
|
-
emitProgress({ phase: 'gen-post:done', platform });
|
|
83
|
-
return { platform, caption: { ...caption, platform, tags: caption.tags ?? [] } };
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
// Auth failures abort the whole run: every other platform would fail the same way.
|
|
87
|
-
if (err instanceof SkillError && err.exitCode === EXIT.NOT_AUTHENTICATED)
|
|
88
|
-
throw err;
|
|
89
|
-
return { platform, error: err instanceof Error ? err.message : String(err) };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function printHuman(results) {
|
|
93
|
-
for (const r of results) {
|
|
94
|
-
const label = PLATFORM_LABELS[r.platform] ?? r.platform;
|
|
95
|
-
process.stdout.write(`\n━━━━━━━━ ${label} ━━━━━━━━\n`);
|
|
96
|
-
if (!r.caption) {
|
|
97
|
-
process.stdout.write(`❌ ${r.error ?? 'generation failed'}\n`);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const { title, description, tags } = r.caption;
|
|
101
|
-
process.stdout.write(`标题 / Title:\n${title}\n\n`);
|
|
102
|
-
process.stdout.write(`描述 / Description:\n${description}\n\n`);
|
|
103
|
-
process.stdout.write(`标签 / Tags:\n${tags.map((t) => `#${t}`).join(' ')}\n`);
|
|
104
|
-
}
|
|
105
|
-
const ok = results.filter((r) => r.caption).length;
|
|
106
|
-
process.stdout.write(`\n🎉 Generated publish copy for ${ok}/${results.length} platform(s)\n`);
|
|
107
|
-
}
|
|
108
|
-
export async function genPost(input, ctxIn) {
|
|
109
|
-
const topic = optString(input.topic);
|
|
110
|
-
const hook = optString(input.hook);
|
|
111
|
-
const instruction = optString(input.instruction);
|
|
112
|
-
const renderId = optString(input.render_id);
|
|
113
|
-
const platforms = resolvePlatforms(input.platform);
|
|
114
|
-
const current = {
|
|
115
|
-
title: optString(input.title) ?? '',
|
|
116
|
-
description: optString(input.description) ?? '',
|
|
117
|
-
tags: normalizeTags(input.tag),
|
|
118
|
-
};
|
|
119
|
-
const hasDraft = !!(current.title || current.description || current.tags.length);
|
|
120
|
-
if (instruction && instruction.length > 500) {
|
|
121
|
-
throw new SkillError('❌ --instruction must be at most 500 characters');
|
|
122
|
-
}
|
|
123
|
-
// With a draft, --instruction rewrites that one platform's caption. Without one it is
|
|
124
|
-
// an extra requirement for a fresh generation, which needs the video's context.
|
|
125
|
-
const rewriting = !!instruction && hasDraft;
|
|
126
|
-
if (hasDraft && !instruction) {
|
|
127
|
-
throw new SkillError('❌ --title / --description / --tag are the draft to rewrite; pass --instruction with them');
|
|
128
|
-
}
|
|
129
|
-
if (rewriting && platforms.length !== 1) {
|
|
130
|
-
throw new SkillError('❌ rewriting a caption (--instruction with --title / --description / --tag) needs exactly one --platform');
|
|
131
|
-
}
|
|
132
|
-
if (!rewriting && !topic && !hook) {
|
|
133
|
-
throw new SkillError('❌ Please provide --topic (what the video is about); --hook is optional extra context');
|
|
134
|
-
}
|
|
135
|
-
const durationSec = toNumber(input.duration, 'duration');
|
|
136
|
-
if (durationSec !== undefined && durationSec < 0) {
|
|
137
|
-
throw new SkillError('❌ --duration must be a non-negative number of seconds');
|
|
138
|
-
}
|
|
139
|
-
const base = {};
|
|
140
|
-
if (renderId)
|
|
141
|
-
base.renderId = renderId;
|
|
142
|
-
const videoProjectId = optString(input.video_project_id);
|
|
143
|
-
if (videoProjectId)
|
|
144
|
-
base.videoProjectId = videoProjectId;
|
|
145
|
-
if (topic)
|
|
146
|
-
base.topic = topic;
|
|
147
|
-
if (hook)
|
|
148
|
-
base.hook = hook;
|
|
149
|
-
const sourceUrl = optString(input.source_url);
|
|
150
|
-
if (sourceUrl)
|
|
151
|
-
base.sourceUrl = sourceUrl;
|
|
152
|
-
if (durationSec !== undefined)
|
|
153
|
-
base.durationSec = durationSec;
|
|
154
|
-
const aspectRatio = optString(input.aspect_ratio);
|
|
155
|
-
if (aspectRatio)
|
|
156
|
-
base.aspectRatio = aspectRatio;
|
|
157
|
-
const language = optString(input.language);
|
|
158
|
-
if (language)
|
|
159
|
-
base.language = language;
|
|
160
|
-
const promptOverride = optString(input.prompt);
|
|
161
|
-
if (promptOverride)
|
|
162
|
-
base.promptOverride = promptOverride;
|
|
163
|
-
if (instruction)
|
|
164
|
-
base.instruction = instruction;
|
|
165
|
-
if (rewriting)
|
|
166
|
-
base.current = current;
|
|
167
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
168
|
-
apiBaseUrl: input.api_base_url,
|
|
169
|
-
privateToken: input.priv_token,
|
|
170
|
-
preflightToken: ctxIn.auth?.token,
|
|
171
|
-
});
|
|
172
|
-
const results = await Promise.all(platforms.map((p) => generateOne(ctx, p, base)));
|
|
173
|
-
const succeeded = results.filter((r) => r.caption);
|
|
174
|
-
if (isTrue(input.json_output)) {
|
|
175
|
-
process.stdout.write(JSON.stringify({
|
|
176
|
-
captions: succeeded.map((r) => ({
|
|
177
|
-
platform: r.platform,
|
|
178
|
-
title: r.caption.title,
|
|
179
|
-
description: r.caption.description,
|
|
180
|
-
tags: r.caption.tags,
|
|
181
|
-
})),
|
|
182
|
-
errors: results.filter((r) => !r.caption).map((r) => ({ platform: r.platform, error: r.error })),
|
|
183
|
-
persisted: !!renderId,
|
|
184
|
-
}) + '\n');
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
printHuman(results);
|
|
188
|
-
}
|
|
189
|
-
if (succeeded.length === 0) {
|
|
190
|
-
throw new SkillError(`❌ publish copy generation failed for every platform: ${results.map((r) => `${r.platform}: ${r.error}`).join('; ')}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
1
|
+
var P=Object.defineProperty;var d=(t,e)=>P(t,"name",{value:e,configurable:!0});import{EXIT as $,mmPost as O,resolveHttpContext as L,SkillError as f}from"../http.js";import{emitProgress as v}from"../progress.js";import{isTrue as M,toNumber as I,toStringArray as x}from"./shared.js";const E=["douyin","xiaohongshu","tiktok"],S={dy:"douyin",抖音:"douyin",xhs:"xiaohongshu",rednote:"xiaohongshu",小红书:"xiaohongshu",tt:"tiktok"},j={douyin:"抖音 Douyin",xiaohongshu:"小红书 Xiaohongshu",tiktok:"TikTok"},N=9e4;function R(t){const e=x(t).flatMap(o=>o.split(/[,,\s]+/)).map(o=>o.trim()).filter(Boolean);if(e.length===0)return[...E];const r=[];for(const o of e){const i=o.toLowerCase(),c=i==="all"||o==="全部"?[...E]:[S[o]??S[i]??i];for(const p of c)r.includes(p)||r.push(p)}return r}d(R,"resolvePlatforms");function a(t){return typeof t!="string"?void 0:t.trim()||void 0}d(a,"optString");function U(t){return x(t).flatMap(e=>e.split(/[,,]+/)).map(e=>e.trim().replace(/^#+/,"")).filter(Boolean)}d(U,"normalizeTags");async function D(t,e,r){v({phase:"gen-post:request",platform:e});try{const o=await O(t,"/publish/caption/generate",{...r,platform:e},{timeoutMs:N});return!o||!o.title&&!o.description?{platform:e,error:"backend returned an empty caption"}:(v({phase:"gen-post:done",platform:e}),{platform:e,caption:{...o,platform:e,tags:o.tags??[]}})}catch(o){if(o instanceof f&&o.exitCode===$.NOT_AUTHENTICATED)throw o;return{platform:e,error:o instanceof Error?o.message:String(o)}}}d(D,"generateOne");function B(t){for(const r of t){const o=j[r.platform]??r.platform;if(process.stdout.write(`
|
|
2
|
+
━━━━━━━━ ${o} ━━━━━━━━
|
|
3
|
+
`),!r.caption){process.stdout.write(`❌ ${r.error??"generation failed"}
|
|
4
|
+
`);continue}const{title:i,description:c,tags:p}=r.caption;process.stdout.write(`标题 / Title:
|
|
5
|
+
${i}
|
|
6
|
+
|
|
7
|
+
`),process.stdout.write(`描述 / Description:
|
|
8
|
+
${c}
|
|
9
|
+
|
|
10
|
+
`),process.stdout.write(`标签 / Tags:
|
|
11
|
+
${p.map(l=>`#${l}`).join(" ")}
|
|
12
|
+
`)}const e=t.filter(r=>r.caption).length;process.stdout.write(`
|
|
13
|
+
🎉 Generated publish copy for ${e}/${t.length} platform(s)
|
|
14
|
+
`)}d(B,"printHuman");async function X(t,e){const r=a(t.topic),o=a(t.hook),i=a(t.instruction),c=a(t.render_id),p=R(t.platform),l={title:a(t.title)??"",description:a(t.description)??"",tags:U(t.tag)},m=!!(l.title||l.description||l.tags.length);if(i&&i.length>500)throw new f("❌ --instruction must be at most 500 characters");const h=!!i&&m;if(m&&!i)throw new f("❌ --title / --description / --tag are the draft to rewrite; pass --instruction with them");if(h&&p.length!==1)throw new f("❌ rewriting a caption (--instruction with --title / --description / --tag) needs exactly one --platform");if(!h&&!r&&!o)throw new f("❌ Please provide --topic (what the video is about); --hook is optional extra context");const u=I(t.duration,"duration");if(u!==void 0&&u<0)throw new f("❌ --duration must be a non-negative number of seconds");const s={};c&&(s.renderId=c);const w=a(t.video_project_id);w&&(s.videoProjectId=w),r&&(s.topic=r),o&&(s.hook=o);const k=a(t.source_url);k&&(s.sourceUrl=k),u!==void 0&&(s.durationSec=u);const T=a(t.aspect_ratio);T&&(s.aspectRatio=T);const y=a(t.language);y&&(s.language=y);const _=a(t.prompt);_&&(s.promptOverride=_),i&&(s.instruction=i),h&&(s.current=l);const A=await L(e.skillName,{apiBaseUrl:t.api_base_url,privateToken:t.priv_token,preflightToken:e.auth?.token}),g=await Promise.all(p.map(n=>D(A,n,s))),b=g.filter(n=>n.caption);if(M(t.json_output)?process.stdout.write(JSON.stringify({captions:b.map(n=>({platform:n.platform,title:n.caption.title,description:n.caption.description,tags:n.caption.tags})),errors:g.filter(n=>!n.caption).map(n=>({platform:n.platform,error:n.error})),persisted:!!c})+`
|
|
15
|
+
`):B(g),b.length===0)throw new f(`❌ publish copy generation failed for every platform: ${g.map(n=>`${n.platform}: ${n.error}`).join("; ")}`)}d(X,"genPost");export{X as genPost};
|
|
@@ -1,185 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* the CLI carries no model matrix — and ab-api re-validates authoritatively.
|
|
9
|
-
*
|
|
10
|
-
* --model accepts a friendly alias or a full id; ab-api also resolves aliases
|
|
11
|
-
* server-side, so an unknown value still passes through to the backend.
|
|
12
|
-
*
|
|
13
|
-
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
14
|
-
* into a data: URI; https / data: URIs pass through unchanged. Reference videos
|
|
15
|
-
* and audio are the exception — they must already be public URLs, see
|
|
16
|
-
* `requirePublicUrls`.
|
|
17
|
-
*/
|
|
18
|
-
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
19
|
-
import { getCapabilities, matchModel, defaultModel } from '../capabilities.js';
|
|
20
|
-
import { emitProgress } from '../progress.js';
|
|
21
|
-
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
22
|
-
/** Generic, catalog-driven validation. No model names live here. */
|
|
23
|
-
function validateAgainstConstraints(descriptor, duration, ratio, resolution, refs) {
|
|
24
|
-
const c = descriptor.constraints;
|
|
25
|
-
const label = descriptor.label || descriptor.id;
|
|
26
|
-
if (c.durations && c.durations.length > 0) {
|
|
27
|
-
if (!c.durations.includes(duration)) {
|
|
28
|
-
throw new SkillError(`❌ ${label} duration must be one of: ${c.durations.join(', ')} s; got: ${duration}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
else if (c.durationMin !== undefined && c.durationMax !== undefined) {
|
|
32
|
-
if (duration < c.durationMin || duration > c.durationMax) {
|
|
33
|
-
throw new SkillError(`❌ ${label} duration must be ${c.durationMin}–${c.durationMax} s, got: ${duration}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (c.aspectRatios && c.aspectRatios.length > 0 && !c.aspectRatios.includes(ratio)) {
|
|
37
|
-
throw new SkillError(`❌ ${label} aspect ratio must be one of: ${c.aspectRatios.join(', ')}; got: ${ratio}`);
|
|
38
|
-
}
|
|
39
|
-
if (c.resolutions && c.resolutions.length > 0 && !c.resolutions.includes(resolution)) {
|
|
40
|
-
throw new SkillError(`❌ ${label} resolution must be one of: ${c.resolutions.join(', ')}; got: ${resolution}`);
|
|
41
|
-
}
|
|
42
|
-
if (c.refImageMax !== undefined && refs.images > c.refImageMax) {
|
|
43
|
-
if (c.refImageMax === 0) {
|
|
44
|
-
throw new SkillError(`❌ ${label} does not support a reference-image array; use --first-frame / --last-frame instead`);
|
|
45
|
-
}
|
|
46
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refImageMax} reference images; got: ${refs.images}`);
|
|
47
|
-
}
|
|
48
|
-
if (c.refVideoMax !== undefined && refs.videos > c.refVideoMax) {
|
|
49
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refVideoMax} reference videos; got: ${refs.videos}`);
|
|
50
|
-
}
|
|
51
|
-
if (c.refAudioMax !== undefined && refs.audios > c.refAudioMax) {
|
|
52
|
-
throw new SkillError(`❌ ${label} accepts at most ${c.refAudioMax} reference audio clips; got: ${refs.audios}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Reference videos / audio must be public https URLs.
|
|
57
|
-
*
|
|
58
|
-
* Unlike images, they are NOT inlined as data URIs: the caps are 50 MB per video
|
|
59
|
-
* and 15 MB per clip, and base64 inflates that by a third before it ever reaches
|
|
60
|
-
* the gateway. Failing here with the offending path beats a truncated upload or
|
|
61
|
-
* an opaque 413 three hops downstream.
|
|
62
|
-
*/
|
|
63
|
-
function requirePublicUrls(values, flag) {
|
|
64
|
-
return values.map((value) => {
|
|
65
|
-
const url = value.trim();
|
|
66
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
67
|
-
throw new SkillError(`❌ ${flag} needs a public http(s) URL (local files are not uploaded), received: ${value}`);
|
|
68
|
-
}
|
|
69
|
-
return url;
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
async function pollVideoStatus(ctx, taskId) {
|
|
73
|
-
return pollUntil(async () => {
|
|
74
|
-
const data = await mmPost(ctx, '/model/getVideoStatus', { taskId }, { timeoutMs: 30_000 });
|
|
75
|
-
const status = data.status ?? 'unknown';
|
|
76
|
-
emitProgress({ phase: 'gen-video:poll', taskId, status });
|
|
77
|
-
if (status === 'succeeded' || status === 'completed') {
|
|
78
|
-
return data.videoUrl ?? '';
|
|
79
|
-
}
|
|
80
|
-
if (status === 'failed') {
|
|
81
|
-
const err = data.errorMsg ?? data.error ?? data.message ?? 'unknown error';
|
|
82
|
-
throw new SkillError(`❌ video generation failed: ${err}`);
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
86
|
-
}
|
|
87
|
-
export async function genVideo(input, ctxIn) {
|
|
88
|
-
const apiBaseUrl = input.api_base_url;
|
|
89
|
-
const caps = await getCapabilities(apiBaseUrl);
|
|
90
|
-
// Resolve the model against the catalog: explicit alias/id, env override, or default.
|
|
91
|
-
const requested = input.model ?? process.env.MM_VIDEO_MODEL;
|
|
92
|
-
const descriptor = requested ? matchModel(caps.video, requested) : defaultModel(caps.video);
|
|
93
|
-
const model = descriptor?.id ?? requested ?? defaultModel(caps.video)?.id ?? '';
|
|
94
|
-
const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
|
|
95
|
-
const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
|
|
96
|
-
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
97
|
-
const referenceVideos = requirePublicUrls(toStringArray(input.reference_video).filter(Boolean), '--reference-video');
|
|
98
|
-
const referenceAudios = requirePublicUrls(toStringArray(input.reference_audio).filter(Boolean), '--reference-audio');
|
|
99
|
-
const prompt = (input.prompt ?? '').trim();
|
|
100
|
-
const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0 || referenceVideos.length > 0);
|
|
101
|
-
if (!prompt && !hasVisual) {
|
|
102
|
-
throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference / --reference-video');
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Two shapes the backend rejects outright; catching them here saves a round
|
|
106
|
-
* trip and names the flag that caused it.
|
|
107
|
-
*
|
|
108
|
-
* - first/last frame pins exact frames, omni-modal reference lets the model
|
|
109
|
-
* re-cut — the API has no meaning for both at once;
|
|
110
|
-
* - audio-only (or text + audio) input is not a supported modality mix.
|
|
111
|
-
*/
|
|
112
|
-
if ((firstFrame || lastFrame) && (referenceVideos.length > 0 || referenceAudios.length > 0)) {
|
|
113
|
-
throw new SkillError('❌ --reference-video / --reference-audio cannot be combined with --first-frame / --last-frame; use --reference for images instead');
|
|
114
|
-
}
|
|
115
|
-
if (referenceAudios.length > 0 && references.length === 0 && referenceVideos.length === 0) {
|
|
116
|
-
throw new SkillError('❌ --reference-audio needs at least one --reference image or --reference-video alongside it');
|
|
117
|
-
}
|
|
118
|
-
const ratio = input.ratio ?? '16:9';
|
|
119
|
-
const resolution = input.resolution ?? '720p';
|
|
120
|
-
const duration = toNumber(input.duration, 'duration') ?? descriptor?.constraints.durationDefault ?? 5;
|
|
121
|
-
const seed = toNumber(input.seed, 'seed');
|
|
122
|
-
// Pre-flight validation only when we recognize the model (have its constraints).
|
|
123
|
-
if (descriptor) {
|
|
124
|
-
validateAgainstConstraints(descriptor, duration, ratio, resolution, {
|
|
125
|
-
images: references.length,
|
|
126
|
-
videos: referenceVideos.length,
|
|
127
|
-
audios: referenceAudios.length,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
131
|
-
apiBaseUrl,
|
|
132
|
-
privateToken: input.priv_token,
|
|
133
|
-
preflightToken: ctxIn.auth?.token,
|
|
134
|
-
});
|
|
135
|
-
const payload = {
|
|
136
|
-
model,
|
|
137
|
-
duration,
|
|
138
|
-
ratio,
|
|
139
|
-
resolution,
|
|
140
|
-
generateAudio: isTrue(input.generate_audio),
|
|
141
|
-
cameraFixed: isTrue(input.camera_fixed),
|
|
142
|
-
};
|
|
143
|
-
if (prompt)
|
|
144
|
-
payload.prompt = prompt;
|
|
145
|
-
if (firstFrame)
|
|
146
|
-
payload.firstFrameImage = firstFrame;
|
|
147
|
-
if (lastFrame)
|
|
148
|
-
payload.lastFrameImage = lastFrame;
|
|
149
|
-
if (references.length > 0)
|
|
150
|
-
payload.referenceImages = references;
|
|
151
|
-
if (referenceVideos.length > 0)
|
|
152
|
-
payload.referenceVideos = referenceVideos;
|
|
153
|
-
if (referenceAudios.length > 0)
|
|
154
|
-
payload.referenceAudios = referenceAudios;
|
|
155
|
-
// Web search only fires on text-only input; sending it alongside attachments
|
|
156
|
-
// would be a knob the model silently ignores.
|
|
157
|
-
if (isTrue(input.web_search) && !hasVisual)
|
|
158
|
-
payload.webSearch = true;
|
|
159
|
-
if (isTrue(input.return_last_frame))
|
|
160
|
-
payload.returnLastFrame = true;
|
|
161
|
-
if (input.negative_prompt && String(input.negative_prompt).trim()) {
|
|
162
|
-
payload.negativePrompt = String(input.negative_prompt).trim();
|
|
163
|
-
}
|
|
164
|
-
if (seed !== undefined)
|
|
165
|
-
payload.seed = seed;
|
|
166
|
-
if (input.person_generation)
|
|
167
|
-
payload.personGeneration = String(input.person_generation);
|
|
168
|
-
emitProgress({ phase: 'gen-video:request', model, ratio, resolution, duration });
|
|
169
|
-
const data = await mmPost(ctx, '/model/genVideo', payload, { timeoutMs: 120_000 });
|
|
170
|
-
const taskId = data.taskId;
|
|
171
|
-
if (!taskId) {
|
|
172
|
-
throw new SkillError(`❌ could not parse the task id; API returned: ${JSON.stringify(data)}`);
|
|
173
|
-
}
|
|
174
|
-
emitProgress({ phase: 'gen-video:async', taskId });
|
|
175
|
-
const videoUrl = await pollVideoStatus(ctx, taskId);
|
|
176
|
-
if (!videoUrl) {
|
|
177
|
-
throw new SkillError(`❌ video generated but no URL was returned, task id: ${taskId}`);
|
|
178
|
-
}
|
|
179
|
-
if (isTrue(input.json_output)) {
|
|
180
|
-
process.stdout.write(JSON.stringify({ url: videoUrl }) + '\n');
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
process.stdout.write(`\n🔗 Video URL (renders directly, no download needed):\n${videoUrl}\n`);
|
|
184
|
-
process.stdout.write(`\n🎉 Video generation done!\n`);
|
|
185
|
-
}
|
|
1
|
+
var P=Object.defineProperty;var m=(e,o)=>P(e,"name",{value:o,configurable:!0});import{mmPost as A,pollUntil as B,resolveHttpContext as L,SkillError as r}from"../http.js";import{getCapabilities as j,matchModel as N,defaultModel as U}from"../capabilities.js";import{emitProgress as x}from"../progress.js";import{isTrue as u,resolveImageInput as b,toNumber as R,toStringArray as V}from"./shared.js";function O(e,o,n,i,a){const t=e.constraints,d=e.label||e.id;if(t.durations&&t.durations.length>0){if(!t.durations.includes(o))throw new r(`❌ ${d} duration must be one of: ${t.durations.join(", ")} s; got: ${o}`)}else if(t.durationMin!==void 0&&t.durationMax!==void 0&&(o<t.durationMin||o>t.durationMax))throw new r(`❌ ${d} duration must be ${t.durationMin}–${t.durationMax} s, got: ${o}`);if(t.aspectRatios&&t.aspectRatios.length>0&&!t.aspectRatios.includes(n))throw new r(`❌ ${d} aspect ratio must be one of: ${t.aspectRatios.join(", ")}; got: ${n}`);if(t.resolutions&&t.resolutions.length>0&&!t.resolutions.includes(i))throw new r(`❌ ${d} resolution must be one of: ${t.resolutions.join(", ")}; got: ${i}`);if(t.refImageMax!==void 0&&a.images>t.refImageMax)throw t.refImageMax===0?new r(`❌ ${d} does not support a reference-image array; use --first-frame / --last-frame instead`):new r(`❌ ${d} accepts at most ${t.refImageMax} reference images; got: ${a.images}`);if(t.refVideoMax!==void 0&&a.videos>t.refVideoMax)throw new r(`❌ ${d} accepts at most ${t.refVideoMax} reference videos; got: ${a.videos}`);if(t.refAudioMax!==void 0&&a.audios>t.refAudioMax)throw new r(`❌ ${d} accepts at most ${t.refAudioMax} reference audio clips; got: ${a.audios}`)}m(O,"validateAgainstConstraints");function F(e,o){return e.map(n=>{const i=n.trim();if(!/^https?:\/\//i.test(i))throw new r(`❌ ${o} needs a public http(s) URL (local files are not uploaded), received: ${n}`);return i})}m(F,"requirePublicUrls");async function q(e,o){return B(async()=>{const n=await A(e,"/model/getVideoStatus",{taskId:o},{timeoutMs:3e4}),i=n.status??"unknown";if(x({phase:"gen-video:poll",taskId:o,status:i}),i==="succeeded"||i==="completed")return n.videoUrl??"";if(i==="failed"){const a=n.errorMsg??n.error??n.message??"unknown error";throw new r(`❌ video generation failed: ${a}`)}return null},{intervalMs:1e4,timeoutMs:3e5})}m(q,"pollVideoStatus");async function G(e,o){const n=e.api_base_url,i=await j(n),a=e.model??process.env.MM_VIDEO_MODEL,t=a?N(i.video,a):U(i.video),d=t?.id??a??U(i.video)?.id??"",g=e.first_frame?b(String(e.first_frame)):"",w=e.last_frame?b(String(e.last_frame)):"",c=V(e.reference).map(b).filter(Boolean),f=F(V(e.reference_video).filter(Boolean),"--reference-video"),l=F(V(e.reference_audio).filter(Boolean),"--reference-audio"),p=(e.prompt??"").trim(),S=!!(g||w||c.length>0||f.length>0);if(!p&&!S)throw new r("❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference / --reference-video");if((g||w)&&(f.length>0||l.length>0))throw new r("❌ --reference-video / --reference-audio cannot be combined with --first-frame / --last-frame; use --reference for images instead");if(l.length>0&&c.length===0&&f.length===0)throw new r("❌ --reference-audio needs at least one --reference image or --reference-video alongside it");const v=e.ratio??"16:9",$=e.resolution??"720p",M=R(e.duration,"duration")??t?.constraints.durationDefault??5,k=R(e.seed,"seed");t&&O(t,M,v,$,{images:c.length,videos:f.length,audios:l.length});const y=await L(o.skillName,{apiBaseUrl:n,privateToken:e.priv_token,preflightToken:o.auth?.token}),s={model:d,duration:M,ratio:v,resolution:$,generateAudio:u(e.generate_audio),cameraFixed:u(e.camera_fixed)};p&&(s.prompt=p),g&&(s.firstFrameImage=g),w&&(s.lastFrameImage=w),c.length>0&&(s.referenceImages=c),f.length>0&&(s.referenceVideos=f),l.length>0&&(s.referenceAudios=l),u(e.web_search)&&!S&&(s.webSearch=!0),u(e.return_last_frame)&&(s.returnLastFrame=!0),e.negative_prompt&&String(e.negative_prompt).trim()&&(s.negativePrompt=String(e.negative_prompt).trim()),k!==void 0&&(s.seed=k),e.person_generation&&(s.personGeneration=String(e.person_generation)),x({phase:"gen-video:request",model:d,ratio:v,resolution:$,duration:M});const I=await A(y,"/model/genVideo",s,{timeoutMs:12e4}),h=I.taskId;if(!h)throw new r(`❌ could not parse the task id; API returned: ${JSON.stringify(I)}`);x({phase:"gen-video:async",taskId:h});const _=await q(y,h);if(!_)throw new r(`❌ video generated but no URL was returned, task id: ${h}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:_})+`
|
|
2
|
+
`);return}process.stdout.write(`
|
|
3
|
+
🔗 Video URL (renders directly, no download needed):
|
|
4
|
+
${_}
|
|
5
|
+
`),process.stdout.write(`
|
|
6
|
+
🎉 Video generation done!
|
|
7
|
+
`)}m(G,"genVideo");export{G as genVideo};
|
|
@@ -1,133 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* The previous Python script also offered `--download` to write the MP3 to
|
|
21
|
-
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
22
|
-
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
23
|
-
* handler stateless.
|
|
24
|
-
*/
|
|
25
|
-
import { mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
26
|
-
import { getCapabilities } from '../capabilities.js';
|
|
27
|
-
import { emitProgress } from '../progress.js';
|
|
28
|
-
import { isTrue, toNumber } from './shared.js';
|
|
29
|
-
/**
|
|
30
|
-
* Fallback voice catalog printed when `--list-voices --local` is passed.
|
|
31
|
-
* Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
|
|
32
|
-
* by hand because the two callers live on different runtimes.
|
|
33
|
-
*
|
|
34
|
-
* This is **not** a list of available voices — it is the two ids the resolver
|
|
35
|
-
* falls back to when nothing else is specified, and one of them is still a
|
|
36
|
-
* placeholder. Never treat it as evidence about what the service offers.
|
|
37
|
-
*
|
|
38
|
-
* Promote to a JSON config once this grows beyond a handful of entries.
|
|
39
|
-
*/
|
|
40
|
-
const LOCAL_VOICE_CATALOG = [
|
|
41
|
-
{ id: 'Chinese (Mandarin)_Male_Announcer', language: 'zh', name: 'Mandarin Male Announcer (legacy default)' },
|
|
42
|
-
// TODO: replace once gen-voice service confirms an English default voice id.
|
|
43
|
-
{ id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
|
|
44
|
-
];
|
|
45
|
-
async function listVoicesRemote(ctx) {
|
|
46
|
-
const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
|
|
47
|
-
const voices = data.list ?? [];
|
|
48
|
-
if (voices.length === 0) {
|
|
49
|
-
process.stdout.write('⚠️ No voices available\n');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
process.stdout.write(`\n${'Voice ID'.padEnd(30)} Name\n`);
|
|
53
|
-
process.stdout.write(`${'-'.repeat(30)} ${'-'.repeat(20)}\n`);
|
|
54
|
-
for (const v of voices) {
|
|
55
|
-
process.stdout.write(`${(v.uniqId ?? '').padEnd(30)} ${v.name ?? ''}\n`);
|
|
56
|
-
}
|
|
57
|
-
process.stdout.write(`\n${voices.length} voice(s) available\n`);
|
|
58
|
-
process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis\n`);
|
|
59
|
-
}
|
|
60
|
-
function listVoicesLocal() {
|
|
61
|
-
process.stderr.write('⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. ' +
|
|
62
|
-
'Drop --local to query the live catalog.\n');
|
|
63
|
-
for (const v of LOCAL_VOICE_CATALOG) {
|
|
64
|
-
process.stdout.write(`${v.id}\t${v.language}\t${v.name}\n`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export async function genVoice(input, ctxIn) {
|
|
68
|
-
// ── --list-voices: catalog mode (no synthesis) ──────────────────────────
|
|
69
|
-
if (isTrue(input.list_voices)) {
|
|
70
|
-
if (isTrue(input.local))
|
|
71
|
-
return listVoicesLocal();
|
|
72
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
73
|
-
apiBaseUrl: input.api_base_url,
|
|
74
|
-
privateToken: input.priv_token,
|
|
75
|
-
preflightToken: ctxIn.auth?.token,
|
|
76
|
-
});
|
|
77
|
-
return listVoicesRemote(ctx);
|
|
78
|
-
}
|
|
79
|
-
// ── synthesize ──────────────────────────────────────────────────────────
|
|
80
|
-
const text = input.text;
|
|
81
|
-
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
82
|
-
throw new SkillError('❌ Please provide --text to specify the text to synthesize');
|
|
83
|
-
}
|
|
84
|
-
const apiBaseUrl = input.api_base_url;
|
|
85
|
-
const voice = (await getCapabilities(apiBaseUrl)).voice;
|
|
86
|
-
const voiceId = input.voice_id ?? voice.defaultVoiceId;
|
|
87
|
-
const speed = toNumber(input.speed, 'speed') ?? 1.0;
|
|
88
|
-
if (speed < voice.speedMin || speed > voice.speedMax) {
|
|
89
|
-
throw new SkillError(`❌ speed must be in the range ${voice.speedMin}~${voice.speedMax}`);
|
|
90
|
-
}
|
|
91
|
-
// Language hint for mixed-script narration. Like `local`, this is deliberately
|
|
92
|
-
// absent from skill.json's `parameters`: the service already applies a sane
|
|
93
|
-
// default for every caller, so handing the model a 40-value enum only invites
|
|
94
|
-
// it to guess. Reachable from the DSL (global/scene `narration.languageBoost`,
|
|
95
|
-
// forwarded by render_video) and by typing the flag yourself. Omitted =
|
|
96
|
-
// the service default.
|
|
97
|
-
const languageBoost = input.language_boost?.trim() || undefined;
|
|
98
|
-
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
99
|
-
apiBaseUrl,
|
|
100
|
-
privateToken: input.priv_token,
|
|
101
|
-
preflightToken: ctxIn.auth?.token,
|
|
102
|
-
});
|
|
103
|
-
emitProgress({ phase: 'gen-voice:request', voiceId, speed });
|
|
104
|
-
const data = await mmPost(ctx, '/tool/tts', {
|
|
105
|
-
provider: voice.provider,
|
|
106
|
-
text,
|
|
107
|
-
voiceSetting: { voiceId, speed },
|
|
108
|
-
outputFormat: 'url',
|
|
109
|
-
...(languageBoost ? { languageBoost } : {}),
|
|
110
|
-
}, { timeoutMs: 120_000 });
|
|
111
|
-
const audioUrl = data.audio;
|
|
112
|
-
if (!audioUrl) {
|
|
113
|
-
throw new SkillError(`❌ API response did not contain an audio URL: ${JSON.stringify(data)}`);
|
|
114
|
-
}
|
|
115
|
-
if (isTrue(input.json_output)) {
|
|
116
|
-
process.stdout.write(JSON.stringify({
|
|
117
|
-
url: audioUrl,
|
|
118
|
-
audio_length_ms: data.extraInfo?.audioLength ?? null,
|
|
119
|
-
subtitles: (data.subtitles ?? []).map((s) => ({
|
|
120
|
-
text: s.text ?? '',
|
|
121
|
-
timeBegin: s.timeBegin ?? 0,
|
|
122
|
-
timeEnd: s.timeEnd ?? 0,
|
|
123
|
-
})),
|
|
124
|
-
}) + '\n');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const lengthMs = data.extraInfo?.audioLength;
|
|
128
|
-
if (typeof lengthMs === 'number') {
|
|
129
|
-
process.stdout.write(`\n🕐 Audio length: ${(lengthMs / 1000).toFixed(1)} s\n`);
|
|
130
|
-
}
|
|
131
|
-
process.stdout.write(`\n🔗 Audio URL (plays directly, no download needed):\n${audioUrl}\n`);
|
|
132
|
-
process.stdout.write(`\n🎉 TTS synthesis done!\n`);
|
|
133
|
-
}
|
|
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
|
|
2
|
+
`);return}process.stdout.write(`
|
|
3
|
+
${"Voice ID".padEnd(30)} Name
|
|
4
|
+
`),process.stdout.write(`${"-".repeat(30)} ${"-".repeat(20)}
|
|
5
|
+
`);for(const s of t)process.stdout.write(`${(s.uniqId??"").padEnd(30)} ${s.name??""}
|
|
6
|
+
`);process.stdout.write(`
|
|
7
|
+
${t.length} voice(s) available
|
|
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.
|
|
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(`
|
|
13
|
+
🕐 Audio length: ${(f/1e3).toFixed(1)} s
|
|
14
|
+
`),process.stdout.write(`
|
|
15
|
+
🔗 Audio URL (plays directly, no download needed):
|
|
16
|
+
${d}
|
|
17
|
+
`),process.stdout.write(`
|
|
18
|
+
🎉 TTS synthesis done!
|
|
19
|
+
`)}l(E,"genVoice");export{E as genVoice};
|
package/dist/handlers/index.js
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Handler registry — maps `entry.handler` strings to their implementation.
|
|
3
|
-
*
|
|
4
|
-
* Only `entry.type` in ('http' | 'builtin') uses this registry; python
|
|
5
|
-
* skills go through spawn() in runner.ts and bypass handlers entirely.
|
|
6
|
-
*/
|
|
7
|
-
import { genImage } from './gen-image.js';
|
|
8
|
-
import { genVoice } from './gen-voice.js';
|
|
9
|
-
import { genVideo } from './gen-video.js';
|
|
10
|
-
import { genDigitalHuman } from './gen-digital-human.js';
|
|
11
|
-
import { templateInit } from './template-init.js';
|
|
12
|
-
import { genPost } from './gen-post.js';
|
|
13
|
-
export const HANDLERS = {
|
|
14
|
-
'gen-image': genImage,
|
|
15
|
-
'gen-voice': genVoice,
|
|
16
|
-
'gen-video': genVideo,
|
|
17
|
-
'gen-digital-human': genDigitalHuman,
|
|
18
|
-
'template-init': templateInit,
|
|
19
|
-
'gen-post': genPost,
|
|
20
|
-
};
|
|
1
|
+
import{genImage as e}from"./gen-image.js";import{genVoice as o}from"./gen-voice.js";import{genVideo as i}from"./gen-video.js";import{genDigitalHuman as m}from"./gen-digital-human.js";import{templateInit as t}from"./template-init.js";import{genPost as n}from"./gen-post.js";const c={"gen-image":e,"gen-voice":o,"gen-video":i,"gen-digital-human":m,"template-init":t,"gen-post":n};export{c as HANDLERS};
|