Files
yaoyaoai/scripts/nodejs/douyin_pipeline.js
fengchuanhn@gmail.com caaa898d90 1
2026-05-18 00:27:16 +08:00

242 lines
7.6 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";
/**
* douyin_pipeline.js — Node 完整仿写流水线
* 对齐 quickjs/douyin_pipeline.js 与 Electron VideoRewriteIntegration.processDouyinShareComplete
*
* 由后端 `/api/v1/nodejs-scripts` 下发Tauri 调用 run_nodejs_script → __nodejsMain(params)
*/
const { DouyinContentParser } = require("./douyin_content_parser.js");
const { DouyinVideoDownloader } = require("./douyin_video_downloader.js");
const { VideoRewriteOptimizer } = require("./video_rewrite_optimizer.js");
const pipelineNative = require("./pipeline_native.js");
const desktopConfig = require("./desktop_config.js");
/**
* Tauri 子进程内请用 __native.logstderr __EVT__ → Rust → nodejs:event
* console.* 被管道接管,不会出现在 tauri dev 终端;仅在没有 __native 时回退到控制台。
*/
const log = (level, msg, fields) => {
if (globalThis.__native?.log) {
globalThis.__native.log(level, msg, fields == null ? null : fields);
return;
}
const tag = `[pipeline.${level}] ${msg}`;
if (fields != null) {
if (level === "error") console.error(tag, fields);
else if (level === "warn") console.warn(tag, fields);
else console.log(tag, fields);
} else if (level === "error") console.error(tag);
else if (level === "warn") console.warn(tag);
else console.log(tag);
};
const progress = (s) => {
try {
globalThis.__native.emitProgress(s);
} catch (_) {
/* ignore */
}
};
async function rewriteWithAI(content, modelConfig, rewriteConfig, customPrompt) {
if (!modelConfig?.apiUrl || !modelConfig?.apiKey) {
log("error", "rewrite.missingConfig");
return null;
}
const prompt = VideoRewriteOptimizer.buildRewritePrompt(
content,
rewriteConfig || {},
customPrompt
);
const result = await pipelineNative.openaiChat({
apiUrl: modelConfig.apiUrl,
apiKey: modelConfig.apiKey,
model: modelConfig.modelId,
messages: [
{ role: "system", content: "你是专业的短视频文案改写助手。" },
{ role: "user", content: prompt },
],
temperature:
typeof modelConfig.temperature === "number" ? modelConfig.temperature : 0.8,
max_tokens: modelConfig.maxTokens || 2000,
});
if (!result?.success) {
log("error", "rewrite.failed", { error: result?.error });
return null;
}
return result.content;
}
/**
* 完整流程:解析 → Puppeteer/直链下载 → ffmpeg 提音频 → ASR → AI 仿写
*/
async function processDouyinShareComplete(
shareText,
modelConfig,
rewriteConfig,
customPrompt
) {
let videoPath = null;
let audioPath = null;
try {
log("info", "pipeline.start", {
inputLength: shareText?.length,
asrMode: modelConfig?.asrMode,
});
progress("正在解析视频URL...");
const parseResult = DouyinContentParser.parseContent(shareText);
if (!parseResult.success || !parseResult.content) {
return { success: false, error: parseResult.error || "内容解析失败" };
}
const { videoUrl, hashtags } = parseResult.content;
log("info", "pipeline.urlExtracted", { videoUrl });
progress("正在下载视频...");
const downloader = new DouyinVideoDownloader();
const downloadResult = DouyinContentParser.isDirectVideoUrl(videoUrl)
? await downloader.downloadDirectVideo(videoUrl)
: await downloader.downloadDouyinVideo(videoUrl);
if (!downloadResult.success || !downloadResult.videoPath) {
return {
success: false,
error: downloadResult.error || "视频下载失败",
};
}
videoPath = downloadResult.videoPath;
log("info", "pipeline.videoDownloaded", { videoPath });
progress("视频下载完成,正在提取音频...");
try {
audioPath = await pipelineNative.ffmpegExtractAudio(videoPath);
} catch (e) {
return {
success: false,
error: `音频提取失败: ${e.message || e}`,
};
}
log("info", "pipeline.audioExtracted", { audioPath });
const asrMode = modelConfig?.asrMode || "online";
let recognizedText = "";
if (asrMode === "online") {
progress("音频提取完成正在上传到OSS...");
const ossConfig = modelConfig.ossConfig;
if (!ossConfig?.bucket) {
return { success: false, error: "未配置阿里云 OSSbucket / region / key" };
}
const upload = await pipelineNative.aliyunOssUpload(audioPath, ossConfig);
if (!upload?.success || !upload.url) {
return {
success: false,
error: `音频上传失败: ${upload?.error || "unknown"}`,
};
}
log("info", "pipeline.ossOk", { audioUrl: upload.url });
progress("音频上传完成,正在识别文案(在线模式)...");
const asr = await pipelineNative.aliyunAsrFiletrans(upload.url, {
apiKey: modelConfig.aliyunApiKey,
model: "qwen3-asr-flash-filetrans",
});
if (!asr?.success) {
return {
success: false,
error: `语音识别失败(在线模式): ${asr?.error || "unknown"}`,
};
}
recognizedText = asr.text || "";
} else {
progress("音频提取完成,正在识别文案(本地模式)...");
const info = modelConfig.asrServerInfo;
if (!info) {
return {
success: false,
error: "未配置本地语音识别服务器,请在 .env 中配置 VITE_ASR_SERVER_URL",
};
}
const asr = await pipelineNative.localAsr(audioPath, info);
if (!asr?.success) {
return {
success: false,
error: `语音识别失败(本地模式): ${asr?.error || "unknown"}`,
};
}
recognizedText = asr.text || "";
}
if (!modelConfig?.apiUrl || !modelConfig?.apiKey) {
return { success: false, error: "未配置AI模型请先在 .env 中配置" };
}
progress("文案识别完成,正在改写...");
const rewrittenDescription = await rewriteWithAI(
recognizedText,
modelConfig,
rewriteConfig,
customPrompt
);
if (!rewrittenDescription) {
return {
success: false,
error: "AI仿写失败请检查模型配置和网络连接",
};
}
const rewrittenTitle =
DouyinContentParser.generateTitleFromDescription(rewrittenDescription);
const keepTags = rewriteConfig?.keepHashtags !== false;
const fullContent =
keepTags && hashtags?.length
? `${rewrittenDescription}\n\n${hashtags.join(" ")}`
: rewrittenDescription;
progress("完成!");
log("info", "pipeline.success", {
originalLength: recognizedText.length,
rewrittenLength: rewrittenDescription.length,
});
return {
success: true,
original: {
videoUrl,
description: recognizedText,
hashtags: hashtags || [],
title: "视频音频识别结果",
},
rewritten: {
description: rewrittenDescription,
title: rewrittenTitle,
fullContent,
},
};
} catch (e) {
const msg = (e && (e.stack || e.message)) || String(e);
log("error", "pipeline.error", { message: msg });
return { success: false, error: msg };
} finally {
if (videoPath) pipelineNative.deleteFile(videoPath);
if (audioPath) pipelineNative.deleteFile(audioPath);
}
}
globalThis.__nodejsMain = async function (params) {
const p = params || {};
const modelConfig = desktopConfig.buildModelConfig();
const check = desktopConfig.validateModelConfig(modelConfig);
if (!check.ok) {
return { success: false, error: check.error };
}
return processDouyinShareComplete(
p.shareText || p.share_text || "",
modelConfig,
p.rewriteConfig,
p.customPrompt || null
);
};