22
This commit is contained in:
225
scripts/nodejs/douyin_pipeline.js
Normal file
225
scripts/nodejs/douyin_pipeline.js
Normal file
@@ -0,0 +1,225 @@
|
||||
"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: "未配置阿里云 OSS(bucket / 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
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user