@gitruck/cli 0.2.22 → 1.0.0
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 +14 -0
- package/README.md +534 -524
- package/dist/index.js +995 -221
- package/package.json +2 -2
- package/skills/gtrk-long2short/SKILL.md +68 -63
- package/skills/gtrk-tools/SKILL.md +22 -1
package/dist/index.js
CHANGED
|
@@ -4197,7 +4197,7 @@ var {
|
|
|
4197
4197
|
|
|
4198
4198
|
// src/index.ts
|
|
4199
4199
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
4200
|
-
import { join as
|
|
4200
|
+
import { join as join33 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4230,6 +4230,12 @@ function ffmpegDir() {
|
|
|
4230
4230
|
function audioCacheDir() {
|
|
4231
4231
|
return join(GITRUCK_HOME, "audio-cache");
|
|
4232
4232
|
}
|
|
4233
|
+
function fontsDir() {
|
|
4234
|
+
return join(GITRUCK_HOME, "fonts");
|
|
4235
|
+
}
|
|
4236
|
+
function tmpDir() {
|
|
4237
|
+
return join(GITRUCK_HOME, "tmp");
|
|
4238
|
+
}
|
|
4233
4239
|
var _migrated = false;
|
|
4234
4240
|
function migrateLegacyHome() {
|
|
4235
4241
|
if (_migrated)
|
|
@@ -5819,7 +5825,8 @@ import { existsSync as existsSync7 } from "node:fs";
|
|
|
5819
5825
|
import { join as join7 } from "node:path";
|
|
5820
5826
|
var isWin = process.platform === "win32";
|
|
5821
5827
|
var bin = (base) => isWin ? `${base}.exe` : base;
|
|
5822
|
-
var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe
|
|
5828
|
+
var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。装它:gtrk deps install --ffmpeg` + `(从同合云镜像拉,自动过 sha256 校验,落 ${ffmpegDir()};agent 可代办)。
|
|
5829
|
+
` + ` 也可自行把 ffmpeg/ffprobe 放到该目录,或用 --ffmpeg-path <目录> 指定已装位置。`;
|
|
5823
5830
|
var _cache = new Map;
|
|
5824
5831
|
function onSystemPath(cmd) {
|
|
5825
5832
|
try {
|
|
@@ -6200,9 +6207,9 @@ function registerInstall(program2) {
|
|
|
6200
6207
|
}
|
|
6201
6208
|
|
|
6202
6209
|
// src/commands/oralcut.ts
|
|
6203
|
-
import { resolve as resolve4, join as
|
|
6210
|
+
import { resolve as resolve4, join as join16, dirname as dirname4, basename as basename7, extname as extname2 } from "node:path";
|
|
6204
6211
|
import { mkdir as mkdir5, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
|
|
6205
|
-
import { existsSync as
|
|
6212
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
6206
6213
|
|
|
6207
6214
|
// src/lib/config.ts
|
|
6208
6215
|
function loadConfig() {
|
|
@@ -6723,8 +6730,194 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
|
|
|
6723
6730
|
|
|
6724
6731
|
// src/lib/media.ts
|
|
6725
6732
|
import { mkdir as mkdir3, stat as stat4 } from "node:fs/promises";
|
|
6726
|
-
import { existsSync as
|
|
6727
|
-
import {
|
|
6733
|
+
import { closeSync, existsSync as existsSync12, openSync, readSync, readdirSync as readdirSync2 } from "node:fs";
|
|
6734
|
+
import { homedir as homedir3 } from "node:os";
|
|
6735
|
+
import { basename as basename5, extname, join as join13 } from "node:path";
|
|
6736
|
+
|
|
6737
|
+
// src/lib/runtime-assets.ts
|
|
6738
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
6739
|
+
import { createHash } from "node:crypto";
|
|
6740
|
+
import { createWriteStream, existsSync as existsSync11, mkdirSync as mkdirSync4, chmodSync, renameSync, rmSync, readdirSync } from "node:fs";
|
|
6741
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
6742
|
+
import { once } from "node:events";
|
|
6743
|
+
import { pipeline } from "node:stream/promises";
|
|
6744
|
+
import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
|
|
6745
|
+
var MANIFEST_URL = "https://api.ai-mcn.tv:9000/broadcast/exe/ffmpeg/dist/manifest.json";
|
|
6746
|
+
var SUPPORTED_SCHEMA = 1;
|
|
6747
|
+
function assertHttps(url) {
|
|
6748
|
+
if (!/^https:\/\//i.test(url)) {
|
|
6749
|
+
throw new Error(`运行时资产下载 MUST 用 https,收到:${url}`);
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
var DISTRIBUTED_PLATFORMS = ["win-x64", "linux-x64", "mac-x64", "mac-arm64"];
|
|
6753
|
+
function resolvePlatformKey(platform = process.platform, arch = process.arch) {
|
|
6754
|
+
const os = { win32: "win", linux: "linux", darwin: "mac" }[platform];
|
|
6755
|
+
const cpu = { x64: "x64", arm64: "arm64" }[arch];
|
|
6756
|
+
const key = os && cpu ? `${os}-${cpu}` : "";
|
|
6757
|
+
if (!key || !DISTRIBUTED_PLATFORMS.includes(key)) {
|
|
6758
|
+
throw new Error(`当前平台 ${platform}-${arch} 无对应的 ffmpeg 分发包(已分发:${DISTRIBUTED_PLATFORMS.join(" / ")})。` + `请自行安装 ffmpeg/ffprobe 到 ${ffmpegDir()},或用 --ffmpeg-path <目录> 指定已装位置。`);
|
|
6759
|
+
}
|
|
6760
|
+
return key;
|
|
6761
|
+
}
|
|
6762
|
+
function parseManifest(raw) {
|
|
6763
|
+
if (!raw || typeof raw !== "object")
|
|
6764
|
+
throw new Error("manifest 不是对象");
|
|
6765
|
+
const m = raw;
|
|
6766
|
+
if (typeof m.schema !== "number")
|
|
6767
|
+
throw new Error("manifest 缺 schema 字段");
|
|
6768
|
+
if (m.schema > SUPPORTED_SCHEMA) {
|
|
6769
|
+
throw new Error(`manifest schema=${m.schema} 高于本 CLI 支持的 ${SUPPORTED_SCHEMA},请先升级 CLI(gtrk upgrade)`);
|
|
6770
|
+
}
|
|
6771
|
+
if (!m.base || !m.ffmpeg || !m.font)
|
|
6772
|
+
throw new Error("manifest 缺 base/ffmpeg/font 字段");
|
|
6773
|
+
assertHttps(m.base);
|
|
6774
|
+
return m;
|
|
6775
|
+
}
|
|
6776
|
+
async function fetchManifest(url = MANIFEST_URL) {
|
|
6777
|
+
assertHttps(url);
|
|
6778
|
+
const r = await fetch(url);
|
|
6779
|
+
if (!r.ok)
|
|
6780
|
+
throw new Error(`取 manifest 失败:HTTP ${r.status} ${url}`);
|
|
6781
|
+
return parseManifest(await r.json());
|
|
6782
|
+
}
|
|
6783
|
+
async function sha256File(path) {
|
|
6784
|
+
const h = createHash("sha256");
|
|
6785
|
+
await pipeline(createReadStream2(path), h);
|
|
6786
|
+
return h.digest("hex");
|
|
6787
|
+
}
|
|
6788
|
+
function systemTarPath() {
|
|
6789
|
+
if (process.platform === "win32") {
|
|
6790
|
+
const sys = join12(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
|
|
6791
|
+
if (existsSync11(sys))
|
|
6792
|
+
return sys;
|
|
6793
|
+
}
|
|
6794
|
+
return "tar";
|
|
6795
|
+
}
|
|
6796
|
+
function hasSystemTar() {
|
|
6797
|
+
try {
|
|
6798
|
+
return spawnSync4(systemTarPath(), ["--version"], { stdio: "ignore" }).status === 0;
|
|
6799
|
+
} catch {
|
|
6800
|
+
return false;
|
|
6801
|
+
}
|
|
6802
|
+
}
|
|
6803
|
+
async function downloadVerified(url, expectSha256, onProgress) {
|
|
6804
|
+
assertHttps(url);
|
|
6805
|
+
mkdirSync4(tmpDir(), { recursive: true });
|
|
6806
|
+
const tmp = join12(tmpDir(), `dl-${Date.now()}-${url.split("/").pop() ?? "asset"}`);
|
|
6807
|
+
try {
|
|
6808
|
+
const r = await fetch(url);
|
|
6809
|
+
if (!r.ok)
|
|
6810
|
+
throw new Error(`下载失败:HTTP ${r.status} ${url}`);
|
|
6811
|
+
if (!r.body)
|
|
6812
|
+
throw new Error(`下载失败:响应无 body ${url}`);
|
|
6813
|
+
const total = Number(r.headers.get("content-length") ?? 0);
|
|
6814
|
+
let got = 0;
|
|
6815
|
+
const reader = r.body.getReader();
|
|
6816
|
+
const ws = createWriteStream(tmp);
|
|
6817
|
+
try {
|
|
6818
|
+
for (;; ) {
|
|
6819
|
+
const { done, value } = await reader.read();
|
|
6820
|
+
if (done)
|
|
6821
|
+
break;
|
|
6822
|
+
got += value.length;
|
|
6823
|
+
if (onProgress)
|
|
6824
|
+
onProgress(got, total);
|
|
6825
|
+
if (!ws.write(value))
|
|
6826
|
+
await once(ws, "drain");
|
|
6827
|
+
}
|
|
6828
|
+
} finally {
|
|
6829
|
+
ws.end();
|
|
6830
|
+
}
|
|
6831
|
+
await once(ws, "close");
|
|
6832
|
+
const actual = await sha256File(tmp);
|
|
6833
|
+
if (actual !== expectSha256) {
|
|
6834
|
+
rmSync(tmp, { force: true });
|
|
6835
|
+
throw new Error(`sha256 校验不通过,已丢弃下载物、未落地任何文件。
|
|
6836
|
+
期望 ${expectSha256}
|
|
6837
|
+
实得 ${actual}
|
|
6838
|
+
来源 ${url}`);
|
|
6839
|
+
}
|
|
6840
|
+
return tmp;
|
|
6841
|
+
} catch (e) {
|
|
6842
|
+
rmSync(tmp, { force: true });
|
|
6843
|
+
throw e;
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6846
|
+
function extractTar(archive, destDir) {
|
|
6847
|
+
if (!hasSystemTar()) {
|
|
6848
|
+
throw new Error(`未找到可用的系统 tar,无法解包 ${archive}。
|
|
6849
|
+
` + `Windows 10+/macOS/Linux 通常自带;若确无,请手工解开该 .tar.xz 并把二进制放到 ${destDir}。`);
|
|
6850
|
+
}
|
|
6851
|
+
mkdirSync4(destDir, { recursive: true });
|
|
6852
|
+
const r = spawnSync4(systemTarPath(), ["-xf", basename4(archive), "-C", destDir], {
|
|
6853
|
+
cwd: dirname3(archive),
|
|
6854
|
+
encoding: "utf8"
|
|
6855
|
+
});
|
|
6856
|
+
if (r.status !== 0) {
|
|
6857
|
+
throw new Error(`tar 解包失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
async function installFfmpeg(m, opts = {}) {
|
|
6861
|
+
const key = resolvePlatformKey();
|
|
6862
|
+
const entry = m.ffmpeg[key];
|
|
6863
|
+
if (!entry) {
|
|
6864
|
+
throw new Error(`manifest 中无 ${key} 的 ffmpeg 包(可用:${Object.keys(m.ffmpeg).join(" / ")})。` + `请自行安装到 ${ffmpegDir()} 或用 --ffmpeg-path。`);
|
|
6865
|
+
}
|
|
6866
|
+
const dest = ffmpegDir();
|
|
6867
|
+
const already = entry.members.every((f) => existsSync11(join12(dest, f)));
|
|
6868
|
+
if (already && !opts.force) {
|
|
6869
|
+
return { installed: false, reason: "已存在", detail: `${dest}(--force 可覆盖)` };
|
|
6870
|
+
}
|
|
6871
|
+
const tmp = await downloadVerified(`${m.base}/${entry.file}`, entry.sha256, opts.onProgress);
|
|
6872
|
+
try {
|
|
6873
|
+
const stage = join12(tmpDir(), `x-${Date.now()}`);
|
|
6874
|
+
extractTar(tmp, stage);
|
|
6875
|
+
for (const f of entry.members) {
|
|
6876
|
+
if (!existsSync11(join12(stage, f))) {
|
|
6877
|
+
rmSync(stage, { recursive: true, force: true });
|
|
6878
|
+
throw new Error(`解包结果缺成员 ${f},已中止,未改动 ${dest}`);
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
mkdirSync4(dest, { recursive: true });
|
|
6882
|
+
for (const f of entry.members) {
|
|
6883
|
+
const target = join12(dest, f);
|
|
6884
|
+
rmSync(target, { force: true });
|
|
6885
|
+
renameSync(join12(stage, f), target);
|
|
6886
|
+
if (process.platform !== "win32")
|
|
6887
|
+
chmodSync(target, 493);
|
|
6888
|
+
}
|
|
6889
|
+
rmSync(stage, { recursive: true, force: true });
|
|
6890
|
+
return { installed: true, detail: `${entry.version} → ${dest}` };
|
|
6891
|
+
} finally {
|
|
6892
|
+
rmSync(tmp, { force: true });
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
async function installFont(m, opts = {}) {
|
|
6896
|
+
const out = [];
|
|
6897
|
+
const dest = fontsDir();
|
|
6898
|
+
for (const [name, entry] of Object.entries(m.font)) {
|
|
6899
|
+
const target = join12(dest, entry.file);
|
|
6900
|
+
if (existsSync11(target) && !opts.force) {
|
|
6901
|
+
out.push({ installed: false, reason: "已存在", detail: `${name} → ${target}` });
|
|
6902
|
+
continue;
|
|
6903
|
+
}
|
|
6904
|
+
const tmp = await downloadVerified(entry.url, entry.sha256, opts.onProgress);
|
|
6905
|
+
mkdirSync4(dest, { recursive: true });
|
|
6906
|
+
rmSync(target, { force: true });
|
|
6907
|
+
renameSync(tmp, target);
|
|
6908
|
+
out.push({ installed: true, detail: `${name} → ${target}` });
|
|
6909
|
+
}
|
|
6910
|
+
return out;
|
|
6911
|
+
}
|
|
6912
|
+
function hasLocalFonts() {
|
|
6913
|
+
try {
|
|
6914
|
+
return readdirSync(fontsDir()).some((f) => /\.(otf|ttf|ttc|otc)$/i.test(f));
|
|
6915
|
+
} catch {
|
|
6916
|
+
return false;
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
// src/lib/media.ts
|
|
6728
6921
|
function parseFps(rate) {
|
|
6729
6922
|
if (typeof rate !== "string")
|
|
6730
6923
|
return 0;
|
|
@@ -6772,14 +6965,14 @@ function probeDuration(path, ffmpegPath) {
|
|
|
6772
6965
|
}
|
|
6773
6966
|
async function artifactPath(inputAbs, ext) {
|
|
6774
6967
|
const s = await stat4(inputAbs);
|
|
6775
|
-
const base =
|
|
6968
|
+
const base = basename5(inputAbs, extname(inputAbs));
|
|
6776
6969
|
await mkdir3(audioCacheDir(), { recursive: true });
|
|
6777
|
-
return
|
|
6970
|
+
return join13(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
|
|
6778
6971
|
}
|
|
6779
6972
|
async function extractAudio(inputAbs, ffmpegPath) {
|
|
6780
6973
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6781
6974
|
const out = await artifactPath(inputAbs, "mp3");
|
|
6782
|
-
if (
|
|
6975
|
+
if (existsSync12(out))
|
|
6783
6976
|
return out;
|
|
6784
6977
|
await runFfmpeg(ffmpeg, [
|
|
6785
6978
|
"-y",
|
|
@@ -6803,7 +6996,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
|
|
|
6803
6996
|
async function compress720p(inputAbs, ffmpegPath) {
|
|
6804
6997
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6805
6998
|
const out = await artifactPath(inputAbs, "720p.mp4");
|
|
6806
|
-
if (
|
|
6999
|
+
if (existsSync12(out))
|
|
6807
7000
|
return out;
|
|
6808
7001
|
await runFfmpeg(ffmpeg, [
|
|
6809
7002
|
"-y",
|
|
@@ -6833,16 +7026,230 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
|
|
|
6833
7026
|
throw new Error(`抽出物时长(${got.toFixed(2)}s)与原片(${originalDuration.toFixed(2)}s)不一致(容差 ${tolSec}s),` + `疑似抽取异常,已中止上传`);
|
|
6834
7027
|
}
|
|
6835
7028
|
}
|
|
7029
|
+
function assFontNames(assText) {
|
|
7030
|
+
const names = new Set;
|
|
7031
|
+
for (const line2 of assText.split(/\r?\n/)) {
|
|
7032
|
+
if (!line2.startsWith("Style:"))
|
|
7033
|
+
continue;
|
|
7034
|
+
const font = line2.slice("Style:".length).split(",")[1]?.trim();
|
|
7035
|
+
if (font)
|
|
7036
|
+
names.add(font);
|
|
7037
|
+
}
|
|
7038
|
+
return [...names];
|
|
7039
|
+
}
|
|
7040
|
+
async function probeFontAvailable(font, ffmpegPath) {
|
|
7041
|
+
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
7042
|
+
try {
|
|
7043
|
+
await runFfmpeg(ffmpeg, [
|
|
7044
|
+
"-v",
|
|
7045
|
+
"error",
|
|
7046
|
+
"-f",
|
|
7047
|
+
"lavfi",
|
|
7048
|
+
"-i",
|
|
7049
|
+
"color=c=black:s=64x64:d=0.04",
|
|
7050
|
+
"-vf",
|
|
7051
|
+
`drawtext=font='${font}':text=A`,
|
|
7052
|
+
"-frames:v",
|
|
7053
|
+
"1",
|
|
7054
|
+
"-f",
|
|
7055
|
+
"null",
|
|
7056
|
+
"-"
|
|
7057
|
+
]);
|
|
7058
|
+
return true;
|
|
7059
|
+
} catch (e) {
|
|
7060
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
7061
|
+
if (/valid font|font.*not found|Fontconfig error/i.test(msg))
|
|
7062
|
+
return false;
|
|
7063
|
+
return null;
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
function assFilterPath(p) {
|
|
7067
|
+
return p.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
7068
|
+
}
|
|
7069
|
+
function nameTableNames(buf) {
|
|
7070
|
+
const out = [];
|
|
7071
|
+
if (buf.length < 6)
|
|
7072
|
+
return out;
|
|
7073
|
+
const count = buf.readUInt16BE(2);
|
|
7074
|
+
const strBase = buf.readUInt16BE(4);
|
|
7075
|
+
for (let i = 0;i < count; i++) {
|
|
7076
|
+
const r = 6 + i * 12;
|
|
7077
|
+
if (r + 12 > buf.length)
|
|
7078
|
+
break;
|
|
7079
|
+
const platformID = buf.readUInt16BE(r);
|
|
7080
|
+
const nameID = buf.readUInt16BE(r + 6);
|
|
7081
|
+
if (nameID !== 1 && nameID !== 4 && nameID !== 16)
|
|
7082
|
+
continue;
|
|
7083
|
+
const len = buf.readUInt16BE(r + 8);
|
|
7084
|
+
const off = strBase + buf.readUInt16BE(r + 10);
|
|
7085
|
+
if (off + len > buf.length)
|
|
7086
|
+
continue;
|
|
7087
|
+
const slice = buf.subarray(off, off + len);
|
|
7088
|
+
let s;
|
|
7089
|
+
if (platformID === 1) {
|
|
7090
|
+
s = slice.toString("latin1");
|
|
7091
|
+
} else {
|
|
7092
|
+
const even = slice.length % 2 === 0 ? slice : slice.subarray(0, slice.length - 1);
|
|
7093
|
+
s = Buffer.from(even).swap16().toString("utf16le");
|
|
7094
|
+
}
|
|
7095
|
+
const v = s.replace(/\0/g, "").trim();
|
|
7096
|
+
if (v)
|
|
7097
|
+
out.push(v);
|
|
7098
|
+
}
|
|
7099
|
+
return out;
|
|
7100
|
+
}
|
|
7101
|
+
function fontFileFamilies(path) {
|
|
7102
|
+
let fd = null;
|
|
7103
|
+
try {
|
|
7104
|
+
fd = openSync(path, "r");
|
|
7105
|
+
const head = Buffer.alloc(12);
|
|
7106
|
+
if (readSync(fd, head, 0, 12, 0) < 12)
|
|
7107
|
+
return [];
|
|
7108
|
+
const bases = [];
|
|
7109
|
+
if (head.toString("latin1", 0, 4) === "ttcf") {
|
|
7110
|
+
const n = head.readUInt32BE(8);
|
|
7111
|
+
const dir = Buffer.alloc(Math.min(n, 64) * 4);
|
|
7112
|
+
readSync(fd, dir, 0, dir.length, 12);
|
|
7113
|
+
for (let i = 0;i < dir.length / 4; i++)
|
|
7114
|
+
bases.push(dir.readUInt32BE(i * 4));
|
|
7115
|
+
} else {
|
|
7116
|
+
bases.push(0);
|
|
7117
|
+
}
|
|
7118
|
+
const out = [];
|
|
7119
|
+
for (const base of bases) {
|
|
7120
|
+
const h = base === 0 ? head : Buffer.alloc(12);
|
|
7121
|
+
if (base !== 0 && readSync(fd, h, 0, 12, base) < 12)
|
|
7122
|
+
continue;
|
|
7123
|
+
const numTables = h.readUInt16BE(4);
|
|
7124
|
+
if (!numTables || numTables > 512)
|
|
7125
|
+
continue;
|
|
7126
|
+
const dir = Buffer.alloc(numTables * 16);
|
|
7127
|
+
if (readSync(fd, dir, 0, dir.length, base + 12) < dir.length)
|
|
7128
|
+
continue;
|
|
7129
|
+
let nameOff = 0;
|
|
7130
|
+
let nameLen = 0;
|
|
7131
|
+
for (let i = 0;i < numTables; i++) {
|
|
7132
|
+
if (dir.toString("latin1", i * 16, i * 16 + 4) === "name") {
|
|
7133
|
+
nameOff = dir.readUInt32BE(i * 16 + 8);
|
|
7134
|
+
nameLen = dir.readUInt32BE(i * 16 + 12);
|
|
7135
|
+
break;
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
if (!nameOff || !nameLen || nameLen > 1 << 20)
|
|
7139
|
+
continue;
|
|
7140
|
+
const nameBuf = Buffer.alloc(nameLen);
|
|
7141
|
+
if (readSync(fd, nameBuf, 0, nameLen, nameOff) < nameLen)
|
|
7142
|
+
continue;
|
|
7143
|
+
out.push(...nameTableNames(nameBuf));
|
|
7144
|
+
}
|
|
7145
|
+
return out;
|
|
7146
|
+
} catch {
|
|
7147
|
+
return [];
|
|
7148
|
+
} finally {
|
|
7149
|
+
if (fd !== null)
|
|
7150
|
+
try {
|
|
7151
|
+
closeSync(fd);
|
|
7152
|
+
} catch {}
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
var FONT_EXT = /\.(otf|ttf|ttc|otc)$/i;
|
|
7156
|
+
function scanFontDir(dir, out, depth = 0) {
|
|
7157
|
+
if (depth > 3)
|
|
7158
|
+
return;
|
|
7159
|
+
let entries;
|
|
7160
|
+
try {
|
|
7161
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
7162
|
+
} catch {
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
for (const e of entries) {
|
|
7166
|
+
const p = join13(dir, e.name);
|
|
7167
|
+
if (e.isDirectory())
|
|
7168
|
+
scanFontDir(p, out, depth + 1);
|
|
7169
|
+
else if (FONT_EXT.test(e.name))
|
|
7170
|
+
for (const n of fontFileFamilies(p))
|
|
7171
|
+
out.add(n);
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7174
|
+
var _famCache = null;
|
|
7175
|
+
function localFontFamilies() {
|
|
7176
|
+
if (_famCache)
|
|
7177
|
+
return _famCache;
|
|
7178
|
+
const out = new Set;
|
|
7179
|
+
scanFontDir(fontsDir(), out);
|
|
7180
|
+
_famCache = [...out];
|
|
7181
|
+
return _famCache;
|
|
7182
|
+
}
|
|
7183
|
+
function systemFontDirs() {
|
|
7184
|
+
const home = homedir3();
|
|
7185
|
+
if (process.platform === "win32") {
|
|
7186
|
+
return [
|
|
7187
|
+
join13(process.env.SystemRoot || "C:\\Windows", "Fonts"),
|
|
7188
|
+
join13(process.env.LOCALAPPDATA || join13(home, "AppData", "Local"), "Microsoft", "Windows", "Fonts")
|
|
7189
|
+
];
|
|
7190
|
+
}
|
|
7191
|
+
if (process.platform === "darwin") {
|
|
7192
|
+
return ["/System/Library/Fonts", "/Library/Fonts", join13(home, "Library", "Fonts")];
|
|
7193
|
+
}
|
|
7194
|
+
return ["/usr/share/fonts", "/usr/local/share/fonts", join13(home, ".local/share/fonts"), join13(home, ".fonts")];
|
|
7195
|
+
}
|
|
7196
|
+
var _sysCache = null;
|
|
7197
|
+
function systemFontFamilies() {
|
|
7198
|
+
if (_sysCache)
|
|
7199
|
+
return _sysCache;
|
|
7200
|
+
const out = new Set;
|
|
7201
|
+
for (const d of systemFontDirs())
|
|
7202
|
+
scanFontDir(d, out);
|
|
7203
|
+
_sysCache = [...out];
|
|
7204
|
+
return _sysCache;
|
|
7205
|
+
}
|
|
7206
|
+
async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
7207
|
+
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
7208
|
+
let filter = `ass='${assFilterPath(assAbs)}'`;
|
|
7209
|
+
if (hasLocalFonts())
|
|
7210
|
+
filter += `:fontsdir='${assFilterPath(fontsDir())}'`;
|
|
7211
|
+
await runFfmpeg(ffmpeg, [
|
|
7212
|
+
"-y",
|
|
7213
|
+
"-v",
|
|
7214
|
+
"error",
|
|
7215
|
+
"-i",
|
|
7216
|
+
videoAbs,
|
|
7217
|
+
"-vf",
|
|
7218
|
+
filter,
|
|
7219
|
+
"-c:v",
|
|
7220
|
+
opts.codec ?? "libx264",
|
|
7221
|
+
"-crf",
|
|
7222
|
+
String(opts.crf ?? 18),
|
|
7223
|
+
"-preset",
|
|
7224
|
+
"medium",
|
|
7225
|
+
"-c:a",
|
|
7226
|
+
"copy",
|
|
7227
|
+
"-movflags",
|
|
7228
|
+
"+faststart",
|
|
7229
|
+
outAbs
|
|
7230
|
+
]);
|
|
7231
|
+
return outAbs;
|
|
7232
|
+
}
|
|
7233
|
+
async function fontUsableForBurn(font, ffmpegPath) {
|
|
7234
|
+
const local = localFontFamilies();
|
|
7235
|
+
const sys = systemFontFamilies();
|
|
7236
|
+
if (local.includes(font) || sys.includes(font))
|
|
7237
|
+
return true;
|
|
7238
|
+
if (local.length === 0 && sys.length === 0) {
|
|
7239
|
+
return await probeFontAvailable(font, ffmpegPath) !== false;
|
|
7240
|
+
}
|
|
7241
|
+
return false;
|
|
7242
|
+
}
|
|
6836
7243
|
|
|
6837
7244
|
// src/lib/materialize.ts
|
|
6838
|
-
import { join as
|
|
7245
|
+
import { join as join15, basename as basename6 } from "node:path";
|
|
6839
7246
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6840
7247
|
|
|
6841
7248
|
// src/lib/render.ts
|
|
6842
7249
|
import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
|
|
6843
|
-
import { existsSync as
|
|
7250
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
6844
7251
|
import { tmpdir } from "node:os";
|
|
6845
|
-
import { join as
|
|
7252
|
+
import { join as join14 } from "node:path";
|
|
6846
7253
|
var AUDIO_SAMPLE_RATE = 48000;
|
|
6847
7254
|
var AUDIO_LAYOUT = "stereo";
|
|
6848
7255
|
var DEFAULT_CRF = 18;
|
|
@@ -7000,7 +7407,7 @@ function materialPathsFromGtrk(gtrk) {
|
|
|
7000
7407
|
continue;
|
|
7001
7408
|
if (!m.path)
|
|
7002
7409
|
throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
|
|
7003
|
-
if (!
|
|
7410
|
+
if (!existsSync13(m.path))
|
|
7004
7411
|
throw new Error(`gtrk 素材文件不存在:${m.path}`);
|
|
7005
7412
|
map[String(m.id)] = m.path;
|
|
7006
7413
|
}
|
|
@@ -7014,7 +7421,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
|
|
|
7014
7421
|
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
7015
7422
|
const materialPaths = materialPathsFromGtrk(gtrk);
|
|
7016
7423
|
const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
|
|
7017
|
-
const filterFile =
|
|
7424
|
+
const filterFile = join14(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
|
|
7018
7425
|
await writeFile3(filterFile, graph, "utf8");
|
|
7019
7426
|
try {
|
|
7020
7427
|
const args = ["-y"];
|
|
@@ -7052,7 +7459,7 @@ function gtrkSourceName(gtrk) {
|
|
|
7052
7459
|
const p = gtrk.materials?.[0]?.path;
|
|
7053
7460
|
if (!p)
|
|
7054
7461
|
return;
|
|
7055
|
-
const b =
|
|
7462
|
+
const b = basename6(p);
|
|
7056
7463
|
const dot = b.lastIndexOf(".");
|
|
7057
7464
|
return dot > 0 ? b.slice(0, dot) : b;
|
|
7058
7465
|
}
|
|
@@ -7064,7 +7471,7 @@ async function materializeResult(opts) {
|
|
|
7064
7471
|
throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
|
|
7065
7472
|
const errors = { ...output.errors ?? {} };
|
|
7066
7473
|
await mkdir4(outDir, { recursive: true });
|
|
7067
|
-
const resultPath =
|
|
7474
|
+
const resultPath = join15(outDir, "result.json");
|
|
7068
7475
|
const writeResult = async (extra) => {
|
|
7069
7476
|
const r = {
|
|
7070
7477
|
ok: Object.keys(errors).length === 0,
|
|
@@ -7086,9 +7493,9 @@ async function materializeResult(opts) {
|
|
|
7086
7493
|
const byFormat = {};
|
|
7087
7494
|
for (const f of files) {
|
|
7088
7495
|
const base = baseFormat(f.format);
|
|
7089
|
-
const fmtDir =
|
|
7496
|
+
const fmtDir = join15(outDir, base);
|
|
7090
7497
|
await mkdir4(fmtDir, { recursive: true });
|
|
7091
|
-
const dest =
|
|
7498
|
+
const dest = join15(fmtDir, f.filename);
|
|
7092
7499
|
try {
|
|
7093
7500
|
await dl(f.download_url, dest);
|
|
7094
7501
|
(byFormat[base] ??= []).push(dest);
|
|
@@ -7104,9 +7511,9 @@ async function materializeResult(opts) {
|
|
|
7104
7511
|
}
|
|
7105
7512
|
let jianyingDraftPath = null;
|
|
7106
7513
|
if (byFormat.jianying && opts.draftDir) {
|
|
7107
|
-
const dest =
|
|
7514
|
+
const dest = join15(opts.draftDir, basename6(outDir));
|
|
7108
7515
|
try {
|
|
7109
|
-
const landing = await copyJianyingDraft(
|
|
7516
|
+
const landing = await copyJianyingDraft(join15(outDir, "jianying"), dest);
|
|
7110
7517
|
if (landing.complete) {
|
|
7111
7518
|
jianyingDraftPath = dest;
|
|
7112
7519
|
log.info(`剪映草稿已落到:${dest}`);
|
|
@@ -7128,7 +7535,7 @@ async function materializeResult(opts) {
|
|
|
7128
7535
|
log.step("本地渲染成片(ffmpeg)…");
|
|
7129
7536
|
const project = await readGtrkFile(gtrkPath);
|
|
7130
7537
|
const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
|
|
7131
|
-
const outMp4 =
|
|
7538
|
+
const outMp4 = join15(outDir, `${name}.mp4`);
|
|
7132
7539
|
const r = await renderGtrk(project, outMp4, {
|
|
7133
7540
|
crf: opts.crf != null ? Number(opts.crf) : undefined,
|
|
7134
7541
|
codec: opts.codec,
|
|
@@ -7150,7 +7557,7 @@ async function materializeResult(opts) {
|
|
|
7150
7557
|
log.step("三方打开(产物已就位,按需自取):");
|
|
7151
7558
|
for (const base of Object.keys(byFormat)) {
|
|
7152
7559
|
const meta = FORMAT_META[base];
|
|
7153
|
-
const target = base === "jianying" ? jianyingDraftPath ??
|
|
7560
|
+
const target = base === "jianying" ? jianyingDraftPath ?? join15(outDir, "jianying") : byFormat[base][0];
|
|
7154
7561
|
console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
|
|
7155
7562
|
}
|
|
7156
7563
|
if (rendered)
|
|
@@ -7218,18 +7625,18 @@ async function runOralCut(input, opts) {
|
|
|
7218
7625
|
routeLogsToStderr();
|
|
7219
7626
|
const cfg = loadConfig();
|
|
7220
7627
|
const inputAbs = resolve4(input);
|
|
7221
|
-
if (!
|
|
7628
|
+
if (!existsSync14(inputAbs))
|
|
7222
7629
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
7223
|
-
const projName =
|
|
7630
|
+
const projName = basename7(inputAbs, extname2(inputAbs));
|
|
7224
7631
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7225
7632
|
if (opts.render && !formats.includes("gtrk"))
|
|
7226
7633
|
formats.push("gtrk");
|
|
7227
7634
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
7228
|
-
const outDir = resolve4(opts.out ??
|
|
7635
|
+
const outDir = resolve4(opts.out ?? join16(dirname4(inputAbs), `${projName}-video-project-${timestamp()}`));
|
|
7229
7636
|
let scriptPath = opts.script ? resolve4(opts.script) : undefined;
|
|
7230
7637
|
if (!scriptPath) {
|
|
7231
|
-
const sibling =
|
|
7232
|
-
if (
|
|
7638
|
+
const sibling = join16(dirname4(inputAbs), `${projName}.txt`);
|
|
7639
|
+
if (existsSync14(sibling)) {
|
|
7233
7640
|
scriptPath = sibling;
|
|
7234
7641
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
7235
7642
|
}
|
|
@@ -7243,14 +7650,14 @@ async function runOralCut(input, opts) {
|
|
|
7243
7650
|
else
|
|
7244
7651
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
7245
7652
|
}
|
|
7246
|
-
log.step(`▶ 智能口播剪辑:${
|
|
7653
|
+
log.step(`▶ 智能口播剪辑:${basename7(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
|
|
7247
7654
|
const extraParams = parseExtraParams(opts.param, opts.paramsJson);
|
|
7248
7655
|
log.step("① 本地预处理(探几何 + 抽音频/720p)…");
|
|
7249
7656
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
7250
7657
|
log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
|
|
7251
7658
|
const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
|
|
7252
7659
|
assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
|
|
7253
|
-
log.info(opts.visualAssist ? `已压 720p 代理(上传物):${
|
|
7660
|
+
log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename7(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename7(artifact)}`);
|
|
7254
7661
|
log.step("② 上传抽出物到云端…");
|
|
7255
7662
|
const buildPayload = (fid) => {
|
|
7256
7663
|
const p = {
|
|
@@ -7290,7 +7697,7 @@ async function runOralCut(input, opts) {
|
|
|
7290
7697
|
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
7291
7698
|
log.info(`task_id = ${taskId}`);
|
|
7292
7699
|
await mkdir5(outDir, { recursive: true });
|
|
7293
|
-
await writeFile5(
|
|
7700
|
+
await writeFile5(join16(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
7294
7701
|
log.step("④ 云端处理中(每 5s 轮询)…");
|
|
7295
7702
|
const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
|
|
7296
7703
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -7314,9 +7721,9 @@ async function runOralCut(input, opts) {
|
|
|
7314
7721
|
}
|
|
7315
7722
|
|
|
7316
7723
|
// src/commands/long2short.ts
|
|
7317
|
-
import { resolve as resolve6, join as
|
|
7318
|
-
import { mkdir as mkdir7, writeFile as
|
|
7319
|
-
import { existsSync as
|
|
7724
|
+
import { resolve as resolve6, join as join19, dirname as dirname7, basename as basename9, extname as extname5 } from "node:path";
|
|
7725
|
+
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
7726
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7320
7727
|
|
|
7321
7728
|
// src/lib/clip-brief.ts
|
|
7322
7729
|
var str = (v) => {
|
|
@@ -7445,13 +7852,113 @@ function renderClipsOverview(clips, ctx) {
|
|
|
7445
7852
|
`)}
|
|
7446
7853
|
`;
|
|
7447
7854
|
}
|
|
7855
|
+
var POLISH_LABEL = {
|
|
7856
|
+
split_screen: "智能分屏",
|
|
7857
|
+
camera: "克制运镜",
|
|
7858
|
+
speed: "整体调速",
|
|
7859
|
+
seam: "接缝过渡",
|
|
7860
|
+
subtitle: "智能字幕",
|
|
7861
|
+
subtitle_remap: "字幕时轴重映射",
|
|
7862
|
+
purify_source: "去除原字幕",
|
|
7863
|
+
unknown: "未具名润色项"
|
|
7864
|
+
};
|
|
7865
|
+
function renderProReport(clips, report, ctx) {
|
|
7866
|
+
const name = ctx.source.split(/[\/]/).pop() || ctx.source;
|
|
7867
|
+
const out = [`# ${name} · 长剪短精剪报告`, ""];
|
|
7868
|
+
out.push(`- 源片:\`${ctx.source}\``);
|
|
7869
|
+
out.push(`- 成片:${clips.length} 条`);
|
|
7870
|
+
const jc = report?.jump_cut;
|
|
7871
|
+
if (typeof jc === "boolean")
|
|
7872
|
+
out.push(`- 跳剪:${jc ? "开" : "关"}`);
|
|
7873
|
+
if (ctx.taskId)
|
|
7874
|
+
out.push(`- task_id:\`${ctx.taskId}\``);
|
|
7875
|
+
out.push("");
|
|
7876
|
+
out.push("| # | 标题 | 时长 | 评分 | 简介 |", "| --- | --- | --- | --- | --- |");
|
|
7877
|
+
for (const [i, clip] of clips.entries()) {
|
|
7878
|
+
const dur = clipDurationMs(clip);
|
|
7879
|
+
const score = num(clip.score);
|
|
7880
|
+
out.push(`| clip${i} | ${cell(str(clip.title) ?? "—")} | ${dur != null ? fmtTime(dur) : "—"} | ${score ?? "—"} | ${cell(str(clip.summary) ?? "—")} |`);
|
|
7881
|
+
}
|
|
7882
|
+
out.push("");
|
|
7883
|
+
const degraded = report?.degraded_items;
|
|
7884
|
+
const degRows = [];
|
|
7885
|
+
if (degraded && typeof degraded === "object" && !Array.isArray(degraded)) {
|
|
7886
|
+
for (const [k, v] of Object.entries(degraded)) {
|
|
7887
|
+
const names = list(v).map((x) => POLISH_LABEL[x] ?? x);
|
|
7888
|
+
if (names.length)
|
|
7889
|
+
degRows.push(`- **${k}**:${names.join("、")}`);
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
if (degRows.length) {
|
|
7893
|
+
out.push("## ⚠️ 润色降级", "");
|
|
7894
|
+
out.push("以下片子照常出片,但列出的润色项**没有做成**——效果与预期不同,别当成完整成片直接发布。", "");
|
|
7895
|
+
out.push(...degRows, "");
|
|
7896
|
+
}
|
|
7897
|
+
for (const [i, clip] of clips.entries()) {
|
|
7898
|
+
const title = str(clip.title);
|
|
7899
|
+
out.push("---", "", `## clip${i}${title ? `「${title}」` : ""}`, "");
|
|
7900
|
+
const bits = [];
|
|
7901
|
+
const dur = clipDurationMs(clip);
|
|
7902
|
+
if (dur != null)
|
|
7903
|
+
bits.push(`时长 ${fmtTime(dur)}`);
|
|
7904
|
+
const segCount = clipSegmentCount(clip);
|
|
7905
|
+
if (segCount != null)
|
|
7906
|
+
bits.push(`${segCount} 个保留片段`);
|
|
7907
|
+
const span = sourceSpan(clip);
|
|
7908
|
+
if (span)
|
|
7909
|
+
bits.push(`源片 ${fmtTime(span[0])}–${fmtTime(span[1])}`);
|
|
7910
|
+
const f = ctx.clipFiles?.[i];
|
|
7911
|
+
if (f)
|
|
7912
|
+
bits.push(`成片 ${baseName(f)}`);
|
|
7913
|
+
if (bits.length)
|
|
7914
|
+
out.push(`- ${bits.join(" · ")}`, "");
|
|
7915
|
+
const score = num(clip.score);
|
|
7916
|
+
const reason = str(clip.score_reason);
|
|
7917
|
+
if (score != null || reason) {
|
|
7918
|
+
out.push("### 入选理由", "");
|
|
7919
|
+
out.push([score != null ? `**评分 ${score}**` : null, reason].filter(Boolean).join(" — "), "");
|
|
7920
|
+
}
|
|
7921
|
+
const summary = str(clip.summary);
|
|
7922
|
+
if (summary)
|
|
7923
|
+
out.push("### 简介", "", summary, "");
|
|
7924
|
+
const note = str(clip.jumpcut_note);
|
|
7925
|
+
if (note)
|
|
7926
|
+
out.push("### 跳剪", "", jumpcutText(note), "");
|
|
7927
|
+
const cats = [
|
|
7928
|
+
["主题", list(clip.themes)],
|
|
7929
|
+
["标签", list(clip.tags)],
|
|
7930
|
+
["类型", list(clip.genres)],
|
|
7931
|
+
["调性", list(clip.moods)]
|
|
7932
|
+
];
|
|
7933
|
+
const shown = cats.filter(([, v]) => v.length);
|
|
7934
|
+
if (shown.length) {
|
|
7935
|
+
out.push("### 分类与调性", "");
|
|
7936
|
+
for (const [k, v] of shown)
|
|
7937
|
+
out.push(`- ${k}:${v.join("、")}`);
|
|
7938
|
+
out.push("");
|
|
7939
|
+
}
|
|
7940
|
+
const hl = Array.isArray(clip.highlight_words) ? clip.highlight_words : [];
|
|
7941
|
+
const hlRows = hl.map((h) => ({ text: str(h?.text), at: num(h?.begin_time) })).filter((h) => h.text);
|
|
7942
|
+
if (hlRows.length) {
|
|
7943
|
+
out.push("### 高光词(源片时码)", "");
|
|
7944
|
+
for (const h of hlRows)
|
|
7945
|
+
out.push(`- ${h.at != null ? `${fmtTime(h.at)} ` : ""}「${h.text}」`);
|
|
7946
|
+
out.push("");
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7949
|
+
return `${out.join(`
|
|
7950
|
+
`).replace(/\n{3,}/g, `
|
|
7951
|
+
|
|
7952
|
+
`).trimEnd()}
|
|
7953
|
+
`;
|
|
7954
|
+
}
|
|
7448
7955
|
|
|
7449
7956
|
// src/lib/tool-runner.ts
|
|
7450
|
-
import { resolve as resolve5, join as
|
|
7451
|
-
import { mkdir as mkdir6, writeFile as
|
|
7452
|
-
import { createWriteStream, existsSync as
|
|
7957
|
+
import { resolve as resolve5, join as join18, dirname as dirname6, basename as basename8, extname as extname4 } from "node:path";
|
|
7958
|
+
import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
|
|
7959
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync16 } from "node:fs";
|
|
7453
7960
|
import { Readable as Readable2 } from "node:stream";
|
|
7454
|
-
import { pipeline } from "node:stream/promises";
|
|
7961
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
7455
7962
|
|
|
7456
7963
|
// src/lib/tool-pricing.ts
|
|
7457
7964
|
var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
|
|
@@ -7534,11 +8041,13 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
|
|
|
7534
8041
|
|
|
7535
8042
|
// src/lib/tool-descriptors.ts
|
|
7536
8043
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
7537
|
-
import {
|
|
8044
|
+
import { readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
|
|
8045
|
+
import { dirname as dirname5, extname as extname3, join as join17, resolve as resolvePath } from "node:path";
|
|
7538
8046
|
var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
|
|
7539
8047
|
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
7540
8048
|
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
7541
8049
|
var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
|
|
8050
|
+
var isAudioFile = (p) => AUDIO_EXTS.includes(extname3(p).toLowerCase());
|
|
7542
8051
|
var PUBLIC_VIDEO_EXTS = [
|
|
7543
8052
|
".mp4",
|
|
7544
8053
|
".avi",
|
|
@@ -8482,18 +8991,37 @@ var videoAiSubtitle = {
|
|
|
8482
8991
|
{ flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
|
|
8483
8992
|
{ flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
|
|
8484
8993
|
],
|
|
8994
|
+
async preprocess(ctx) {
|
|
8995
|
+
const inputAbs = ctx.inputAbs;
|
|
8996
|
+
if (ctx.opts.needPure === true) {
|
|
8997
|
+
ctx.warn("--need-pure 需要画面(去原字幕是画面操作),本次整片上传;只要字幕文件时去掉该 flag 即可只传音频");
|
|
8998
|
+
return inputAbs;
|
|
8999
|
+
}
|
|
9000
|
+
if (isAudioFile(inputAbs))
|
|
9001
|
+
return inputAbs;
|
|
9002
|
+
const audio = await extractAudio(inputAbs, ctx.ffmpegPath);
|
|
9003
|
+
return audio;
|
|
9004
|
+
},
|
|
8485
9005
|
buildPayload(fileId, ctx) {
|
|
8486
9006
|
const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
|
|
8487
9007
|
if (!language)
|
|
8488
9008
|
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
8489
9009
|
const payload = { file_id: fileId, language };
|
|
9010
|
+
const inputAbs = ctx.inputAbs;
|
|
9011
|
+
if (inputAbs && !isAudioFile(inputAbs)) {
|
|
9012
|
+
try {
|
|
9013
|
+
const geo = probeGeometry(inputAbs, ctx.ffmpegPath);
|
|
9014
|
+
if (geo.width > 0 && geo.height > 0)
|
|
9015
|
+
payload.video_size = [geo.width, geo.height];
|
|
9016
|
+
} catch {
|
|
9017
|
+
ctx.warn("探原片几何失败,本次不回传 video_size(服务端将按 1920x1080 兜底出字幕)");
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
8490
9020
|
if (ctx.opts.translateLanguage != null) {
|
|
8491
9021
|
const t = String(ctx.opts.translateLanguage).trim();
|
|
8492
9022
|
if (t)
|
|
8493
9023
|
payload.translate_language = t;
|
|
8494
9024
|
}
|
|
8495
|
-
if (ctx.opts.needRender === true)
|
|
8496
|
-
payload.need_render = true;
|
|
8497
9025
|
if (ctx.opts.needPure === true)
|
|
8498
9026
|
payload.need_pure = true;
|
|
8499
9027
|
if (ctx.opts.subtitleType != null) {
|
|
@@ -8523,10 +9051,136 @@ var videoAiSubtitle = {
|
|
|
8523
9051
|
files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
|
|
8524
9052
|
return files;
|
|
8525
9053
|
},
|
|
9054
|
+
async postprocess(ctx, landed) {
|
|
9055
|
+
if (ctx.opts.needRender !== true)
|
|
9056
|
+
return;
|
|
9057
|
+
const inputAbs = ctx.inputAbs;
|
|
9058
|
+
if (isAudioFile(inputAbs)) {
|
|
9059
|
+
throw new Error("--need-render 需要视频输入:本次输入是音频文件,无画面可烧");
|
|
9060
|
+
}
|
|
9061
|
+
const ass = landed.find((p) => p.toLowerCase().endsWith(".ass"));
|
|
9062
|
+
if (!ass)
|
|
9063
|
+
throw new Error("未拉回 .ass 字幕文件,无法本地烧录");
|
|
9064
|
+
const fonts = assFontNames(await readFile5(ass, "utf8"));
|
|
9065
|
+
const missing = [];
|
|
9066
|
+
for (const f of fonts) {
|
|
9067
|
+
if (!await fontUsableForBurn(f, ctx.ffmpegPath))
|
|
9068
|
+
missing.push(f);
|
|
9069
|
+
}
|
|
9070
|
+
if (missing.length) {
|
|
9071
|
+
throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装它:gtrk deps install --font(落 ~/.gitruck/fonts,不写系统字体表)。
|
|
9072
|
+
` + ` 装上后重跑即可(字幕文件已落地,无需重新提交任务)。不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
|
|
9073
|
+
}
|
|
9074
|
+
const out = join17(dirname5(ass), `${ctx.baseName}-subtitled.mp4`);
|
|
9075
|
+
await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
|
|
9076
|
+
return [out];
|
|
9077
|
+
},
|
|
8526
9078
|
mapResult(out) {
|
|
8527
9079
|
return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
|
|
8528
9080
|
}
|
|
8529
9081
|
};
|
|
9082
|
+
var PRO_DURATION_PREFS = ["auto", "short", "medium", "long"];
|
|
9083
|
+
var videoLong2ShortPro = {
|
|
9084
|
+
name: "video_long2short_pro",
|
|
9085
|
+
title: "长剪短·精剪(直接出片)",
|
|
9086
|
+
description: "长内容按语义抽多条高光短片并一键出成品:选段+跳剪+分屏内核,叠加模糊底画布适配/克制运镜/调速保音高/智能字幕。只出成片,不产工程文件——要可编辑工程请用 gtrk long2short(粗剪)。",
|
|
9087
|
+
kind: "cloud",
|
|
9088
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
9089
|
+
priceKey: "video_long2short_pro",
|
|
9090
|
+
outputHint: "逐条高光成片 mp4 + 切片报告 clips.md",
|
|
9091
|
+
enabled: true,
|
|
9092
|
+
taskType: "video_long2short_pro",
|
|
9093
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
9094
|
+
options: [
|
|
9095
|
+
{ flag: "--language <code>", desc: "源语种代码(精剪必填;取值以服务端支持列表为准)" },
|
|
9096
|
+
{ flag: "--output-language <code>", desc: "选段/文案的输出语种(未传则同源语种)" },
|
|
9097
|
+
{ flag: "--main-topic <text>", desc: "主题引导(影响选段偏好)" },
|
|
9098
|
+
{ flag: "--output-size <s>", desc: "成片画幅 9:16|16:9|1:1 或自定义 WxH(未传则服务端默认)" },
|
|
9099
|
+
{ flag: "--no-jump-cut", desc: "关闭跳剪(默认开:片内去水词/冗余,只删不重排)" },
|
|
9100
|
+
{ flag: "--duration-pref <p>", desc: `成片时长偏好 ${PRO_DURATION_PREFS.join("/")}(成片条数由内容语义决定、不可指定)` },
|
|
9101
|
+
{ flag: "--max-clip-sec <n>", desc: "单条成片时长安全上限(秒;未传则服务端默认)" },
|
|
9102
|
+
{ flag: "--split-screen", desc: "开启智能分屏(多人同框段合成分屏画面)" },
|
|
9103
|
+
{ flag: "--split-orientation <o>", desc: "分屏方向 auto|lr|tb(未传则服务端默认)" },
|
|
9104
|
+
{ flag: "--speed-factor <n>", desc: "整体调速倍率(保音高;未传则服务端默认)" },
|
|
9105
|
+
{ flag: "--no-camera-move", desc: "关闭克制运镜(默认开:亚像素推/拉)" },
|
|
9106
|
+
{ flag: "--no-subtitle", desc: "关闭智能字幕(默认开:成片内烧录字幕)" },
|
|
9107
|
+
{ flag: "--subtitle-translate-language <code>", desc: "字幕译文语种(未传则单语字幕)" }
|
|
9108
|
+
],
|
|
9109
|
+
buildPayload(fileId, ctx) {
|
|
9110
|
+
const o = ctx.opts;
|
|
9111
|
+
const language = o.language == null ? "" : String(o.language).trim();
|
|
9112
|
+
if (!language)
|
|
9113
|
+
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
9114
|
+
const p = { file_id: fileId, language };
|
|
9115
|
+
if (o.outputLanguage != null && String(o.outputLanguage).trim())
|
|
9116
|
+
p.output_language = String(o.outputLanguage).trim();
|
|
9117
|
+
if (o.mainTopic != null && String(o.mainTopic).trim())
|
|
9118
|
+
p.main_topic = String(o.mainTopic).trim();
|
|
9119
|
+
if (o.outputSize != null && String(o.outputSize).trim())
|
|
9120
|
+
p.output_size = String(o.outputSize).trim();
|
|
9121
|
+
if (o.jumpCut === false)
|
|
9122
|
+
p.jump_cut = false;
|
|
9123
|
+
const duration = {};
|
|
9124
|
+
if (o.durationPref != null && String(o.durationPref).trim())
|
|
9125
|
+
duration.pref = String(o.durationPref).trim();
|
|
9126
|
+
const mcs = parseCountFlag(o.maxClipSec, "--max-clip-sec");
|
|
9127
|
+
if (mcs != null)
|
|
9128
|
+
duration.max_clip_sec = mcs;
|
|
9129
|
+
if (Object.keys(duration).length)
|
|
9130
|
+
p.duration = duration;
|
|
9131
|
+
if (o.splitScreen === true) {
|
|
9132
|
+
const ss = { enable: true };
|
|
9133
|
+
if (o.splitOrientation != null && String(o.splitOrientation).trim())
|
|
9134
|
+
ss.orientation = String(o.splitOrientation).trim();
|
|
9135
|
+
p.split_screen = ss;
|
|
9136
|
+
}
|
|
9137
|
+
if (o.speedFactor != null) {
|
|
9138
|
+
const n = Number(o.speedFactor);
|
|
9139
|
+
if (!Number.isFinite(n))
|
|
9140
|
+
throw new Error(`--speed-factor 需要有限数值,拿到「${o.speedFactor}」`);
|
|
9141
|
+
p.speed = { factor: n };
|
|
9142
|
+
}
|
|
9143
|
+
if (o.cameraMove === false)
|
|
9144
|
+
p.camera_move = { enable: false };
|
|
9145
|
+
const sub = {};
|
|
9146
|
+
if (o.subtitle === false)
|
|
9147
|
+
sub.enable = false;
|
|
9148
|
+
if (o.subtitleTranslateLanguage != null && String(o.subtitleTranslateLanguage).trim()) {
|
|
9149
|
+
sub.translate_language = String(o.subtitleTranslateLanguage).trim();
|
|
9150
|
+
}
|
|
9151
|
+
if (Object.keys(sub).length)
|
|
9152
|
+
p.subtitle = sub;
|
|
9153
|
+
return p;
|
|
9154
|
+
},
|
|
9155
|
+
mapOutputs(out, _ctx) {
|
|
9156
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
9157
|
+
const files = [];
|
|
9158
|
+
for (const [i, clip] of clips.entries()) {
|
|
9159
|
+
const f = clip?.file;
|
|
9160
|
+
const url = typeof f?.download_url === "string" ? f.download_url : undefined;
|
|
9161
|
+
if (!url)
|
|
9162
|
+
continue;
|
|
9163
|
+
files.push({ url, filename: `clip${i}${extFromUrl(url, ".mp4")}` });
|
|
9164
|
+
}
|
|
9165
|
+
return files;
|
|
9166
|
+
},
|
|
9167
|
+
mapResult(out) {
|
|
9168
|
+
return { clips: out.clips ?? [], report: out.report ?? {} };
|
|
9169
|
+
},
|
|
9170
|
+
async postprocess(ctx, landed, out) {
|
|
9171
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
9172
|
+
if (!clips.length)
|
|
9173
|
+
return;
|
|
9174
|
+
const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
|
|
9175
|
+
const byIndex = clips.map((_c, i) => mp4s.find((p) => new RegExp(`clip${i}.[a-z0-9]+$`, "i").test(p)));
|
|
9176
|
+
const dest = join17(dirname5(landed[0] ?? "."), "clips.md");
|
|
9177
|
+
await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
|
|
9178
|
+
source: ctx.inputAbs ?? ctx.baseName,
|
|
9179
|
+
clipFiles: byIndex
|
|
9180
|
+
}), "utf8");
|
|
9181
|
+
return [dest];
|
|
9182
|
+
}
|
|
9183
|
+
};
|
|
8530
9184
|
function parseCountFlag(v, flag) {
|
|
8531
9185
|
if (v == null)
|
|
8532
9186
|
return;
|
|
@@ -8818,6 +9472,7 @@ var TOOL_REGISTRY = [
|
|
|
8818
9472
|
imageClassicTemplate,
|
|
8819
9473
|
imageVerticalStitch,
|
|
8820
9474
|
videoSplitScreen,
|
|
9475
|
+
videoLong2ShortPro,
|
|
8821
9476
|
audioTtsClone,
|
|
8822
9477
|
mad
|
|
8823
9478
|
];
|
|
@@ -8918,7 +9573,7 @@ function validateToolInput(descriptor, inputAbs) {
|
|
|
8918
9573
|
return;
|
|
8919
9574
|
if (!inputAbs)
|
|
8920
9575
|
throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
|
|
8921
|
-
if (!
|
|
9576
|
+
if (!existsSync16(inputAbs))
|
|
8922
9577
|
throw new Error(`输入不存在:${inputAbs}`);
|
|
8923
9578
|
if (spec.kind === "directory")
|
|
8924
9579
|
return;
|
|
@@ -8935,13 +9590,13 @@ function validateToolInputs(descriptor, inputAbsList) {
|
|
|
8935
9590
|
throw new Error(`${descriptor.name} 需要至少一个输入文件(可传多个,顺序即拼装顺序)`);
|
|
8936
9591
|
const exts = descriptor.input.exts ?? defaultExtsFor(descriptor.input.kind);
|
|
8937
9592
|
for (const p of inputAbsList) {
|
|
8938
|
-
if (!
|
|
9593
|
+
if (!existsSync16(p))
|
|
8939
9594
|
throw new Error(`输入不存在:${p}`);
|
|
8940
9595
|
if (exts && exts.length) {
|
|
8941
9596
|
const e = extname4(p).toLowerCase();
|
|
8942
9597
|
if (!exts.includes(e)) {
|
|
8943
9598
|
const noun = descriptor.input.kind === "videos" ? "视频" : "图片";
|
|
8944
|
-
throw new Error(`${descriptor.name} 需要${noun}输入,但「${
|
|
9599
|
+
throw new Error(`${descriptor.name} 需要${noun}输入,但「${basename8(p)}」是「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
|
|
8945
9600
|
}
|
|
8946
9601
|
}
|
|
8947
9602
|
}
|
|
@@ -8959,7 +9614,7 @@ async function downloadStream(url, dest) {
|
|
|
8959
9614
|
const res = await fetch(url);
|
|
8960
9615
|
if (!res.ok || !res.body)
|
|
8961
9616
|
throw new Error(`下载失败 HTTP ${res.status}:${url}`);
|
|
8962
|
-
await
|
|
9617
|
+
await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(dest));
|
|
8963
9618
|
}
|
|
8964
9619
|
async function pollToolTask(cfg, taskType, taskId, opts = {}) {
|
|
8965
9620
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
@@ -9039,10 +9694,10 @@ function resolveOutDir(descriptor, inputAbs, out) {
|
|
|
9039
9694
|
if (out)
|
|
9040
9695
|
return resolve5(out);
|
|
9041
9696
|
if (inputAbs) {
|
|
9042
|
-
const base =
|
|
9043
|
-
return
|
|
9697
|
+
const base = basename8(inputAbs, extname4(inputAbs));
|
|
9698
|
+
return join18(dirname6(inputAbs), `${base}-${descriptor.name}`);
|
|
9044
9699
|
}
|
|
9045
|
-
return
|
|
9700
|
+
return join18(process.cwd(), `${descriptor.name}-${timestamp2()}`);
|
|
9046
9701
|
}
|
|
9047
9702
|
async function safeFingerprint(inputAbs) {
|
|
9048
9703
|
try {
|
|
@@ -9060,7 +9715,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9060
9715
|
const isMulti = MULTI_INPUT_KINDS.has(descriptor.input.kind);
|
|
9061
9716
|
const inputList = isMulti ? (Array.isArray(inputArg) ? inputArg : inputArg != null ? [inputArg] : []).map((p) => resolve5(p)) : undefined;
|
|
9062
9717
|
const inputAbs = isMulti ? inputList[0] : typeof inputArg === "string" ? resolve5(inputArg) : undefined;
|
|
9063
|
-
const baseName2 = inputAbs ?
|
|
9718
|
+
const baseName2 = inputAbs ? basename8(inputAbs, extname4(inputAbs)) : descriptor.name;
|
|
9064
9719
|
if (isMulti) {
|
|
9065
9720
|
validateToolInputs(descriptor, inputList);
|
|
9066
9721
|
} else {
|
|
@@ -9131,7 +9786,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9131
9786
|
}
|
|
9132
9787
|
await mkdir6(outDir, { recursive: true });
|
|
9133
9788
|
const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
9134
|
-
await
|
|
9789
|
+
await writeFile7(join18(outDir, "task.json"), JSON.stringify({
|
|
9135
9790
|
tool: descriptor.name,
|
|
9136
9791
|
taskType,
|
|
9137
9792
|
taskId,
|
|
@@ -9153,7 +9808,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9153
9808
|
const errors = {};
|
|
9154
9809
|
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
9155
9810
|
for (const it of items) {
|
|
9156
|
-
const dest =
|
|
9811
|
+
const dest = join18(outDir, it.filename);
|
|
9157
9812
|
try {
|
|
9158
9813
|
await deps.downloadStream(it.url, dest);
|
|
9159
9814
|
files.push(dest);
|
|
@@ -9161,11 +9816,20 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9161
9816
|
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
9162
9817
|
}
|
|
9163
9818
|
}
|
|
9819
|
+
if (descriptor.postprocess) {
|
|
9820
|
+
try {
|
|
9821
|
+
const extra = await descriptor.postprocess(ctx, [...files], outputResult);
|
|
9822
|
+
if (Array.isArray(extra))
|
|
9823
|
+
files.push(...extra);
|
|
9824
|
+
} catch (e) {
|
|
9825
|
+
errors[`${descriptor.name}:postprocess`] = e instanceof Error ? e.message : String(e);
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9164
9828
|
let resultFile;
|
|
9165
9829
|
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
9166
9830
|
if (structured != null) {
|
|
9167
|
-
resultFile =
|
|
9168
|
-
await
|
|
9831
|
+
resultFile = join18(outDir, "result-output.json");
|
|
9832
|
+
await writeFile7(resultFile, JSON.stringify(structured, null, 2));
|
|
9169
9833
|
}
|
|
9170
9834
|
if (items.length === 0 && resultFile == null) {
|
|
9171
9835
|
errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
|
|
@@ -9183,7 +9847,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9183
9847
|
...resultFile ? { resultFile } : {},
|
|
9184
9848
|
...Object.keys(errors).length ? { errors } : {}
|
|
9185
9849
|
};
|
|
9186
|
-
await
|
|
9850
|
+
await writeFile7(join18(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
9187
9851
|
return result;
|
|
9188
9852
|
}
|
|
9189
9853
|
|
|
@@ -9240,8 +9904,8 @@ async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
|
|
|
9240
9904
|
let complete = 0;
|
|
9241
9905
|
let attempted = 0;
|
|
9242
9906
|
for (const [i, dir] of clipDirs.entries()) {
|
|
9243
|
-
const src =
|
|
9244
|
-
if (!
|
|
9907
|
+
const src = join19(dir, "jianying");
|
|
9908
|
+
if (!existsSync17(src)) {
|
|
9245
9909
|
paths.push(null);
|
|
9246
9910
|
continue;
|
|
9247
9911
|
}
|
|
@@ -9273,15 +9937,15 @@ async function runLong2Short(input, opts) {
|
|
|
9273
9937
|
routeLogsToStderr();
|
|
9274
9938
|
const cfg = loadConfig();
|
|
9275
9939
|
const inputAbs = resolve6(input);
|
|
9276
|
-
if (!
|
|
9940
|
+
if (!existsSync17(inputAbs))
|
|
9277
9941
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
9278
|
-
const projName =
|
|
9942
|
+
const projName = basename9(inputAbs, extname5(inputAbs));
|
|
9279
9943
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9280
9944
|
if (!formats.includes("gtrk"))
|
|
9281
9945
|
formats.push("gtrk");
|
|
9282
9946
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
9283
|
-
const outDir = resolve6(opts.out ??
|
|
9284
|
-
const outName =
|
|
9947
|
+
const outDir = resolve6(opts.out ?? join19(dirname7(inputAbs), `${projName}-long2short`));
|
|
9948
|
+
const outName = basename9(outDir);
|
|
9285
9949
|
let draftRoot;
|
|
9286
9950
|
if (wantJianying) {
|
|
9287
9951
|
draftRoot = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
@@ -9290,14 +9954,14 @@ async function runLong2Short(input, opts) {
|
|
|
9290
9954
|
else
|
|
9291
9955
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
9292
9956
|
}
|
|
9293
|
-
const draftTarget = draftRoot ?
|
|
9294
|
-
log.step(`▶ 长剪短:${
|
|
9957
|
+
const draftTarget = draftRoot ? join19(draftRoot, outName) : undefined;
|
|
9958
|
+
log.step(`▶ 长剪短:${basename9(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
|
|
9295
9959
|
log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
|
|
9296
9960
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
9297
9961
|
log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${(geo.duration / 60).toFixed(1)}min`);
|
|
9298
9962
|
const artifact = opts.splitScreen ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
|
|
9299
9963
|
assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
|
|
9300
|
-
log.info(opts.splitScreen ? `已压 720p 代理(上传物):${
|
|
9964
|
+
log.info(opts.splitScreen ? `已压 720p 代理(上传物):${basename9(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename9(artifact)}`);
|
|
9301
9965
|
const payloadProbe = buildLong2ShortPayload("__dry_run__", opts, geo, inputAbs, formats, draftTarget);
|
|
9302
9966
|
log.step("② 上传抽出物到云端…");
|
|
9303
9967
|
const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE2, (fid) => buildLong2ShortPayload(fid, opts, geo, inputAbs, formats, draftTarget), {
|
|
@@ -9311,7 +9975,7 @@ async function runLong2Short(input, opts) {
|
|
|
9311
9975
|
const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
|
|
9312
9976
|
log.info(`task_id = ${taskId}`);
|
|
9313
9977
|
await mkdir7(outDir, { recursive: true });
|
|
9314
|
-
await
|
|
9978
|
+
await writeFile8(join19(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
9315
9979
|
log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
|
|
9316
9980
|
const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
|
|
9317
9981
|
timeoutMs: POLL_TIMEOUT_MS,
|
|
@@ -9334,14 +9998,14 @@ async function runLong2Short(input, opts) {
|
|
|
9334
9998
|
continue;
|
|
9335
9999
|
}
|
|
9336
10000
|
if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
|
|
9337
|
-
dest =
|
|
10001
|
+
dest = join19(dirname7(inputAbs), dest);
|
|
9338
10002
|
try {
|
|
9339
|
-
await mkdir7(
|
|
10003
|
+
await mkdir7(dirname7(dest), { recursive: true });
|
|
9340
10004
|
await download(url, dest);
|
|
9341
10005
|
splitLanded++;
|
|
9342
10006
|
} catch (e) {
|
|
9343
|
-
errors[`split:${
|
|
9344
|
-
log.warn(`分屏素材下载失败(${
|
|
10007
|
+
errors[`split:${basename9(dest)}`] = e instanceof Error ? e.message : String(e);
|
|
10008
|
+
log.warn(`分屏素材下载失败(${basename9(dest)}),对应工程中该素材将缺席`);
|
|
9345
10009
|
}
|
|
9346
10010
|
}
|
|
9347
10011
|
if (splitLanded)
|
|
@@ -9350,7 +10014,7 @@ async function runLong2Short(input, opts) {
|
|
|
9350
10014
|
log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
|
|
9351
10015
|
const clipResults = [];
|
|
9352
10016
|
for (const [i, clip] of clips.entries()) {
|
|
9353
|
-
const clipDir =
|
|
10017
|
+
const clipDir = join19(outDir, `clip${i}`);
|
|
9354
10018
|
const { files: clipFiles, ...clipMeta } = clip;
|
|
9355
10019
|
try {
|
|
9356
10020
|
const r = await materializeResult({
|
|
@@ -9391,13 +10055,13 @@ async function runLong2Short(input, opts) {
|
|
|
9391
10055
|
for (const [i, cr] of clipResults.entries()) {
|
|
9392
10056
|
try {
|
|
9393
10057
|
await mkdir7(cr.dir, { recursive: true });
|
|
9394
|
-
await
|
|
10058
|
+
await writeFile8(join19(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
|
|
9395
10059
|
} catch (e) {
|
|
9396
10060
|
errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
|
|
9397
10061
|
}
|
|
9398
10062
|
}
|
|
9399
10063
|
try {
|
|
9400
|
-
await
|
|
10064
|
+
await writeFile8(join19(outDir, "clips.md"), renderClipsOverview(clips, {
|
|
9401
10065
|
source: inputAbs,
|
|
9402
10066
|
jumpCut: opts.jumpCut !== false,
|
|
9403
10067
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
@@ -9407,7 +10071,7 @@ async function runLong2Short(input, opts) {
|
|
|
9407
10071
|
} catch (e) {
|
|
9408
10072
|
errors["clips.md"] = e instanceof Error ? e.message : String(e);
|
|
9409
10073
|
}
|
|
9410
|
-
await
|
|
10074
|
+
await writeFile8(join19(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
|
|
9411
10075
|
const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
|
|
9412
10076
|
const rootResult = {
|
|
9413
10077
|
ok,
|
|
@@ -9418,10 +10082,10 @@ async function runLong2Short(input, opts) {
|
|
|
9418
10082
|
outDir,
|
|
9419
10083
|
clips: clipResults,
|
|
9420
10084
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
9421
|
-
reportFile:
|
|
10085
|
+
reportFile: join19(outDir, "report.json"),
|
|
9422
10086
|
...Object.keys(errors).length ? { errors } : {}
|
|
9423
10087
|
};
|
|
9424
|
-
await
|
|
10088
|
+
await writeFile8(join19(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
|
|
9425
10089
|
if (opts.json)
|
|
9426
10090
|
console.log(JSON.stringify(rootResult));
|
|
9427
10091
|
if (opts.open) {
|
|
@@ -9437,7 +10101,7 @@ async function runLong2Short(input, opts) {
|
|
|
9437
10101
|
}
|
|
9438
10102
|
|
|
9439
10103
|
// src/commands/oralcut-result.ts
|
|
9440
|
-
import { resolve as resolve7, join as
|
|
10104
|
+
import { resolve as resolve7, join as join20 } from "node:path";
|
|
9441
10105
|
var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
|
|
9442
10106
|
function timestamp3() {
|
|
9443
10107
|
const d = new Date;
|
|
@@ -9471,7 +10135,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9471
10135
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
9472
10136
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
9473
10137
|
}
|
|
9474
|
-
const outDir = resolve7(opts.out ??
|
|
10138
|
+
const outDir = resolve7(opts.out ?? join20(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
|
|
9475
10139
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
9476
10140
|
await materializeResult({
|
|
9477
10141
|
outDir,
|
|
@@ -9489,10 +10153,10 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9489
10153
|
}
|
|
9490
10154
|
|
|
9491
10155
|
// src/commands/upgrade.ts
|
|
9492
|
-
import { spawnSync as
|
|
10156
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9493
10157
|
var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
|
|
9494
10158
|
function run(cmd) {
|
|
9495
|
-
const r =
|
|
10159
|
+
const r = spawnSync5(cmd, { stdio: "inherit", shell: true });
|
|
9496
10160
|
return r.status ?? 1;
|
|
9497
10161
|
}
|
|
9498
10162
|
function registerUpgrade(program2) {
|
|
@@ -9531,17 +10195,17 @@ function registerUpgrade(program2) {
|
|
|
9531
10195
|
}
|
|
9532
10196
|
|
|
9533
10197
|
// src/commands/render.ts
|
|
9534
|
-
import { resolve as resolve8, dirname as
|
|
9535
|
-
import { existsSync as
|
|
10198
|
+
import { resolve as resolve8, dirname as dirname8, join as join21, basename as basename10, extname as extname6 } from "node:path";
|
|
10199
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
9536
10200
|
function registerRender(program2) {
|
|
9537
10201
|
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) => {
|
|
9538
10202
|
if (opts.json)
|
|
9539
10203
|
routeLogsToStderr();
|
|
9540
10204
|
const gtrkAbs = resolve8(gtrk);
|
|
9541
|
-
if (!
|
|
10205
|
+
if (!existsSync18(gtrkAbs))
|
|
9542
10206
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
9543
|
-
const outMp4 = resolve8(opts.out ??
|
|
9544
|
-
log.step(`▶ 本地渲染:${
|
|
10207
|
+
const outMp4 = resolve8(opts.out ?? join21(dirname8(gtrkAbs), `${basename10(gtrkAbs, extname6(gtrkAbs))}.mp4`));
|
|
10208
|
+
log.step(`▶ 本地渲染:${basename10(gtrkAbs)} → ${basename10(outMp4)}`);
|
|
9545
10209
|
const project = await readGtrkFile(gtrkAbs);
|
|
9546
10210
|
const result = await renderGtrk(project, outMp4, {
|
|
9547
10211
|
crf: opts.crf != null ? Number(opts.crf) : undefined,
|
|
@@ -9556,7 +10220,7 @@ function registerRender(program2) {
|
|
|
9556
10220
|
log.tickEnd();
|
|
9557
10221
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
9558
10222
|
if (opts.open)
|
|
9559
|
-
openFolder(
|
|
10223
|
+
openFolder(dirname8(outMp4));
|
|
9560
10224
|
if (opts.json) {
|
|
9561
10225
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
9562
10226
|
}
|
|
@@ -9564,14 +10228,14 @@ function registerRender(program2) {
|
|
|
9564
10228
|
}
|
|
9565
10229
|
|
|
9566
10230
|
// src/commands/split.ts
|
|
9567
|
-
import { resolve as resolve9, join as
|
|
9568
|
-
import { existsSync as
|
|
9569
|
-
import { readFile as
|
|
9570
|
-
import { createHash } from "node:crypto";
|
|
10231
|
+
import { resolve as resolve9, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
|
|
10232
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
10233
|
+
import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
|
|
10234
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9571
10235
|
|
|
9572
10236
|
// src/lib/gtrk-writeback.ts
|
|
9573
|
-
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
9574
|
-
import { dirname as
|
|
10237
|
+
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync } from "node:fs";
|
|
10238
|
+
import { dirname as dirname9, join as join22, basename as basename11 } from "node:path";
|
|
9575
10239
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9576
10240
|
function readGtrk(path) {
|
|
9577
10241
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -9598,10 +10262,10 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
9598
10262
|
}
|
|
9599
10263
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
9600
10264
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
9601
|
-
const tmp =
|
|
10265
|
+
const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9602
10266
|
try {
|
|
9603
10267
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9604
|
-
|
|
10268
|
+
renameSync2(tmp, path);
|
|
9605
10269
|
} catch (e) {
|
|
9606
10270
|
try {
|
|
9607
10271
|
unlinkSync(tmp);
|
|
@@ -9614,10 +10278,10 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
9614
10278
|
if (cur !== expectedMtimeMs) {
|
|
9615
10279
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
9616
10280
|
}
|
|
9617
|
-
const tmp =
|
|
10281
|
+
const tmp = join22(dirname9(path), `.${basename11(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9618
10282
|
try {
|
|
9619
10283
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9620
|
-
|
|
10284
|
+
renameSync2(tmp, path);
|
|
9621
10285
|
} catch (e) {
|
|
9622
10286
|
try {
|
|
9623
10287
|
unlinkSync(tmp);
|
|
@@ -9634,7 +10298,7 @@ function registerSplit(program2) {
|
|
|
9634
10298
|
});
|
|
9635
10299
|
}
|
|
9636
10300
|
function firstExisting(cands) {
|
|
9637
|
-
return cands.find((p) =>
|
|
10301
|
+
return cands.find((p) => existsSync19(p));
|
|
9638
10302
|
}
|
|
9639
10303
|
function resolvePaths(opts) {
|
|
9640
10304
|
const project = opts.project ? resolve9(opts.project) : undefined;
|
|
@@ -9642,22 +10306,22 @@ function resolvePaths(opts) {
|
|
|
9642
10306
|
if (opts.gtrk) {
|
|
9643
10307
|
gtrkPath = resolve9(opts.gtrk);
|
|
9644
10308
|
} else if (project) {
|
|
9645
|
-
gtrkPath = firstExisting([
|
|
10309
|
+
gtrkPath = firstExisting([join23(project, "gtrk", "project.gtrk"), join23(project, "project.gtrk")]) ?? join23(project, "gtrk", "project.gtrk");
|
|
9646
10310
|
} else {
|
|
9647
10311
|
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
9648
10312
|
}
|
|
9649
|
-
if (!
|
|
10313
|
+
if (!existsSync19(gtrkPath))
|
|
9650
10314
|
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
9651
10315
|
let transcriptPath;
|
|
9652
10316
|
if (opts.transcript)
|
|
9653
10317
|
transcriptPath = resolve9(opts.transcript);
|
|
9654
10318
|
else if (project)
|
|
9655
10319
|
transcriptPath = firstExisting([
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
10320
|
+
join23(project, "transcript", "transcript.json"),
|
|
10321
|
+
join23(project, "json", "transcript.json"),
|
|
10322
|
+
join23(project, "transcript.json")
|
|
9659
10323
|
]);
|
|
9660
|
-
const baseDir = project ??
|
|
10324
|
+
const baseDir = project ?? dirname10(gtrkPath);
|
|
9661
10325
|
return { baseDir, gtrkPath, transcriptPath };
|
|
9662
10326
|
}
|
|
9663
10327
|
function slugify2(name) {
|
|
@@ -9665,11 +10329,11 @@ function slugify2(name) {
|
|
|
9665
10329
|
return s || "project";
|
|
9666
10330
|
}
|
|
9667
10331
|
async function loadTranscript(path) {
|
|
9668
|
-
const t = JSON.parse(await
|
|
10332
|
+
const t = JSON.parse(await readFile6(path, "utf8"));
|
|
9669
10333
|
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
9670
10334
|
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
9671
10335
|
}
|
|
9672
|
-
t.text_hash =
|
|
10336
|
+
t.text_hash = createHash2("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
|
|
9673
10337
|
`), "utf8").digest("hex");
|
|
9674
10338
|
return t;
|
|
9675
10339
|
}
|
|
@@ -9680,16 +10344,16 @@ async function runSplit(splitdoc, opts) {
|
|
|
9680
10344
|
return splitdoc ? runLand(resolve9(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
9681
10345
|
}
|
|
9682
10346
|
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
9683
|
-
if (!transcriptPath || !
|
|
10347
|
+
if (!transcriptPath || !existsSync19(transcriptPath))
|
|
9684
10348
|
throw new Error(TRANSCRIPT_MISSING);
|
|
9685
10349
|
log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
|
|
9686
10350
|
const transcript = await loadTranscript(transcriptPath);
|
|
9687
10351
|
const { gtrk } = readGtrk(gtrkPath);
|
|
9688
10352
|
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
9689
|
-
const splitDir =
|
|
10353
|
+
const splitDir = join23(baseDir, "split");
|
|
9690
10354
|
await mkdir8(splitDir, { recursive: true });
|
|
9691
|
-
const viewPath =
|
|
9692
|
-
await
|
|
10355
|
+
const viewPath = join23(splitDir, "view.json");
|
|
10356
|
+
await writeFile9(viewPath, JSON.stringify(view, null, 2));
|
|
9693
10357
|
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
9694
10358
|
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
9695
10359
|
const result = {
|
|
@@ -9706,12 +10370,12 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9706
10370
|
return result;
|
|
9707
10371
|
}
|
|
9708
10372
|
async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
9709
|
-
if (!
|
|
10373
|
+
if (!existsSync19(splitdocPath))
|
|
9710
10374
|
throw new Error(`找不到拆分稿:${splitdocPath}`);
|
|
9711
|
-
if (!transcriptPath || !
|
|
10375
|
+
if (!transcriptPath || !existsSync19(transcriptPath))
|
|
9712
10376
|
throw new Error(TRANSCRIPT_MISSING);
|
|
9713
10377
|
log.step("▶ 校验拆分稿并落地…");
|
|
9714
|
-
const doc = JSON.parse(await
|
|
10378
|
+
const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
|
|
9715
10379
|
const transcript = await loadTranscript(transcriptPath);
|
|
9716
10380
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
9717
10381
|
assertGtrkV1(gtrk);
|
|
@@ -9734,7 +10398,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9734
10398
|
}
|
|
9735
10399
|
const projectedAt = new Date().toISOString();
|
|
9736
10400
|
const view = projectTranscript(transcript, gtrk, { projectedAt });
|
|
9737
|
-
const projectSlug = slugify2(
|
|
10401
|
+
const projectSlug = slugify2(basename12(baseDir));
|
|
9738
10402
|
const landing = buildLanding(doc, view, {
|
|
9739
10403
|
utteranceIds: ctx.utteranceIds,
|
|
9740
10404
|
projectSlug,
|
|
@@ -9745,14 +10409,14 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9745
10409
|
}
|
|
9746
10410
|
});
|
|
9747
10411
|
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
9748
|
-
const splitDir =
|
|
10412
|
+
const splitDir = join23(baseDir, "split");
|
|
9749
10413
|
await mkdir8(splitDir, { recursive: true });
|
|
9750
|
-
const dispatchPath =
|
|
9751
|
-
await
|
|
10414
|
+
const dispatchPath = join23(splitDir, "dispatch.json");
|
|
10415
|
+
await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
9752
10416
|
let mdPath = null;
|
|
9753
10417
|
if (opts.md) {
|
|
9754
|
-
mdPath =
|
|
9755
|
-
await
|
|
10418
|
+
mdPath = join23(splitDir, "visual-split.md");
|
|
10419
|
+
await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
9756
10420
|
}
|
|
9757
10421
|
log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
|
|
9758
10422
|
for (const s of landing.skipped)
|
|
@@ -9783,9 +10447,9 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9783
10447
|
}
|
|
9784
10448
|
|
|
9785
10449
|
// src/commands/matrix.ts
|
|
9786
|
-
import { resolve as resolve11, join as
|
|
9787
|
-
import { existsSync as
|
|
9788
|
-
import { readFile as
|
|
10450
|
+
import { resolve as resolve11, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
|
|
10451
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
10452
|
+
import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
|
|
9789
10453
|
|
|
9790
10454
|
// src/lib/solid-png.ts
|
|
9791
10455
|
import { deflateSync } from "node:zlib";
|
|
@@ -10521,7 +11185,7 @@ function layBrollTracks(opts) {
|
|
|
10521
11185
|
}
|
|
10522
11186
|
|
|
10523
11187
|
// src/lib/material-integrity.ts
|
|
10524
|
-
import { existsSync as
|
|
11188
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
10525
11189
|
import { resolve as resolve10 } from "node:path";
|
|
10526
11190
|
var INTEGRITY_LIST_CAP = 10;
|
|
10527
11191
|
var r35 = (n) => Math.round(n * 1000) / 1000;
|
|
@@ -10582,7 +11246,7 @@ function collectMaterialRefs(gtrk) {
|
|
|
10582
11246
|
return out;
|
|
10583
11247
|
}
|
|
10584
11248
|
function checkMaterialIntegrity(opts) {
|
|
10585
|
-
const exists = opts.exists ?? ((p) =>
|
|
11249
|
+
const exists = opts.exists ?? ((p) => existsSync20(p));
|
|
10586
11250
|
const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
|
|
10587
11251
|
const refs = collectMaterialRefs(opts.gtrk);
|
|
10588
11252
|
const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
|
|
@@ -10925,16 +11589,16 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10925
11589
|
let baseDir;
|
|
10926
11590
|
if (opts.dispatch) {
|
|
10927
11591
|
dispatchPath = resolve11(opts.dispatch);
|
|
10928
|
-
baseDir =
|
|
11592
|
+
baseDir = dirname11(dirname11(dispatchPath));
|
|
10929
11593
|
} else if (opts.project) {
|
|
10930
11594
|
baseDir = resolve11(opts.project);
|
|
10931
|
-
dispatchPath =
|
|
11595
|
+
dispatchPath = join24(baseDir, "split", "dispatch.json");
|
|
10932
11596
|
} else {
|
|
10933
11597
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
10934
11598
|
}
|
|
10935
|
-
if (!
|
|
11599
|
+
if (!existsSync21(dispatchPath))
|
|
10936
11600
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
|
|
10937
|
-
const dispatch = JSON.parse(await
|
|
11601
|
+
const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
|
|
10938
11602
|
const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
|
|
10939
11603
|
const earlyGtrkPath = locateGtrk(baseDir);
|
|
10940
11604
|
let earlyGtrk;
|
|
@@ -10989,7 +11653,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10989
11653
|
if (totalQueries > 0 && okCount === 0) {
|
|
10990
11654
|
throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
|
|
10991
11655
|
}
|
|
10992
|
-
const projectSlug = slugify3(
|
|
11656
|
+
const projectSlug = slugify3(basename13(baseDir));
|
|
10993
11657
|
const plan = buildPlan({
|
|
10994
11658
|
generatedAt: new Date().toISOString(),
|
|
10995
11659
|
memberType: tier,
|
|
@@ -10997,10 +11661,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10997
11661
|
columnId,
|
|
10998
11662
|
beats
|
|
10999
11663
|
});
|
|
11000
|
-
const splitDir =
|
|
11664
|
+
const splitDir = join24(baseDir, "split");
|
|
11001
11665
|
await mkdir9(splitDir, { recursive: true });
|
|
11002
|
-
const planPath =
|
|
11003
|
-
await
|
|
11666
|
+
const planPath = join24(splitDir, "broll-plan.json");
|
|
11667
|
+
await writeFile10(planPath, JSON.stringify(plan, null, 2));
|
|
11004
11668
|
log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
|
|
11005
11669
|
log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
|
|
11006
11670
|
const layN = parseLay(opts.lay);
|
|
@@ -11047,13 +11711,13 @@ function parseScoreFloor(raw) {
|
|
|
11047
11711
|
return SCORE_FLOOR_DEFAULT;
|
|
11048
11712
|
}
|
|
11049
11713
|
function locateGtrk(baseDir) {
|
|
11050
|
-
const cands = [
|
|
11051
|
-
return cands.find((p) =>
|
|
11714
|
+
const cands = [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")];
|
|
11715
|
+
return cands.find((p) => existsSync21(p));
|
|
11052
11716
|
}
|
|
11053
11717
|
async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
|
|
11054
11718
|
const gtrkPath = locateGtrk(baseDir);
|
|
11055
11719
|
if (!gtrkPath) {
|
|
11056
|
-
log.warn(`未找到工程文件(${
|
|
11720
|
+
log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
11057
11721
|
return;
|
|
11058
11722
|
}
|
|
11059
11723
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
@@ -11061,8 +11725,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11061
11725
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
11062
11726
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
11063
11727
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
11064
|
-
const gtrkDir =
|
|
11065
|
-
const previewDir =
|
|
11728
|
+
const gtrkDir = dirname11(gtrkPath);
|
|
11729
|
+
const previewDir = join24(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
11066
11730
|
await mkdir9(previewDir, { recursive: true });
|
|
11067
11731
|
const prevSource = new Map;
|
|
11068
11732
|
const prevBroll = gtrk.struct_meta?.broll;
|
|
@@ -11085,8 +11749,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11085
11749
|
if (!cand)
|
|
11086
11750
|
continue;
|
|
11087
11751
|
const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
|
|
11088
|
-
const abs =
|
|
11089
|
-
if (
|
|
11752
|
+
const abs = join24(gtrkDir, ...rel.split("/"));
|
|
11753
|
+
if (existsSync21(abs)) {
|
|
11090
11754
|
const prev = prevSource.get(clipId);
|
|
11091
11755
|
if (prev !== "raw") {
|
|
11092
11756
|
downloads.set(clipId, { rel, source: prev ?? "preview" });
|
|
@@ -11148,12 +11812,12 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
11148
11812
|
const canvas = gtrk.video_size;
|
|
11149
11813
|
const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
|
|
11150
11814
|
const rel = solidRelPath(spec);
|
|
11151
|
-
const abs =
|
|
11815
|
+
const abs = join24(gtrkDir, ...rel.split("/"));
|
|
11152
11816
|
try {
|
|
11153
|
-
if (!
|
|
11154
|
-
await mkdir9(
|
|
11817
|
+
if (!existsSync21(abs)) {
|
|
11818
|
+
await mkdir9(dirname11(abs), { recursive: true });
|
|
11155
11819
|
const tmp = `${abs}.tmp-${process.pid}`;
|
|
11156
|
-
await
|
|
11820
|
+
await writeFile10(tmp, encodeSolidPng(spec));
|
|
11157
11821
|
await rename(tmp, abs);
|
|
11158
11822
|
}
|
|
11159
11823
|
} catch (e) {
|
|
@@ -11216,7 +11880,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11216
11880
|
if (previewUrl) {
|
|
11217
11881
|
const bytes = await tryFetch(previewUrl);
|
|
11218
11882
|
if (bytes) {
|
|
11219
|
-
await
|
|
11883
|
+
await writeFile10(absPath, bytes);
|
|
11220
11884
|
return "preview";
|
|
11221
11885
|
}
|
|
11222
11886
|
}
|
|
@@ -11224,7 +11888,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11224
11888
|
return null;
|
|
11225
11889
|
const raw = await tryFetch(cand.url);
|
|
11226
11890
|
if (raw) {
|
|
11227
|
-
await
|
|
11891
|
+
await writeFile10(absPath, raw);
|
|
11228
11892
|
log.warn(`clip ${cand.clip_id} 无 preview 代理,已回落原片(${(raw.length / 1048576).toFixed(1)}MB)`);
|
|
11229
11893
|
return "raw";
|
|
11230
11894
|
}
|
|
@@ -11247,7 +11911,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11247
11911
|
};
|
|
11248
11912
|
if (opts.out) {
|
|
11249
11913
|
const outPath = resolve11(opts.out);
|
|
11250
|
-
await
|
|
11914
|
+
await writeFile10(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
11251
11915
|
log.ok(`结果已落盘:${outPath}`);
|
|
11252
11916
|
result.outPath = outPath;
|
|
11253
11917
|
} else if (!opts.json) {
|
|
@@ -11266,9 +11930,9 @@ function slugify3(name) {
|
|
|
11266
11930
|
}
|
|
11267
11931
|
|
|
11268
11932
|
// src/commands/mg.ts
|
|
11269
|
-
import { resolve as resolve12, join as
|
|
11270
|
-
import { existsSync as
|
|
11271
|
-
import { readFile as
|
|
11933
|
+
import { resolve as resolve12, join as join25, dirname as dirname12, basename as basename14 } from "node:path";
|
|
11934
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
11935
|
+
import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
|
|
11272
11936
|
|
|
11273
11937
|
// src/lib/mg-lint.ts
|
|
11274
11938
|
var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
|
|
@@ -12929,29 +13593,29 @@ async function runMg(words, opts) {
|
|
|
12929
13593
|
function resolveDispatch(opts) {
|
|
12930
13594
|
if (opts.dispatch) {
|
|
12931
13595
|
const dispatchPath = resolve12(opts.dispatch);
|
|
12932
|
-
return { dispatchPath, baseDir:
|
|
13596
|
+
return { dispatchPath, baseDir: dirname12(dirname12(dispatchPath)) };
|
|
12933
13597
|
}
|
|
12934
13598
|
if (opts.project) {
|
|
12935
13599
|
const baseDir = resolve12(opts.project);
|
|
12936
|
-
return { dispatchPath:
|
|
13600
|
+
return { dispatchPath: join25(baseDir, "split", "dispatch.json"), baseDir };
|
|
12937
13601
|
}
|
|
12938
13602
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
12939
13603
|
}
|
|
12940
13604
|
function locateGtrk2(baseDir) {
|
|
12941
|
-
return [
|
|
13605
|
+
return [join25(baseDir, "gtrk", "project.gtrk"), join25(baseDir, "project.gtrk")].find((p) => existsSync22(p));
|
|
12942
13606
|
}
|
|
12943
13607
|
function locateSrcHtml(baseDir, compositionId) {
|
|
12944
13608
|
for (const d of MG_SRC_DIRS) {
|
|
12945
|
-
const p =
|
|
12946
|
-
if (
|
|
13609
|
+
const p = join25(baseDir, d, `${compositionId}.html`);
|
|
13610
|
+
if (existsSync22(p))
|
|
12947
13611
|
return p;
|
|
12948
13612
|
}
|
|
12949
13613
|
return;
|
|
12950
13614
|
}
|
|
12951
13615
|
async function readMgQueue(dispatchPath) {
|
|
12952
|
-
if (!
|
|
13616
|
+
if (!existsSync22(dispatchPath))
|
|
12953
13617
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
|
|
12954
|
-
const dispatch = JSON.parse(await
|
|
13618
|
+
const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
|
|
12955
13619
|
const queue = dispatch.mg ?? dispatch.rrv_mg;
|
|
12956
13620
|
return Array.isArray(queue) ? queue : [];
|
|
12957
13621
|
}
|
|
@@ -13025,10 +13689,10 @@ async function runLay(opts) {
|
|
|
13025
13689
|
const srcPath = locateSrcHtml(baseDir, q.composition_id);
|
|
13026
13690
|
if (!srcPath) {
|
|
13027
13691
|
skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
|
|
13028
|
-
log.warn(`${q.beat}:缺 ${
|
|
13692
|
+
log.warn(`${q.beat}:缺 ${join25(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
|
|
13029
13693
|
continue;
|
|
13030
13694
|
}
|
|
13031
|
-
const html = await
|
|
13695
|
+
const html = await readFile8(srcPath, "utf8");
|
|
13032
13696
|
const category = typeof q.category === "string" ? q.category : undefined;
|
|
13033
13697
|
const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
|
|
13034
13698
|
const lint = lintParticle(html, {
|
|
@@ -13073,7 +13737,7 @@ async function runLay(opts) {
|
|
|
13073
13737
|
});
|
|
13074
13738
|
}
|
|
13075
13739
|
if (!gtrkPath || !project) {
|
|
13076
|
-
log.warn(`未找到工程文件(${
|
|
13740
|
+
log.warn(`未找到工程文件(${join25(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
|
|
13077
13741
|
return done(opts, {
|
|
13078
13742
|
ok: skipped.length === 0,
|
|
13079
13743
|
mode: "lay",
|
|
@@ -13090,7 +13754,7 @@ async function runLay(opts) {
|
|
|
13090
13754
|
});
|
|
13091
13755
|
}
|
|
13092
13756
|
const { gtrk, mtimeMs } = project;
|
|
13093
|
-
const gtrkDir =
|
|
13757
|
+
const gtrkDir = dirname12(gtrkPath);
|
|
13094
13758
|
const covered = new Set(items.map((it) => it.composition_id));
|
|
13095
13759
|
const orphans = laidBefore.filter((id) => !covered.has(id));
|
|
13096
13760
|
const inDispatch = new Set(allQueue.map((q) => q.composition_id));
|
|
@@ -13132,9 +13796,9 @@ async function runLay(opts) {
|
|
|
13132
13796
|
if (opts.replaceAll && orphans.length > 0) {
|
|
13133
13797
|
log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
|
|
13134
13798
|
}
|
|
13135
|
-
await mkdir10(
|
|
13799
|
+
await mkdir10(join25(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
13136
13800
|
for (const it of items) {
|
|
13137
|
-
await copyFile(srcByComp.get(it.composition_id),
|
|
13801
|
+
await copyFile(srcByComp.get(it.composition_id), join25(gtrkDir, ...it.html_rel.split("/")));
|
|
13138
13802
|
}
|
|
13139
13803
|
const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
|
|
13140
13804
|
const written = withTimecodeSource(next, "mg", reproj);
|
|
@@ -13178,12 +13842,12 @@ async function runLint(args, opts) {
|
|
|
13178
13842
|
const file = args[0];
|
|
13179
13843
|
if (!file)
|
|
13180
13844
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
13181
|
-
const html = await
|
|
13182
|
-
const nameId =
|
|
13845
|
+
const html = await readFile8(resolve12(file), "utf8");
|
|
13846
|
+
const nameId = basename14(file).replace(/\.html?$/i, "");
|
|
13183
13847
|
let dispatchIds;
|
|
13184
13848
|
let slotDuration;
|
|
13185
13849
|
let compositionId;
|
|
13186
|
-
if (opts.dispatch &&
|
|
13850
|
+
if (opts.dispatch && existsSync22(resolve12(opts.dispatch))) {
|
|
13187
13851
|
const queue = await readMgQueue(resolve12(opts.dispatch));
|
|
13188
13852
|
dispatchIds = queue.map((q) => q.composition_id);
|
|
13189
13853
|
const byName = queue.find((q) => q.composition_id === nameId);
|
|
@@ -13207,7 +13871,7 @@ async function runLint(args, opts) {
|
|
|
13207
13871
|
for (const vv of lint.violations)
|
|
13208
13872
|
(vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
|
|
13209
13873
|
if (lint.ok)
|
|
13210
|
-
log.ok(`lint 通过(${
|
|
13874
|
+
log.ok(`lint 通过(${basename14(file)};opaque=${lint.opaque})`);
|
|
13211
13875
|
else
|
|
13212
13876
|
log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
|
|
13213
13877
|
const result = { mode: "lint", ...lint, ok: lint.ok };
|
|
@@ -13249,9 +13913,9 @@ function done(opts, result) {
|
|
|
13249
13913
|
}
|
|
13250
13914
|
|
|
13251
13915
|
// src/lib/mad/mad.ts
|
|
13252
|
-
import { mkdir as mkdir13, writeFile as
|
|
13253
|
-
import { existsSync as
|
|
13254
|
-
import { resolve as resolve13, join as
|
|
13916
|
+
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
13917
|
+
import { existsSync as existsSync25, statSync as statSync3 } from "node:fs";
|
|
13918
|
+
import { resolve as resolve13, join as join29 } from "node:path";
|
|
13255
13919
|
|
|
13256
13920
|
// src/lib/convert/types.ts
|
|
13257
13921
|
function num2(v, d = 0) {
|
|
@@ -14030,19 +14694,19 @@ function madJsx(opts) {
|
|
|
14030
14694
|
}
|
|
14031
14695
|
|
|
14032
14696
|
// src/lib/mad/scan.ts
|
|
14033
|
-
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
14034
|
-
import { extname as extname7, join as
|
|
14697
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
14698
|
+
import { extname as extname7, join as join26 } from "node:path";
|
|
14035
14699
|
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
14036
14700
|
function scanFolder(dirAbs, opts = {}) {
|
|
14037
14701
|
const probe = opts.probe ?? probeGeometry;
|
|
14038
14702
|
const warn = opts.warn ?? (() => {});
|
|
14039
14703
|
let entries;
|
|
14040
14704
|
try {
|
|
14041
|
-
entries =
|
|
14705
|
+
entries = readdirSync3(dirAbs);
|
|
14042
14706
|
} catch {
|
|
14043
14707
|
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
14044
14708
|
}
|
|
14045
|
-
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) =>
|
|
14709
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join26(dirAbs, n)).filter((p) => {
|
|
14046
14710
|
try {
|
|
14047
14711
|
return statSync2(p).isFile();
|
|
14048
14712
|
} catch {
|
|
@@ -14220,10 +14884,10 @@ function beatQuantized(natLens, analysis) {
|
|
|
14220
14884
|
}
|
|
14221
14885
|
|
|
14222
14886
|
// src/lib/mad/data.ts
|
|
14223
|
-
import { createHash as
|
|
14224
|
-
import { mkdir as mkdir11, readFile as
|
|
14225
|
-
import { existsSync as
|
|
14226
|
-
import { join as
|
|
14887
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
14888
|
+
import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
|
|
14889
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
14890
|
+
import { join as join27 } from "node:path";
|
|
14227
14891
|
function madCacheDir() {
|
|
14228
14892
|
return homeFile("mad-cache");
|
|
14229
14893
|
}
|
|
@@ -14235,7 +14899,7 @@ function manifestUrl() {
|
|
|
14235
14899
|
}
|
|
14236
14900
|
var REQUIRED_KEYS = ["mad_pool"];
|
|
14237
14901
|
function sha256Hex(buf) {
|
|
14238
|
-
return
|
|
14902
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
14239
14903
|
}
|
|
14240
14904
|
async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
14241
14905
|
const ctrl = new AbortController;
|
|
@@ -14248,14 +14912,14 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
|
14248
14912
|
}
|
|
14249
14913
|
async function atomicWrite(dest, data) {
|
|
14250
14914
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
14251
|
-
await
|
|
14915
|
+
await writeFile11(tmp, data);
|
|
14252
14916
|
await rename2(tmp, dest);
|
|
14253
14917
|
}
|
|
14254
14918
|
async function verifyFile(path, sha256) {
|
|
14255
|
-
if (!
|
|
14919
|
+
if (!existsSync23(path))
|
|
14256
14920
|
return false;
|
|
14257
14921
|
try {
|
|
14258
|
-
const buf = await
|
|
14922
|
+
const buf = await readFile9(path);
|
|
14259
14923
|
return sha256Hex(buf) === sha256;
|
|
14260
14924
|
} catch {
|
|
14261
14925
|
return false;
|
|
@@ -14280,7 +14944,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
|
14280
14944
|
for (const e of entries) {
|
|
14281
14945
|
const m = /^v(\d+)$/.exec(e);
|
|
14282
14946
|
if (m && Number(m[1]) !== keepVersion) {
|
|
14283
|
-
await rm(
|
|
14947
|
+
await rm(join27(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
14284
14948
|
}
|
|
14285
14949
|
}
|
|
14286
14950
|
} catch {}
|
|
@@ -14289,7 +14953,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14289
14953
|
const { cacheRoot, warn } = deps;
|
|
14290
14954
|
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
14291
14955
|
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
14292
|
-
const snapshotPath =
|
|
14956
|
+
const snapshotPath = join27(cacheRoot, "manifest.json");
|
|
14293
14957
|
let manifest = null;
|
|
14294
14958
|
let online = false;
|
|
14295
14959
|
try {
|
|
@@ -14304,11 +14968,11 @@ async function ensureMadData(opts, deps) {
|
|
|
14304
14968
|
warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
|
|
14305
14969
|
}
|
|
14306
14970
|
if (!manifest) {
|
|
14307
|
-
if (!
|
|
14971
|
+
if (!existsSync23(snapshotPath)) {
|
|
14308
14972
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
14309
14973
|
}
|
|
14310
14974
|
try {
|
|
14311
|
-
manifest = validateManifest(JSON.parse(await
|
|
14975
|
+
manifest = validateManifest(JSON.parse(await readFile9(snapshotPath, "utf8")));
|
|
14312
14976
|
} catch {
|
|
14313
14977
|
throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
14314
14978
|
}
|
|
@@ -14319,14 +14983,14 @@ async function ensureMadData(opts, deps) {
|
|
|
14319
14983
|
}
|
|
14320
14984
|
}
|
|
14321
14985
|
const version = manifest.version;
|
|
14322
|
-
const verDir =
|
|
14323
|
-
const poolPath =
|
|
14986
|
+
const verDir = join27(cacheRoot, `v${version}`);
|
|
14987
|
+
const poolPath = join27(verDir, "mad_pool.json");
|
|
14324
14988
|
const poolMeta = manifest.datasets.mad_pool;
|
|
14325
14989
|
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
14326
14990
|
const needDownload = !!opts.refresh || !cacheValid;
|
|
14327
14991
|
if (needDownload) {
|
|
14328
14992
|
if (!online) {
|
|
14329
|
-
if (
|
|
14993
|
+
if (existsSync23(poolPath)) {
|
|
14330
14994
|
throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
14331
14995
|
}
|
|
14332
14996
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
@@ -14350,7 +15014,7 @@ async function ensureMadData(opts, deps) {
|
|
|
14350
15014
|
}
|
|
14351
15015
|
let pool;
|
|
14352
15016
|
try {
|
|
14353
|
-
pool = JSON.parse(await
|
|
15017
|
+
pool = JSON.parse(await readFile9(poolPath, "utf8"));
|
|
14354
15018
|
if (!Array.isArray(pool))
|
|
14355
15019
|
throw new Error("mad_pool 非数组");
|
|
14356
15020
|
} catch (e) {
|
|
@@ -14361,11 +15025,11 @@ async function ensureMadData(opts, deps) {
|
|
|
14361
15025
|
|
|
14362
15026
|
// src/lib/mad/pool.ts
|
|
14363
15027
|
import { gunzipSync } from "node:zlib";
|
|
14364
|
-
import { mkdir as mkdir12, readFile as
|
|
14365
|
-
import { existsSync as
|
|
14366
|
-
import { join as
|
|
15028
|
+
import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
|
|
15029
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
15030
|
+
import { join as join28 } from "node:path";
|
|
14367
15031
|
function shardPath(verDir, shard) {
|
|
14368
|
-
return
|
|
15032
|
+
return join28(verDir, "ir", `${shard}.json.gz`);
|
|
14369
15033
|
}
|
|
14370
15034
|
function decodeShard(gz) {
|
|
14371
15035
|
const json = gunzipSync(gz).toString("utf8");
|
|
@@ -14382,9 +15046,9 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14382
15046
|
if (cached)
|
|
14383
15047
|
return cached;
|
|
14384
15048
|
const path = shardPath(verDir, shard);
|
|
14385
|
-
if (
|
|
15049
|
+
if (existsSync24(path)) {
|
|
14386
15050
|
try {
|
|
14387
|
-
const s2 = decodeShard(await
|
|
15051
|
+
const s2 = decodeShard(await readFile10(path));
|
|
14388
15052
|
memo.set(shard, s2);
|
|
14389
15053
|
return s2;
|
|
14390
15054
|
} catch {
|
|
@@ -14400,7 +15064,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14400
15064
|
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
14401
15065
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
14402
15066
|
const s = decodeShard(buf);
|
|
14403
|
-
await mkdir12(
|
|
15067
|
+
await mkdir12(join28(verDir, "ir"), { recursive: true });
|
|
14404
15068
|
await atomicWrite(path, buf);
|
|
14405
15069
|
memo.set(shard, s);
|
|
14406
15070
|
return s;
|
|
@@ -14414,7 +15078,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14414
15078
|
return ir;
|
|
14415
15079
|
},
|
|
14416
15080
|
shardCached(shard) {
|
|
14417
|
-
return memo.has(shard) ||
|
|
15081
|
+
return memo.has(shard) || existsSync24(shardPath(verDir, shard));
|
|
14418
15082
|
}
|
|
14419
15083
|
};
|
|
14420
15084
|
}
|
|
@@ -14498,7 +15162,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14498
15162
|
if (!inputArg)
|
|
14499
15163
|
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
14500
15164
|
const dirAbs = resolve13(inputArg);
|
|
14501
|
-
if (!
|
|
15165
|
+
if (!existsSync25(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
14502
15166
|
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
14503
15167
|
}
|
|
14504
15168
|
const { videos, orientation, skipped } = scanFolder(dirAbs, {
|
|
@@ -14612,10 +15276,10 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14612
15276
|
const header = madHeader(version, now.toISOString());
|
|
14613
15277
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
14614
15278
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
14615
|
-
const outDir = opts.out ? resolve13(opts.out) :
|
|
15279
|
+
const outDir = opts.out ? resolve13(opts.out) : join29(process.cwd(), `mad-${timestamp4(now)}`);
|
|
14616
15280
|
await mkdir13(outDir, { recursive: true });
|
|
14617
|
-
const jsxPath =
|
|
14618
|
-
await
|
|
15281
|
+
const jsxPath = join29(outDir, "mad.jsx");
|
|
15282
|
+
await writeFile12(jsxPath, jsx);
|
|
14619
15283
|
const result = {
|
|
14620
15284
|
ok: true,
|
|
14621
15285
|
tool: "mad",
|
|
@@ -14626,7 +15290,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14626
15290
|
degradeLevel: level,
|
|
14627
15291
|
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
14628
15292
|
};
|
|
14629
|
-
await
|
|
15293
|
+
await writeFile12(join29(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
14630
15294
|
warn(completionMessage(jsxPath, level));
|
|
14631
15295
|
return result;
|
|
14632
15296
|
}
|
|
@@ -14759,9 +15423,9 @@ async function runMadInTool(inputArg, opts) {
|
|
|
14759
15423
|
}
|
|
14760
15424
|
|
|
14761
15425
|
// src/commands/transcript.ts
|
|
14762
|
-
import { existsSync as
|
|
14763
|
-
import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as
|
|
14764
|
-
import { basename as
|
|
15426
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
15427
|
+
import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile13 } from "node:fs/promises";
|
|
15428
|
+
import { basename as basename15, dirname as dirname13, extname as extname8, join as join30, resolve as resolve14 } from "node:path";
|
|
14765
15429
|
|
|
14766
15430
|
// src/lib/transcript.ts
|
|
14767
15431
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -14898,7 +15562,7 @@ async function validateTranscriptInput(input) {
|
|
|
14898
15562
|
throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
|
|
14899
15563
|
}
|
|
14900
15564
|
const inputAbs = resolve14(input);
|
|
14901
|
-
if (!
|
|
15565
|
+
if (!existsSync26(inputAbs))
|
|
14902
15566
|
throw new Error(`本地视频不存在:${inputAbs}`);
|
|
14903
15567
|
const info = await stat6(inputAbs);
|
|
14904
15568
|
if (!info.isFile())
|
|
@@ -14910,17 +15574,17 @@ async function validateTranscriptInput(input) {
|
|
|
14910
15574
|
return inputAbs;
|
|
14911
15575
|
}
|
|
14912
15576
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
14913
|
-
const base =
|
|
14914
|
-
const output = out ? resolve14(out) :
|
|
15577
|
+
const base = basename15(inputAbs, extname8(inputAbs));
|
|
15578
|
+
const output = out ? resolve14(out) : join30(dirname13(inputAbs), `${base}-transcript.md`);
|
|
14915
15579
|
if (extname8(output).toLowerCase() !== ".md")
|
|
14916
15580
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
14917
15581
|
return output;
|
|
14918
15582
|
}
|
|
14919
15583
|
async function writeMarkdownAtomic(path, markdown) {
|
|
14920
|
-
await mkdir14(
|
|
15584
|
+
await mkdir14(dirname13(path), { recursive: true });
|
|
14921
15585
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
14922
15586
|
try {
|
|
14923
|
-
await
|
|
15587
|
+
await writeFile13(temp, markdown, "utf8");
|
|
14924
15588
|
await rename3(temp, path);
|
|
14925
15589
|
} finally {
|
|
14926
15590
|
await rm2(temp, { force: true });
|
|
@@ -14933,8 +15597,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
14933
15597
|
const output = resolveTranscriptOutput(inputAbs, opts.out);
|
|
14934
15598
|
const deps = buildDeps(depsOverride);
|
|
14935
15599
|
const language = opts.lang?.trim() || "zh-CN";
|
|
14936
|
-
const sourceName =
|
|
14937
|
-
const title =
|
|
15600
|
+
const sourceName = basename15(inputAbs);
|
|
15601
|
+
const title = basename15(inputAbs, extname8(inputAbs));
|
|
14938
15602
|
log.step(`▶ 视频转文字稿:${sourceName}`);
|
|
14939
15603
|
log.step("① 本地探测视频…");
|
|
14940
15604
|
const geometry = deps.probe(inputAbs, opts.ffmpegPath);
|
|
@@ -14946,7 +15610,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
14946
15610
|
log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
|
|
14947
15611
|
const audio = await deps.extract(inputAbs, opts.ffmpegPath);
|
|
14948
15612
|
deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
|
|
14949
|
-
log.info(`上传物:${
|
|
15613
|
+
log.info(`上传物:${basename15(audio)}(仅音频衍生物)`);
|
|
14950
15614
|
log.step("③ 上传音频并提交 ASR…");
|
|
14951
15615
|
const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
|
|
14952
15616
|
const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE4, payload, {
|
|
@@ -14994,9 +15658,9 @@ function registerTranscript(program2) {
|
|
|
14994
15658
|
}
|
|
14995
15659
|
|
|
14996
15660
|
// src/commands/music-visualizer.ts
|
|
14997
|
-
import { resolve as resolve15, join as
|
|
14998
|
-
import { mkdir as mkdir15, writeFile as
|
|
14999
|
-
import { existsSync as
|
|
15661
|
+
import { resolve as resolve15, join as join31, dirname as dirname14, basename as basename16, extname as extname9 } from "node:path";
|
|
15662
|
+
import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
|
|
15663
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
15000
15664
|
var TASK_TYPE5 = "music_visualizer";
|
|
15001
15665
|
var PRICE_KEY2 = "music_visualizer";
|
|
15002
15666
|
var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
@@ -15044,7 +15708,7 @@ function timestamp5() {
|
|
|
15044
15708
|
return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
15045
15709
|
}
|
|
15046
15710
|
function assertExt(pathAbs, exts, label) {
|
|
15047
|
-
if (!
|
|
15711
|
+
if (!existsSync27(pathAbs))
|
|
15048
15712
|
throw new Error(`${label}不存在:${pathAbs}`);
|
|
15049
15713
|
const e = extname9(pathAbs).toLowerCase();
|
|
15050
15714
|
if (exts.length && !exts.includes(e)) {
|
|
@@ -15113,9 +15777,9 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15113
15777
|
if (coverAbs)
|
|
15114
15778
|
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
15115
15779
|
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
15116
|
-
const projName =
|
|
15117
|
-
const outDir = resolve15(opts.out ??
|
|
15118
|
-
log.step(`▶ 音乐可视化:${
|
|
15780
|
+
const projName = basename16(audioAbs, extname9(audioAbs));
|
|
15781
|
+
const outDir = resolve15(opts.out ?? join31(dirname14(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
15782
|
+
log.step(`▶ 音乐可视化:${basename16(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
15119
15783
|
let billingHint;
|
|
15120
15784
|
try {
|
|
15121
15785
|
billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
|
|
@@ -15161,7 +15825,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15161
15825
|
const { taskId } = submitted;
|
|
15162
15826
|
log.info(`task_id = ${taskId}`);
|
|
15163
15827
|
await mkdir15(outDir, { recursive: true });
|
|
15164
|
-
await
|
|
15828
|
+
await writeFile14(join31(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
|
|
15165
15829
|
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
15166
15830
|
const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
|
|
15167
15831
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -15172,7 +15836,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15172
15836
|
const files = [];
|
|
15173
15837
|
const errors = {};
|
|
15174
15838
|
if (url) {
|
|
15175
|
-
const dest =
|
|
15839
|
+
const dest = join31(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
15176
15840
|
try {
|
|
15177
15841
|
await downloadStream(url, dest);
|
|
15178
15842
|
files.push(dest);
|
|
@@ -15194,7 +15858,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15194
15858
|
...Object.keys(errors).length ? { errors } : {},
|
|
15195
15859
|
finishedAt: new Date().toISOString()
|
|
15196
15860
|
};
|
|
15197
|
-
await
|
|
15861
|
+
await writeFile14(join31(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
15198
15862
|
if (opts.json) {
|
|
15199
15863
|
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
15200
15864
|
`);
|
|
@@ -15207,12 +15871,121 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
15207
15871
|
log.ok(`完成。产物目录:${outDir}`);
|
|
15208
15872
|
}
|
|
15209
15873
|
|
|
15874
|
+
// src/commands/deps.ts
|
|
15875
|
+
import { existsSync as existsSync28, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
|
|
15876
|
+
import { join as join32 } from "node:path";
|
|
15877
|
+
function registerDeps(program2) {
|
|
15878
|
+
const deps = program2.command("deps").description("运行时资产:查看状态 / 显式安装 ffmpeg 与渲染字体");
|
|
15879
|
+
deps.command("status").description("查看 ffmpeg / 字体的当前来源、版本与授权信息").option("--ffmpeg-path <dir>", "显式 ffmpeg/ffprobe 目录").action(async (opts) => {
|
|
15880
|
+
await runStatus2(opts.ffmpegPath);
|
|
15881
|
+
});
|
|
15882
|
+
deps.command("install").description("从同合云镜像安装运行时资产(已存在则跳过)").option("--ffmpeg", "只装 ffmpeg/ffprobe").option("--font", "只装渲染字体").option("--force", "已存在也覆盖重装").action(async (opts) => {
|
|
15883
|
+
await runInstall(opts);
|
|
15884
|
+
});
|
|
15885
|
+
}
|
|
15886
|
+
function fmtMB(n) {
|
|
15887
|
+
return `${(n / 1048576).toFixed(1)} MB`;
|
|
15888
|
+
}
|
|
15889
|
+
async function runStatus2(ffmpegPath) {
|
|
15890
|
+
log.step("① ffmpeg / ffprobe");
|
|
15891
|
+
const res = resolveFfmpeg(ffmpegPath);
|
|
15892
|
+
if (res) {
|
|
15893
|
+
log.ok(`来源:${res.source}`);
|
|
15894
|
+
try {
|
|
15895
|
+
const cap = probeCapabilities(res);
|
|
15896
|
+
log.info(cap.version);
|
|
15897
|
+
log.info(`libx264 ${cap.hasLibx264 ? "✅" : "❌"} ass 滤镜 ${cap.filters.has("ass") ? "✅" : "❌"}`);
|
|
15898
|
+
} catch {
|
|
15899
|
+
log.warn("二进制存在但探测能力失败");
|
|
15900
|
+
}
|
|
15901
|
+
} else {
|
|
15902
|
+
log.warn(`未找到。装它:gtrk deps install --ffmpeg`);
|
|
15903
|
+
}
|
|
15904
|
+
log.step("② 渲染字体");
|
|
15905
|
+
const fdir = fontsDir();
|
|
15906
|
+
const fonts = existsSync28(fdir) ? readdirSync4(fdir).filter((f) => /\.(otf|ttf|ttc|otc)$/i.test(f)) : [];
|
|
15907
|
+
if (fonts.length) {
|
|
15908
|
+
log.ok(`${fdir}(${fonts.length} 个)`);
|
|
15909
|
+
for (const f of fonts)
|
|
15910
|
+
log.info(`${f} ${fmtMB(statSync4(join32(fdir, f)).size)}`);
|
|
15911
|
+
log.info("经 ass 滤镜 fontsdir 供给 libass,未安装进系统字体表");
|
|
15912
|
+
} else {
|
|
15913
|
+
log.warn(`${fdir} 无字体。装它:gtrk deps install --font`);
|
|
15914
|
+
log.info("(系统已装所需字体时也可直接用,两处任一命中即可)");
|
|
15915
|
+
}
|
|
15916
|
+
log.step("③ 分发物授权与源码");
|
|
15917
|
+
let m = null;
|
|
15918
|
+
try {
|
|
15919
|
+
m = await fetchManifest();
|
|
15920
|
+
} catch (e) {
|
|
15921
|
+
log.warn(`取 manifest 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
15922
|
+
}
|
|
15923
|
+
if (m) {
|
|
15924
|
+
let key = "";
|
|
15925
|
+
try {
|
|
15926
|
+
key = resolvePlatformKey();
|
|
15927
|
+
} catch {}
|
|
15928
|
+
for (const [k, v] of Object.entries(m.ffmpeg)) {
|
|
15929
|
+
const mark = k === key ? "→" : " ";
|
|
15930
|
+
log.info(`${mark} ${k.padEnd(10)} ${v.version.padEnd(20)} ${v.license} 源码 ${v.source}`);
|
|
15931
|
+
}
|
|
15932
|
+
for (const [name, v] of Object.entries(m.font)) {
|
|
15933
|
+
log.info(` 字体 ${name} ${v.license}`);
|
|
15934
|
+
}
|
|
15935
|
+
log.info(`合规说明:${m.base}/SOURCE.md`);
|
|
15936
|
+
log.info("分发不附加任何使用限制(不限 gtrk 用户、不禁转发)");
|
|
15937
|
+
}
|
|
15938
|
+
}
|
|
15939
|
+
async function runInstall(opts) {
|
|
15940
|
+
const doFfmpeg = opts.ffmpeg || !opts.font;
|
|
15941
|
+
const doFont = opts.font || !opts.ffmpeg;
|
|
15942
|
+
log.step(`读 manifest:${MANIFEST_URL}`);
|
|
15943
|
+
const m = await fetchManifest();
|
|
15944
|
+
log.info(`schema ${m.schema},生成于 ${m.generated}`);
|
|
15945
|
+
let lastPct = -1;
|
|
15946
|
+
let lastAt = 0;
|
|
15947
|
+
const onProgress = (got, total) => {
|
|
15948
|
+
if (total <= 0)
|
|
15949
|
+
return;
|
|
15950
|
+
const pct = Math.floor(got / total * 100);
|
|
15951
|
+
const now = Date.now();
|
|
15952
|
+
if (pct < 100 && pct - lastPct < 5 && now - lastAt < 2000)
|
|
15953
|
+
return;
|
|
15954
|
+
lastPct = pct;
|
|
15955
|
+
lastAt = now;
|
|
15956
|
+
log.tick(`下载 ${fmtMB(got)} / ${fmtMB(total)}(${pct}%)`);
|
|
15957
|
+
};
|
|
15958
|
+
if (doFfmpeg) {
|
|
15959
|
+
const key = resolvePlatformKey();
|
|
15960
|
+
log.step(`① ffmpeg(${key})`);
|
|
15961
|
+
const r = await installFfmpeg(m, { force: opts.force, onProgress });
|
|
15962
|
+
log.tickEnd();
|
|
15963
|
+
if (r.installed)
|
|
15964
|
+
log.ok(`已安装 ${r.detail}`);
|
|
15965
|
+
else
|
|
15966
|
+
log.info(`跳过(${r.reason}):${r.detail}`);
|
|
15967
|
+
}
|
|
15968
|
+
if (doFont) {
|
|
15969
|
+
log.step("② 渲染字体");
|
|
15970
|
+
const rs = await installFont(m, { force: opts.force, onProgress });
|
|
15971
|
+
log.tickEnd();
|
|
15972
|
+
for (const r of rs) {
|
|
15973
|
+
if (r.installed)
|
|
15974
|
+
log.ok(`已安装 ${r.detail}`);
|
|
15975
|
+
else
|
|
15976
|
+
log.info(`跳过(${r.reason}):${r.detail}`);
|
|
15977
|
+
}
|
|
15978
|
+
log.info("未写系统字体表、未改注册表——烧录时经 ass 滤镜 fontsdir 供给");
|
|
15979
|
+
}
|
|
15980
|
+
log.ok(`完成。二进制在 ${ffmpegDir()},字体在 ${fontsDir()}`);
|
|
15981
|
+
}
|
|
15982
|
+
|
|
15210
15983
|
// src/index.ts
|
|
15211
15984
|
try {
|
|
15212
15985
|
process.loadEnvFile?.();
|
|
15213
15986
|
} catch {}
|
|
15214
15987
|
migrateLegacyHome();
|
|
15215
|
-
var { version } = JSON.parse(readFileSync6(
|
|
15988
|
+
var { version } = JSON.parse(readFileSync6(join33(packageRoot(), "package.json"), "utf8"));
|
|
15216
15989
|
var program2 = new Command;
|
|
15217
15990
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
15218
15991
|
registerInstall(program2);
|
|
@@ -15230,6 +16003,7 @@ registerMg(program2);
|
|
|
15230
16003
|
registerTool(program2);
|
|
15231
16004
|
registerTranscript(program2);
|
|
15232
16005
|
registerMusicVisualizer(program2);
|
|
16006
|
+
registerDeps(program2);
|
|
15233
16007
|
program2.parseAsync(process.argv).catch((e) => {
|
|
15234
16008
|
console.error(`
|
|
15235
16009
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|