Files
yaoyaoai/scripts/nodejs/infinitetalk_api_generate.js
fengchuanhn@gmail.com 1325ff92e5 1
2026-05-18 18:40:03 +08:00

242 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* 调用本地 InfiniteTalk FastAPIpython-runtime/main.py
* 配置INFINITETALK_API_URL默认 http://127.0.0.1:8765
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
const http = require("http");
const https = require("https");
const desktopConfig = require("./desktop_config.js");
const POLL_MS = 3000;
const MAX_WAIT_MS = 60 * 60 * 1000;
function apiBase() {
const raw =
desktopConfig.get("INFINITETALK_API_URL") ||
desktopConfig.get("DIGITAL_HUMAN_API_URL") ||
"http://127.0.0.1:8765";
return String(raw).trim().replace(/\/+$/, "");
}
function requestJson(method, urlPath, body, headers = {}) {
return new Promise((resolve, reject) => {
const base = apiBase();
const url = new URL(urlPath.startsWith("http") ? urlPath : `${base}${urlPath}`);
const lib = url.protocol === "https:" ? https : http;
const opts = {
method,
hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: url.pathname + url.search,
headers,
};
const req = lib.request(opts, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
let data;
try {
data = text ? JSON.parse(text) : {};
} catch {
data = { raw: text };
}
if (res.statusCode >= 400) {
reject(
new Error(
data.detail?.message ||
data.message ||
data.detail ||
`HTTP ${res.statusCode}: ${text.slice(0, 300)}`,
),
);
return;
}
resolve(data);
});
});
req.on("error", reject);
if (body) req.write(body);
req.end();
});
}
function postMultipart(urlPath, fields, files) {
return new Promise((resolve, reject) => {
const boundary = `----Aiclient${Date.now()}`;
const parts = [];
for (const [name, value] of Object.entries(fields)) {
parts.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`,
),
);
}
for (const { name, filePath, filename, contentType } of files) {
const data = fs.readFileSync(filePath);
parts.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"; filename="${filename}"\r\nContent-Type: ${contentType}\r\n\r\n`,
),
);
parts.push(data);
parts.push(Buffer.from("\r\n"));
}
parts.push(Buffer.from(`--${boundary}--\r\n`));
const body = Buffer.concat(parts);
const base = apiBase();
const url = new URL(urlPath.startsWith("http") ? urlPath : `${base}${urlPath}`);
const lib = url.protocol === "https:" ? https : http;
const opts = {
method: "POST",
hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: url.pathname + url.search,
headers: {
"Content-Type": `multipart/form-data; boundary=${boundary}`,
"Content-Length": body.length,
},
};
const req = lib.request(opts, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
let data;
try {
data = text ? JSON.parse(text) : {};
} catch {
data = { raw: text };
}
if (res.statusCode >= 400) {
reject(
new Error(
data.detail?.message ||
data.message ||
`HTTP ${res.statusCode}: ${text.slice(0, 300)}`,
),
);
return;
}
resolve(data);
});
});
req.on("error", reject);
req.write(body);
req.end();
});
}
function downloadFile(urlPath, destPath) {
return new Promise((resolve, reject) => {
const base = apiBase();
const url = new URL(urlPath.startsWith("http") ? urlPath : `${base}${urlPath}`);
const lib = url.protocol === "https:" ? https : http;
const file = fs.createWriteStream(destPath);
lib
.get(url.href, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
file.close();
return downloadFile(res.headers.location, destPath).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
reject(new Error(`下载失败 HTTP ${res.statusCode}`));
return;
}
res.pipe(file);
file.on("finish", () => file.close(() => resolve(destPath)));
})
.on("error", reject);
});
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function guessMime(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (ext === ".mp3") return "audio/mpeg";
if (ext === ".wav") return "audio/wav";
if (ext === ".mov") return "video/quicktime";
return "video/mp4";
}
globalThis.__nodejsMain = async function (params) {
const audioPath = String(params?.audioPath || "").trim();
const avatarVideoPath = String(params?.avatarVideoPath || "").trim();
if (!audioPath || !fs.existsSync(audioPath)) {
return { success: false, error: "缺少或找不到音频文件" };
}
if (!avatarVideoPath || !fs.existsSync(avatarVideoPath)) {
return { success: false, error: "缺少或找不到形象视频" };
}
try {
const health = await requestJson("GET", "/health");
if (!health.ok) {
return {
success: false,
error: `InfiniteTalk 服务未就绪: ${(health.issues || []).join("; ")}`,
};
}
} catch (e) {
return {
success: false,
error: `无法连接 InfiniteTalk API (${apiBase()}): ${e.message}`,
};
}
const create = await postMultipart(
"/api/v1/talking-video",
{ prompt: String(params?.prompt || "") },
[
{
name: "video",
filePath: avatarVideoPath,
filename: path.basename(avatarVideoPath),
contentType: guessMime(avatarVideoPath),
},
{
name: "audio",
filePath: audioPath,
filename: path.basename(audioPath),
contentType: guessMime(audioPath),
},
],
);
const jobId = create.job_id;
if (!jobId) {
return { success: false, error: "未返回 job_id" };
}
const started = Date.now();
while (Date.now() - started < MAX_WAIT_MS) {
const st = await requestJson("GET", `/api/v1/talking-video/${jobId}`);
if (st.status === "succeeded") {
const outDir = path.join(os.tmpdir(), "aiclient-infinitetalk");
fs.mkdirSync(outDir, { recursive: true });
const dest = path.join(outDir, `talking_${jobId}.mp4`);
await downloadFile(`/api/v1/talking-video/${jobId}/download`, dest);
return { success: true, videoPath: dest };
}
if (st.status === "failed") {
return {
success: false,
error: String(st.message || "InfiniteTalk 任务失败"),
};
}
await sleep(POLL_MS);
}
return { success: false, error: "InfiniteTalk 生成超时" };
};