@gitruck/cli 0.2.9 → 0.2.11
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/AGENT.md +36 -8
- package/README.md +485 -384
- package/assets/agent-example-1.png +0 -0
- package/assets/agent-example-2.png +0 -0
- package/assets/agent-example-3.png +0 -0
- package/assets/agent-example-4.png +0 -0
- package/assets/gtrk-agent-intro.png +0 -0
- package/dist/index.js +1833 -186
- package/package.json +2 -2
- package/skills/gtrk-music-visualizer/SKILL.md +71 -0
- package/skills/gtrk-style-maker/SKILL.md +2 -2
- package/skills/gtrk-style-maker/references/craft-output-spec.md +2 -2
- package/skills/gtrk-tools/SKILL.md +51 -12
- package/skills/gtrk-transcript/SKILL.md +53 -0
- package/skills/gtrk-transcript/agents/openai.yaml +4 -0
package/dist/index.js
CHANGED
|
@@ -1892,11 +1892,11 @@ var require_index_umd = __commonJS((exports, module) => {
|
|
|
1892
1892
|
};
|
|
1893
1893
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1894
1894
|
function adopt(value) {
|
|
1895
|
-
return value instanceof P ? value : new P(function(
|
|
1896
|
-
|
|
1895
|
+
return value instanceof P ? value : new P(function(resolve4) {
|
|
1896
|
+
resolve4(value);
|
|
1897
1897
|
});
|
|
1898
1898
|
}
|
|
1899
|
-
return new (P || (P = Promise))(function(
|
|
1899
|
+
return new (P || (P = Promise))(function(resolve4, reject) {
|
|
1900
1900
|
function fulfilled(value) {
|
|
1901
1901
|
try {
|
|
1902
1902
|
step(generator.next(value));
|
|
@@ -1912,7 +1912,7 @@ var require_index_umd = __commonJS((exports, module) => {
|
|
|
1912
1912
|
}
|
|
1913
1913
|
}
|
|
1914
1914
|
function step(result) {
|
|
1915
|
-
result.done ?
|
|
1915
|
+
result.done ? resolve4(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
1916
1916
|
}
|
|
1917
1917
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
1918
1918
|
});
|
|
@@ -4197,7 +4197,7 @@ var {
|
|
|
4197
4197
|
|
|
4198
4198
|
// src/index.ts
|
|
4199
4199
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4200
|
-
import { join as
|
|
4200
|
+
import { join as join28 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4246,9 +4246,10 @@ function migrateLegacyHome() {
|
|
|
4246
4246
|
}
|
|
4247
4247
|
|
|
4248
4248
|
// src/commands/skills.ts
|
|
4249
|
-
import {
|
|
4250
|
-
import { join as join2 } from "node:path";
|
|
4249
|
+
import { spawnSync } from "node:child_process";
|
|
4251
4250
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync as cpSync2 } from "node:fs";
|
|
4251
|
+
import { homedir as homedir2 } from "node:os";
|
|
4252
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
4252
4253
|
|
|
4253
4254
|
// src/lib/log.ts
|
|
4254
4255
|
var c = {
|
|
@@ -4284,40 +4285,248 @@ var SKILL_NAMES = [
|
|
|
4284
4285
|
"gtrk-mg",
|
|
4285
4286
|
"gtrk-ai-drama",
|
|
4286
4287
|
"gtrk-style-maker",
|
|
4287
|
-
"gtrk-
|
|
4288
|
+
"gtrk-transcript",
|
|
4289
|
+
"gtrk-tools",
|
|
4290
|
+
"gtrk-music-visualizer"
|
|
4288
4291
|
];
|
|
4289
|
-
|
|
4290
|
-
|
|
4292
|
+
var SUPPLEMENTAL_AGENTS = [
|
|
4293
|
+
{
|
|
4294
|
+
id: "workbuddy",
|
|
4295
|
+
displayName: "WorkBuddy",
|
|
4296
|
+
dataDir: ".workbuddy"
|
|
4297
|
+
},
|
|
4298
|
+
{
|
|
4299
|
+
id: "qoderwork",
|
|
4300
|
+
displayName: "QoderWork",
|
|
4301
|
+
dataDir: ".qoderwork"
|
|
4302
|
+
},
|
|
4303
|
+
{
|
|
4304
|
+
id: "comate",
|
|
4305
|
+
displayName: "Baidu Comate",
|
|
4306
|
+
dataDir: ".comate"
|
|
4307
|
+
}
|
|
4308
|
+
];
|
|
4309
|
+
var SUPPLEMENTAL_AGENT_IDS = new Set(SUPPLEMENTAL_AGENTS.map((agent) => agent.id));
|
|
4310
|
+
var AGENT_ALIASES = {
|
|
4311
|
+
claude: "claude-code",
|
|
4312
|
+
"trae-global": "trae",
|
|
4313
|
+
"tencent-workbuddy": "workbuddy",
|
|
4314
|
+
"qoder-work": "qoderwork",
|
|
4315
|
+
"baidu-comate": "comate",
|
|
4316
|
+
"wenxin-comate": "comate",
|
|
4317
|
+
qwen: "qwen-code",
|
|
4318
|
+
kimi: "kimi-code-cli",
|
|
4319
|
+
"kimi-code": "kimi-code-cli",
|
|
4320
|
+
iflow: "iflow-cli",
|
|
4321
|
+
codearts: "codearts-agent",
|
|
4322
|
+
"tongyi-lingma": "lingma",
|
|
4323
|
+
"tencent-codebuddy": "codebuddy"
|
|
4324
|
+
};
|
|
4325
|
+
var SAFE_AGENT_ID = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u;
|
|
4326
|
+
var ANSI_ESCAPE = /\x1B\[[0-?]*[ -/]*[@-~]/gu;
|
|
4327
|
+
var PROMPTSCRIPT_GLOBAL_ERROR = "PromptScript: PromptScript does not support global skill installation";
|
|
4328
|
+
function parseAgentIds(input) {
|
|
4329
|
+
const values = (input ?? "").split(/[\s,]+/u).map((value) => value.trim().toLowerCase()).filter(Boolean).map((value) => AGENT_ALIASES[value] ?? value);
|
|
4330
|
+
const invalid = values.filter((value) => !SAFE_AGENT_ID.test(value));
|
|
4331
|
+
if (invalid.length > 0) {
|
|
4332
|
+
throw new Error(`Agent ID 格式不合法:${invalid.join(", ")}`);
|
|
4333
|
+
}
|
|
4334
|
+
return [...new Set(values)];
|
|
4335
|
+
}
|
|
4336
|
+
function splitAgentSelection(input) {
|
|
4337
|
+
const requested = parseAgentIds(input);
|
|
4338
|
+
return {
|
|
4339
|
+
requested,
|
|
4340
|
+
upstream: requested.filter((id) => !SUPPLEMENTAL_AGENT_IDS.has(id)),
|
|
4341
|
+
supplemental: requested.filter((id) => SUPPLEMENTAL_AGENT_IDS.has(id))
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
function resolveSupplementalAgentTargets(opts = {}, home = homedir2(), pathExists = existsSync2) {
|
|
4345
|
+
const selection = splitAgentSelection(opts.agents);
|
|
4346
|
+
const explicitIds = new Set(selection.supplemental);
|
|
4347
|
+
return SUPPLEMENTAL_AGENTS.filter((agent) => {
|
|
4348
|
+
if (opts.all)
|
|
4349
|
+
return true;
|
|
4350
|
+
if (selection.requested.length > 0)
|
|
4351
|
+
return explicitIds.has(agent.id);
|
|
4352
|
+
return pathExists(join2(home, agent.dataDir));
|
|
4353
|
+
}).map((agent) => ({
|
|
4354
|
+
id: agent.id,
|
|
4355
|
+
displayName: agent.displayName,
|
|
4356
|
+
destRoot: join2(home, agent.dataDir, "skills")
|
|
4357
|
+
}));
|
|
4358
|
+
}
|
|
4359
|
+
function plainTerminalLine(line2) {
|
|
4360
|
+
return line2.replace(ANSI_ESCAPE, "").trim();
|
|
4361
|
+
}
|
|
4362
|
+
function filterKnownAdapterOutput(output) {
|
|
4363
|
+
const newline = output.includes(`\r
|
|
4364
|
+
`) ? `\r
|
|
4365
|
+
` : `
|
|
4366
|
+
`;
|
|
4367
|
+
const lines = output.split(/\r?\n/u);
|
|
4368
|
+
const failureStart = lines.findIndex((line2) => plainTerminalLine(line2).includes("Failed to install"));
|
|
4369
|
+
if (failureStart < 0)
|
|
4370
|
+
return { output, suppressedPromptScriptFailures: 0 };
|
|
4371
|
+
const doneIndex = lines.findIndex((line2, index) => index > failureStart && plainTerminalLine(line2).includes("Done!"));
|
|
4372
|
+
if (doneIndex < 0)
|
|
4373
|
+
return { output, suppressedPromptScriptFailures: 0 };
|
|
4374
|
+
const failureLines = lines.slice(failureStart, doneIndex).map(plainTerminalLine).filter((line2) => line2.includes("✗"));
|
|
4375
|
+
if (failureLines.length === 0 || failureLines.some((line2) => !line2.includes(PROMPTSCRIPT_GLOBAL_ERROR))) {
|
|
4376
|
+
return { output, suppressedPromptScriptFailures: 0 };
|
|
4377
|
+
}
|
|
4378
|
+
let removeStart = failureStart;
|
|
4379
|
+
while (removeStart > 0) {
|
|
4380
|
+
const previous = plainTerminalLine(lines[removeStart - 1] ?? "");
|
|
4381
|
+
if (previous !== "" && previous !== "│" && previous !== "|")
|
|
4382
|
+
break;
|
|
4383
|
+
removeStart -= 1;
|
|
4384
|
+
}
|
|
4385
|
+
let removeEnd = doneIndex + 1;
|
|
4386
|
+
while (removeEnd < lines.length && plainTerminalLine(lines[removeEnd] ?? "") === "") {
|
|
4387
|
+
removeEnd += 1;
|
|
4388
|
+
}
|
|
4389
|
+
return {
|
|
4390
|
+
output: [...lines.slice(0, removeStart), ...lines.slice(removeEnd)].join(newline),
|
|
4391
|
+
suppressedPromptScriptFailures: failureLines.length
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
4394
|
+
function buildSkillsAdapterArgs(source, opts = {}) {
|
|
4395
|
+
const args = ["-y", "skills", "add", source, "-g", "-y"];
|
|
4396
|
+
if (opts.all) {
|
|
4397
|
+
args.push("--all");
|
|
4398
|
+
} else {
|
|
4399
|
+
for (const agent of splitAgentSelection(opts.agents).upstream) {
|
|
4400
|
+
args.push("--agent", agent);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
if (opts.copy)
|
|
4404
|
+
args.push("--copy");
|
|
4405
|
+
return args;
|
|
4406
|
+
}
|
|
4407
|
+
function npxInvocation(args) {
|
|
4408
|
+
const npmExecPath = process.env.npm_execpath;
|
|
4409
|
+
const candidates = [
|
|
4410
|
+
npmExecPath && join2(dirname2(npmExecPath), "npx-cli.js"),
|
|
4411
|
+
join2(dirname2(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
|
|
4412
|
+
resolve(dirname2(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npx-cli.js")
|
|
4413
|
+
].filter((value) => Boolean(value));
|
|
4414
|
+
const npxCli = candidates.find((value) => existsSync2(value));
|
|
4415
|
+
if (npxCli)
|
|
4416
|
+
return { command: process.execPath, args: [npxCli, ...args], shell: false };
|
|
4417
|
+
return { command: "npx", args, shell: process.platform === "win32" };
|
|
4418
|
+
}
|
|
4419
|
+
function validateBundledSkills(source) {
|
|
4291
4420
|
let allOk = true;
|
|
4292
4421
|
for (const name of SKILL_NAMES) {
|
|
4293
|
-
const
|
|
4294
|
-
if (!existsSync2(
|
|
4295
|
-
log.warn(`找不到打包的 skill 源:${
|
|
4422
|
+
const manifest = join2(source, name, "SKILL.md");
|
|
4423
|
+
if (!existsSync2(manifest)) {
|
|
4424
|
+
log.warn(`找不到打包的 skill 源:${manifest}(跳过 ${name},不影响命令行)`);
|
|
4296
4425
|
allOk = false;
|
|
4297
|
-
continue;
|
|
4298
4426
|
}
|
|
4427
|
+
}
|
|
4428
|
+
return allOk;
|
|
4429
|
+
}
|
|
4430
|
+
function copySkillsToTarget(dir, source, targetLabel) {
|
|
4431
|
+
const destRoot = resolve(dir);
|
|
4432
|
+
let allOk = validateBundledSkills(source);
|
|
4433
|
+
for (const name of SKILL_NAMES) {
|
|
4434
|
+
const src = join2(source, name);
|
|
4435
|
+
if (!existsSync2(join2(src, "SKILL.md")))
|
|
4436
|
+
continue;
|
|
4299
4437
|
const dest = join2(destRoot, name);
|
|
4300
4438
|
try {
|
|
4301
4439
|
mkdirSync2(dest, { recursive: true });
|
|
4302
4440
|
cpSync2(src, dest, { recursive: true });
|
|
4303
|
-
log.ok(`已安装
|
|
4304
|
-
} catch (
|
|
4441
|
+
log.ok(`已安装 ${name}${targetLabel ? ` → ${targetLabel}` : ""}:${join2(dest, "SKILL.md")}`);
|
|
4442
|
+
} catch (error) {
|
|
4305
4443
|
allOk = false;
|
|
4306
|
-
log.warn(`skill 安装失败(${name},不影响命令行使用):${
|
|
4444
|
+
log.warn(`skill 安装失败(${name},不影响命令行使用):${error instanceof Error ? error.message : String(error)}`);
|
|
4307
4445
|
}
|
|
4308
4446
|
}
|
|
4309
|
-
|
|
4447
|
+
if (targetLabel) {
|
|
4448
|
+
log.info(`已写入 ${targetLabel};若当前会话未出现新 Skill,请重启或刷新该 Agent。`);
|
|
4449
|
+
} else {
|
|
4450
|
+
log.info("已写入自定义 skills 目录;具体调用入口以该 Agent 的界面为准。");
|
|
4451
|
+
}
|
|
4310
4452
|
return allOk;
|
|
4311
4453
|
}
|
|
4454
|
+
function copySkillsToDirectory(dir, source = join2(packageRoot(), "skills")) {
|
|
4455
|
+
return copySkillsToTarget(dir, source);
|
|
4456
|
+
}
|
|
4457
|
+
function installSkill(opts = {}) {
|
|
4458
|
+
const source = resolve(opts.source ?? join2(packageRoot(), "skills"));
|
|
4459
|
+
if (opts.dir)
|
|
4460
|
+
return copySkillsToDirectory(opts.dir, source);
|
|
4461
|
+
let selection;
|
|
4462
|
+
try {
|
|
4463
|
+
selection = splitAgentSelection(opts.agents);
|
|
4464
|
+
} catch (error) {
|
|
4465
|
+
log.err(error instanceof Error ? error.message : String(error));
|
|
4466
|
+
return false;
|
|
4467
|
+
}
|
|
4468
|
+
const sourcesOk = validateBundledSkills(source);
|
|
4469
|
+
const shouldRunAdapter = opts.all || selection.requested.length === 0 || selection.upstream.length > 0;
|
|
4470
|
+
let adapterOk = true;
|
|
4471
|
+
if (shouldRunAdapter) {
|
|
4472
|
+
const args = buildSkillsAdapterArgs(source, {
|
|
4473
|
+
...opts,
|
|
4474
|
+
agents: selection.upstream.join(",")
|
|
4475
|
+
});
|
|
4476
|
+
log.info("使用通用 Agent Skills 适配器:自动探测宿主、统一存储,并链接到各 Agent。");
|
|
4477
|
+
const invocation = npxInvocation(args);
|
|
4478
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
4479
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
4480
|
+
shell: invocation.shell,
|
|
4481
|
+
encoding: "utf8",
|
|
4482
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4483
|
+
});
|
|
4484
|
+
const defaultAutoInstall = !opts.all && selection.requested.length === 0;
|
|
4485
|
+
const filtered = defaultAutoInstall ? filterKnownAdapterOutput(result.stdout ?? "") : { output: result.stdout ?? "", suppressedPromptScriptFailures: 0 };
|
|
4486
|
+
if (filtered.output) {
|
|
4487
|
+
process.stdout.write(filtered.output);
|
|
4488
|
+
if (!filtered.output.endsWith(`
|
|
4489
|
+
`))
|
|
4490
|
+
process.stdout.write(`
|
|
4491
|
+
`);
|
|
4492
|
+
}
|
|
4493
|
+
if (result.stderr)
|
|
4494
|
+
process.stderr.write(result.stderr);
|
|
4495
|
+
if (filtered.suppressedPromptScriptFailures > 0) {
|
|
4496
|
+
log.info("已跳过 PromptScript:它只支持项目级 Skill,不参与本次全局安装;其他 Agent 不受影响。");
|
|
4497
|
+
}
|
|
4498
|
+
if (result.error) {
|
|
4499
|
+
adapterOk = false;
|
|
4500
|
+
log.warn(`无法启动 skills 适配器:${result.error.message}`);
|
|
4501
|
+
log.info(`可手动重试:npx -y skills add "${source}" -g -y`);
|
|
4502
|
+
} else if (result.status !== 0) {
|
|
4503
|
+
adapterOk = false;
|
|
4504
|
+
log.warn(`skills 适配器安装失败(退出码 ${result.status ?? "未知"})。`);
|
|
4505
|
+
log.info(`可查看支持的 Agent ID:npx -y skills add "${source}" --list`);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
let supplementalOk = true;
|
|
4509
|
+
const supplementalTargets = resolveSupplementalAgentTargets(opts, opts.home ?? homedir2());
|
|
4510
|
+
for (const target of supplementalTargets) {
|
|
4511
|
+
log.info(`使用 gtrk 补充适配:${target.displayName} → ${target.destRoot}`);
|
|
4512
|
+
if (!copySkillsToTarget(target.destRoot, source, target.displayName))
|
|
4513
|
+
supplementalOk = false;
|
|
4514
|
+
}
|
|
4515
|
+
if (adapterOk && supplementalOk) {
|
|
4516
|
+
log.info("若当前会话没有立刻出现新 skill,请刷新窗口或新开一个会话。");
|
|
4517
|
+
}
|
|
4518
|
+
return sourcesOk && adapterOk && supplementalOk;
|
|
4519
|
+
}
|
|
4312
4520
|
function registerSkills(program2) {
|
|
4313
|
-
const skills = program2.command("skills").description("
|
|
4314
|
-
skills.command("install").description("把 gtrk 全家框架 skill
|
|
4315
|
-
installSkill(
|
|
4521
|
+
const skills = program2.command("skills").description("管理跨 Agent Skills(通用适配器 + gtrk 补充宿主)");
|
|
4522
|
+
skills.command("install").description("把 gtrk 全家框架 skill 安装到自动检测或指定的 Agent").option("--agents <list>", "指定 Agent ID,逗号分隔,如 codex,trae-cn,workbuddy,comate").option("--all", "安装到上游和 gtrk 已登记的全部 Agent").option("--copy", "每个 Agent 各复制一份(默认统一存储 + symlink/junction)").option("--dir <dir>", "兼容模式:直接复制到单个 skills 目录,绕过 Agent 适配器").action((opts) => {
|
|
4523
|
+
if (!installSkill(opts))
|
|
4524
|
+
process.exitCode = 1;
|
|
4316
4525
|
});
|
|
4317
4526
|
}
|
|
4318
4527
|
|
|
4319
4528
|
// src/commands/init.ts
|
|
4320
|
-
import { join as join9, resolve as
|
|
4529
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
4321
4530
|
import { existsSync as existsSync8 } from "node:fs";
|
|
4322
4531
|
|
|
4323
4532
|
// src/lib/user-config.ts
|
|
@@ -4345,7 +4554,7 @@ function writeUserConfig(patch) {
|
|
|
4345
4554
|
}
|
|
4346
4555
|
|
|
4347
4556
|
// src/lib/jianying.ts
|
|
4348
|
-
import { join as join4, resolve } from "node:path";
|
|
4557
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
4349
4558
|
import { existsSync as existsSync4 } from "node:fs";
|
|
4350
4559
|
function probeJianyingDraftDir() {
|
|
4351
4560
|
const local = process.env.LOCALAPPDATA;
|
|
@@ -4359,7 +4568,7 @@ function probeJianyingDraftDir() {
|
|
|
4359
4568
|
}
|
|
4360
4569
|
function resolveJianyingDraftDir(opt) {
|
|
4361
4570
|
if (opt && opt !== "auto")
|
|
4362
|
-
return
|
|
4571
|
+
return resolve2(opt);
|
|
4363
4572
|
const saved = readUserConfig().jianyingDraftDir;
|
|
4364
4573
|
if (saved && existsSync4(saved))
|
|
4365
4574
|
return saved;
|
|
@@ -4386,7 +4595,7 @@ function launch(cmd) {
|
|
|
4386
4595
|
|
|
4387
4596
|
// src/lib/prompt.ts
|
|
4388
4597
|
import { stdin, stdout } from "node:process";
|
|
4389
|
-
import { spawnSync } from "node:child_process";
|
|
4598
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4390
4599
|
var c2 = {
|
|
4391
4600
|
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
4392
4601
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
@@ -4394,24 +4603,24 @@ var c2 = {
|
|
|
4394
4603
|
function readClipboard() {
|
|
4395
4604
|
try {
|
|
4396
4605
|
if (process.platform === "win32") {
|
|
4397
|
-
const r =
|
|
4606
|
+
const r = spawnSync2("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
|
|
4398
4607
|
encoding: "utf8"
|
|
4399
4608
|
});
|
|
4400
4609
|
return (r.stdout ?? "").replace(/\r?\n$/, "");
|
|
4401
4610
|
}
|
|
4402
4611
|
if (process.platform === "darwin") {
|
|
4403
|
-
return
|
|
4612
|
+
return spawnSync2("pbpaste", { encoding: "utf8" }).stdout ?? "";
|
|
4404
4613
|
}
|
|
4405
|
-
const x =
|
|
4614
|
+
const x = spawnSync2("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
|
|
4406
4615
|
if (x.status === 0)
|
|
4407
4616
|
return x.stdout ?? "";
|
|
4408
|
-
return
|
|
4617
|
+
return spawnSync2("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
|
|
4409
4618
|
} catch {
|
|
4410
4619
|
return "";
|
|
4411
4620
|
}
|
|
4412
4621
|
}
|
|
4413
4622
|
function ask(message, opts = {}) {
|
|
4414
|
-
return new Promise((
|
|
4623
|
+
return new Promise((resolve3) => {
|
|
4415
4624
|
const hint = opts.defaultValue ? c2.dim(` (${opts.defaultValue})`) : "";
|
|
4416
4625
|
stdout.write(`${c2.cyan("?")} ${message}${hint} `);
|
|
4417
4626
|
let value = "";
|
|
@@ -4432,7 +4641,7 @@ function ask(message, opts = {}) {
|
|
|
4432
4641
|
stdout.write(`
|
|
4433
4642
|
`);
|
|
4434
4643
|
cleanup();
|
|
4435
|
-
|
|
4644
|
+
resolve3(value || opts.defaultValue || "");
|
|
4436
4645
|
return;
|
|
4437
4646
|
}
|
|
4438
4647
|
if (ch === "\x03") {
|
|
@@ -5139,7 +5348,7 @@ function effectiveVocab(config) {
|
|
|
5139
5348
|
}
|
|
5140
5349
|
|
|
5141
5350
|
// src/lib/ffmpeg.ts
|
|
5142
|
-
import { spawn as spawn2, spawnSync as
|
|
5351
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
5143
5352
|
import { existsSync as existsSync6 } from "node:fs";
|
|
5144
5353
|
import { join as join6 } from "node:path";
|
|
5145
5354
|
var isWin = process.platform === "win32";
|
|
@@ -5148,7 +5357,7 @@ var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpe
|
|
|
5148
5357
|
var _cache = new Map;
|
|
5149
5358
|
function onSystemPath(cmd) {
|
|
5150
5359
|
try {
|
|
5151
|
-
return
|
|
5360
|
+
return spawnSync3(cmd, ["-version"], { stdio: "ignore" }).status === 0;
|
|
5152
5361
|
} catch {
|
|
5153
5362
|
return false;
|
|
5154
5363
|
}
|
|
@@ -5183,14 +5392,14 @@ function requireFfmpeg(ffmpegPath) {
|
|
|
5183
5392
|
return r;
|
|
5184
5393
|
}
|
|
5185
5394
|
function ffprobeJson(ffprobePath, args) {
|
|
5186
|
-
const r =
|
|
5395
|
+
const r = spawnSync3(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
5187
5396
|
if (r.status !== 0) {
|
|
5188
5397
|
throw new Error(`ffprobe 失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
|
|
5189
5398
|
}
|
|
5190
5399
|
return JSON.parse(r.stdout || "{}");
|
|
5191
5400
|
}
|
|
5192
5401
|
function runFfmpeg(ffmpegPath, args, onLine) {
|
|
5193
|
-
return new Promise((
|
|
5402
|
+
return new Promise((resolve3, reject) => {
|
|
5194
5403
|
const p = spawn2(ffmpegPath, args, { env: process.env });
|
|
5195
5404
|
let tail = "";
|
|
5196
5405
|
p.stderr.on("data", (buf) => {
|
|
@@ -5205,20 +5414,20 @@ function runFfmpeg(ffmpegPath, args, onLine) {
|
|
|
5205
5414
|
p.on("error", (e) => reject(e));
|
|
5206
5415
|
p.on("close", (code) => {
|
|
5207
5416
|
if (code === 0)
|
|
5208
|
-
|
|
5417
|
+
resolve3();
|
|
5209
5418
|
else
|
|
5210
5419
|
reject(new Error(`ffmpeg 退出码 ${code}:${tail.slice(-600)}`));
|
|
5211
5420
|
});
|
|
5212
5421
|
});
|
|
5213
5422
|
}
|
|
5214
5423
|
function probeCapabilities(res) {
|
|
5215
|
-
const ver =
|
|
5424
|
+
const ver = spawnSync3(res.ffmpeg, ["-version"], { encoding: "utf8" });
|
|
5216
5425
|
const version = (ver.stdout || "").split(/\r?\n/)[0]?.trim() || "unknown";
|
|
5217
|
-
const enc =
|
|
5426
|
+
const enc = spawnSync3(res.ffmpeg, ["-hide_banner", "-encoders"], {
|
|
5218
5427
|
encoding: "utf8",
|
|
5219
5428
|
maxBuffer: 16 * 1024 * 1024
|
|
5220
5429
|
});
|
|
5221
|
-
const fil =
|
|
5430
|
+
const fil = spawnSync3(res.ffmpeg, ["-hide_banner", "-filters"], {
|
|
5222
5431
|
encoding: "utf8",
|
|
5223
5432
|
maxBuffer: 16 * 1024 * 1024
|
|
5224
5433
|
});
|
|
@@ -5452,7 +5661,7 @@ async function runInit(opts) {
|
|
|
5452
5661
|
const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
|
|
5453
5662
|
if (manual) {
|
|
5454
5663
|
if (existsSync8(manual))
|
|
5455
|
-
jianyingDraftDir =
|
|
5664
|
+
jianyingDraftDir = resolve3(manual);
|
|
5456
5665
|
else
|
|
5457
5666
|
log.warn(`目录不存在,已跳过:${manual}`);
|
|
5458
5667
|
}
|
|
@@ -5475,7 +5684,7 @@ async function afterConfigDoctor() {
|
|
|
5475
5684
|
if (healthy) {
|
|
5476
5685
|
log.step("装好了!两种用法任选:");
|
|
5477
5686
|
log.info('① 命令行直接剪:gtrk oralcut "<毛片.mp4>" --script "<文字稿.txt>"(无稿就别加 --script)');
|
|
5478
|
-
log.info("②
|
|
5687
|
+
log.info("② 刷新你常用的 AI Agent,在它的 Skills 入口选择或点名 gtrk-oralcut;不同客户端也都可以直接描述剪辑需求触发。");
|
|
5479
5688
|
log.info("想有自己栏目的风格体系?在 agent 里跑 /gtrk-style-maker 建一次「你的厨房」(skill 家族 + 栏目配置);不建就直接用默认,照常开剪。");
|
|
5480
5689
|
} else {
|
|
5481
5690
|
log.warn("上面体检有项没通,按提示处理好再开剪(多半是 API Key 或剪映目录)。");
|
|
@@ -5493,7 +5702,7 @@ async function runInitNonInteractive(opts) {
|
|
|
5493
5702
|
let jianyingDraftDir;
|
|
5494
5703
|
const dirOpt = opts.jianyingDraftDir;
|
|
5495
5704
|
if (dirOpt && dirOpt !== "auto")
|
|
5496
|
-
jianyingDraftDir =
|
|
5705
|
+
jianyingDraftDir = resolve3(dirOpt);
|
|
5497
5706
|
else if (dirOpt === "auto" || !existing.jianyingDraftDir)
|
|
5498
5707
|
jianyingDraftDir = probeJianyingDraftDir();
|
|
5499
5708
|
else
|
|
@@ -5509,16 +5718,23 @@ async function runInitNonInteractive(opts) {
|
|
|
5509
5718
|
|
|
5510
5719
|
// src/commands/install.ts
|
|
5511
5720
|
function registerInstall(program2) {
|
|
5512
|
-
program2.command("install").description("一条命令装全:安装 /gtrk-oralcut skill + 配置(对标飞书 lark-cli install)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--skills-dir <dir>", "
|
|
5721
|
+
program2.command("install").description("一条命令装全:安装 /gtrk-oralcut skill + 配置(对标飞书 lark-cli install)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--skills-dir <dir>", "自定义单个 skills 目录(优先于 Agent 自动检测)").option("--skill-agents <list>", "skills CLI Agent ID,逗号分隔,如 codex,cursor,trae-cn").option("--all-agents", "把 skill 安装到上游和 gtrk 已登记的全部 Agent").option("--copy-skills", "每个 Agent 各复制一份(默认统一存储 + symlink/junction)").option("--reconfigure", "重走配置向导(默认:已配过则保留现有配置、只刷新 skill)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(async (opts) => {
|
|
5513
5722
|
log.step("① 安装 / 刷新 agent skill…");
|
|
5514
|
-
installSkill({
|
|
5723
|
+
const skillsOk = installSkill({
|
|
5724
|
+
dir: opts.skillsDir,
|
|
5725
|
+
agents: opts.skillAgents,
|
|
5726
|
+
all: opts.allAgents,
|
|
5727
|
+
copy: opts.copySkills
|
|
5728
|
+
});
|
|
5729
|
+
if (!skillsOk)
|
|
5730
|
+
process.exitCode = 1;
|
|
5515
5731
|
log.step("② 配置 + 体检…");
|
|
5516
5732
|
await runInit(opts);
|
|
5517
5733
|
});
|
|
5518
5734
|
}
|
|
5519
5735
|
|
|
5520
5736
|
// src/commands/oralcut.ts
|
|
5521
|
-
import { resolve as
|
|
5737
|
+
import { resolve as resolve4, join as join14, dirname as dirname3, basename as basename5, extname as extname2 } from "node:path";
|
|
5522
5738
|
import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
|
|
5523
5739
|
import { existsSync as existsSync12 } from "node:fs";
|
|
5524
5740
|
|
|
@@ -5548,6 +5764,10 @@ class CloudError extends Error {
|
|
|
5548
5764
|
this.name = "CloudError";
|
|
5549
5765
|
}
|
|
5550
5766
|
}
|
|
5767
|
+
function cloudErrorCode(error) {
|
|
5768
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
5769
|
+
return typeof code === "number" ? code : undefined;
|
|
5770
|
+
}
|
|
5551
5771
|
async function parseJson(res) {
|
|
5552
5772
|
try {
|
|
5553
5773
|
return await res.json();
|
|
@@ -5555,38 +5775,58 @@ async function parseJson(res) {
|
|
|
5555
5775
|
throw new Error(`服务响应解析失败 (HTTP ${res.status})`);
|
|
5556
5776
|
}
|
|
5557
5777
|
}
|
|
5558
|
-
|
|
5559
|
-
const
|
|
5560
|
-
|
|
5561
|
-
|
|
5778
|
+
function globalBunFile() {
|
|
5779
|
+
const bun = globalThis.Bun;
|
|
5780
|
+
return bun ? bun.file.bind(bun) : undefined;
|
|
5781
|
+
}
|
|
5782
|
+
async function uploadFile(cfg, path, runtime = {}) {
|
|
5783
|
+
const fetchFn = runtime.fetchFn ?? fetch;
|
|
5784
|
+
const bunFile = runtime.bunFile ?? globalBunFile();
|
|
5785
|
+
const useBun = runtime.runtime ? runtime.runtime === "bun" : bunFile != null;
|
|
5786
|
+
let res;
|
|
5787
|
+
if (useBun) {
|
|
5788
|
+
if (!bunFile)
|
|
5789
|
+
throw new Error("Bun 上传运行时缺少 Bun.file");
|
|
5790
|
+
const form = new FormData;
|
|
5791
|
+
form.append("file", bunFile(path), basename(path));
|
|
5792
|
+
res = await fetchFn(`${cfg.base}/base/file/upload`, {
|
|
5793
|
+
method: "POST",
|
|
5794
|
+
headers: { Authorization: cfg.apiKey },
|
|
5795
|
+
body: form
|
|
5796
|
+
});
|
|
5797
|
+
} else {
|
|
5798
|
+
const size = (await stat(path)).size;
|
|
5799
|
+
const boundary = `----gtrkFormBoundary${randomBytes(16).toString("hex")}`;
|
|
5800
|
+
const head = Buffer.from(`--${boundary}\r
|
|
5562
5801
|
` + `Content-Disposition: form-data; name="file"; filename="${basename(path)}"\r
|
|
5563
5802
|
` + `Content-Type: application/octet-stream\r
|
|
5564
5803
|
\r
|
|
5565
5804
|
`, "utf8");
|
|
5566
|
-
|
|
5805
|
+
const tail = Buffer.from(`\r
|
|
5567
5806
|
--${boundary}--\r
|
|
5568
5807
|
`, "utf8");
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5808
|
+
async function* multipart() {
|
|
5809
|
+
yield head;
|
|
5810
|
+
for await (const chunk of createReadStream(path))
|
|
5811
|
+
yield chunk;
|
|
5812
|
+
yield tail;
|
|
5813
|
+
}
|
|
5814
|
+
res = await fetchFn(`${cfg.base}/base/file/upload`, {
|
|
5815
|
+
method: "POST",
|
|
5816
|
+
headers: {
|
|
5817
|
+
Authorization: cfg.apiKey,
|
|
5818
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
5819
|
+
"Content-Length": String(head.length + size + tail.length)
|
|
5820
|
+
},
|
|
5821
|
+
body: Readable.toWeb(Readable.from(multipart())),
|
|
5822
|
+
duplex: "half"
|
|
5823
|
+
});
|
|
5574
5824
|
}
|
|
5575
|
-
const res = await fetch(`${cfg.base}/base/file/upload`, {
|
|
5576
|
-
method: "POST",
|
|
5577
|
-
headers: {
|
|
5578
|
-
Authorization: cfg.apiKey,
|
|
5579
|
-
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
5580
|
-
"Content-Length": String(head.length + size + tail.length)
|
|
5581
|
-
},
|
|
5582
|
-
body: Readable.toWeb(Readable.from(multipart())),
|
|
5583
|
-
duplex: "half"
|
|
5584
|
-
});
|
|
5585
5825
|
const r = await parseJson(res);
|
|
5586
5826
|
const fid = r.data?.file_id ?? r.data?.id;
|
|
5587
5827
|
if (r.code === 200 && fid)
|
|
5588
5828
|
return String(fid);
|
|
5589
|
-
throw new
|
|
5829
|
+
throw new CloudError(r.code, `上传失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
|
|
5590
5830
|
}
|
|
5591
5831
|
async function submitTask(cfg, taskType, payload) {
|
|
5592
5832
|
const res = await fetch(`${cfg.base}/task/${taskType}`, {
|
|
@@ -5597,7 +5837,7 @@ async function submitTask(cfg, taskType, payload) {
|
|
|
5597
5837
|
const r = await parseJson(res);
|
|
5598
5838
|
if (r.code === 200 && r.data?.task_id)
|
|
5599
5839
|
return String(r.data.task_id);
|
|
5600
|
-
throw new
|
|
5840
|
+
throw new CloudError(r.code, `提交失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
|
|
5601
5841
|
}
|
|
5602
5842
|
async function getTaskResult(cfg, taskType, taskId) {
|
|
5603
5843
|
const res = await fetch(`${cfg.base}/task/${taskType}/${taskId}`, {
|
|
@@ -5885,10 +6125,12 @@ async function putPart(cfg, uploadId, idx, view) {
|
|
|
5885
6125
|
var CACHE_DIR = gitruckHome();
|
|
5886
6126
|
var CACHE_FILE = join10(CACHE_DIR, "upload-cache.json");
|
|
5887
6127
|
var SESSION_FILE = join10(CACHE_DIR, "upload-sessions.json");
|
|
5888
|
-
|
|
5889
|
-
const s = await stat3(path);
|
|
6128
|
+
function fingerprintFromStat(s) {
|
|
5890
6129
|
return `${s.size}:${Math.round(s.mtimeMs)}`;
|
|
5891
6130
|
}
|
|
6131
|
+
async function fingerprint(path) {
|
|
6132
|
+
return fingerprintFromStat(await stat3(path));
|
|
6133
|
+
}
|
|
5892
6134
|
async function load() {
|
|
5893
6135
|
if (!existsSync9(CACHE_FILE))
|
|
5894
6136
|
return {};
|
|
@@ -5902,6 +6144,12 @@ async function save(cache) {
|
|
|
5902
6144
|
await mkdir(CACHE_DIR, { recursive: true });
|
|
5903
6145
|
await writeFile2(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
5904
6146
|
}
|
|
6147
|
+
var defaultUploadCacheDeps = {
|
|
6148
|
+
stat: stat3,
|
|
6149
|
+
uploadFile,
|
|
6150
|
+
uploadChunked,
|
|
6151
|
+
cacheStore: { load, save }
|
|
6152
|
+
};
|
|
5905
6153
|
async function invalidateUpload(path) {
|
|
5906
6154
|
const fp = await fingerprint(path);
|
|
5907
6155
|
const cache = await load();
|
|
@@ -5940,19 +6188,22 @@ var fileSessionStore = {
|
|
|
5940
6188
|
}
|
|
5941
6189
|
}
|
|
5942
6190
|
};
|
|
5943
|
-
async function uploadCached(cfg, path, opts) {
|
|
5944
|
-
const
|
|
5945
|
-
const
|
|
6191
|
+
async function uploadCached(cfg, path, opts, deps = defaultUploadCacheDeps) {
|
|
6192
|
+
const s0 = await deps.stat(path);
|
|
6193
|
+
const fp = fingerprintFromStat(s0);
|
|
6194
|
+
const cache = await deps.cacheStore.load();
|
|
5946
6195
|
const hit = cache[fp]?.fileId;
|
|
5947
6196
|
if (!opts?.force && hit)
|
|
5948
6197
|
return { fileId: hit, cached: true };
|
|
5949
|
-
const
|
|
5950
|
-
const fileId = s0.size >= CHUNK_THRESHOLD ? await uploadChunked(cfg, path, {
|
|
6198
|
+
const fileId = s0.size >= CHUNK_THRESHOLD ? await deps.uploadChunked(cfg, path, {
|
|
5951
6199
|
fingerprint: fp,
|
|
5952
6200
|
store: fileSessionStore,
|
|
5953
6201
|
force: opts?.force
|
|
5954
|
-
}) : await uploadFile(cfg, path);
|
|
5955
|
-
const s = await
|
|
6202
|
+
}) : await deps.uploadFile(cfg, path);
|
|
6203
|
+
const s = await deps.stat(path);
|
|
6204
|
+
if (s.size !== s0.size || Math.round(s.mtimeMs) !== Math.round(s0.mtimeMs)) {
|
|
6205
|
+
throw new Error("上传过程中输入文件发生变化,请等待文件写入完成后重试");
|
|
6206
|
+
}
|
|
5956
6207
|
cache[fp] = {
|
|
5957
6208
|
fileId,
|
|
5958
6209
|
size: s.size,
|
|
@@ -5960,10 +6211,50 @@ async function uploadCached(cfg, path, opts) {
|
|
|
5960
6211
|
path,
|
|
5961
6212
|
uploadedAt: Date.now()
|
|
5962
6213
|
};
|
|
5963
|
-
await save(cache);
|
|
6214
|
+
await deps.cacheStore.save(cache);
|
|
5964
6215
|
return { fileId, cached: false };
|
|
5965
6216
|
}
|
|
5966
6217
|
|
|
6218
|
+
// src/lib/upload-submit.ts
|
|
6219
|
+
var MATERIAL_NOT_FOUND = 6004;
|
|
6220
|
+
var DEFAULT_VISIBILITY_BACKOFF_MS = [250, 750, 1500, 3000];
|
|
6221
|
+
var defaultDeps = {
|
|
6222
|
+
uploadCached,
|
|
6223
|
+
invalidateUpload,
|
|
6224
|
+
submitTask,
|
|
6225
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
|
|
6226
|
+
};
|
|
6227
|
+
async function submitFreshFile(cfg, taskType, fileId, buildPayload, backoffMs, deps) {
|
|
6228
|
+
for (let attempt = 0;; attempt++) {
|
|
6229
|
+
try {
|
|
6230
|
+
return await deps.submitTask(cfg, taskType, buildPayload(fileId));
|
|
6231
|
+
} catch (error) {
|
|
6232
|
+
if (cloudErrorCode(error) !== MATERIAL_NOT_FOUND || attempt >= backoffMs.length)
|
|
6233
|
+
throw error;
|
|
6234
|
+
await deps.sleep(backoffMs[attempt]);
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options = {}, deps = defaultDeps) {
|
|
6239
|
+
let uploaded = await deps.uploadCached(cfg, path, { force: options.force });
|
|
6240
|
+
options.onUploaded?.(uploaded);
|
|
6241
|
+
const backoffMs = options.visibilityBackoffMs ?? DEFAULT_VISIBILITY_BACKOFF_MS;
|
|
6242
|
+
if (uploaded.cached) {
|
|
6243
|
+
try {
|
|
6244
|
+
const taskId2 = await deps.submitTask(cfg, taskType, buildPayload(uploaded.fileId));
|
|
6245
|
+
return { taskId: taskId2, fileId: uploaded.fileId, cached: true };
|
|
6246
|
+
} catch (error) {
|
|
6247
|
+
if (cloudErrorCode(error) !== MATERIAL_NOT_FOUND)
|
|
6248
|
+
throw error;
|
|
6249
|
+
options.onCacheInvalid?.();
|
|
6250
|
+
await deps.invalidateUpload(path);
|
|
6251
|
+
uploaded = await deps.uploadCached(cfg, path, { force: true });
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
const taskId = await submitFreshFile(cfg, taskType, uploaded.fileId, buildPayload, backoffMs, deps);
|
|
6255
|
+
return { taskId, fileId: uploaded.fileId, cached: false };
|
|
6256
|
+
}
|
|
6257
|
+
|
|
5967
6258
|
// src/lib/media.ts
|
|
5968
6259
|
import { mkdir as mkdir2, stat as stat4 } from "node:fs/promises";
|
|
5969
6260
|
import { existsSync as existsSync10 } from "node:fs";
|
|
@@ -6456,7 +6747,7 @@ async function runOralCut(input, opts) {
|
|
|
6456
6747
|
if (opts.json)
|
|
6457
6748
|
routeLogsToStderr();
|
|
6458
6749
|
const cfg = loadConfig();
|
|
6459
|
-
const inputAbs =
|
|
6750
|
+
const inputAbs = resolve4(input);
|
|
6460
6751
|
if (!existsSync12(inputAbs))
|
|
6461
6752
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
6462
6753
|
const projName = basename5(inputAbs, extname2(inputAbs));
|
|
@@ -6464,10 +6755,10 @@ async function runOralCut(input, opts) {
|
|
|
6464
6755
|
if (opts.render && !formats.includes("gtrk"))
|
|
6465
6756
|
formats.push("gtrk");
|
|
6466
6757
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
6467
|
-
const outDir =
|
|
6468
|
-
let scriptPath = opts.script ?
|
|
6758
|
+
const outDir = resolve4(opts.out ?? join14(dirname3(inputAbs), `${projName}-video-project-${timestamp()}`));
|
|
6759
|
+
let scriptPath = opts.script ? resolve4(opts.script) : undefined;
|
|
6469
6760
|
if (!scriptPath) {
|
|
6470
|
-
const sibling = join14(
|
|
6761
|
+
const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
|
|
6471
6762
|
if (existsSync12(sibling)) {
|
|
6472
6763
|
scriptPath = sibling;
|
|
6473
6764
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
@@ -6491,8 +6782,6 @@ async function runOralCut(input, opts) {
|
|
|
6491
6782
|
assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
|
|
6492
6783
|
log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename5(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename5(artifact)}`);
|
|
6493
6784
|
log.step("② 上传抽出物到云端…");
|
|
6494
|
-
let up = await uploadCached(cfg, artifact, { force: opts.reupload });
|
|
6495
|
-
log.info(up.cached ? `命中上传缓存,复用 file_id = ${up.fileId}(免二次上传)` : `file_id = ${up.fileId}`);
|
|
6496
6785
|
const buildPayload = (fid) => {
|
|
6497
6786
|
const p = {
|
|
6498
6787
|
file_id: fid,
|
|
@@ -6519,19 +6808,16 @@ async function runOralCut(input, opts) {
|
|
|
6519
6808
|
}
|
|
6520
6809
|
return p;
|
|
6521
6810
|
};
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
} else
|
|
6533
|
-
throw e;
|
|
6534
|
-
}
|
|
6811
|
+
const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE, buildPayload, {
|
|
6812
|
+
force: opts.reupload,
|
|
6813
|
+
onUploaded: (uploaded) => {
|
|
6814
|
+
log.info(uploaded.cached ? `命中上传缓存,复用 file_id = ${uploaded.fileId}(免二次上传)` : `file_id = ${uploaded.fileId}`);
|
|
6815
|
+
log.step("③ 提交智能口播剪辑任务…");
|
|
6816
|
+
},
|
|
6817
|
+
onCacheInvalid: () => log.warn("缓存的 file_id 在云端已失效,重新上传后重试…")
|
|
6818
|
+
});
|
|
6819
|
+
const { taskId } = submitted;
|
|
6820
|
+
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
6535
6821
|
log.info(`task_id = ${taskId}`);
|
|
6536
6822
|
await mkdir4(outDir, { recursive: true });
|
|
6537
6823
|
await writeFile5(join14(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
@@ -6558,7 +6844,7 @@ async function runOralCut(input, opts) {
|
|
|
6558
6844
|
}
|
|
6559
6845
|
|
|
6560
6846
|
// src/commands/oralcut-result.ts
|
|
6561
|
-
import { resolve as
|
|
6847
|
+
import { resolve as resolve5, join as join15 } from "node:path";
|
|
6562
6848
|
var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
|
|
6563
6849
|
function timestamp2() {
|
|
6564
6850
|
const d = new Date;
|
|
@@ -6592,7 +6878,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
6592
6878
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
6593
6879
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
6594
6880
|
}
|
|
6595
|
-
const outDir =
|
|
6881
|
+
const outDir = resolve5(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
|
|
6596
6882
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
6597
6883
|
await materializeResult({
|
|
6598
6884
|
outDir,
|
|
@@ -6610,10 +6896,10 @@ async function runOralCutResult(taskId, opts) {
|
|
|
6610
6896
|
}
|
|
6611
6897
|
|
|
6612
6898
|
// src/commands/upgrade.ts
|
|
6613
|
-
import { spawnSync as
|
|
6899
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
6614
6900
|
var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
|
|
6615
6901
|
function run(cmd) {
|
|
6616
|
-
const r =
|
|
6902
|
+
const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
|
|
6617
6903
|
return r.status ?? 1;
|
|
6618
6904
|
}
|
|
6619
6905
|
function registerUpgrade(program2) {
|
|
@@ -6642,7 +6928,7 @@ function registerUpgrade(program2) {
|
|
|
6642
6928
|
process.exitCode = 1;
|
|
6643
6929
|
return;
|
|
6644
6930
|
}
|
|
6645
|
-
log.step("②
|
|
6931
|
+
log.step("② 通过通用适配器刷新 Agent Skills…");
|
|
6646
6932
|
if (run("gtrk skills install") !== 0) {
|
|
6647
6933
|
log.warn("skill 没刷成,手动跑一次:gtrk skills install");
|
|
6648
6934
|
}
|
|
@@ -6652,16 +6938,16 @@ function registerUpgrade(program2) {
|
|
|
6652
6938
|
}
|
|
6653
6939
|
|
|
6654
6940
|
// src/commands/render.ts
|
|
6655
|
-
import { resolve as
|
|
6941
|
+
import { resolve as resolve6, dirname as dirname4, join as join16, basename as basename6, extname as extname3 } from "node:path";
|
|
6656
6942
|
import { existsSync as existsSync13 } from "node:fs";
|
|
6657
6943
|
function registerRender(program2) {
|
|
6658
6944
|
program2.command("render <gtrk>").description("本地渲染:gtrk 工程按 EDL 用本地 ffmpeg 渲染成片 mp4(素材取原片本地路径)").option("-o, --out <file>", "输出 mp4 路径(缺省 = <gtrk 同目录>/<gtrk 名>.mp4)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18)").option("--codec <c>", "视频编码(默认 h264)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统)").option("--no-open", "完成后不自动打开产物目录").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (gtrk, opts) => {
|
|
6659
6945
|
if (opts.json)
|
|
6660
6946
|
routeLogsToStderr();
|
|
6661
|
-
const gtrkAbs =
|
|
6947
|
+
const gtrkAbs = resolve6(gtrk);
|
|
6662
6948
|
if (!existsSync13(gtrkAbs))
|
|
6663
6949
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
6664
|
-
const outMp4 =
|
|
6950
|
+
const outMp4 = resolve6(opts.out ?? join16(dirname4(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
|
|
6665
6951
|
log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
|
|
6666
6952
|
const project = await readGtrkFile(gtrkAbs);
|
|
6667
6953
|
const result = await renderGtrk(project, outMp4, {
|
|
@@ -6677,7 +6963,7 @@ function registerRender(program2) {
|
|
|
6677
6963
|
log.tickEnd();
|
|
6678
6964
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
6679
6965
|
if (opts.open)
|
|
6680
|
-
openFolder(
|
|
6966
|
+
openFolder(dirname4(outMp4));
|
|
6681
6967
|
if (opts.json) {
|
|
6682
6968
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
6683
6969
|
}
|
|
@@ -6685,7 +6971,7 @@ function registerRender(program2) {
|
|
|
6685
6971
|
}
|
|
6686
6972
|
|
|
6687
6973
|
// src/commands/split.ts
|
|
6688
|
-
import { resolve as
|
|
6974
|
+
import { resolve as resolve7, join as join18, dirname as dirname6, basename as basename8 } from "node:path";
|
|
6689
6975
|
import { existsSync as existsSync14 } from "node:fs";
|
|
6690
6976
|
import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
|
|
6691
6977
|
import { createHash } from "node:crypto";
|
|
@@ -6810,7 +7096,7 @@ function projectTranscript(transcript, gtrk, opts = {}) {
|
|
|
6810
7096
|
|
|
6811
7097
|
// src/lib/gtrk-writeback.ts
|
|
6812
7098
|
import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
6813
|
-
import { dirname as
|
|
7099
|
+
import { dirname as dirname5, join as join17, basename as basename7 } from "node:path";
|
|
6814
7100
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6815
7101
|
function readGtrk(path) {
|
|
6816
7102
|
const raw = readFileSync4(path, "utf8");
|
|
@@ -6837,7 +7123,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
6837
7123
|
}
|
|
6838
7124
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
6839
7125
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
6840
|
-
const tmp = join17(
|
|
7126
|
+
const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6841
7127
|
try {
|
|
6842
7128
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6843
7129
|
renameSync(tmp, path);
|
|
@@ -6853,7 +7139,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
6853
7139
|
if (cur !== expectedMtimeMs) {
|
|
6854
7140
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
6855
7141
|
}
|
|
6856
|
-
const tmp = join17(
|
|
7142
|
+
const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6857
7143
|
try {
|
|
6858
7144
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6859
7145
|
renameSync(tmp, path);
|
|
@@ -6876,10 +7162,10 @@ function firstExisting(cands) {
|
|
|
6876
7162
|
return cands.find((p) => existsSync14(p));
|
|
6877
7163
|
}
|
|
6878
7164
|
function resolvePaths(opts) {
|
|
6879
|
-
const project = opts.project ?
|
|
7165
|
+
const project = opts.project ? resolve7(opts.project) : undefined;
|
|
6880
7166
|
let gtrkPath;
|
|
6881
7167
|
if (opts.gtrk) {
|
|
6882
|
-
gtrkPath =
|
|
7168
|
+
gtrkPath = resolve7(opts.gtrk);
|
|
6883
7169
|
} else if (project) {
|
|
6884
7170
|
gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
|
|
6885
7171
|
} else {
|
|
@@ -6889,14 +7175,14 @@ function resolvePaths(opts) {
|
|
|
6889
7175
|
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
6890
7176
|
let transcriptPath;
|
|
6891
7177
|
if (opts.transcript)
|
|
6892
|
-
transcriptPath =
|
|
7178
|
+
transcriptPath = resolve7(opts.transcript);
|
|
6893
7179
|
else if (project)
|
|
6894
7180
|
transcriptPath = firstExisting([
|
|
6895
7181
|
join18(project, "transcript", "transcript.json"),
|
|
6896
7182
|
join18(project, "json", "transcript.json"),
|
|
6897
7183
|
join18(project, "transcript.json")
|
|
6898
7184
|
]);
|
|
6899
|
-
const baseDir = project ??
|
|
7185
|
+
const baseDir = project ?? dirname6(gtrkPath);
|
|
6900
7186
|
return { baseDir, gtrkPath, transcriptPath };
|
|
6901
7187
|
}
|
|
6902
7188
|
function slugify(name) {
|
|
@@ -6916,7 +7202,7 @@ async function runSplit(splitdoc, opts) {
|
|
|
6916
7202
|
if (opts.json)
|
|
6917
7203
|
routeLogsToStderr();
|
|
6918
7204
|
const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
|
|
6919
|
-
return splitdoc ? runLand(
|
|
7205
|
+
return splitdoc ? runLand(resolve7(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
6920
7206
|
}
|
|
6921
7207
|
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
6922
7208
|
if (!transcriptPath || !existsSync14(transcriptPath))
|
|
@@ -7022,7 +7308,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
7022
7308
|
}
|
|
7023
7309
|
|
|
7024
7310
|
// src/commands/matrix.ts
|
|
7025
|
-
import { resolve as
|
|
7311
|
+
import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
|
|
7026
7312
|
import { existsSync as existsSync15 } from "node:fs";
|
|
7027
7313
|
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
|
|
7028
7314
|
|
|
@@ -7561,10 +7847,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
7561
7847
|
let dispatchPath;
|
|
7562
7848
|
let baseDir;
|
|
7563
7849
|
if (opts.dispatch) {
|
|
7564
|
-
dispatchPath =
|
|
7565
|
-
baseDir =
|
|
7850
|
+
dispatchPath = resolve8(opts.dispatch);
|
|
7851
|
+
baseDir = dirname7(dirname7(dispatchPath));
|
|
7566
7852
|
} else if (opts.project) {
|
|
7567
|
-
baseDir =
|
|
7853
|
+
baseDir = resolve8(opts.project);
|
|
7568
7854
|
dispatchPath = join19(baseDir, "split", "dispatch.json");
|
|
7569
7855
|
} else {
|
|
7570
7856
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
@@ -7669,7 +7955,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
|
|
|
7669
7955
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
7670
7956
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
7671
7957
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
7672
|
-
const gtrkDir =
|
|
7958
|
+
const gtrkDir = dirname7(gtrkPath);
|
|
7673
7959
|
const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
7674
7960
|
await mkdir6(previewDir, { recursive: true });
|
|
7675
7961
|
const prevSource = new Map;
|
|
@@ -7782,7 +8068,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
|
7782
8068
|
counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
|
|
7783
8069
|
};
|
|
7784
8070
|
if (opts.out) {
|
|
7785
|
-
const outPath =
|
|
8071
|
+
const outPath = resolve8(opts.out);
|
|
7786
8072
|
await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
7787
8073
|
log.ok(`结果已落盘:${outPath}`);
|
|
7788
8074
|
result.outPath = outPath;
|
|
@@ -7802,7 +8088,7 @@ function slugify2(name) {
|
|
|
7802
8088
|
}
|
|
7803
8089
|
|
|
7804
8090
|
// src/commands/mg.ts
|
|
7805
|
-
import { resolve as
|
|
8091
|
+
import { resolve as resolve9, join as join20, dirname as dirname8, basename as basename10 } from "node:path";
|
|
7806
8092
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7807
8093
|
import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
|
|
7808
8094
|
|
|
@@ -8025,11 +8311,11 @@ async function runMg(words, opts) {
|
|
|
8025
8311
|
}
|
|
8026
8312
|
function resolveDispatch(opts) {
|
|
8027
8313
|
if (opts.dispatch) {
|
|
8028
|
-
const dispatchPath =
|
|
8029
|
-
return { dispatchPath, baseDir:
|
|
8314
|
+
const dispatchPath = resolve9(opts.dispatch);
|
|
8315
|
+
return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
|
|
8030
8316
|
}
|
|
8031
8317
|
if (opts.project) {
|
|
8032
|
-
const baseDir =
|
|
8318
|
+
const baseDir = resolve9(opts.project);
|
|
8033
8319
|
return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
|
|
8034
8320
|
}
|
|
8035
8321
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
@@ -8106,7 +8392,7 @@ async function runLay(opts) {
|
|
|
8106
8392
|
}
|
|
8107
8393
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
8108
8394
|
assertGtrkV1(gtrk);
|
|
8109
|
-
const gtrkDir =
|
|
8395
|
+
const gtrkDir = dirname8(gtrkPath);
|
|
8110
8396
|
await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
8111
8397
|
for (const it of items) {
|
|
8112
8398
|
await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
|
|
@@ -8127,10 +8413,10 @@ async function runLint(args, opts) {
|
|
|
8127
8413
|
const file = args[0];
|
|
8128
8414
|
if (!file)
|
|
8129
8415
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
8130
|
-
const html = await readFile6(
|
|
8416
|
+
const html = await readFile6(resolve9(file), "utf8");
|
|
8131
8417
|
let dispatchIds;
|
|
8132
|
-
if (opts.dispatch && existsSync16(
|
|
8133
|
-
dispatchIds = (await readMgQueue(
|
|
8418
|
+
if (opts.dispatch && existsSync16(resolve9(opts.dispatch))) {
|
|
8419
|
+
dispatchIds = (await readMgQueue(resolve9(opts.dispatch))).map((q) => q.composition_id);
|
|
8134
8420
|
}
|
|
8135
8421
|
const lint = lintParticle(html, { dispatchIds });
|
|
8136
8422
|
for (const vv of lint.violations)
|
|
@@ -8180,6 +8466,21 @@ import { extname as extname4 } from "node:path";
|
|
|
8180
8466
|
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
8181
8467
|
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
8182
8468
|
var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
|
|
8469
|
+
var PUBLIC_VIDEO_EXTS = [
|
|
8470
|
+
".mp4",
|
|
8471
|
+
".avi",
|
|
8472
|
+
".mpg",
|
|
8473
|
+
".mov",
|
|
8474
|
+
".flv",
|
|
8475
|
+
".mxf",
|
|
8476
|
+
".mpeg",
|
|
8477
|
+
".ogg",
|
|
8478
|
+
".3gp",
|
|
8479
|
+
".wmv",
|
|
8480
|
+
".h264",
|
|
8481
|
+
".m4v",
|
|
8482
|
+
".ts"
|
|
8483
|
+
];
|
|
8183
8484
|
function defaultExtsFor(kind) {
|
|
8184
8485
|
if (kind === "image")
|
|
8185
8486
|
return IMAGE_EXTS;
|
|
@@ -8228,7 +8529,7 @@ var imageMove = {
|
|
|
8228
8529
|
description: "把一张静态图生成带运镜的短视频。",
|
|
8229
8530
|
kind: "cloud",
|
|
8230
8531
|
input: { kind: "image" },
|
|
8231
|
-
|
|
8532
|
+
priceKey: "image_move_local",
|
|
8232
8533
|
outputHint: "运镜视频",
|
|
8233
8534
|
enabled: true,
|
|
8234
8535
|
taskType: "image_move",
|
|
@@ -8261,7 +8562,7 @@ var imageMatting = {
|
|
|
8261
8562
|
description: "把图片主体从背景抠出,产透明背景 png(可经 --param 请求额外背景底板输出)。",
|
|
8262
8563
|
kind: "cloud",
|
|
8263
8564
|
input: { kind: "image" },
|
|
8264
|
-
|
|
8565
|
+
priceKey: "image_matting",
|
|
8265
8566
|
outputHint: "透明 png",
|
|
8266
8567
|
enabled: true,
|
|
8267
8568
|
taskType: "image_matting",
|
|
@@ -8279,13 +8580,382 @@ var imageMatting = {
|
|
|
8279
8580
|
return files;
|
|
8280
8581
|
}
|
|
8281
8582
|
};
|
|
8583
|
+
var imageBlackborderRemove = {
|
|
8584
|
+
name: "image_blackborder_remove",
|
|
8585
|
+
title: "图片去黑边",
|
|
8586
|
+
description: "自动检测并裁去单张图片四周的黑边,保留有效画面。",
|
|
8587
|
+
kind: "cloud",
|
|
8588
|
+
input: { kind: "image" },
|
|
8589
|
+
priceKey: "image_blackborder_remove",
|
|
8590
|
+
outputHint: "去黑边图片",
|
|
8591
|
+
enabled: true,
|
|
8592
|
+
taskType: "image_blackborder_remove",
|
|
8593
|
+
buildPayload(fileId) {
|
|
8594
|
+
return { file_id: fileId };
|
|
8595
|
+
},
|
|
8596
|
+
mapOutputs(out, ctx) {
|
|
8597
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8598
|
+
return url ? [{ url, filename: `${ctx.baseName}-blackborder-removed${extFromUrl(url, ".jpg")}` }] : [];
|
|
8599
|
+
}
|
|
8600
|
+
};
|
|
8601
|
+
var imageCanvasAdapt = {
|
|
8602
|
+
name: "image_canvas_adapt",
|
|
8603
|
+
title: "图片比例转换",
|
|
8604
|
+
description: "把单张图片适配到目标画布尺寸,可选适配、矩形裁剪或方形裁剪。",
|
|
8605
|
+
kind: "cloud",
|
|
8606
|
+
input: { kind: "image" },
|
|
8607
|
+
priceKey: "image_canvas_adapt",
|
|
8608
|
+
outputHint: "比例适配图片",
|
|
8609
|
+
enabled: true,
|
|
8610
|
+
taskType: "image_canvas_adapt",
|
|
8611
|
+
options: [
|
|
8612
|
+
{ flag: "--canvas-width <px>", desc: "目标画布宽度(像素;未传则使用服务端默认)" },
|
|
8613
|
+
{ flag: "--canvas-height <px>", desc: "目标画布高度(像素;未传则使用服务端默认)" },
|
|
8614
|
+
{
|
|
8615
|
+
flag: "--canvas-type <normal|rectangle|square>",
|
|
8616
|
+
desc: "画布模式:normal、rectangle 或 square(未传则使用服务端默认)"
|
|
8617
|
+
}
|
|
8618
|
+
],
|
|
8619
|
+
buildPayload(fileId, ctx) {
|
|
8620
|
+
const payload = { file_id: fileId };
|
|
8621
|
+
for (const [optKey, payloadKey, flag] of [
|
|
8622
|
+
["canvasWidth", "canvas_width", "--canvas-width"],
|
|
8623
|
+
["canvasHeight", "canvas_height", "--canvas-height"]
|
|
8624
|
+
]) {
|
|
8625
|
+
if (ctx.opts[optKey] == null)
|
|
8626
|
+
continue;
|
|
8627
|
+
const value = Number(ctx.opts[optKey]);
|
|
8628
|
+
if (!Number.isFinite(value))
|
|
8629
|
+
throw new Error(`${flag} 必须是数字`);
|
|
8630
|
+
payload[payloadKey] = value;
|
|
8631
|
+
}
|
|
8632
|
+
if (ctx.opts.canvasType != null) {
|
|
8633
|
+
const value = String(ctx.opts.canvasType);
|
|
8634
|
+
if (value !== "normal" && value !== "rectangle" && value !== "square") {
|
|
8635
|
+
throw new Error("--canvas-type 只支持 normal、rectangle 或 square");
|
|
8636
|
+
}
|
|
8637
|
+
payload.canvas_type = value;
|
|
8638
|
+
}
|
|
8639
|
+
return payload;
|
|
8640
|
+
},
|
|
8641
|
+
mapOutputs(out, ctx) {
|
|
8642
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8643
|
+
return url ? [{ url, filename: `${ctx.baseName}-canvas-adapted${extFromUrl(url, ".jpg")}` }] : [];
|
|
8644
|
+
}
|
|
8645
|
+
};
|
|
8646
|
+
var imagePurify = {
|
|
8647
|
+
name: "image_purify",
|
|
8648
|
+
title: "图片净化",
|
|
8649
|
+
description: "清理你有权处理的图片中的水印、Logo 或叠加元素。",
|
|
8650
|
+
kind: "cloud",
|
|
8651
|
+
input: { kind: "image" },
|
|
8652
|
+
priceKey: "image_purify",
|
|
8653
|
+
outputHint: "净化图片",
|
|
8654
|
+
enabled: true,
|
|
8655
|
+
taskType: "image_purify",
|
|
8656
|
+
buildPayload(fileId) {
|
|
8657
|
+
return { file_id: fileId };
|
|
8658
|
+
},
|
|
8659
|
+
mapOutputs(out, ctx) {
|
|
8660
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8661
|
+
return url ? [{ url, filename: `${ctx.baseName}-purified${extFromUrl(url, ".jpg")}` }] : [];
|
|
8662
|
+
}
|
|
8663
|
+
};
|
|
8664
|
+
var videoBlackborderRemove = {
|
|
8665
|
+
name: "video_blackborder_remove",
|
|
8666
|
+
title: "视频去黑边",
|
|
8667
|
+
description: "自动检测并裁去单条视频四周的黑边,保留有效画面与原音轨。",
|
|
8668
|
+
kind: "cloud",
|
|
8669
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8670
|
+
priceKey: "video_blackborder_remove",
|
|
8671
|
+
outputHint: "去黑边视频",
|
|
8672
|
+
enabled: true,
|
|
8673
|
+
taskType: "video_blackborder_remove",
|
|
8674
|
+
buildPayload(fileId) {
|
|
8675
|
+
return { file_id: fileId };
|
|
8676
|
+
},
|
|
8677
|
+
mapOutputs(out, ctx) {
|
|
8678
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8679
|
+
return url ? [{ url, filename: `${ctx.baseName}-blackborder-removed${extFromUrl(url, ".mp4")}` }] : [];
|
|
8680
|
+
}
|
|
8681
|
+
};
|
|
8682
|
+
var videoCanvasAdapt = {
|
|
8683
|
+
name: "video_canvas_adapt",
|
|
8684
|
+
title: "视频比例转换",
|
|
8685
|
+
description: "把单条视频适配到目标画布,可选截取时间片段并移除音轨。",
|
|
8686
|
+
kind: "cloud",
|
|
8687
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8688
|
+
priceKey: "video_canvas_adapt",
|
|
8689
|
+
outputHint: "比例适配视频",
|
|
8690
|
+
enabled: true,
|
|
8691
|
+
taskType: "video_canvas_adapt",
|
|
8692
|
+
options: [
|
|
8693
|
+
{ flag: "--canvas-width <px>", desc: "目标画布宽度(像素;未传则使用服务端默认)" },
|
|
8694
|
+
{ flag: "--canvas-height <px>", desc: "目标画布高度(像素;未传则使用服务端默认)" },
|
|
8695
|
+
{
|
|
8696
|
+
flag: "--canvas-type <normal|rectangle|square>",
|
|
8697
|
+
desc: "画布模式:normal、rectangle 或 square(未传则使用服务端默认)"
|
|
8698
|
+
},
|
|
8699
|
+
{ flag: "--clip-start <frame>", desc: "截取起始帧序号(未传则使用服务端默认)" },
|
|
8700
|
+
{ flag: "--clip-end <frame>", desc: "截取结束帧序号(未传则使用服务端默认)" },
|
|
8701
|
+
{ flag: "--without-audio", desc: "输出视频不保留音轨" }
|
|
8702
|
+
],
|
|
8703
|
+
buildPayload(fileId, ctx) {
|
|
8704
|
+
const payload = { file_id: fileId };
|
|
8705
|
+
for (const [optKey, payloadKey, flag] of [
|
|
8706
|
+
["canvasWidth", "target_width", "--canvas-width"],
|
|
8707
|
+
["canvasHeight", "target_height", "--canvas-height"]
|
|
8708
|
+
]) {
|
|
8709
|
+
if (ctx.opts[optKey] == null)
|
|
8710
|
+
continue;
|
|
8711
|
+
const value = Number(ctx.opts[optKey]);
|
|
8712
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
8713
|
+
throw new Error(`${flag} 必须是有限整数`);
|
|
8714
|
+
}
|
|
8715
|
+
payload[payloadKey] = value;
|
|
8716
|
+
}
|
|
8717
|
+
for (const [optKey, payloadKey, flag] of [
|
|
8718
|
+
["clipStart", "start", "--clip-start"],
|
|
8719
|
+
["clipEnd", "end", "--clip-end"]
|
|
8720
|
+
]) {
|
|
8721
|
+
if (ctx.opts[optKey] == null)
|
|
8722
|
+
continue;
|
|
8723
|
+
const value = Number(ctx.opts[optKey]);
|
|
8724
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
8725
|
+
throw new Error(`${flag} 必须是有限整数帧号`);
|
|
8726
|
+
}
|
|
8727
|
+
payload[payloadKey] = value;
|
|
8728
|
+
}
|
|
8729
|
+
if (ctx.opts.canvasType != null) {
|
|
8730
|
+
const value = String(ctx.opts.canvasType);
|
|
8731
|
+
if (value !== "normal" && value !== "rectangle" && value !== "square") {
|
|
8732
|
+
throw new Error("--canvas-type 只支持 normal、rectangle 或 square");
|
|
8733
|
+
}
|
|
8734
|
+
payload.canvas_type = value;
|
|
8735
|
+
}
|
|
8736
|
+
if (ctx.opts.withoutAudio === true)
|
|
8737
|
+
payload.need_audio = false;
|
|
8738
|
+
return payload;
|
|
8739
|
+
},
|
|
8740
|
+
mapOutputs(out, ctx) {
|
|
8741
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8742
|
+
return url ? [{ url, filename: `${ctx.baseName}-canvas-adapted${extFromUrl(url, ".mp4")}` }] : [];
|
|
8743
|
+
}
|
|
8744
|
+
};
|
|
8745
|
+
var videoStabilizer = {
|
|
8746
|
+
name: "video_stabilizer",
|
|
8747
|
+
title: "视频防抖",
|
|
8748
|
+
description: "稳定手持或运动拍摄画面;exp 为实验方式,产物观感需自行检查。",
|
|
8749
|
+
kind: "cloud",
|
|
8750
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8751
|
+
priceKey: "video_stabilizer",
|
|
8752
|
+
outputHint: "防抖视频",
|
|
8753
|
+
enabled: true,
|
|
8754
|
+
taskType: "video_stabilizer",
|
|
8755
|
+
options: [{ flag: "--stabilizer-method <fast|exp|turbo>", desc: "防抖方式(未传则使用服务端 turbo 默认值)" }],
|
|
8756
|
+
buildPayload(fileId, ctx) {
|
|
8757
|
+
const payload = { file_id: fileId };
|
|
8758
|
+
if (ctx.opts.stabilizerMethod == null)
|
|
8759
|
+
return payload;
|
|
8760
|
+
const method = String(ctx.opts.stabilizerMethod);
|
|
8761
|
+
if (method !== "fast" && method !== "exp" && method !== "turbo") {
|
|
8762
|
+
throw new Error("--stabilizer-method 只支持 fast、exp 或 turbo");
|
|
8763
|
+
}
|
|
8764
|
+
payload.method = method;
|
|
8765
|
+
return payload;
|
|
8766
|
+
},
|
|
8767
|
+
mapOutputs(out, ctx) {
|
|
8768
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8769
|
+
return url ? [{ url, filename: `${ctx.baseName}-stabilized${extFromUrl(url, ".mp4")}` }] : [];
|
|
8770
|
+
}
|
|
8771
|
+
};
|
|
8772
|
+
var videoVaporwave = {
|
|
8773
|
+
name: "video_vaporwave",
|
|
8774
|
+
title: "视频蒸汽波滤镜",
|
|
8775
|
+
description: "按精确预设名称为单条视频应用蒸汽波风格滤镜。",
|
|
8776
|
+
kind: "cloud",
|
|
8777
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8778
|
+
priceKey: "video_vaporwave",
|
|
8779
|
+
outputHint: "蒸汽波滤镜视频",
|
|
8780
|
+
enabled: true,
|
|
8781
|
+
taskType: "video_vaporwave",
|
|
8782
|
+
options: [{ flag: "--vaporwave-filter <name>", desc: "精确滤镜名称(默认:愈漸升溫)" }],
|
|
8783
|
+
buildPayload(fileId, ctx) {
|
|
8784
|
+
const raw = ctx.opts.vaporwaveFilter;
|
|
8785
|
+
const filter = raw == null ? "愈漸升溫" : String(raw);
|
|
8786
|
+
if (!filter.trim())
|
|
8787
|
+
throw new Error("--vaporwave-filter 不能为空");
|
|
8788
|
+
return { file_id: fileId, filter };
|
|
8789
|
+
},
|
|
8790
|
+
mapOutputs(out, ctx) {
|
|
8791
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8792
|
+
return url ? [{ url, filename: `${ctx.baseName}-vaporwave${extFromUrl(url, ".mp4")}` }] : [];
|
|
8793
|
+
}
|
|
8794
|
+
};
|
|
8795
|
+
function parseNormalizedRoi(value) {
|
|
8796
|
+
let rawValues;
|
|
8797
|
+
let acceptNumericStrings = false;
|
|
8798
|
+
if (typeof value === "string") {
|
|
8799
|
+
acceptNumericStrings = true;
|
|
8800
|
+
const parts = value.split(",");
|
|
8801
|
+
if (parts.length !== 4 || parts.some((part) => !part.trim())) {
|
|
8802
|
+
throw new Error("--purify-roi 必须是 x,y,w,h 四个归一化数字");
|
|
8803
|
+
}
|
|
8804
|
+
rawValues = parts;
|
|
8805
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
8806
|
+
const roi = value;
|
|
8807
|
+
rawValues = [roi.x, roi.y, roi.w, roi.h];
|
|
8808
|
+
} else {
|
|
8809
|
+
throw new Error("ROI 必须包含归一化数字 x、y、w、h");
|
|
8810
|
+
}
|
|
8811
|
+
const numbers = rawValues.map((item) => {
|
|
8812
|
+
if (typeof item === "number")
|
|
8813
|
+
return item;
|
|
8814
|
+
if (acceptNumericStrings && typeof item === "string")
|
|
8815
|
+
return Number(item);
|
|
8816
|
+
return Number.NaN;
|
|
8817
|
+
});
|
|
8818
|
+
if (numbers.some((item) => !Number.isFinite(item))) {
|
|
8819
|
+
throw new Error("ROI 的 x、y、w、h 必须是有限数字");
|
|
8820
|
+
}
|
|
8821
|
+
const [x, y, w, h] = numbers;
|
|
8822
|
+
if (x < 0 || x > 1 || y < 0 || y > 1 || w <= 0 || w > 1 || h <= 0 || h > 1 || x + w > 1 || y + h > 1) {
|
|
8823
|
+
throw new Error("ROI 必须满足 0≤x,y≤1、0<w,h≤1、x+w≤1、y+h≤1");
|
|
8824
|
+
}
|
|
8825
|
+
return { x, y, w, h };
|
|
8826
|
+
}
|
|
8827
|
+
var videoPurify = {
|
|
8828
|
+
name: "video_purify",
|
|
8829
|
+
title: "视频净化",
|
|
8830
|
+
description: "清理你有权处理的视频中的水印、字幕或指定区域;不承诺还原被遮挡的原始内容。",
|
|
8831
|
+
kind: "cloud",
|
|
8832
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8833
|
+
priceKey: "video_purify",
|
|
8834
|
+
outputHint: "净化视频",
|
|
8835
|
+
enabled: true,
|
|
8836
|
+
taskType: "video_purify",
|
|
8837
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
8838
|
+
options: [
|
|
8839
|
+
{
|
|
8840
|
+
flag: "--purify-scope <full_screen|subtitle|custom>",
|
|
8841
|
+
desc: "净化范围;未传时使用服务端 full_screen 默认值"
|
|
8842
|
+
},
|
|
8843
|
+
{
|
|
8844
|
+
flag: "--purify-method <ffmpeg|raft>",
|
|
8845
|
+
desc: "净化方式;未传时使用服务端 ffmpeg 默认值,raft 仅支持 20 分钟以内视频"
|
|
8846
|
+
},
|
|
8847
|
+
{ flag: "--purify-roi <x,y,w,h>", desc: "custom 模式的归一化矩形区域" }
|
|
8848
|
+
],
|
|
8849
|
+
buildPayload(fileId, ctx) {
|
|
8850
|
+
const payload = { file_id: fileId };
|
|
8851
|
+
const scopeRaw = ctx.opts.purifyScope;
|
|
8852
|
+
const scope = scopeRaw == null ? undefined : String(scopeRaw);
|
|
8853
|
+
if (scope != null && scope !== "full_screen" && scope !== "subtitle" && scope !== "custom") {
|
|
8854
|
+
throw new Error("--purify-scope 只支持 full_screen、subtitle 或 custom");
|
|
8855
|
+
}
|
|
8856
|
+
if (scope != null)
|
|
8857
|
+
payload.purify_scope = scope;
|
|
8858
|
+
const methodRaw = ctx.opts.purifyMethod;
|
|
8859
|
+
const method = methodRaw == null ? undefined : String(methodRaw);
|
|
8860
|
+
if (method != null && method !== "ffmpeg" && method !== "raft") {
|
|
8861
|
+
throw new Error("--purify-method 只支持 ffmpeg 或 raft");
|
|
8862
|
+
}
|
|
8863
|
+
if (method != null)
|
|
8864
|
+
payload.purify_func_type = method;
|
|
8865
|
+
const roiRaw = ctx.opts.purifyRoi;
|
|
8866
|
+
if (roiRaw != null) {
|
|
8867
|
+
if (scope !== "custom") {
|
|
8868
|
+
throw new Error("--purify-roi 只能与 --purify-scope custom 一起使用");
|
|
8869
|
+
}
|
|
8870
|
+
payload.roi = parseNormalizedRoi(roiRaw);
|
|
8871
|
+
} else if (scope === "custom") {
|
|
8872
|
+
if (ctx.extraParams.roi == null) {
|
|
8873
|
+
throw new Error("--purify-scope custom 必须同时提供 --purify-roi 或 params-json.roi");
|
|
8874
|
+
}
|
|
8875
|
+
parseNormalizedRoi(ctx.extraParams.roi);
|
|
8876
|
+
}
|
|
8877
|
+
return payload;
|
|
8878
|
+
},
|
|
8879
|
+
mapOutputs(out, ctx) {
|
|
8880
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8881
|
+
return url ? [{ url, filename: `${ctx.baseName}-purified${extFromUrl(url, ".mp4")}` }] : [];
|
|
8882
|
+
}
|
|
8883
|
+
};
|
|
8884
|
+
var videoUpscale = {
|
|
8885
|
+
name: "video_upscale",
|
|
8886
|
+
title: "视频超分",
|
|
8887
|
+
description: "对一分钟以内的低分辨率视频做实验性 GPU 超分;效果需自行检查。",
|
|
8888
|
+
kind: "cloud",
|
|
8889
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS, maxDurationSec: 60 },
|
|
8890
|
+
priceKey: "video_upscale",
|
|
8891
|
+
outputHint: "超分视频",
|
|
8892
|
+
enabled: true,
|
|
8893
|
+
taskType: "video_upscale",
|
|
8894
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
8895
|
+
options: [
|
|
8896
|
+
{ flag: "--upscale-times <2|3|4>", desc: "超分倍数;未传时使用服务端 2 倍默认值" },
|
|
8897
|
+
{ flag: "--upscale-type <Reality|Anime>", desc: "写实或动漫类型;未传时使用服务端 Reality 默认值" }
|
|
8898
|
+
],
|
|
8899
|
+
buildPayload(fileId, ctx) {
|
|
8900
|
+
const payload = { file_id: fileId };
|
|
8901
|
+
if (ctx.opts.upscaleTimes != null) {
|
|
8902
|
+
const times = Number(ctx.opts.upscaleTimes);
|
|
8903
|
+
if (!Number.isInteger(times) || times !== 2 && times !== 3 && times !== 4) {
|
|
8904
|
+
throw new Error("--upscale-times 只支持 2、3 或 4");
|
|
8905
|
+
}
|
|
8906
|
+
payload.times = times;
|
|
8907
|
+
}
|
|
8908
|
+
if (ctx.opts.upscaleType != null) {
|
|
8909
|
+
const upscaleType = String(ctx.opts.upscaleType);
|
|
8910
|
+
if (upscaleType !== "Reality" && upscaleType !== "Anime") {
|
|
8911
|
+
throw new Error("--upscale-type 只支持 Reality 或 Anime");
|
|
8912
|
+
}
|
|
8913
|
+
payload.upscale_type = upscaleType;
|
|
8914
|
+
}
|
|
8915
|
+
return payload;
|
|
8916
|
+
},
|
|
8917
|
+
mapOutputs(out, ctx) {
|
|
8918
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8919
|
+
return url ? [{ url, filename: `${ctx.baseName}-upscaled${extFromUrl(url, ".mp4")}` }] : [];
|
|
8920
|
+
}
|
|
8921
|
+
};
|
|
8922
|
+
var videoInterpolate = {
|
|
8923
|
+
name: "video_interpolate",
|
|
8924
|
+
title: "视频插帧",
|
|
8925
|
+
description: "以 GPU 帧插值提升视频流畅度;不附加未经服务端声明的时长限制。",
|
|
8926
|
+
kind: "cloud",
|
|
8927
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8928
|
+
priceKey: "video_interpolate",
|
|
8929
|
+
outputHint: "插帧视频",
|
|
8930
|
+
enabled: true,
|
|
8931
|
+
taskType: "video_interpolate",
|
|
8932
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
8933
|
+
options: [
|
|
8934
|
+
{ flag: "--interpolate-multiplier <2|3|4>", desc: "插帧倍数;未传时使用服务端 2 倍默认值" }
|
|
8935
|
+
],
|
|
8936
|
+
buildPayload(fileId, ctx) {
|
|
8937
|
+
const payload = { file_id: fileId };
|
|
8938
|
+
if (ctx.opts.interpolateMultiplier != null) {
|
|
8939
|
+
const multiplier = Number(ctx.opts.interpolateMultiplier);
|
|
8940
|
+
if (!Number.isInteger(multiplier) || multiplier !== 2 && multiplier !== 3 && multiplier !== 4) {
|
|
8941
|
+
throw new Error("--interpolate-multiplier 只支持 2、3 或 4");
|
|
8942
|
+
}
|
|
8943
|
+
payload.multiplier = multiplier;
|
|
8944
|
+
}
|
|
8945
|
+
return payload;
|
|
8946
|
+
},
|
|
8947
|
+
mapOutputs(out, ctx) {
|
|
8948
|
+
const url = pickUrl(out, ["download_url"]);
|
|
8949
|
+
return url ? [{ url, filename: `${ctx.baseName}-interpolated${extFromUrl(url, ".mp4")}` }] : [];
|
|
8950
|
+
}
|
|
8951
|
+
};
|
|
8282
8952
|
var videoMatting = {
|
|
8283
8953
|
name: "video_matting",
|
|
8284
8954
|
title: "视频抠像",
|
|
8285
8955
|
description: "把视频主体从背景抠出,产透明背景 webm(像素级、原片直传不压代理;单片 ≤10 分钟)。",
|
|
8286
8956
|
kind: "cloud",
|
|
8287
8957
|
input: { kind: "video", maxDurationSec: 600 },
|
|
8288
|
-
|
|
8958
|
+
priceKey: "video_matting",
|
|
8289
8959
|
outputHint: "透明 webm",
|
|
8290
8960
|
enabled: true,
|
|
8291
8961
|
taskType: "video_matting",
|
|
@@ -8304,13 +8974,411 @@ var videoMatting = {
|
|
|
8304
8974
|
return files;
|
|
8305
8975
|
}
|
|
8306
8976
|
};
|
|
8977
|
+
var audioSeparation = {
|
|
8978
|
+
name: "audio_separation",
|
|
8979
|
+
title: "人声伴奏分离",
|
|
8980
|
+
description: "把单条音频分离为人声与伴奏,可返回其中一项或两项。",
|
|
8981
|
+
kind: "cloud",
|
|
8982
|
+
input: { kind: "audio" },
|
|
8983
|
+
priceKey: "audio_separation",
|
|
8984
|
+
outputHint: "人声与伴奏音频",
|
|
8985
|
+
enabled: true,
|
|
8986
|
+
taskType: "audio_separation",
|
|
8987
|
+
options: [{ flag: "--mode <fast|turbo>", desc: "处理档位(默认 fast,可选 turbo)" }],
|
|
8988
|
+
buildPayload(fileId, ctx) {
|
|
8989
|
+
const raw = ctx.opts.mode;
|
|
8990
|
+
const mode = raw == null ? "fast" : String(raw);
|
|
8991
|
+
if (mode !== "fast" && mode !== "turbo")
|
|
8992
|
+
throw new Error("--mode 只支持 fast 或 turbo");
|
|
8993
|
+
return { file_id: fileId, mode };
|
|
8994
|
+
},
|
|
8995
|
+
mapOutputs(out, ctx) {
|
|
8996
|
+
if (!Array.isArray(out.files))
|
|
8997
|
+
return [];
|
|
8998
|
+
const items = [];
|
|
8999
|
+
for (const raw of out.files) {
|
|
9000
|
+
if (!raw || typeof raw !== "object")
|
|
9001
|
+
continue;
|
|
9002
|
+
const file = raw;
|
|
9003
|
+
const type = file.type;
|
|
9004
|
+
const url = file.download_url;
|
|
9005
|
+
if (type !== "vocals" && type !== "instrumental" || typeof url !== "string" || !url.trim())
|
|
9006
|
+
continue;
|
|
9007
|
+
items.push({ url, filename: `${ctx.baseName}-${type}${extFromUrl(url, ".wav")}` });
|
|
9008
|
+
}
|
|
9009
|
+
return items;
|
|
9010
|
+
}
|
|
9011
|
+
};
|
|
9012
|
+
var audioNoiseReduce = {
|
|
9013
|
+
name: "audio_noise_reduce",
|
|
9014
|
+
title: "音频降噪",
|
|
9015
|
+
description: "对音频或视频中的声音降噪,输出降噪后的音频。",
|
|
9016
|
+
kind: "cloud",
|
|
9017
|
+
input: { kind: "audio", exts: [...AUDIO_EXTS, ...VIDEO_EXTS] },
|
|
9018
|
+
priceKey: "audio_noise_reduce",
|
|
9019
|
+
outputHint: "降噪音频",
|
|
9020
|
+
enabled: true,
|
|
9021
|
+
taskType: "audio_noise_reduce",
|
|
9022
|
+
options: [{ flag: "--prop-decrease <0..1>", desc: "降噪强度(0 到 1;未传则使用服务端默认)" }],
|
|
9023
|
+
buildPayload(fileId, ctx) {
|
|
9024
|
+
const payload = { file_id: fileId };
|
|
9025
|
+
if (ctx.opts.propDecrease != null) {
|
|
9026
|
+
const value = Number(ctx.opts.propDecrease);
|
|
9027
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
9028
|
+
throw new Error("--prop-decrease 必须是 0 到 1 的数字");
|
|
9029
|
+
payload.prop_decrease = value;
|
|
9030
|
+
}
|
|
9031
|
+
return payload;
|
|
9032
|
+
},
|
|
9033
|
+
mapOutputs(out, ctx) {
|
|
9034
|
+
const url = pickUrl(out, ["download_url"]);
|
|
9035
|
+
return url ? [{ url, filename: `${ctx.baseName}-denoise${extFromUrl(url, ".wav")}` }] : [];
|
|
9036
|
+
}
|
|
9037
|
+
};
|
|
9038
|
+
var audioSilenceRemove = {
|
|
9039
|
+
name: "audio_silence_remove",
|
|
9040
|
+
title: "静音片段移除",
|
|
9041
|
+
description: "移除音频中过长的静音片段,输出压缩停顿后的音频。",
|
|
9042
|
+
kind: "cloud",
|
|
9043
|
+
input: { kind: "audio" },
|
|
9044
|
+
priceKey: "audio_silence_remove",
|
|
9045
|
+
outputHint: "去静音音频",
|
|
9046
|
+
enabled: true,
|
|
9047
|
+
taskType: "audio_silence_remove",
|
|
9048
|
+
options: [
|
|
9049
|
+
{ flag: "--min-silence-len <ms>", desc: "被视为静音片段的最短毫秒数(未传则使用服务端默认)" },
|
|
9050
|
+
{ flag: "--desired-silence-len <ms>", desc: "处理后保留的静音毫秒数(未传则使用服务端默认)" }
|
|
9051
|
+
],
|
|
9052
|
+
buildPayload(fileId, ctx) {
|
|
9053
|
+
const payload = { file_id: fileId };
|
|
9054
|
+
for (const [optKey, payloadKey, flag] of [
|
|
9055
|
+
["minSilenceLen", "min_silence_len", "--min-silence-len"],
|
|
9056
|
+
["desiredSilenceLen", "desired_silence_len", "--desired-silence-len"]
|
|
9057
|
+
]) {
|
|
9058
|
+
if (ctx.opts[optKey] == null)
|
|
9059
|
+
continue;
|
|
9060
|
+
const value = Number(ctx.opts[optKey]);
|
|
9061
|
+
if (!Number.isFinite(value) || value < 0)
|
|
9062
|
+
throw new Error(`${flag} 必须是非负毫秒数`);
|
|
9063
|
+
payload[payloadKey] = value;
|
|
9064
|
+
}
|
|
9065
|
+
return payload;
|
|
9066
|
+
},
|
|
9067
|
+
mapOutputs(out, ctx) {
|
|
9068
|
+
const url = pickUrl(out, ["download_url"]);
|
|
9069
|
+
return url ? [{ url, filename: `${ctx.baseName}-desilence${extFromUrl(url, ".wav")}` }] : [];
|
|
9070
|
+
}
|
|
9071
|
+
};
|
|
9072
|
+
var videoSegment = {
|
|
9073
|
+
name: "video_segment",
|
|
9074
|
+
title: "视频机械分镜",
|
|
9075
|
+
description: "按画面变动率把视频切成分镜区间,输出结构化 JSON(场景数与各段起止/时长),不产切片文件。",
|
|
9076
|
+
kind: "cloud",
|
|
9077
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
9078
|
+
priceKey: "video_segment",
|
|
9079
|
+
outputHint: "分镜区间结构(result-output.json)",
|
|
9080
|
+
enabled: true,
|
|
9081
|
+
taskType: "video_segment",
|
|
9082
|
+
options: [
|
|
9083
|
+
{ flag: "--detector <content|adaptive>", desc: "切分算法(未传则使用服务端默认)" },
|
|
9084
|
+
{ flag: "--threshold <number>", desc: "切分阈值(未传则使用服务端默认)" }
|
|
9085
|
+
],
|
|
9086
|
+
buildPayload(fileId, ctx) {
|
|
9087
|
+
const payload = { file_id: fileId, only_struct: true };
|
|
9088
|
+
if (ctx.opts.detector != null) {
|
|
9089
|
+
const detector = String(ctx.opts.detector);
|
|
9090
|
+
if (detector !== "content" && detector !== "adaptive") {
|
|
9091
|
+
throw new Error("--detector 只支持 content 或 adaptive");
|
|
9092
|
+
}
|
|
9093
|
+
payload.detector = detector;
|
|
9094
|
+
}
|
|
9095
|
+
if (ctx.opts.threshold != null) {
|
|
9096
|
+
const value = Number(ctx.opts.threshold);
|
|
9097
|
+
if (!Number.isFinite(value))
|
|
9098
|
+
throw new Error("--threshold 必须是数字");
|
|
9099
|
+
payload.threshold = value;
|
|
9100
|
+
}
|
|
9101
|
+
return payload;
|
|
9102
|
+
},
|
|
9103
|
+
mapResult(out) {
|
|
9104
|
+
return out;
|
|
9105
|
+
}
|
|
9106
|
+
};
|
|
9107
|
+
var videoAiSegment = {
|
|
9108
|
+
name: "video_ai_segment",
|
|
9109
|
+
title: "视频智能分镜",
|
|
9110
|
+
description: "按语义把视频切成带类目、景别、标签、描述的镜头结构,输出结构化 JSON,不产切片文件。",
|
|
9111
|
+
kind: "cloud",
|
|
9112
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
9113
|
+
priceKey: "video_ai_segment",
|
|
9114
|
+
outputHint: "语义分镜结构(result-output.json)",
|
|
9115
|
+
enabled: true,
|
|
9116
|
+
taskType: "video_ai_segment",
|
|
9117
|
+
options: [
|
|
9118
|
+
{ flag: "--segment-mode <scene|shot_type|narrative|subject>", desc: "分镜维度(未传则使用服务端默认)" }
|
|
9119
|
+
],
|
|
9120
|
+
buildPayload(fileId, ctx) {
|
|
9121
|
+
const payload = { file_id: fileId, only_struct: true };
|
|
9122
|
+
if (ctx.opts.segmentMode != null) {
|
|
9123
|
+
const mode = String(ctx.opts.segmentMode);
|
|
9124
|
+
if (mode !== "scene" && mode !== "shot_type" && mode !== "narrative" && mode !== "subject") {
|
|
9125
|
+
throw new Error("--segment-mode 只支持 scene、shot_type、narrative 或 subject");
|
|
9126
|
+
}
|
|
9127
|
+
payload.mode = mode;
|
|
9128
|
+
}
|
|
9129
|
+
return payload;
|
|
9130
|
+
},
|
|
9131
|
+
mapResult(out) {
|
|
9132
|
+
return out;
|
|
9133
|
+
}
|
|
9134
|
+
};
|
|
9135
|
+
var videoMotionCut = {
|
|
9136
|
+
name: "video_motion_cut",
|
|
9137
|
+
title: "视频运镜高光",
|
|
9138
|
+
description: "用光流分析提取运镜/高光片段,输出结构化 JSON(每片帧号、秒级时码与运动特征),不产文件。",
|
|
9139
|
+
kind: "cloud",
|
|
9140
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
9141
|
+
priceKey: "video_motion_cut",
|
|
9142
|
+
outputHint: "运镜高光片段结构(result-output.json)",
|
|
9143
|
+
enabled: true,
|
|
9144
|
+
taskType: "video_motion_cut",
|
|
9145
|
+
buildPayload(fileId) {
|
|
9146
|
+
return { file_id: fileId };
|
|
9147
|
+
},
|
|
9148
|
+
mapResult(out) {
|
|
9149
|
+
return out;
|
|
9150
|
+
}
|
|
9151
|
+
};
|
|
9152
|
+
var audioSpeakerSplit = {
|
|
9153
|
+
name: "audio_speaker_split",
|
|
9154
|
+
title: "按说话人分轨",
|
|
9155
|
+
description: "把多说话人音频按说话人分成独立音轨,并产说话人时间线结构;--only-struct 只出结构不切文件。",
|
|
9156
|
+
kind: "cloud",
|
|
9157
|
+
input: { kind: "audio" },
|
|
9158
|
+
priceKey: "audio_speaker_split",
|
|
9159
|
+
outputHint: "各说话人音轨 + 时间线结构",
|
|
9160
|
+
enabled: true,
|
|
9161
|
+
taskType: "audio_speaker_split",
|
|
9162
|
+
options: [{ flag: "--only-struct", desc: "只输出说话人时间线结构,不切分音轨文件" }],
|
|
9163
|
+
buildPayload(fileId, ctx) {
|
|
9164
|
+
return { file_id: fileId, only_struct: ctx.opts.onlyStruct === true };
|
|
9165
|
+
},
|
|
9166
|
+
mapOutputs(out, ctx) {
|
|
9167
|
+
if (!Array.isArray(out.files))
|
|
9168
|
+
return [];
|
|
9169
|
+
const items = [];
|
|
9170
|
+
for (const raw of out.files) {
|
|
9171
|
+
if (!raw || typeof raw !== "object")
|
|
9172
|
+
continue;
|
|
9173
|
+
const file = raw;
|
|
9174
|
+
const url = file.download_url;
|
|
9175
|
+
if (typeof url !== "string" || !url.trim())
|
|
9176
|
+
continue;
|
|
9177
|
+
const speaker = typeof file.speaker === "string" && file.speaker.trim() ? file.speaker : `speaker_${items.length + 1}`;
|
|
9178
|
+
items.push({ url, filename: `${ctx.baseName}-${speaker}${extFromUrl(url, ".wav")}` });
|
|
9179
|
+
}
|
|
9180
|
+
return items;
|
|
9181
|
+
},
|
|
9182
|
+
mapResult(out) {
|
|
9183
|
+
return { spoken_list: Array.isArray(out.spoken_list) ? out.spoken_list : [] };
|
|
9184
|
+
}
|
|
9185
|
+
};
|
|
9186
|
+
var audioStretch = {
|
|
9187
|
+
name: "audio_stretch",
|
|
9188
|
+
title: "音频变调变速",
|
|
9189
|
+
description: "对音频独立调整音高(半音)与速度(倍率),输出处理后的音频。",
|
|
9190
|
+
kind: "cloud",
|
|
9191
|
+
input: { kind: "audio" },
|
|
9192
|
+
priceKey: "audio_stretch",
|
|
9193
|
+
outputHint: "变调变速音频",
|
|
9194
|
+
enabled: true,
|
|
9195
|
+
taskType: "audio_stretch",
|
|
9196
|
+
options: [
|
|
9197
|
+
{ flag: "--semitones <n>", desc: "变调半音(升正降负;未传则不变调)" },
|
|
9198
|
+
{ flag: "--speed <n>", desc: "变速倍率(必须 > 0;未传则不变速)" }
|
|
9199
|
+
],
|
|
9200
|
+
buildPayload(fileId, ctx) {
|
|
9201
|
+
const payload = { file_id: fileId };
|
|
9202
|
+
if (ctx.opts.semitones != null) {
|
|
9203
|
+
const value = Number(ctx.opts.semitones);
|
|
9204
|
+
if (!Number.isFinite(value))
|
|
9205
|
+
throw new Error("--semitones 必须是数字");
|
|
9206
|
+
payload.semitones = value;
|
|
9207
|
+
}
|
|
9208
|
+
if (ctx.opts.speed != null) {
|
|
9209
|
+
const value = Number(ctx.opts.speed);
|
|
9210
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
9211
|
+
throw new Error("--speed 必须是大于 0 的数字");
|
|
9212
|
+
payload.speed = value;
|
|
9213
|
+
}
|
|
9214
|
+
return payload;
|
|
9215
|
+
},
|
|
9216
|
+
mapOutputs(out, ctx) {
|
|
9217
|
+
const url = pickUrl(out, ["download_url"]);
|
|
9218
|
+
return url ? [{ url, filename: `${ctx.baseName}-stretch${extFromUrl(url, ".wav")}` }] : [];
|
|
9219
|
+
}
|
|
9220
|
+
};
|
|
9221
|
+
var pianoAudioToMidi = {
|
|
9222
|
+
name: "piano_audio_to_midi",
|
|
9223
|
+
title: "钢琴音频转 MIDI",
|
|
9224
|
+
description: "把钢琴演奏音频扒谱为 MIDI 文件。",
|
|
9225
|
+
kind: "cloud",
|
|
9226
|
+
input: { kind: "audio" },
|
|
9227
|
+
priceKey: "piano_audio_to_midi",
|
|
9228
|
+
outputHint: "MIDI 文件(.mid)",
|
|
9229
|
+
enabled: true,
|
|
9230
|
+
taskType: "piano_audio_to_midi",
|
|
9231
|
+
buildPayload(fileId) {
|
|
9232
|
+
return { file_id: fileId };
|
|
9233
|
+
},
|
|
9234
|
+
mapOutputs(out, ctx) {
|
|
9235
|
+
const url = pickUrl(out, ["download_url"]);
|
|
9236
|
+
return url ? [{ url, filename: `${ctx.baseName}${extFromUrl(url, ".mid")}` }] : [];
|
|
9237
|
+
}
|
|
9238
|
+
};
|
|
9239
|
+
var pianoAudioEnhance = {
|
|
9240
|
+
name: "piano_audio_enhance",
|
|
9241
|
+
title: "钢琴音频修复增强",
|
|
9242
|
+
description: "修复并增强钢琴录音音质,产高质量 WAV 与配套 MIDI。",
|
|
9243
|
+
kind: "cloud",
|
|
9244
|
+
input: { kind: "audio" },
|
|
9245
|
+
priceKey: "piano_audio_enhance",
|
|
9246
|
+
outputHint: "高质量 WAV + MIDI",
|
|
9247
|
+
enabled: true,
|
|
9248
|
+
taskType: "piano_audio_enhance",
|
|
9249
|
+
buildPayload(fileId) {
|
|
9250
|
+
return { file_id: fileId };
|
|
9251
|
+
},
|
|
9252
|
+
mapOutputs(out, ctx) {
|
|
9253
|
+
const files = [];
|
|
9254
|
+
const main = pickUrl(out, ["download_url"]);
|
|
9255
|
+
if (main)
|
|
9256
|
+
files.push({ url: main, filename: `${ctx.baseName}-enhanced${extFromUrl(main, ".wav")}` });
|
|
9257
|
+
const midi = pickUrl(out, ["midi_download_url"]);
|
|
9258
|
+
if (midi)
|
|
9259
|
+
files.push({ url: midi, filename: `${ctx.baseName}${extFromUrl(midi, ".mid")}` });
|
|
9260
|
+
return files;
|
|
9261
|
+
}
|
|
9262
|
+
};
|
|
9263
|
+
var imageToSquare = {
|
|
9264
|
+
name: "image_to_square",
|
|
9265
|
+
title: "长图转方图",
|
|
9266
|
+
description: "把长图智能转为方形图,可指定最大边长(默认 4000,上限 20000)。",
|
|
9267
|
+
kind: "cloud",
|
|
9268
|
+
input: { kind: "image" },
|
|
9269
|
+
priceKey: "image_to_square",
|
|
9270
|
+
outputHint: "方形图片",
|
|
9271
|
+
enabled: true,
|
|
9272
|
+
taskType: "image_to_square",
|
|
9273
|
+
options: [{ flag: "--max-line <px>", desc: "最大边长像素(默认 4000,上限 20000)" }],
|
|
9274
|
+
buildPayload(fileId, ctx) {
|
|
9275
|
+
const payload = { file_id: fileId };
|
|
9276
|
+
if (ctx.opts.maxLine != null) {
|
|
9277
|
+
const value = Number(ctx.opts.maxLine);
|
|
9278
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > 20000) {
|
|
9279
|
+
throw new Error("--max-line 必须是 1 到 20000 之间的整数");
|
|
9280
|
+
}
|
|
9281
|
+
payload.max_line = value;
|
|
9282
|
+
}
|
|
9283
|
+
return payload;
|
|
9284
|
+
},
|
|
9285
|
+
mapOutputs(out, ctx) {
|
|
9286
|
+
const url = pickUrl(out, ["download_url"]);
|
|
9287
|
+
return url ? [{ url, filename: `${ctx.baseName}-square${extFromUrl(url, ".jpg")}` }] : [];
|
|
9288
|
+
}
|
|
9289
|
+
};
|
|
9290
|
+
var imageToLive = {
|
|
9291
|
+
name: "image_to_live",
|
|
9292
|
+
title: "智能 LivePhoto",
|
|
9293
|
+
description: "把一张静态图片生成微动的 LivePhoto 视频(产物是视频)。",
|
|
9294
|
+
kind: "cloud",
|
|
9295
|
+
input: { kind: "image" },
|
|
9296
|
+
priceKey: "image_to_live",
|
|
9297
|
+
outputHint: "微动视频(.mp4)",
|
|
9298
|
+
enabled: true,
|
|
9299
|
+
taskType: "image_to_live",
|
|
9300
|
+
buildPayload(fileId) {
|
|
9301
|
+
return { file_id: fileId };
|
|
9302
|
+
},
|
|
9303
|
+
mapOutputs(out, ctx) {
|
|
9304
|
+
const url = pickUrl(out, ["download_url", "video_download_url", "url"]);
|
|
9305
|
+
return url ? [{ url, filename: `${ctx.baseName}-live${extFromUrl(url, ".mp4")}` }] : [];
|
|
9306
|
+
}
|
|
9307
|
+
};
|
|
9308
|
+
var AI_SUBTITLE_TYPES = ["default", "outline", "cinema_yellow", "immersive_box", "wide_spacing", "deep_shadow", "boxed"];
|
|
9309
|
+
var AI_SUBTITLE_COLORS = ["雅黑", "淡绿", "森林绿", "湖蓝", "道奇蓝", "钢蓝", "浅粉红", "深橙", "珊瑚橙", "橙红", "土豪金"];
|
|
9310
|
+
var videoAiSubtitle = {
|
|
9311
|
+
name: "video_ai_subtitle",
|
|
9312
|
+
title: "智能视频字幕",
|
|
9313
|
+
description: "为视频(或音频)智能生成字幕,可选双语翻译、烧录进视频、去除原字幕;另产 LLM 摘要与字级时间轴。",
|
|
9314
|
+
kind: "cloud",
|
|
9315
|
+
input: { kind: "video", exts: [...PUBLIC_VIDEO_EXTS, ...AUDIO_EXTS] },
|
|
9316
|
+
priceKey: "video_ai_subtitle",
|
|
9317
|
+
outputHint: "字幕 .ass + 可选烧录/去字幕视频 + 摘要/字级时轴结构",
|
|
9318
|
+
enabled: true,
|
|
9319
|
+
taskType: "video_ai_subtitle",
|
|
9320
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
9321
|
+
options: [
|
|
9322
|
+
{ flag: "--language <code>", desc: "源语种代码(必填;具体取值由服务端支持列表校验)" },
|
|
9323
|
+
{ flag: "--translate-language <code>", desc: "译文目标语种(未传则单语)" },
|
|
9324
|
+
{ flag: "--need-render", desc: "把字幕烧录进视频(仅视频输入有效)" },
|
|
9325
|
+
{ flag: "--need-pure", desc: "先去除原视频中的字幕" },
|
|
9326
|
+
{ flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
|
|
9327
|
+
{ flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
|
|
9328
|
+
],
|
|
9329
|
+
buildPayload(fileId, ctx) {
|
|
9330
|
+
const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
|
|
9331
|
+
if (!language)
|
|
9332
|
+
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
9333
|
+
const payload = { file_id: fileId, language };
|
|
9334
|
+
if (ctx.opts.translateLanguage != null) {
|
|
9335
|
+
const t = String(ctx.opts.translateLanguage).trim();
|
|
9336
|
+
if (t)
|
|
9337
|
+
payload.translate_language = t;
|
|
9338
|
+
}
|
|
9339
|
+
if (ctx.opts.needRender === true)
|
|
9340
|
+
payload.need_render = true;
|
|
9341
|
+
if (ctx.opts.needPure === true)
|
|
9342
|
+
payload.need_pure = true;
|
|
9343
|
+
if (ctx.opts.subtitleType != null) {
|
|
9344
|
+
const v = String(ctx.opts.subtitleType);
|
|
9345
|
+
if (!AI_SUBTITLE_TYPES.includes(v))
|
|
9346
|
+
throw new Error(`--subtitle-type 只支持 ${AI_SUBTITLE_TYPES.join("、")}`);
|
|
9347
|
+
payload.subtitle_type = v;
|
|
9348
|
+
}
|
|
9349
|
+
if (ctx.opts.subtitleColor != null) {
|
|
9350
|
+
const v = String(ctx.opts.subtitleColor);
|
|
9351
|
+
if (!AI_SUBTITLE_COLORS.includes(v))
|
|
9352
|
+
throw new Error(`--subtitle-color 只支持 ${AI_SUBTITLE_COLORS.join("、")}`);
|
|
9353
|
+
payload.subtitle_color = v;
|
|
9354
|
+
}
|
|
9355
|
+
return payload;
|
|
9356
|
+
},
|
|
9357
|
+
mapOutputs(out, ctx) {
|
|
9358
|
+
const files = [];
|
|
9359
|
+
const sub = pickUrl(out, ["subtitle_file_download_url"]);
|
|
9360
|
+
if (sub)
|
|
9361
|
+
files.push({ url: sub, filename: `${ctx.baseName}${extFromUrl(sub, ".ass")}` });
|
|
9362
|
+
const rendered = pickUrl(out, ["rendered_file_download_url"]);
|
|
9363
|
+
if (rendered)
|
|
9364
|
+
files.push({ url: rendered, filename: `${ctx.baseName}-subtitled${extFromUrl(rendered, ".mp4")}` });
|
|
9365
|
+
const pure = pickUrl(out, ["pure_file_download_url"]);
|
|
9366
|
+
if (pure)
|
|
9367
|
+
files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
|
|
9368
|
+
return files;
|
|
9369
|
+
},
|
|
9370
|
+
mapResult(out) {
|
|
9371
|
+
return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
|
|
9372
|
+
}
|
|
9373
|
+
};
|
|
8307
9374
|
var mad = {
|
|
8308
9375
|
name: "mad",
|
|
8309
9376
|
title: "一键剪 MAD",
|
|
8310
9377
|
description: "素材文件夹(3~10 条视频)+ 可选 BGM → 自动选技法 → 单一 .jsx,AE 2020+ 跑一遍出 15~30s 卡点成片工程。仅支持 AE。",
|
|
8311
9378
|
kind: "local",
|
|
8312
9379
|
input: { kind: "directory" },
|
|
8313
|
-
|
|
9380
|
+
priceKey: "audio_music_analyze",
|
|
9381
|
+
pricingContext: "仅 --bgm 卡点时",
|
|
8314
9382
|
outputHint: "AE 母合成工程 .jsx",
|
|
8315
9383
|
enabled: true,
|
|
8316
9384
|
options: [
|
|
@@ -8321,7 +9389,35 @@ var mad = {
|
|
|
8321
9389
|
]
|
|
8322
9390
|
};
|
|
8323
9391
|
var RESERVED_NAMES = new Set(["list"]);
|
|
8324
|
-
var TOOL_REGISTRY = [
|
|
9392
|
+
var TOOL_REGISTRY = [
|
|
9393
|
+
imageMove,
|
|
9394
|
+
imageMatting,
|
|
9395
|
+
imageBlackborderRemove,
|
|
9396
|
+
imageCanvasAdapt,
|
|
9397
|
+
imagePurify,
|
|
9398
|
+
videoMatting,
|
|
9399
|
+
videoBlackborderRemove,
|
|
9400
|
+
videoCanvasAdapt,
|
|
9401
|
+
videoStabilizer,
|
|
9402
|
+
videoVaporwave,
|
|
9403
|
+
videoPurify,
|
|
9404
|
+
videoUpscale,
|
|
9405
|
+
videoInterpolate,
|
|
9406
|
+
audioSeparation,
|
|
9407
|
+
audioNoiseReduce,
|
|
9408
|
+
audioSilenceRemove,
|
|
9409
|
+
videoSegment,
|
|
9410
|
+
videoAiSegment,
|
|
9411
|
+
videoMotionCut,
|
|
9412
|
+
audioSpeakerSplit,
|
|
9413
|
+
audioStretch,
|
|
9414
|
+
pianoAudioToMidi,
|
|
9415
|
+
pianoAudioEnhance,
|
|
9416
|
+
imageToSquare,
|
|
9417
|
+
imageToLive,
|
|
9418
|
+
videoAiSubtitle,
|
|
9419
|
+
mad
|
|
9420
|
+
];
|
|
8325
9421
|
function findTool(name, registry = TOOL_REGISTRY) {
|
|
8326
9422
|
return registry.find((d) => d.name === name);
|
|
8327
9423
|
}
|
|
@@ -8339,15 +9435,101 @@ function validateRegistry(registry = TOOL_REGISTRY) {
|
|
|
8339
9435
|
throw new Error(`未启用工具缺 disabledReason:「${d.name}」`);
|
|
8340
9436
|
if (d.kind === "cloud" && !d.taskType)
|
|
8341
9437
|
throw new Error(`cloud 型工具缺 taskType:「${d.name}」`);
|
|
9438
|
+
if (d.kind === "cloud" && !d.priceKey)
|
|
9439
|
+
throw new Error(`cloud 型工具缺 priceKey:「${d.name}」`);
|
|
9440
|
+
if (d.kind === "cloud" && !d.mapOutputs && !d.mapResult) {
|
|
9441
|
+
throw new Error(`cloud 型工具须至少声明 mapOutputs 或 mapResult 之一:「${d.name}」`);
|
|
9442
|
+
}
|
|
8342
9443
|
}
|
|
8343
9444
|
}
|
|
8344
9445
|
|
|
8345
9446
|
// src/lib/tool-runner.ts
|
|
8346
|
-
import { resolve as
|
|
9447
|
+
import { resolve as resolve10, join as join21, dirname as dirname9, basename as basename11, extname as extname5 } from "node:path";
|
|
8347
9448
|
import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
|
|
8348
9449
|
import { createWriteStream, existsSync as existsSync17 } from "node:fs";
|
|
8349
9450
|
import { Readable as Readable2 } from "node:stream";
|
|
8350
9451
|
import { pipeline } from "node:stream/promises";
|
|
9452
|
+
|
|
9453
|
+
// src/lib/tool-pricing.ts
|
|
9454
|
+
var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
|
|
9455
|
+
var PRICE_UNAVAILABLE_HINT = "实时价格暂不可用,以服务端结算为准";
|
|
9456
|
+
var PRICE_REQUEST_TIMEOUT_MS = 5000;
|
|
9457
|
+
function validNumber(v) {
|
|
9458
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
9459
|
+
}
|
|
9460
|
+
function parseToolPriceList(value) {
|
|
9461
|
+
if (!Array.isArray(value))
|
|
9462
|
+
throw new Error("价格表响应不是数组");
|
|
9463
|
+
const prices = new Map;
|
|
9464
|
+
for (const raw of value) {
|
|
9465
|
+
if (!raw || typeof raw !== "object")
|
|
9466
|
+
continue;
|
|
9467
|
+
const item = raw;
|
|
9468
|
+
const key = typeof item.key === "string" ? item.key.trim() : "";
|
|
9469
|
+
const measure = typeof item.measure === "string" ? item.measure.trim() : "";
|
|
9470
|
+
if (!key || !measure || !validNumber(item.price) || !validNumber(item.exPrice))
|
|
9471
|
+
continue;
|
|
9472
|
+
prices.set(key, {
|
|
9473
|
+
...validNumber(item.taskTypeId) ? { taskTypeId: item.taskTypeId } : {},
|
|
9474
|
+
...typeof item.name === "string" ? { name: item.name } : {},
|
|
9475
|
+
key,
|
|
9476
|
+
price: item.price,
|
|
9477
|
+
exPrice: item.exPrice,
|
|
9478
|
+
measure,
|
|
9479
|
+
...typeof item.note === "string" ? { note: item.note } : {}
|
|
9480
|
+
});
|
|
9481
|
+
}
|
|
9482
|
+
return prices;
|
|
9483
|
+
}
|
|
9484
|
+
async function fetchToolPrices(fetchFn = fetch) {
|
|
9485
|
+
const res = await fetchFn(TOOL_PRICE_LIST_URL, {
|
|
9486
|
+
method: "GET",
|
|
9487
|
+
headers: { Accept: "application/json" },
|
|
9488
|
+
signal: AbortSignal.timeout(PRICE_REQUEST_TIMEOUT_MS)
|
|
9489
|
+
});
|
|
9490
|
+
if (!res.ok)
|
|
9491
|
+
throw new Error(`价格表请求失败 HTTP ${res.status}`);
|
|
9492
|
+
return parseToolPriceList(await res.json());
|
|
9493
|
+
}
|
|
9494
|
+
function formatToolPrice(item, pricingContext) {
|
|
9495
|
+
let price;
|
|
9496
|
+
if (item.price === 0 && item.exPrice === 0) {
|
|
9497
|
+
price = `免费(0 积分/${item.measure})`;
|
|
9498
|
+
} else if (item.price === item.exPrice) {
|
|
9499
|
+
price = `${item.price} 积分/${item.measure}`;
|
|
9500
|
+
} else {
|
|
9501
|
+
price = `标准价 ${item.price} 积分/${item.measure};超额价 ${item.exPrice} 积分/${item.measure}`;
|
|
9502
|
+
}
|
|
9503
|
+
return pricingContext ? `${pricingContext}:${price}` : price;
|
|
9504
|
+
}
|
|
9505
|
+
function resolveToolPricingFromMap(priceKey, prices, pricingContext) {
|
|
9506
|
+
const item = prices?.get(priceKey);
|
|
9507
|
+
if (!item) {
|
|
9508
|
+
return {
|
|
9509
|
+
billingHint: pricingContext ? `${pricingContext}:${PRICE_UNAVAILABLE_HINT}` : PRICE_UNAVAILABLE_HINT,
|
|
9510
|
+
pricing: { key: priceKey, available: false }
|
|
9511
|
+
};
|
|
9512
|
+
}
|
|
9513
|
+
return {
|
|
9514
|
+
billingHint: formatToolPrice(item, pricingContext),
|
|
9515
|
+
pricing: {
|
|
9516
|
+
key: priceKey,
|
|
9517
|
+
available: true,
|
|
9518
|
+
price: item.price,
|
|
9519
|
+
exPrice: item.exPrice,
|
|
9520
|
+
measure: item.measure
|
|
9521
|
+
}
|
|
9522
|
+
};
|
|
9523
|
+
}
|
|
9524
|
+
async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
|
|
9525
|
+
try {
|
|
9526
|
+
return resolveToolPricingFromMap(priceKey, await fetchToolPrices(fetchFn), pricingContext);
|
|
9527
|
+
} catch {
|
|
9528
|
+
return resolveToolPricingFromMap(priceKey, undefined, pricingContext);
|
|
9529
|
+
}
|
|
9530
|
+
}
|
|
9531
|
+
|
|
9532
|
+
// src/lib/tool-runner.ts
|
|
8351
9533
|
var DEFAULT_POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
8352
9534
|
var DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
8353
9535
|
function coerceValue2(v) {
|
|
@@ -8461,10 +9643,10 @@ function timestamp3() {
|
|
|
8461
9643
|
}
|
|
8462
9644
|
function resolveOutDir(descriptor, inputAbs, out) {
|
|
8463
9645
|
if (out)
|
|
8464
|
-
return
|
|
9646
|
+
return resolve10(out);
|
|
8465
9647
|
if (inputAbs) {
|
|
8466
9648
|
const base = basename11(inputAbs, extname5(inputAbs));
|
|
8467
|
-
return join21(
|
|
9649
|
+
return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
|
|
8468
9650
|
}
|
|
8469
9651
|
return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
|
|
8470
9652
|
}
|
|
@@ -8481,7 +9663,7 @@ function emitBilling(hint) {
|
|
|
8481
9663
|
`);
|
|
8482
9664
|
}
|
|
8483
9665
|
async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
8484
|
-
const inputAbs = inputArg ?
|
|
9666
|
+
const inputAbs = inputArg ? resolve10(inputArg) : undefined;
|
|
8485
9667
|
const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
|
|
8486
9668
|
validateToolInput(descriptor, inputAbs);
|
|
8487
9669
|
const probe = deps.probeDurationSec ?? probeDuration;
|
|
@@ -8502,25 +9684,27 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8502
9684
|
uploadPath = await descriptor.preprocess(ctx);
|
|
8503
9685
|
if (!uploadPath)
|
|
8504
9686
|
throw new Error(`${descriptor.name} 缺上传物(input=none 的 cloud 型工具需 preprocess 产上传物)`);
|
|
8505
|
-
|
|
8506
|
-
|
|
9687
|
+
let billingHint;
|
|
9688
|
+
try {
|
|
9689
|
+
billingHint = (await (deps.resolvePricing ?? resolveToolPricing)(descriptor.priceKey, descriptor.pricingContext)).billingHint;
|
|
9690
|
+
} catch {
|
|
9691
|
+
billingHint = "实时价格暂不可用,以服务端结算为准";
|
|
9692
|
+
}
|
|
9693
|
+
emitBilling(billingHint);
|
|
8507
9694
|
const buildPayload = (fid) => {
|
|
8508
9695
|
const p = descriptor.buildPayload ? descriptor.buildPayload(fid, ctx) : { file_id: fid };
|
|
8509
9696
|
mergeParams(p, extraParams);
|
|
8510
9697
|
return p;
|
|
8511
9698
|
};
|
|
8512
9699
|
const taskType = descriptor.taskType;
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
} else
|
|
8522
|
-
throw e;
|
|
8523
|
-
}
|
|
9700
|
+
const submitted = await uploadAndSubmitTask(deps.cfg, uploadPath, taskType, buildPayload, { force: opts.reupload }, {
|
|
9701
|
+
uploadCached: deps.uploadCached,
|
|
9702
|
+
invalidateUpload: deps.invalidateUpload,
|
|
9703
|
+
submitTask: deps.submitTask,
|
|
9704
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
|
|
9705
|
+
});
|
|
9706
|
+
const { taskId } = submitted;
|
|
9707
|
+
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
8524
9708
|
await mkdir8(outDir, { recursive: true });
|
|
8525
9709
|
const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
8526
9710
|
await writeFile8(join21(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
|
|
@@ -8531,11 +9715,10 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8531
9715
|
sleep: deps.sleep,
|
|
8532
9716
|
now: deps.now
|
|
8533
9717
|
});
|
|
8534
|
-
const
|
|
9718
|
+
const outputResult = output;
|
|
8535
9719
|
const files = [];
|
|
8536
9720
|
const errors = {};
|
|
8537
|
-
|
|
8538
|
-
errors["output"] = "任务完成但未解析到产物下载链接(output_result 形态异常)";
|
|
9721
|
+
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
8539
9722
|
for (const it of items) {
|
|
8540
9723
|
const dest = join21(outDir, it.filename);
|
|
8541
9724
|
try {
|
|
@@ -8545,7 +9728,16 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8545
9728
|
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
8546
9729
|
}
|
|
8547
9730
|
}
|
|
8548
|
-
|
|
9731
|
+
let resultFile;
|
|
9732
|
+
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
9733
|
+
if (structured != null) {
|
|
9734
|
+
resultFile = join21(outDir, "result-output.json");
|
|
9735
|
+
await writeFile8(resultFile, JSON.stringify(structured, null, 2));
|
|
9736
|
+
}
|
|
9737
|
+
if (items.length === 0 && resultFile == null) {
|
|
9738
|
+
errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
|
|
9739
|
+
}
|
|
9740
|
+
const ok = Object.keys(errors).length === 0 && (files.length > 0 || resultFile != null);
|
|
8549
9741
|
const result = {
|
|
8550
9742
|
ok,
|
|
8551
9743
|
tool: descriptor.name,
|
|
@@ -8554,6 +9746,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8554
9746
|
fileId: up.fileId,
|
|
8555
9747
|
outDir,
|
|
8556
9748
|
files,
|
|
9749
|
+
...resultFile ? { resultFile } : {},
|
|
8557
9750
|
...Object.keys(errors).length ? { errors } : {}
|
|
8558
9751
|
};
|
|
8559
9752
|
await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
@@ -8563,7 +9756,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8563
9756
|
// src/lib/mad/mad.ts
|
|
8564
9757
|
import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
|
|
8565
9758
|
import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
|
|
8566
|
-
import { resolve as
|
|
9759
|
+
import { resolve as resolve11, join as join25 } from "node:path";
|
|
8567
9760
|
|
|
8568
9761
|
// src/lib/convert/types.ts
|
|
8569
9762
|
function num(v, d = 0) {
|
|
@@ -9733,9 +10926,6 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
9733
10926
|
|
|
9734
10927
|
// src/lib/mad/cloud-beat.ts
|
|
9735
10928
|
var ANALYZE_TASK = "audio_music_analyze";
|
|
9736
|
-
function isCode(e, code) {
|
|
9737
|
-
return !!e && typeof e === "object" && e.code === code;
|
|
9738
|
-
}
|
|
9739
10929
|
function extractAnalysis(output) {
|
|
9740
10930
|
const o = output ?? {};
|
|
9741
10931
|
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "number" && Number.isFinite(x)) : [];
|
|
@@ -9746,19 +10936,14 @@ function extractAnalysis(output) {
|
|
|
9746
10936
|
};
|
|
9747
10937
|
}
|
|
9748
10938
|
async function analyzeBgm(cfg, bgmAbs, deps) {
|
|
9749
|
-
let up = await deps.uploadCached(cfg, bgmAbs, {});
|
|
9750
10939
|
const payload = (fid) => ({ file_id: fid });
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
taskId = await deps.submitTask(cfg, ANALYZE_TASK, payload(up.fileId));
|
|
9759
|
-
} else
|
|
9760
|
-
throw e;
|
|
9761
|
-
}
|
|
10940
|
+
const submitted = await uploadAndSubmitTask(cfg, bgmAbs, ANALYZE_TASK, payload, {}, {
|
|
10941
|
+
uploadCached: deps.uploadCached,
|
|
10942
|
+
invalidateUpload: deps.invalidateUpload,
|
|
10943
|
+
submitTask: deps.submitTask,
|
|
10944
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
|
|
10945
|
+
});
|
|
10946
|
+
const { taskId } = submitted;
|
|
9762
10947
|
const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
|
|
9763
10948
|
return extractAnalysis(output);
|
|
9764
10949
|
}
|
|
@@ -9817,7 +11002,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
9817
11002
|
const probeDur = deps.probeDurationFn ?? probeDuration;
|
|
9818
11003
|
if (!inputArg)
|
|
9819
11004
|
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
9820
|
-
const dirAbs =
|
|
11005
|
+
const dirAbs = resolve11(inputArg);
|
|
9821
11006
|
if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
9822
11007
|
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
9823
11008
|
}
|
|
@@ -9849,7 +11034,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
9849
11034
|
let level = 3;
|
|
9850
11035
|
let analysis = null;
|
|
9851
11036
|
let bgmForTrack;
|
|
9852
|
-
const bgmAbs = opts.bgm ?
|
|
11037
|
+
const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
|
|
9853
11038
|
if (bgmAbs) {
|
|
9854
11039
|
let dur = -1;
|
|
9855
11040
|
try {
|
|
@@ -9872,7 +11057,8 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
9872
11057
|
level = 2;
|
|
9873
11058
|
bgmForTrack = bgmAbs;
|
|
9874
11059
|
} else {
|
|
9875
|
-
|
|
11060
|
+
const pricing = await (deps.resolvePricing ?? resolveToolPricing)("audio_music_analyze", "仅 --bgm 卡点时");
|
|
11061
|
+
emitBilling2(pricing.billingHint);
|
|
9876
11062
|
const beatCloud = deps.beatCloud ?? { uploadCached, invalidateUpload, submitTask, pollToolTask };
|
|
9877
11063
|
try {
|
|
9878
11064
|
analysis = await analyzeBgm(cfg, bgmAbs, beatCloud);
|
|
@@ -9931,7 +11117,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
9931
11117
|
const header = madHeader(version, now.toISOString());
|
|
9932
11118
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
9933
11119
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
9934
|
-
const outDir = opts.out ?
|
|
11120
|
+
const outDir = opts.out ? resolve11(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
|
|
9935
11121
|
await mkdir11(outDir, { recursive: true });
|
|
9936
11122
|
const jsxPath = join25(outDir, "mad.jsx");
|
|
9937
11123
|
await writeFile10(jsxPath, jsx);
|
|
@@ -9984,7 +11170,7 @@ async function runToolCommand(words, opts, registry = TOOL_REGISTRY, deps) {
|
|
|
9984
11170
|
throw new Error("用法:`gtrk tool <name> [input]` 跑工具;`gtrk tool list` 查全部工具");
|
|
9985
11171
|
}
|
|
9986
11172
|
if (name === "list") {
|
|
9987
|
-
runList(opts, registry);
|
|
11173
|
+
await runList(opts, registry);
|
|
9988
11174
|
return;
|
|
9989
11175
|
}
|
|
9990
11176
|
const descriptor = findTool(name, registry);
|
|
@@ -9994,16 +11180,26 @@ async function runToolCommand(words, opts, registry = TOOL_REGISTRY, deps) {
|
|
|
9994
11180
|
}
|
|
9995
11181
|
return runTool(descriptor, words[1], opts, deps);
|
|
9996
11182
|
}
|
|
9997
|
-
function runList(opts, registry = TOOL_REGISTRY) {
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
11183
|
+
async function runList(opts, registry = TOOL_REGISTRY, loadPrices = fetchToolPrices) {
|
|
11184
|
+
let prices;
|
|
11185
|
+
try {
|
|
11186
|
+
prices = await loadPrices();
|
|
11187
|
+
} catch {
|
|
11188
|
+
prices = undefined;
|
|
11189
|
+
}
|
|
11190
|
+
const rows = registry.map((d) => {
|
|
11191
|
+
const resolved = resolveToolPricingFromMap(d.priceKey ?? d.name, prices, d.pricingContext);
|
|
11192
|
+
return {
|
|
11193
|
+
name: d.name,
|
|
11194
|
+
title: d.title,
|
|
11195
|
+
input: d.input.kind,
|
|
11196
|
+
output: d.outputHint,
|
|
11197
|
+
billingHint: resolved.billingHint,
|
|
11198
|
+
pricing: resolved.pricing,
|
|
11199
|
+
enabled: d.enabled,
|
|
11200
|
+
...d.disabledReason ? { disabledReason: d.disabledReason } : {}
|
|
11201
|
+
};
|
|
11202
|
+
});
|
|
10007
11203
|
if (opts.json) {
|
|
10008
11204
|
console.log(JSON.stringify(rows));
|
|
10009
11205
|
return;
|
|
@@ -10066,12 +11262,461 @@ async function runMadInTool(inputArg, opts) {
|
|
|
10066
11262
|
return { ok: r.ok, tool: r.tool, outDir: r.outDir, files: r.files };
|
|
10067
11263
|
}
|
|
10068
11264
|
|
|
11265
|
+
// src/commands/transcript.ts
|
|
11266
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
11267
|
+
import { mkdir as mkdir12, rename as rename2, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
|
|
11268
|
+
import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
|
|
11269
|
+
|
|
11270
|
+
// src/lib/transcript.ts
|
|
11271
|
+
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
11272
|
+
function finiteNumber(value) {
|
|
11273
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
11274
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
11275
|
+
}
|
|
11276
|
+
function timeFrom(item, secondsKey, msKey, shortKey) {
|
|
11277
|
+
const seconds = finiteNumber(item[secondsKey]);
|
|
11278
|
+
if (seconds != null)
|
|
11279
|
+
return seconds;
|
|
11280
|
+
const milliseconds = finiteNumber(item[msKey]);
|
|
11281
|
+
if (milliseconds != null)
|
|
11282
|
+
return milliseconds / 1000;
|
|
11283
|
+
return finiteNumber(item[shortKey]) ?? 0;
|
|
11284
|
+
}
|
|
11285
|
+
function normalizeTimedList(value) {
|
|
11286
|
+
if (!Array.isArray(value))
|
|
11287
|
+
return [];
|
|
11288
|
+
const items = [];
|
|
11289
|
+
for (const raw of value) {
|
|
11290
|
+
if (!raw || typeof raw !== "object")
|
|
11291
|
+
continue;
|
|
11292
|
+
const item = raw;
|
|
11293
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
11294
|
+
if (!text)
|
|
11295
|
+
continue;
|
|
11296
|
+
const start = timeFrom(item, "start_time", "begin_time_ms", "st");
|
|
11297
|
+
const end = timeFrom(item, "end_time", "end_time_ms", "ed");
|
|
11298
|
+
items.push({ text, start, end: Math.max(start, end) });
|
|
11299
|
+
}
|
|
11300
|
+
return items.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
11301
|
+
}
|
|
11302
|
+
function normalizeAsrOutput(output) {
|
|
11303
|
+
let sentences = normalizeTimedList(output.sentence_tc_list ?? output.sentence_list);
|
|
11304
|
+
const words = normalizeTimedList(output.word_tc_list ?? output.word_list);
|
|
11305
|
+
let text = "";
|
|
11306
|
+
for (const key of ["asr_text", "text"]) {
|
|
11307
|
+
const value = output[key];
|
|
11308
|
+
if (typeof value === "string" && value.trim()) {
|
|
11309
|
+
text = value.trim();
|
|
11310
|
+
break;
|
|
11311
|
+
}
|
|
11312
|
+
}
|
|
11313
|
+
if (sentences.length === 0 && words.length > 0) {
|
|
11314
|
+
sentences = [{
|
|
11315
|
+
text: text || words.map((word) => word.text).join(""),
|
|
11316
|
+
start: words[0]?.start ?? 0,
|
|
11317
|
+
end: words[words.length - 1]?.end ?? 0
|
|
11318
|
+
}];
|
|
11319
|
+
}
|
|
11320
|
+
if (!text && sentences.length > 0)
|
|
11321
|
+
text = sentences.map((sentence) => sentence.text).join(`
|
|
11322
|
+
`);
|
|
11323
|
+
if (!text.trim() || sentences.length === 0) {
|
|
11324
|
+
throw new Error("ASR 任务已完成,但没有返回可用的文字或句级时间戳");
|
|
11325
|
+
}
|
|
11326
|
+
return { text: text.trim(), sentences, words };
|
|
11327
|
+
}
|
|
11328
|
+
function formatTimestamp(seconds) {
|
|
11329
|
+
const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
|
|
11330
|
+
const h = Math.floor(total / 3600);
|
|
11331
|
+
const m = Math.floor(total % 3600 / 60);
|
|
11332
|
+
const s = total % 60;
|
|
11333
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
11334
|
+
}
|
|
11335
|
+
function ensureTerminalPunctuation(text) {
|
|
11336
|
+
const value = text.trim();
|
|
11337
|
+
if (!value)
|
|
11338
|
+
return value;
|
|
11339
|
+
return /[。!?!?;;::]$/.test(value) ? value : `${value}。`;
|
|
11340
|
+
}
|
|
11341
|
+
function localDateTime(date) {
|
|
11342
|
+
const p = (value) => String(value).padStart(2, "0");
|
|
11343
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
|
|
11344
|
+
}
|
|
11345
|
+
function renderTranscriptMarkdown(input) {
|
|
11346
|
+
const timed = input.asr.sentences.flatMap((sentence) => [
|
|
11347
|
+
`**[${formatTimestamp(sentence.start)}]**`,
|
|
11348
|
+
"",
|
|
11349
|
+
ensureTerminalPunctuation(sentence.text),
|
|
11350
|
+
""
|
|
11351
|
+
]);
|
|
11352
|
+
return [
|
|
11353
|
+
`# ${input.title}`,
|
|
11354
|
+
"",
|
|
11355
|
+
`> 生成时间:${localDateTime(input.generatedAt)} `,
|
|
11356
|
+
`> 视频时长:${formatTimestamp(input.durationSec)} `,
|
|
11357
|
+
`> 来源:本地视频 \`${input.sourceName}\` `,
|
|
11358
|
+
`> 识别语言:${input.language}`,
|
|
11359
|
+
"",
|
|
11360
|
+
"## 总结",
|
|
11361
|
+
"",
|
|
11362
|
+
AGENT_SUMMARY_PENDING,
|
|
11363
|
+
"> 待驱动 CLI 的 Agent 阅读下方完整文字稿后,在此生成总结。",
|
|
11364
|
+
"",
|
|
11365
|
+
"## 文字记录",
|
|
11366
|
+
"",
|
|
11367
|
+
...timed,
|
|
11368
|
+
"## 纯文本",
|
|
11369
|
+
"",
|
|
11370
|
+
input.asr.text.trim(),
|
|
11371
|
+
""
|
|
11372
|
+
].join(`
|
|
11373
|
+
`);
|
|
11374
|
+
}
|
|
11375
|
+
|
|
11376
|
+
// src/commands/transcript.ts
|
|
11377
|
+
var TASK_TYPE3 = "asr";
|
|
11378
|
+
var PRICE_KEY = "asr";
|
|
11379
|
+
function buildDeps(overrides = {}) {
|
|
11380
|
+
return {
|
|
11381
|
+
cfg: overrides.cfg ?? loadConfig(),
|
|
11382
|
+
probe: overrides.probe ?? probeGeometry,
|
|
11383
|
+
extract: overrides.extract ?? extractAudio,
|
|
11384
|
+
assertDuration: overrides.assertDuration ?? assertDurationConsistent,
|
|
11385
|
+
resolvePricing: overrides.resolvePricing ?? ((key) => resolveToolPricing(key)),
|
|
11386
|
+
upload: overrides.upload ?? uploadCached,
|
|
11387
|
+
invalidate: overrides.invalidate ?? invalidateUpload,
|
|
11388
|
+
submit: overrides.submit ?? submitTask,
|
|
11389
|
+
sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
|
|
11390
|
+
poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
|
|
11391
|
+
writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
|
|
11392
|
+
now: overrides.now ?? (() => new Date)
|
|
11393
|
+
};
|
|
11394
|
+
}
|
|
11395
|
+
function looksLikeRemote(value) {
|
|
11396
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(value.trim());
|
|
11397
|
+
}
|
|
11398
|
+
async function validateTranscriptInput(input) {
|
|
11399
|
+
if (!input.trim())
|
|
11400
|
+
throw new Error("缺少本地视频路径。用法:gtrk transcript <本地视频>");
|
|
11401
|
+
if (looksLikeRemote(input)) {
|
|
11402
|
+
throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
|
|
11403
|
+
}
|
|
11404
|
+
const inputAbs = resolve12(input);
|
|
11405
|
+
if (!existsSync21(inputAbs))
|
|
11406
|
+
throw new Error(`本地视频不存在:${inputAbs}`);
|
|
11407
|
+
const info = await stat6(inputAbs);
|
|
11408
|
+
if (!info.isFile())
|
|
11409
|
+
throw new Error(`输入不是文件:${inputAbs}`);
|
|
11410
|
+
const extension = extname7(inputAbs).toLowerCase();
|
|
11411
|
+
if (!(defaultExtsFor("video") ?? []).includes(extension)) {
|
|
11412
|
+
throw new Error(`不支持的视频格式「${extension || "无扩展名"}」;请输入本地视频文件`);
|
|
11413
|
+
}
|
|
11414
|
+
return inputAbs;
|
|
11415
|
+
}
|
|
11416
|
+
function resolveTranscriptOutput(inputAbs, out) {
|
|
11417
|
+
const base = basename12(inputAbs, extname7(inputAbs));
|
|
11418
|
+
const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
|
|
11419
|
+
if (extname7(output).toLowerCase() !== ".md")
|
|
11420
|
+
throw new Error("--out 必须指向一个 .md 文件");
|
|
11421
|
+
return output;
|
|
11422
|
+
}
|
|
11423
|
+
async function writeMarkdownAtomic(path, markdown) {
|
|
11424
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
11425
|
+
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
11426
|
+
try {
|
|
11427
|
+
await writeFile11(temp, markdown, "utf8");
|
|
11428
|
+
await rename2(temp, path);
|
|
11429
|
+
} finally {
|
|
11430
|
+
await rm2(temp, { force: true });
|
|
11431
|
+
}
|
|
11432
|
+
}
|
|
11433
|
+
async function runTranscript(input, opts = {}, depsOverride) {
|
|
11434
|
+
if (opts.json)
|
|
11435
|
+
routeLogsToStderr();
|
|
11436
|
+
const inputAbs = await validateTranscriptInput(input);
|
|
11437
|
+
const output = resolveTranscriptOutput(inputAbs, opts.out);
|
|
11438
|
+
const deps = buildDeps(depsOverride);
|
|
11439
|
+
const language = opts.lang?.trim() || "zh-CN";
|
|
11440
|
+
const sourceName = basename12(inputAbs);
|
|
11441
|
+
const title = basename12(inputAbs, extname7(inputAbs));
|
|
11442
|
+
log.step(`▶ 视频转文字稿:${sourceName}`);
|
|
11443
|
+
log.step("① 本地探测视频…");
|
|
11444
|
+
const geometry = deps.probe(inputAbs, opts.ffmpegPath);
|
|
11445
|
+
if (!(geometry.duration > 0))
|
|
11446
|
+
throw new Error("未探测到有效视频时长,无法转写");
|
|
11447
|
+
log.info(`视频时长 ${geometry.duration.toFixed(1)}s`);
|
|
11448
|
+
const pricing = await deps.resolvePricing(PRICE_KEY);
|
|
11449
|
+
log.info(`实时计费:${pricing.billingHint}`);
|
|
11450
|
+
log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
|
|
11451
|
+
const audio = await deps.extract(inputAbs, opts.ffmpegPath);
|
|
11452
|
+
deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
|
|
11453
|
+
log.info(`上传物:${basename12(audio)}(仅音频衍生物)`);
|
|
11454
|
+
log.step("③ 上传音频并提交 ASR…");
|
|
11455
|
+
const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
|
|
11456
|
+
const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE3, payload, {
|
|
11457
|
+
force: opts.reupload,
|
|
11458
|
+
onCacheInvalid: () => log.warn("缓存的 file_id 已失效,重新上传后重试…")
|
|
11459
|
+
}, {
|
|
11460
|
+
uploadCached: deps.upload,
|
|
11461
|
+
invalidateUpload: deps.invalidate,
|
|
11462
|
+
submitTask: deps.submit,
|
|
11463
|
+
sleep: deps.sleep
|
|
11464
|
+
});
|
|
11465
|
+
const { taskId } = submitted;
|
|
11466
|
+
const uploaded = { fileId: submitted.fileId, cached: submitted.cached };
|
|
11467
|
+
log.info(`task_id = ${taskId}`);
|
|
11468
|
+
log.step("④ 云端识别中…");
|
|
11469
|
+
const raw = await deps.poll(deps.cfg, TASK_TYPE3, taskId, (status, progress) => {
|
|
11470
|
+
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
11471
|
+
});
|
|
11472
|
+
log.tickEnd();
|
|
11473
|
+
const asr = normalizeAsrOutput(raw);
|
|
11474
|
+
const markdown = renderTranscriptMarkdown({
|
|
11475
|
+
title,
|
|
11476
|
+
sourceName,
|
|
11477
|
+
durationSec: geometry.duration,
|
|
11478
|
+
language,
|
|
11479
|
+
generatedAt: deps.now(),
|
|
11480
|
+
asr
|
|
11481
|
+
});
|
|
11482
|
+
await deps.writeMarkdown(output, markdown);
|
|
11483
|
+
return { ok: true, taskId, fileId: uploaded.fileId, output, summaryPending: true };
|
|
11484
|
+
}
|
|
11485
|
+
function configureTranscriptCommand(cmd, deps) {
|
|
11486
|
+
return cmd.description("本地视频转文字稿:原视频不上传,只上传抽取音频,生成单个待 Agent 补总结的 Markdown").option("-o, --out <file>", "输出 Markdown 文件(缺省 <视频同目录>/<视频名>-transcript.md)").option("--lang <code>", "识别语言代码(默认 zh-CN)", "zh-CN").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录").option("--reupload", "强制重新上传抽取音频,忽略上传缓存").option("--json", "机读模式:stdout 只输出最终结果 JSON").action(async (video, opts) => {
|
|
11487
|
+
const result = await runTranscript(video, opts, deps);
|
|
11488
|
+
if (opts.json)
|
|
11489
|
+
console.log(JSON.stringify(result));
|
|
11490
|
+
else {
|
|
11491
|
+
log.ok(`带时码文字稿已生成:${result.output}`);
|
|
11492
|
+
log.warn("总结仍待驱动 CLI 的 Agent 阅读全文后写回同一个 Markdown");
|
|
11493
|
+
}
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
function registerTranscript(program2) {
|
|
11497
|
+
configureTranscriptCommand(program2.command("transcript <video>"));
|
|
11498
|
+
}
|
|
11499
|
+
|
|
11500
|
+
// src/commands/music-visualizer.ts
|
|
11501
|
+
import { resolve as resolve13, join as join27, dirname as dirname11, basename as basename13, extname as extname8 } from "node:path";
|
|
11502
|
+
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
11503
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
11504
|
+
var TASK_TYPE4 = "music_visualizer";
|
|
11505
|
+
var PRICE_KEY2 = "music_visualizer";
|
|
11506
|
+
var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
11507
|
+
var AUDIO_EXTS2 = defaultExtsFor("audio") ?? [];
|
|
11508
|
+
var IMAGE_EXTS2 = defaultExtsFor("image") ?? [];
|
|
11509
|
+
var VIDEO_EXTS3 = defaultExtsFor("video") ?? [];
|
|
11510
|
+
var collectParam3 = (v, acc) => {
|
|
11511
|
+
acc.push(v);
|
|
11512
|
+
return acc;
|
|
11513
|
+
};
|
|
11514
|
+
function coerceValue3(v) {
|
|
11515
|
+
if (v === "true")
|
|
11516
|
+
return true;
|
|
11517
|
+
if (v === "false")
|
|
11518
|
+
return false;
|
|
11519
|
+
if (v.trim() !== "" && !Number.isNaN(Number(v)))
|
|
11520
|
+
return Number(v);
|
|
11521
|
+
return v;
|
|
11522
|
+
}
|
|
11523
|
+
function parseExtraParams3(pairs, jsonStr) {
|
|
11524
|
+
const out = {};
|
|
11525
|
+
for (const pair of pairs) {
|
|
11526
|
+
const i = pair.indexOf("=");
|
|
11527
|
+
if (i < 0)
|
|
11528
|
+
throw new Error(`--param 需要 key=value 格式:「${pair}」`);
|
|
11529
|
+
out[pair.slice(0, i).trim()] = coerceValue3(pair.slice(i + 1));
|
|
11530
|
+
}
|
|
11531
|
+
if (jsonStr) {
|
|
11532
|
+
let parsed;
|
|
11533
|
+
try {
|
|
11534
|
+
parsed = JSON.parse(jsonStr);
|
|
11535
|
+
} catch {
|
|
11536
|
+
throw new Error(`--params-json 不是合法 JSON:${jsonStr}`);
|
|
11537
|
+
}
|
|
11538
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
11539
|
+
throw new Error("--params-json 必须是一个 JSON 对象");
|
|
11540
|
+
}
|
|
11541
|
+
Object.assign(out, parsed);
|
|
11542
|
+
}
|
|
11543
|
+
return out;
|
|
11544
|
+
}
|
|
11545
|
+
function timestamp5() {
|
|
11546
|
+
const d = new Date;
|
|
11547
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
11548
|
+
return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
11549
|
+
}
|
|
11550
|
+
function assertExt(pathAbs, exts, label) {
|
|
11551
|
+
if (!existsSync22(pathAbs))
|
|
11552
|
+
throw new Error(`${label}不存在:${pathAbs}`);
|
|
11553
|
+
const e = extname8(pathAbs).toLowerCase();
|
|
11554
|
+
if (exts.length && !exts.includes(e)) {
|
|
11555
|
+
throw new Error(`${label}扩展名不支持:${e}(支持 ${exts.join(" ")})`);
|
|
11556
|
+
}
|
|
11557
|
+
}
|
|
11558
|
+
function buildStyleFields(opts) {
|
|
11559
|
+
const fields = {};
|
|
11560
|
+
if (opts.track != null)
|
|
11561
|
+
fields.track = String(opts.track);
|
|
11562
|
+
if (opts.artist != null)
|
|
11563
|
+
fields.artist = String(opts.artist);
|
|
11564
|
+
if (opts.resolution != null) {
|
|
11565
|
+
const m = /^(\d+)[xX](\d+)$/.exec(opts.resolution.trim());
|
|
11566
|
+
if (!m)
|
|
11567
|
+
throw new Error(`--resolution 必须是 <宽>x<高> 格式(如 1920x1080):${opts.resolution}`);
|
|
11568
|
+
const width = Number(m[1]);
|
|
11569
|
+
const height = Number(m[2]);
|
|
11570
|
+
if (!(width > 0) || !(height > 0))
|
|
11571
|
+
throw new Error("--resolution 的宽高必须为正整数");
|
|
11572
|
+
fields.resolution = { width, height };
|
|
11573
|
+
}
|
|
11574
|
+
if (opts.fps != null) {
|
|
11575
|
+
const fps = Number(opts.fps);
|
|
11576
|
+
if (!Number.isInteger(fps) || fps < 30 || fps > 60)
|
|
11577
|
+
throw new Error("--fps 必须是 30 到 60 的整数");
|
|
11578
|
+
fields.fps = fps;
|
|
11579
|
+
}
|
|
11580
|
+
if (opts.c1 != null) {
|
|
11581
|
+
if (!HEX_RE.test(opts.c1))
|
|
11582
|
+
throw new Error("--c1 必须是十六进制颜色(如 #ff0066 或 #f06)");
|
|
11583
|
+
fields.c1 = opts.c1;
|
|
11584
|
+
}
|
|
11585
|
+
if (opts.c2 != null) {
|
|
11586
|
+
if (!HEX_RE.test(opts.c2))
|
|
11587
|
+
throw new Error("--c2 必须是十六进制颜色(如 #6600ff 或 #60f)");
|
|
11588
|
+
fields.c2 = opts.c2;
|
|
11589
|
+
}
|
|
11590
|
+
if (opts.blur != null) {
|
|
11591
|
+
const blur = Number(opts.blur);
|
|
11592
|
+
if (!Number.isInteger(blur) || blur < 0 || blur > 40)
|
|
11593
|
+
throw new Error("--blur 必须是 0 到 40 的整数");
|
|
11594
|
+
fields.blur = blur;
|
|
11595
|
+
}
|
|
11596
|
+
return fields;
|
|
11597
|
+
}
|
|
11598
|
+
function registerMusicVisualizer(program2) {
|
|
11599
|
+
program2.command("music-visualizer <audio>").description("音乐可视化:一首歌 → 频谱可视化成片(可选背景/封面 + 模板/配色样式)").option("-t, --template <id>", "可视化模板 id(必填;取值见云端 API 文档 / 服务端模板列表)").option("--background <图或视频>", "可选背景素材(图片或视频,独立上传)").option("--cover <图>", "可选封面图(仅图片,独立上传)").option("--track <名>", "曲名(叠加文字)").option("--artist <名>", "歌手名(叠加文字)").option("--resolution <WxH>", "输出分辨率(如 1080x1920;默认服务端 1920x1080)").option("--fps <n>", "帧率 30-60(默认服务端 30)").option("--c1 <hex>", "频谱主色十六进制(如 #ffffff;默认服务端白)").option("--c2 <hex>", "频谱第二色十六进制(给定则双色渐变)").option("--blur <n>", "背景模糊 0-40(默认服务端 16)").option("-o, --out <dir>", "产物目录(缺省 = <音频同目录>/<音频名>-visualizer-<时间戳>)").option("--param <k=v>", "透传任意云端参数(标量、可重复)", collectParam3, []).option("--params-json <json>", "透传任意云端参数(JSON 对象)").option("--reupload", "强制重新上传,忽略本地上传缓存").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (audio, opts) => {
|
|
11600
|
+
await runMusicVisualizer(audio, opts);
|
|
11601
|
+
});
|
|
11602
|
+
}
|
|
11603
|
+
async function runMusicVisualizer(audio, opts) {
|
|
11604
|
+
if (opts.json)
|
|
11605
|
+
routeLogsToStderr();
|
|
11606
|
+
const cfg = loadConfig();
|
|
11607
|
+
const audioAbs = resolve13(audio);
|
|
11608
|
+
assertExt(audioAbs, AUDIO_EXTS2, "音频");
|
|
11609
|
+
const template = opts.template == null ? "" : String(opts.template).trim();
|
|
11610
|
+
if (!template)
|
|
11611
|
+
throw new Error("--template 必填:请指定可视化模板 id(取值见云端 API 文档 / 服务端模板列表)");
|
|
11612
|
+
const styleFields = buildStyleFields(opts);
|
|
11613
|
+
const bgAbs = opts.background ? resolve13(opts.background) : undefined;
|
|
11614
|
+
if (bgAbs)
|
|
11615
|
+
assertExt(bgAbs, [...IMAGE_EXTS2, ...VIDEO_EXTS3], "背景素材");
|
|
11616
|
+
const coverAbs = opts.cover ? resolve13(opts.cover) : undefined;
|
|
11617
|
+
if (coverAbs)
|
|
11618
|
+
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
11619
|
+
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
11620
|
+
const projName = basename13(audioAbs, extname8(audioAbs));
|
|
11621
|
+
const outDir = resolve13(opts.out ?? join27(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
11622
|
+
log.step(`▶ 音乐可视化:${basename13(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
11623
|
+
let billingHint;
|
|
11624
|
+
try {
|
|
11625
|
+
billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
|
|
11626
|
+
} catch {
|
|
11627
|
+
billingHint = "实时价格暂不可用,以服务端结算为准";
|
|
11628
|
+
}
|
|
11629
|
+
process.stderr.write(`\x1B[33m⚠️ 计费提示:${billingHint}\x1B[0m
|
|
11630
|
+
`);
|
|
11631
|
+
let backgroundFileId;
|
|
11632
|
+
if (bgAbs) {
|
|
11633
|
+
log.step("① 上传背景素材…");
|
|
11634
|
+
backgroundFileId = (await uploadCached(cfg, bgAbs, { force: opts.reupload })).fileId;
|
|
11635
|
+
log.info(`背景 file_id = ${backgroundFileId}`);
|
|
11636
|
+
}
|
|
11637
|
+
let coverFileId;
|
|
11638
|
+
if (coverAbs) {
|
|
11639
|
+
log.step("① 上传封面图…");
|
|
11640
|
+
coverFileId = (await uploadCached(cfg, coverAbs, { force: opts.reupload })).fileId;
|
|
11641
|
+
log.info(`封面 file_id = ${coverFileId}`);
|
|
11642
|
+
}
|
|
11643
|
+
const buildPayload = (fid) => {
|
|
11644
|
+
const p = { file_id: fid, template_id: template };
|
|
11645
|
+
if (backgroundFileId)
|
|
11646
|
+
p.background_file_id = backgroundFileId;
|
|
11647
|
+
if (coverFileId)
|
|
11648
|
+
p.cover_file_id = coverFileId;
|
|
11649
|
+
Object.assign(p, styleFields);
|
|
11650
|
+
for (const [k, v] of Object.entries(extraParams)) {
|
|
11651
|
+
const cur = p[k];
|
|
11652
|
+
const bothObj = !!cur && !!v && typeof cur === "object" && typeof v === "object" && !Array.isArray(cur) && !Array.isArray(v);
|
|
11653
|
+
p[k] = bothObj ? { ...cur, ...v } : v;
|
|
11654
|
+
}
|
|
11655
|
+
return p;
|
|
11656
|
+
};
|
|
11657
|
+
const submitted = await uploadAndSubmitTask(cfg, audioAbs, TASK_TYPE4, buildPayload, {
|
|
11658
|
+
force: opts.reupload,
|
|
11659
|
+
onUploaded: (u) => {
|
|
11660
|
+
log.info(u.cached ? `命中上传缓存,复用主音频 file_id = ${u.fileId}` : `主音频 file_id = ${u.fileId}`);
|
|
11661
|
+
log.step("② 提交音乐可视化任务…");
|
|
11662
|
+
},
|
|
11663
|
+
onCacheInvalid: () => log.warn("缓存的 file_id 在云端已失效,重新上传后重试…")
|
|
11664
|
+
});
|
|
11665
|
+
const { taskId } = submitted;
|
|
11666
|
+
log.info(`task_id = ${taskId}`);
|
|
11667
|
+
await mkdir13(outDir, { recursive: true });
|
|
11668
|
+
await writeFile12(join27(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
|
|
11669
|
+
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
11670
|
+
const result = await pollTask(cfg, TASK_TYPE4, taskId, (status, progress) => {
|
|
11671
|
+
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
11672
|
+
});
|
|
11673
|
+
log.tickEnd();
|
|
11674
|
+
const out = result;
|
|
11675
|
+
const url = [out.download_url, out.video_download_url, out.url].find((u) => typeof u === "string" && !!u.trim());
|
|
11676
|
+
const files = [];
|
|
11677
|
+
const errors = {};
|
|
11678
|
+
if (url) {
|
|
11679
|
+
const dest = join27(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
11680
|
+
try {
|
|
11681
|
+
await downloadStream(url, dest);
|
|
11682
|
+
files.push(dest);
|
|
11683
|
+
} catch (e) {
|
|
11684
|
+
errors["output"] = e instanceof Error ? e.message : String(e);
|
|
11685
|
+
}
|
|
11686
|
+
} else {
|
|
11687
|
+
errors["output"] = "任务完成但未解析到成片下载链接(output_result 形态异常)";
|
|
11688
|
+
}
|
|
11689
|
+
const ok = files.length > 0 && Object.keys(errors).length === 0;
|
|
11690
|
+
const resultJson = {
|
|
11691
|
+
ok,
|
|
11692
|
+
command: "music-visualizer",
|
|
11693
|
+
taskType: TASK_TYPE4,
|
|
11694
|
+
taskId,
|
|
11695
|
+
fileId: submitted.fileId,
|
|
11696
|
+
outDir,
|
|
11697
|
+
files,
|
|
11698
|
+
...Object.keys(errors).length ? { errors } : {},
|
|
11699
|
+
finishedAt: new Date().toISOString()
|
|
11700
|
+
};
|
|
11701
|
+
await writeFile12(join27(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
11702
|
+
if (opts.json) {
|
|
11703
|
+
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
11704
|
+
`);
|
|
11705
|
+
}
|
|
11706
|
+
if (!ok) {
|
|
11707
|
+
log.err(`产物下载失败:${JSON.stringify(errors)}。可凭 task.json 的 taskId 稍后恢复。`);
|
|
11708
|
+
process.exitCode = 1;
|
|
11709
|
+
return;
|
|
11710
|
+
}
|
|
11711
|
+
log.ok(`完成。产物目录:${outDir}`);
|
|
11712
|
+
}
|
|
11713
|
+
|
|
10069
11714
|
// src/index.ts
|
|
10070
11715
|
try {
|
|
10071
11716
|
process.loadEnvFile?.();
|
|
10072
11717
|
} catch {}
|
|
10073
11718
|
migrateLegacyHome();
|
|
10074
|
-
var { version } = JSON.parse(readFileSync5(
|
|
11719
|
+
var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
|
|
10075
11720
|
var program2 = new Command;
|
|
10076
11721
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
10077
11722
|
registerInstall(program2);
|
|
@@ -10086,6 +11731,8 @@ registerSplit(program2);
|
|
|
10086
11731
|
registerMatrix(program2);
|
|
10087
11732
|
registerMg(program2);
|
|
10088
11733
|
registerTool(program2);
|
|
11734
|
+
registerTranscript(program2);
|
|
11735
|
+
registerMusicVisualizer(program2);
|
|
10089
11736
|
program2.parseAsync(process.argv).catch((e) => {
|
|
10090
11737
|
console.error(`
|
|
10091
11738
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|