226 lines
7.0 KiB
JavaScript
226 lines
7.0 KiB
JavaScript
"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
|
||
);
|
||
};
|