@gitruck/cli 0.2.10 → 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/README.md +452 -400
- 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 +856 -102
- 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 +15 -4
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 = {
|
|
@@ -4285,40 +4286,247 @@ var SKILL_NAMES = [
|
|
|
4285
4286
|
"gtrk-ai-drama",
|
|
4286
4287
|
"gtrk-style-maker",
|
|
4287
4288
|
"gtrk-transcript",
|
|
4288
|
-
"gtrk-tools"
|
|
4289
|
+
"gtrk-tools",
|
|
4290
|
+
"gtrk-music-visualizer"
|
|
4289
4291
|
];
|
|
4290
|
-
|
|
4291
|
-
|
|
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) {
|
|
4292
4420
|
let allOk = true;
|
|
4293
4421
|
for (const name of SKILL_NAMES) {
|
|
4294
|
-
const
|
|
4295
|
-
if (!existsSync2(
|
|
4296
|
-
log.warn(`找不到打包的 skill 源:${
|
|
4422
|
+
const manifest = join2(source, name, "SKILL.md");
|
|
4423
|
+
if (!existsSync2(manifest)) {
|
|
4424
|
+
log.warn(`找不到打包的 skill 源:${manifest}(跳过 ${name},不影响命令行)`);
|
|
4297
4425
|
allOk = false;
|
|
4298
|
-
continue;
|
|
4299
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;
|
|
4300
4437
|
const dest = join2(destRoot, name);
|
|
4301
4438
|
try {
|
|
4302
4439
|
mkdirSync2(dest, { recursive: true });
|
|
4303
4440
|
cpSync2(src, dest, { recursive: true });
|
|
4304
|
-
log.ok(`已安装
|
|
4305
|
-
} catch (
|
|
4441
|
+
log.ok(`已安装 ${name}${targetLabel ? ` → ${targetLabel}` : ""}:${join2(dest, "SKILL.md")}`);
|
|
4442
|
+
} catch (error) {
|
|
4306
4443
|
allOk = false;
|
|
4307
|
-
log.warn(`skill 安装失败(${name},不影响命令行使用):${
|
|
4444
|
+
log.warn(`skill 安装失败(${name},不影响命令行使用):${error instanceof Error ? error.message : String(error)}`);
|
|
4308
4445
|
}
|
|
4309
4446
|
}
|
|
4310
|
-
|
|
4447
|
+
if (targetLabel) {
|
|
4448
|
+
log.info(`已写入 ${targetLabel};若当前会话未出现新 Skill,请重启或刷新该 Agent。`);
|
|
4449
|
+
} else {
|
|
4450
|
+
log.info("已写入自定义 skills 目录;具体调用入口以该 Agent 的界面为准。");
|
|
4451
|
+
}
|
|
4311
4452
|
return allOk;
|
|
4312
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
|
+
}
|
|
4313
4520
|
function registerSkills(program2) {
|
|
4314
|
-
const skills = program2.command("skills").description("
|
|
4315
|
-
skills.command("install").description("把 gtrk 全家框架 skill
|
|
4316
|
-
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;
|
|
4317
4525
|
});
|
|
4318
4526
|
}
|
|
4319
4527
|
|
|
4320
4528
|
// src/commands/init.ts
|
|
4321
|
-
import { join as join9, resolve as
|
|
4529
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
4322
4530
|
import { existsSync as existsSync8 } from "node:fs";
|
|
4323
4531
|
|
|
4324
4532
|
// src/lib/user-config.ts
|
|
@@ -4346,7 +4554,7 @@ function writeUserConfig(patch) {
|
|
|
4346
4554
|
}
|
|
4347
4555
|
|
|
4348
4556
|
// src/lib/jianying.ts
|
|
4349
|
-
import { join as join4, resolve } from "node:path";
|
|
4557
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
4350
4558
|
import { existsSync as existsSync4 } from "node:fs";
|
|
4351
4559
|
function probeJianyingDraftDir() {
|
|
4352
4560
|
const local = process.env.LOCALAPPDATA;
|
|
@@ -4360,7 +4568,7 @@ function probeJianyingDraftDir() {
|
|
|
4360
4568
|
}
|
|
4361
4569
|
function resolveJianyingDraftDir(opt) {
|
|
4362
4570
|
if (opt && opt !== "auto")
|
|
4363
|
-
return
|
|
4571
|
+
return resolve2(opt);
|
|
4364
4572
|
const saved = readUserConfig().jianyingDraftDir;
|
|
4365
4573
|
if (saved && existsSync4(saved))
|
|
4366
4574
|
return saved;
|
|
@@ -4387,7 +4595,7 @@ function launch(cmd) {
|
|
|
4387
4595
|
|
|
4388
4596
|
// src/lib/prompt.ts
|
|
4389
4597
|
import { stdin, stdout } from "node:process";
|
|
4390
|
-
import { spawnSync } from "node:child_process";
|
|
4598
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4391
4599
|
var c2 = {
|
|
4392
4600
|
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
4393
4601
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
@@ -4395,24 +4603,24 @@ var c2 = {
|
|
|
4395
4603
|
function readClipboard() {
|
|
4396
4604
|
try {
|
|
4397
4605
|
if (process.platform === "win32") {
|
|
4398
|
-
const r =
|
|
4606
|
+
const r = spawnSync2("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
|
|
4399
4607
|
encoding: "utf8"
|
|
4400
4608
|
});
|
|
4401
4609
|
return (r.stdout ?? "").replace(/\r?\n$/, "");
|
|
4402
4610
|
}
|
|
4403
4611
|
if (process.platform === "darwin") {
|
|
4404
|
-
return
|
|
4612
|
+
return spawnSync2("pbpaste", { encoding: "utf8" }).stdout ?? "";
|
|
4405
4613
|
}
|
|
4406
|
-
const x =
|
|
4614
|
+
const x = spawnSync2("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
|
|
4407
4615
|
if (x.status === 0)
|
|
4408
4616
|
return x.stdout ?? "";
|
|
4409
|
-
return
|
|
4617
|
+
return spawnSync2("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
|
|
4410
4618
|
} catch {
|
|
4411
4619
|
return "";
|
|
4412
4620
|
}
|
|
4413
4621
|
}
|
|
4414
4622
|
function ask(message, opts = {}) {
|
|
4415
|
-
return new Promise((
|
|
4623
|
+
return new Promise((resolve3) => {
|
|
4416
4624
|
const hint = opts.defaultValue ? c2.dim(` (${opts.defaultValue})`) : "";
|
|
4417
4625
|
stdout.write(`${c2.cyan("?")} ${message}${hint} `);
|
|
4418
4626
|
let value = "";
|
|
@@ -4433,7 +4641,7 @@ function ask(message, opts = {}) {
|
|
|
4433
4641
|
stdout.write(`
|
|
4434
4642
|
`);
|
|
4435
4643
|
cleanup();
|
|
4436
|
-
|
|
4644
|
+
resolve3(value || opts.defaultValue || "");
|
|
4437
4645
|
return;
|
|
4438
4646
|
}
|
|
4439
4647
|
if (ch === "\x03") {
|
|
@@ -5140,7 +5348,7 @@ function effectiveVocab(config) {
|
|
|
5140
5348
|
}
|
|
5141
5349
|
|
|
5142
5350
|
// src/lib/ffmpeg.ts
|
|
5143
|
-
import { spawn as spawn2, spawnSync as
|
|
5351
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
5144
5352
|
import { existsSync as existsSync6 } from "node:fs";
|
|
5145
5353
|
import { join as join6 } from "node:path";
|
|
5146
5354
|
var isWin = process.platform === "win32";
|
|
@@ -5149,7 +5357,7 @@ var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpe
|
|
|
5149
5357
|
var _cache = new Map;
|
|
5150
5358
|
function onSystemPath(cmd) {
|
|
5151
5359
|
try {
|
|
5152
|
-
return
|
|
5360
|
+
return spawnSync3(cmd, ["-version"], { stdio: "ignore" }).status === 0;
|
|
5153
5361
|
} catch {
|
|
5154
5362
|
return false;
|
|
5155
5363
|
}
|
|
@@ -5184,14 +5392,14 @@ function requireFfmpeg(ffmpegPath) {
|
|
|
5184
5392
|
return r;
|
|
5185
5393
|
}
|
|
5186
5394
|
function ffprobeJson(ffprobePath, args) {
|
|
5187
|
-
const r =
|
|
5395
|
+
const r = spawnSync3(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
5188
5396
|
if (r.status !== 0) {
|
|
5189
5397
|
throw new Error(`ffprobe 失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
|
|
5190
5398
|
}
|
|
5191
5399
|
return JSON.parse(r.stdout || "{}");
|
|
5192
5400
|
}
|
|
5193
5401
|
function runFfmpeg(ffmpegPath, args, onLine) {
|
|
5194
|
-
return new Promise((
|
|
5402
|
+
return new Promise((resolve3, reject) => {
|
|
5195
5403
|
const p = spawn2(ffmpegPath, args, { env: process.env });
|
|
5196
5404
|
let tail = "";
|
|
5197
5405
|
p.stderr.on("data", (buf) => {
|
|
@@ -5206,20 +5414,20 @@ function runFfmpeg(ffmpegPath, args, onLine) {
|
|
|
5206
5414
|
p.on("error", (e) => reject(e));
|
|
5207
5415
|
p.on("close", (code) => {
|
|
5208
5416
|
if (code === 0)
|
|
5209
|
-
|
|
5417
|
+
resolve3();
|
|
5210
5418
|
else
|
|
5211
5419
|
reject(new Error(`ffmpeg 退出码 ${code}:${tail.slice(-600)}`));
|
|
5212
5420
|
});
|
|
5213
5421
|
});
|
|
5214
5422
|
}
|
|
5215
5423
|
function probeCapabilities(res) {
|
|
5216
|
-
const ver =
|
|
5424
|
+
const ver = spawnSync3(res.ffmpeg, ["-version"], { encoding: "utf8" });
|
|
5217
5425
|
const version = (ver.stdout || "").split(/\r?\n/)[0]?.trim() || "unknown";
|
|
5218
|
-
const enc =
|
|
5426
|
+
const enc = spawnSync3(res.ffmpeg, ["-hide_banner", "-encoders"], {
|
|
5219
5427
|
encoding: "utf8",
|
|
5220
5428
|
maxBuffer: 16 * 1024 * 1024
|
|
5221
5429
|
});
|
|
5222
|
-
const fil =
|
|
5430
|
+
const fil = spawnSync3(res.ffmpeg, ["-hide_banner", "-filters"], {
|
|
5223
5431
|
encoding: "utf8",
|
|
5224
5432
|
maxBuffer: 16 * 1024 * 1024
|
|
5225
5433
|
});
|
|
@@ -5453,7 +5661,7 @@ async function runInit(opts) {
|
|
|
5453
5661
|
const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
|
|
5454
5662
|
if (manual) {
|
|
5455
5663
|
if (existsSync8(manual))
|
|
5456
|
-
jianyingDraftDir =
|
|
5664
|
+
jianyingDraftDir = resolve3(manual);
|
|
5457
5665
|
else
|
|
5458
5666
|
log.warn(`目录不存在,已跳过:${manual}`);
|
|
5459
5667
|
}
|
|
@@ -5476,7 +5684,7 @@ async function afterConfigDoctor() {
|
|
|
5476
5684
|
if (healthy) {
|
|
5477
5685
|
log.step("装好了!两种用法任选:");
|
|
5478
5686
|
log.info('① 命令行直接剪:gtrk oralcut "<毛片.mp4>" --script "<文字稿.txt>"(无稿就别加 --script)');
|
|
5479
|
-
log.info("②
|
|
5687
|
+
log.info("② 刷新你常用的 AI Agent,在它的 Skills 入口选择或点名 gtrk-oralcut;不同客户端也都可以直接描述剪辑需求触发。");
|
|
5480
5688
|
log.info("想有自己栏目的风格体系?在 agent 里跑 /gtrk-style-maker 建一次「你的厨房」(skill 家族 + 栏目配置);不建就直接用默认,照常开剪。");
|
|
5481
5689
|
} else {
|
|
5482
5690
|
log.warn("上面体检有项没通,按提示处理好再开剪(多半是 API Key 或剪映目录)。");
|
|
@@ -5494,7 +5702,7 @@ async function runInitNonInteractive(opts) {
|
|
|
5494
5702
|
let jianyingDraftDir;
|
|
5495
5703
|
const dirOpt = opts.jianyingDraftDir;
|
|
5496
5704
|
if (dirOpt && dirOpt !== "auto")
|
|
5497
|
-
jianyingDraftDir =
|
|
5705
|
+
jianyingDraftDir = resolve3(dirOpt);
|
|
5498
5706
|
else if (dirOpt === "auto" || !existing.jianyingDraftDir)
|
|
5499
5707
|
jianyingDraftDir = probeJianyingDraftDir();
|
|
5500
5708
|
else
|
|
@@ -5510,16 +5718,23 @@ async function runInitNonInteractive(opts) {
|
|
|
5510
5718
|
|
|
5511
5719
|
// src/commands/install.ts
|
|
5512
5720
|
function registerInstall(program2) {
|
|
5513
|
-
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) => {
|
|
5514
5722
|
log.step("① 安装 / 刷新 agent skill…");
|
|
5515
|
-
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;
|
|
5516
5731
|
log.step("② 配置 + 体检…");
|
|
5517
5732
|
await runInit(opts);
|
|
5518
5733
|
});
|
|
5519
5734
|
}
|
|
5520
5735
|
|
|
5521
5736
|
// src/commands/oralcut.ts
|
|
5522
|
-
import { resolve as
|
|
5737
|
+
import { resolve as resolve4, join as join14, dirname as dirname3, basename as basename5, extname as extname2 } from "node:path";
|
|
5523
5738
|
import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
|
|
5524
5739
|
import { existsSync as existsSync12 } from "node:fs";
|
|
5525
5740
|
|
|
@@ -6007,7 +6222,7 @@ var defaultDeps = {
|
|
|
6007
6222
|
uploadCached,
|
|
6008
6223
|
invalidateUpload,
|
|
6009
6224
|
submitTask,
|
|
6010
|
-
sleep: (ms) => new Promise((
|
|
6225
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
|
|
6011
6226
|
};
|
|
6012
6227
|
async function submitFreshFile(cfg, taskType, fileId, buildPayload, backoffMs, deps) {
|
|
6013
6228
|
for (let attempt = 0;; attempt++) {
|
|
@@ -6532,7 +6747,7 @@ async function runOralCut(input, opts) {
|
|
|
6532
6747
|
if (opts.json)
|
|
6533
6748
|
routeLogsToStderr();
|
|
6534
6749
|
const cfg = loadConfig();
|
|
6535
|
-
const inputAbs =
|
|
6750
|
+
const inputAbs = resolve4(input);
|
|
6536
6751
|
if (!existsSync12(inputAbs))
|
|
6537
6752
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
6538
6753
|
const projName = basename5(inputAbs, extname2(inputAbs));
|
|
@@ -6540,10 +6755,10 @@ async function runOralCut(input, opts) {
|
|
|
6540
6755
|
if (opts.render && !formats.includes("gtrk"))
|
|
6541
6756
|
formats.push("gtrk");
|
|
6542
6757
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
6543
|
-
const outDir =
|
|
6544
|
-
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;
|
|
6545
6760
|
if (!scriptPath) {
|
|
6546
|
-
const sibling = join14(
|
|
6761
|
+
const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
|
|
6547
6762
|
if (existsSync12(sibling)) {
|
|
6548
6763
|
scriptPath = sibling;
|
|
6549
6764
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
@@ -6629,7 +6844,7 @@ async function runOralCut(input, opts) {
|
|
|
6629
6844
|
}
|
|
6630
6845
|
|
|
6631
6846
|
// src/commands/oralcut-result.ts
|
|
6632
|
-
import { resolve as
|
|
6847
|
+
import { resolve as resolve5, join as join15 } from "node:path";
|
|
6633
6848
|
var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
|
|
6634
6849
|
function timestamp2() {
|
|
6635
6850
|
const d = new Date;
|
|
@@ -6663,7 +6878,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
6663
6878
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
6664
6879
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
6665
6880
|
}
|
|
6666
|
-
const outDir =
|
|
6881
|
+
const outDir = resolve5(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
|
|
6667
6882
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
6668
6883
|
await materializeResult({
|
|
6669
6884
|
outDir,
|
|
@@ -6681,10 +6896,10 @@ async function runOralCutResult(taskId, opts) {
|
|
|
6681
6896
|
}
|
|
6682
6897
|
|
|
6683
6898
|
// src/commands/upgrade.ts
|
|
6684
|
-
import { spawnSync as
|
|
6899
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
6685
6900
|
var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
|
|
6686
6901
|
function run(cmd) {
|
|
6687
|
-
const r =
|
|
6902
|
+
const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
|
|
6688
6903
|
return r.status ?? 1;
|
|
6689
6904
|
}
|
|
6690
6905
|
function registerUpgrade(program2) {
|
|
@@ -6713,7 +6928,7 @@ function registerUpgrade(program2) {
|
|
|
6713
6928
|
process.exitCode = 1;
|
|
6714
6929
|
return;
|
|
6715
6930
|
}
|
|
6716
|
-
log.step("②
|
|
6931
|
+
log.step("② 通过通用适配器刷新 Agent Skills…");
|
|
6717
6932
|
if (run("gtrk skills install") !== 0) {
|
|
6718
6933
|
log.warn("skill 没刷成,手动跑一次:gtrk skills install");
|
|
6719
6934
|
}
|
|
@@ -6723,16 +6938,16 @@ function registerUpgrade(program2) {
|
|
|
6723
6938
|
}
|
|
6724
6939
|
|
|
6725
6940
|
// src/commands/render.ts
|
|
6726
|
-
import { resolve as
|
|
6941
|
+
import { resolve as resolve6, dirname as dirname4, join as join16, basename as basename6, extname as extname3 } from "node:path";
|
|
6727
6942
|
import { existsSync as existsSync13 } from "node:fs";
|
|
6728
6943
|
function registerRender(program2) {
|
|
6729
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) => {
|
|
6730
6945
|
if (opts.json)
|
|
6731
6946
|
routeLogsToStderr();
|
|
6732
|
-
const gtrkAbs =
|
|
6947
|
+
const gtrkAbs = resolve6(gtrk);
|
|
6733
6948
|
if (!existsSync13(gtrkAbs))
|
|
6734
6949
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
6735
|
-
const outMp4 =
|
|
6950
|
+
const outMp4 = resolve6(opts.out ?? join16(dirname4(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
|
|
6736
6951
|
log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
|
|
6737
6952
|
const project = await readGtrkFile(gtrkAbs);
|
|
6738
6953
|
const result = await renderGtrk(project, outMp4, {
|
|
@@ -6748,7 +6963,7 @@ function registerRender(program2) {
|
|
|
6748
6963
|
log.tickEnd();
|
|
6749
6964
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
6750
6965
|
if (opts.open)
|
|
6751
|
-
openFolder(
|
|
6966
|
+
openFolder(dirname4(outMp4));
|
|
6752
6967
|
if (opts.json) {
|
|
6753
6968
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
6754
6969
|
}
|
|
@@ -6756,7 +6971,7 @@ function registerRender(program2) {
|
|
|
6756
6971
|
}
|
|
6757
6972
|
|
|
6758
6973
|
// src/commands/split.ts
|
|
6759
|
-
import { resolve as
|
|
6974
|
+
import { resolve as resolve7, join as join18, dirname as dirname6, basename as basename8 } from "node:path";
|
|
6760
6975
|
import { existsSync as existsSync14 } from "node:fs";
|
|
6761
6976
|
import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
|
|
6762
6977
|
import { createHash } from "node:crypto";
|
|
@@ -6881,7 +7096,7 @@ function projectTranscript(transcript, gtrk, opts = {}) {
|
|
|
6881
7096
|
|
|
6882
7097
|
// src/lib/gtrk-writeback.ts
|
|
6883
7098
|
import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
6884
|
-
import { dirname as
|
|
7099
|
+
import { dirname as dirname5, join as join17, basename as basename7 } from "node:path";
|
|
6885
7100
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6886
7101
|
function readGtrk(path) {
|
|
6887
7102
|
const raw = readFileSync4(path, "utf8");
|
|
@@ -6908,7 +7123,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
6908
7123
|
}
|
|
6909
7124
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
6910
7125
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
6911
|
-
const tmp = join17(
|
|
7126
|
+
const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6912
7127
|
try {
|
|
6913
7128
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6914
7129
|
renameSync(tmp, path);
|
|
@@ -6924,7 +7139,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
6924
7139
|
if (cur !== expectedMtimeMs) {
|
|
6925
7140
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
6926
7141
|
}
|
|
6927
|
-
const tmp = join17(
|
|
7142
|
+
const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6928
7143
|
try {
|
|
6929
7144
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6930
7145
|
renameSync(tmp, path);
|
|
@@ -6947,10 +7162,10 @@ function firstExisting(cands) {
|
|
|
6947
7162
|
return cands.find((p) => existsSync14(p));
|
|
6948
7163
|
}
|
|
6949
7164
|
function resolvePaths(opts) {
|
|
6950
|
-
const project = opts.project ?
|
|
7165
|
+
const project = opts.project ? resolve7(opts.project) : undefined;
|
|
6951
7166
|
let gtrkPath;
|
|
6952
7167
|
if (opts.gtrk) {
|
|
6953
|
-
gtrkPath =
|
|
7168
|
+
gtrkPath = resolve7(opts.gtrk);
|
|
6954
7169
|
} else if (project) {
|
|
6955
7170
|
gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
|
|
6956
7171
|
} else {
|
|
@@ -6960,14 +7175,14 @@ function resolvePaths(opts) {
|
|
|
6960
7175
|
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
6961
7176
|
let transcriptPath;
|
|
6962
7177
|
if (opts.transcript)
|
|
6963
|
-
transcriptPath =
|
|
7178
|
+
transcriptPath = resolve7(opts.transcript);
|
|
6964
7179
|
else if (project)
|
|
6965
7180
|
transcriptPath = firstExisting([
|
|
6966
7181
|
join18(project, "transcript", "transcript.json"),
|
|
6967
7182
|
join18(project, "json", "transcript.json"),
|
|
6968
7183
|
join18(project, "transcript.json")
|
|
6969
7184
|
]);
|
|
6970
|
-
const baseDir = project ??
|
|
7185
|
+
const baseDir = project ?? dirname6(gtrkPath);
|
|
6971
7186
|
return { baseDir, gtrkPath, transcriptPath };
|
|
6972
7187
|
}
|
|
6973
7188
|
function slugify(name) {
|
|
@@ -6987,7 +7202,7 @@ async function runSplit(splitdoc, opts) {
|
|
|
6987
7202
|
if (opts.json)
|
|
6988
7203
|
routeLogsToStderr();
|
|
6989
7204
|
const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
|
|
6990
|
-
return splitdoc ? runLand(
|
|
7205
|
+
return splitdoc ? runLand(resolve7(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
6991
7206
|
}
|
|
6992
7207
|
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
6993
7208
|
if (!transcriptPath || !existsSync14(transcriptPath))
|
|
@@ -7093,7 +7308,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
7093
7308
|
}
|
|
7094
7309
|
|
|
7095
7310
|
// src/commands/matrix.ts
|
|
7096
|
-
import { resolve as
|
|
7311
|
+
import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
|
|
7097
7312
|
import { existsSync as existsSync15 } from "node:fs";
|
|
7098
7313
|
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
|
|
7099
7314
|
|
|
@@ -7632,10 +7847,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
7632
7847
|
let dispatchPath;
|
|
7633
7848
|
let baseDir;
|
|
7634
7849
|
if (opts.dispatch) {
|
|
7635
|
-
dispatchPath =
|
|
7636
|
-
baseDir =
|
|
7850
|
+
dispatchPath = resolve8(opts.dispatch);
|
|
7851
|
+
baseDir = dirname7(dirname7(dispatchPath));
|
|
7637
7852
|
} else if (opts.project) {
|
|
7638
|
-
baseDir =
|
|
7853
|
+
baseDir = resolve8(opts.project);
|
|
7639
7854
|
dispatchPath = join19(baseDir, "split", "dispatch.json");
|
|
7640
7855
|
} else {
|
|
7641
7856
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
@@ -7740,7 +7955,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
|
|
|
7740
7955
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
7741
7956
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
7742
7957
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
7743
|
-
const gtrkDir =
|
|
7958
|
+
const gtrkDir = dirname7(gtrkPath);
|
|
7744
7959
|
const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
7745
7960
|
await mkdir6(previewDir, { recursive: true });
|
|
7746
7961
|
const prevSource = new Map;
|
|
@@ -7853,7 +8068,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
|
7853
8068
|
counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
|
|
7854
8069
|
};
|
|
7855
8070
|
if (opts.out) {
|
|
7856
|
-
const outPath =
|
|
8071
|
+
const outPath = resolve8(opts.out);
|
|
7857
8072
|
await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
7858
8073
|
log.ok(`结果已落盘:${outPath}`);
|
|
7859
8074
|
result.outPath = outPath;
|
|
@@ -7873,7 +8088,7 @@ function slugify2(name) {
|
|
|
7873
8088
|
}
|
|
7874
8089
|
|
|
7875
8090
|
// src/commands/mg.ts
|
|
7876
|
-
import { resolve as
|
|
8091
|
+
import { resolve as resolve9, join as join20, dirname as dirname8, basename as basename10 } from "node:path";
|
|
7877
8092
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7878
8093
|
import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
|
|
7879
8094
|
|
|
@@ -8096,11 +8311,11 @@ async function runMg(words, opts) {
|
|
|
8096
8311
|
}
|
|
8097
8312
|
function resolveDispatch(opts) {
|
|
8098
8313
|
if (opts.dispatch) {
|
|
8099
|
-
const dispatchPath =
|
|
8100
|
-
return { dispatchPath, baseDir:
|
|
8314
|
+
const dispatchPath = resolve9(opts.dispatch);
|
|
8315
|
+
return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
|
|
8101
8316
|
}
|
|
8102
8317
|
if (opts.project) {
|
|
8103
|
-
const baseDir =
|
|
8318
|
+
const baseDir = resolve9(opts.project);
|
|
8104
8319
|
return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
|
|
8105
8320
|
}
|
|
8106
8321
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
@@ -8177,7 +8392,7 @@ async function runLay(opts) {
|
|
|
8177
8392
|
}
|
|
8178
8393
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
8179
8394
|
assertGtrkV1(gtrk);
|
|
8180
|
-
const gtrkDir =
|
|
8395
|
+
const gtrkDir = dirname8(gtrkPath);
|
|
8181
8396
|
await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
8182
8397
|
for (const it of items) {
|
|
8183
8398
|
await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
|
|
@@ -8198,10 +8413,10 @@ async function runLint(args, opts) {
|
|
|
8198
8413
|
const file = args[0];
|
|
8199
8414
|
if (!file)
|
|
8200
8415
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
8201
|
-
const html = await readFile6(
|
|
8416
|
+
const html = await readFile6(resolve9(file), "utf8");
|
|
8202
8417
|
let dispatchIds;
|
|
8203
|
-
if (opts.dispatch && existsSync16(
|
|
8204
|
-
dispatchIds = (await readMgQueue(
|
|
8418
|
+
if (opts.dispatch && existsSync16(resolve9(opts.dispatch))) {
|
|
8419
|
+
dispatchIds = (await readMgQueue(resolve9(opts.dispatch))).map((q) => q.composition_id);
|
|
8205
8420
|
}
|
|
8206
8421
|
const lint = lintParticle(html, { dispatchIds });
|
|
8207
8422
|
for (const vv of lint.violations)
|
|
@@ -8854,6 +9069,308 @@ var audioSilenceRemove = {
|
|
|
8854
9069
|
return url ? [{ url, filename: `${ctx.baseName}-desilence${extFromUrl(url, ".wav")}` }] : [];
|
|
8855
9070
|
}
|
|
8856
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
|
+
};
|
|
8857
9374
|
var mad = {
|
|
8858
9375
|
name: "mad",
|
|
8859
9376
|
title: "一键剪 MAD",
|
|
@@ -8889,6 +9406,16 @@ var TOOL_REGISTRY = [
|
|
|
8889
9406
|
audioSeparation,
|
|
8890
9407
|
audioNoiseReduce,
|
|
8891
9408
|
audioSilenceRemove,
|
|
9409
|
+
videoSegment,
|
|
9410
|
+
videoAiSegment,
|
|
9411
|
+
videoMotionCut,
|
|
9412
|
+
audioSpeakerSplit,
|
|
9413
|
+
audioStretch,
|
|
9414
|
+
pianoAudioToMidi,
|
|
9415
|
+
pianoAudioEnhance,
|
|
9416
|
+
imageToSquare,
|
|
9417
|
+
imageToLive,
|
|
9418
|
+
videoAiSubtitle,
|
|
8892
9419
|
mad
|
|
8893
9420
|
];
|
|
8894
9421
|
function findTool(name, registry = TOOL_REGISTRY) {
|
|
@@ -8910,11 +9437,14 @@ function validateRegistry(registry = TOOL_REGISTRY) {
|
|
|
8910
9437
|
throw new Error(`cloud 型工具缺 taskType:「${d.name}」`);
|
|
8911
9438
|
if (d.kind === "cloud" && !d.priceKey)
|
|
8912
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
|
+
}
|
|
8913
9443
|
}
|
|
8914
9444
|
}
|
|
8915
9445
|
|
|
8916
9446
|
// src/lib/tool-runner.ts
|
|
8917
|
-
import { resolve as
|
|
9447
|
+
import { resolve as resolve10, join as join21, dirname as dirname9, basename as basename11, extname as extname5 } from "node:path";
|
|
8918
9448
|
import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
|
|
8919
9449
|
import { createWriteStream, existsSync as existsSync17 } from "node:fs";
|
|
8920
9450
|
import { Readable as Readable2 } from "node:stream";
|
|
@@ -9113,10 +9643,10 @@ function timestamp3() {
|
|
|
9113
9643
|
}
|
|
9114
9644
|
function resolveOutDir(descriptor, inputAbs, out) {
|
|
9115
9645
|
if (out)
|
|
9116
|
-
return
|
|
9646
|
+
return resolve10(out);
|
|
9117
9647
|
if (inputAbs) {
|
|
9118
9648
|
const base = basename11(inputAbs, extname5(inputAbs));
|
|
9119
|
-
return join21(
|
|
9649
|
+
return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
|
|
9120
9650
|
}
|
|
9121
9651
|
return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
|
|
9122
9652
|
}
|
|
@@ -9133,7 +9663,7 @@ function emitBilling(hint) {
|
|
|
9133
9663
|
`);
|
|
9134
9664
|
}
|
|
9135
9665
|
async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
9136
|
-
const inputAbs = inputArg ?
|
|
9666
|
+
const inputAbs = inputArg ? resolve10(inputArg) : undefined;
|
|
9137
9667
|
const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
|
|
9138
9668
|
validateToolInput(descriptor, inputAbs);
|
|
9139
9669
|
const probe = deps.probeDurationSec ?? probeDuration;
|
|
@@ -9171,7 +9701,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9171
9701
|
uploadCached: deps.uploadCached,
|
|
9172
9702
|
invalidateUpload: deps.invalidateUpload,
|
|
9173
9703
|
submitTask: deps.submitTask,
|
|
9174
|
-
sleep: deps.sleep ?? ((ms) => new Promise((
|
|
9704
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
|
|
9175
9705
|
});
|
|
9176
9706
|
const { taskId } = submitted;
|
|
9177
9707
|
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
@@ -9185,11 +9715,10 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9185
9715
|
sleep: deps.sleep,
|
|
9186
9716
|
now: deps.now
|
|
9187
9717
|
});
|
|
9188
|
-
const
|
|
9718
|
+
const outputResult = output;
|
|
9189
9719
|
const files = [];
|
|
9190
9720
|
const errors = {};
|
|
9191
|
-
|
|
9192
|
-
errors["output"] = "任务完成但未解析到产物下载链接(output_result 形态异常)";
|
|
9721
|
+
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
9193
9722
|
for (const it of items) {
|
|
9194
9723
|
const dest = join21(outDir, it.filename);
|
|
9195
9724
|
try {
|
|
@@ -9199,7 +9728,16 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9199
9728
|
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
9200
9729
|
}
|
|
9201
9730
|
}
|
|
9202
|
-
|
|
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);
|
|
9203
9741
|
const result = {
|
|
9204
9742
|
ok,
|
|
9205
9743
|
tool: descriptor.name,
|
|
@@ -9208,6 +9746,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9208
9746
|
fileId: up.fileId,
|
|
9209
9747
|
outDir,
|
|
9210
9748
|
files,
|
|
9749
|
+
...resultFile ? { resultFile } : {},
|
|
9211
9750
|
...Object.keys(errors).length ? { errors } : {}
|
|
9212
9751
|
};
|
|
9213
9752
|
await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
@@ -9217,7 +9756,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9217
9756
|
// src/lib/mad/mad.ts
|
|
9218
9757
|
import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
|
|
9219
9758
|
import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
|
|
9220
|
-
import { resolve as
|
|
9759
|
+
import { resolve as resolve11, join as join25 } from "node:path";
|
|
9221
9760
|
|
|
9222
9761
|
// src/lib/convert/types.ts
|
|
9223
9762
|
function num(v, d = 0) {
|
|
@@ -10402,7 +10941,7 @@ async function analyzeBgm(cfg, bgmAbs, deps) {
|
|
|
10402
10941
|
uploadCached: deps.uploadCached,
|
|
10403
10942
|
invalidateUpload: deps.invalidateUpload,
|
|
10404
10943
|
submitTask: deps.submitTask,
|
|
10405
|
-
sleep: deps.sleep ?? ((ms) => new Promise((
|
|
10944
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
|
|
10406
10945
|
});
|
|
10407
10946
|
const { taskId } = submitted;
|
|
10408
10947
|
const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
|
|
@@ -10463,7 +11002,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
10463
11002
|
const probeDur = deps.probeDurationFn ?? probeDuration;
|
|
10464
11003
|
if (!inputArg)
|
|
10465
11004
|
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
10466
|
-
const dirAbs =
|
|
11005
|
+
const dirAbs = resolve11(inputArg);
|
|
10467
11006
|
if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
10468
11007
|
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
10469
11008
|
}
|
|
@@ -10495,7 +11034,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
10495
11034
|
let level = 3;
|
|
10496
11035
|
let analysis = null;
|
|
10497
11036
|
let bgmForTrack;
|
|
10498
|
-
const bgmAbs = opts.bgm ?
|
|
11037
|
+
const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
|
|
10499
11038
|
if (bgmAbs) {
|
|
10500
11039
|
let dur = -1;
|
|
10501
11040
|
try {
|
|
@@ -10578,7 +11117,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
10578
11117
|
const header = madHeader(version, now.toISOString());
|
|
10579
11118
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
10580
11119
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
10581
|
-
const outDir = opts.out ?
|
|
11120
|
+
const outDir = opts.out ? resolve11(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
|
|
10582
11121
|
await mkdir11(outDir, { recursive: true });
|
|
10583
11122
|
const jsxPath = join25(outDir, "mad.jsx");
|
|
10584
11123
|
await writeFile10(jsxPath, jsx);
|
|
@@ -10726,7 +11265,7 @@ async function runMadInTool(inputArg, opts) {
|
|
|
10726
11265
|
// src/commands/transcript.ts
|
|
10727
11266
|
import { existsSync as existsSync21 } from "node:fs";
|
|
10728
11267
|
import { mkdir as mkdir12, rename as rename2, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
|
|
10729
|
-
import { basename as basename12, dirname as
|
|
11268
|
+
import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
|
|
10730
11269
|
|
|
10731
11270
|
// src/lib/transcript.ts
|
|
10732
11271
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -10847,7 +11386,7 @@ function buildDeps(overrides = {}) {
|
|
|
10847
11386
|
upload: overrides.upload ?? uploadCached,
|
|
10848
11387
|
invalidate: overrides.invalidate ?? invalidateUpload,
|
|
10849
11388
|
submit: overrides.submit ?? submitTask,
|
|
10850
|
-
sleep: overrides.sleep ?? ((ms) => new Promise((
|
|
11389
|
+
sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
|
|
10851
11390
|
poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
|
|
10852
11391
|
writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
|
|
10853
11392
|
now: overrides.now ?? (() => new Date)
|
|
@@ -10862,7 +11401,7 @@ async function validateTranscriptInput(input) {
|
|
|
10862
11401
|
if (looksLikeRemote(input)) {
|
|
10863
11402
|
throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
|
|
10864
11403
|
}
|
|
10865
|
-
const inputAbs =
|
|
11404
|
+
const inputAbs = resolve12(input);
|
|
10866
11405
|
if (!existsSync21(inputAbs))
|
|
10867
11406
|
throw new Error(`本地视频不存在:${inputAbs}`);
|
|
10868
11407
|
const info = await stat6(inputAbs);
|
|
@@ -10876,13 +11415,13 @@ async function validateTranscriptInput(input) {
|
|
|
10876
11415
|
}
|
|
10877
11416
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
10878
11417
|
const base = basename12(inputAbs, extname7(inputAbs));
|
|
10879
|
-
const output = out ?
|
|
11418
|
+
const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
|
|
10880
11419
|
if (extname7(output).toLowerCase() !== ".md")
|
|
10881
11420
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
10882
11421
|
return output;
|
|
10883
11422
|
}
|
|
10884
11423
|
async function writeMarkdownAtomic(path, markdown) {
|
|
10885
|
-
await mkdir12(
|
|
11424
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
10886
11425
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
10887
11426
|
try {
|
|
10888
11427
|
await writeFile11(temp, markdown, "utf8");
|
|
@@ -10958,12 +11497,226 @@ function registerTranscript(program2) {
|
|
|
10958
11497
|
configureTranscriptCommand(program2.command("transcript <video>"));
|
|
10959
11498
|
}
|
|
10960
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
|
+
|
|
10961
11714
|
// src/index.ts
|
|
10962
11715
|
try {
|
|
10963
11716
|
process.loadEnvFile?.();
|
|
10964
11717
|
} catch {}
|
|
10965
11718
|
migrateLegacyHome();
|
|
10966
|
-
var { version } = JSON.parse(readFileSync5(
|
|
11719
|
+
var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
|
|
10967
11720
|
var program2 = new Command;
|
|
10968
11721
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
10969
11722
|
registerInstall(program2);
|
|
@@ -10979,6 +11732,7 @@ registerMatrix(program2);
|
|
|
10979
11732
|
registerMg(program2);
|
|
10980
11733
|
registerTool(program2);
|
|
10981
11734
|
registerTranscript(program2);
|
|
11735
|
+
registerMusicVisualizer(program2);
|
|
10982
11736
|
program2.parseAsync(process.argv).catch((e) => {
|
|
10983
11737
|
console.error(`
|
|
10984
11738
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|