Files
yaoyaoai/scripts/nodejs/douyin_pipeline.js
fengchuanhn@gmail.com 3db327d93b 22
2026-05-17 12:54:13 +08:00

226 lines
7.0 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");
const log = (level, msg, fields) =>
globalThis.__native.log(level, msg, fields == null ? null : fields);
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.slice(0, 80) });
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
);
};