diff --git a/src-tauri/src/asr.rs b/src-tauri/src/asr.rs index 8326e97..b491c4b 100644 --- a/src-tauri/src/asr.rs +++ b/src-tauri/src/asr.rs @@ -70,11 +70,19 @@ pub async fn transcribe( .timeout(Duration::from_secs(60)) .build()?; - // 1) submit + // 1) submit — filetrans 用 file_url,其它模型用 file_urls + let input = if model == "qwen3-asr-flash-filetrans" { + json!({ "file_url": audio_url }) + } else { + json!({ "file_urls": [audio_url] }) + }; let submit_body = json!({ "model": model, - "input": { "file_urls": [audio_url] }, - "parameters": {} + "input": input, + "parameters": { + "channel_id": [0], + "enable_itn": false + } }); let submit: Value = client .post(SUBMIT_URL) @@ -124,10 +132,15 @@ pub async fn transcribe( return Err(AsrError::Timeout); } - // 3) fetch transcription file + // 3) fetch transcription file (filetrans: output.result; batch: output.results[0]) let trans_url = poll_resp - .pointer("/output/results/0/transcription_url") + .pointer("/output/result/transcription_url") .and_then(Value::as_str) + .or_else(|| { + poll_resp + .pointer("/output/results/0/transcription_url") + .and_then(Value::as_str) + }) .ok_or_else(|| AsrError::Api(format!("no transcription_url, raw={poll_resp}")))? .to_string(); let trans_json: Value = client.get(&trans_url).send().await?.json().await?; diff --git a/src/components/workflow/StepScriptEditor.vue b/src/components/workflow/StepScriptEditor.vue index d464f29..1ae1fc7 100644 --- a/src/components/workflow/StepScriptEditor.vue +++ b/src/components/workflow/StepScriptEditor.vue @@ -1,9 +1,23 @@ diff --git a/src/config/videoPipeline.js b/src/config/videoPipeline.js index d83386d..5ea3dd6 100644 --- a/src/config/videoPipeline.js +++ b/src/config/videoPipeline.js @@ -5,20 +5,12 @@ import { invoke } from "@tauri-apps/api/core"; -/** @returns {Promise<{ ok: true } | { ok: false, message: string }>} */ -export async function ensureAppConfigReady() { +/** @returns {Promise<{ ok: true, map: Record } | { ok: false, message: string }>} */ +export async function loadAppConfigMap() { try { const map = await invoke("get_app_config"); if (map && typeof map === "object" && Object.keys(map).length > 0) { - const apiKey =map.BAILIAN_API_KEY ; - if (!apiKey) { - return { - ok: false, - message: - "缺少 BAILIAN_API_KEY 配置管理 或侧栏「本地配置」中设置", - }; - } - return { ok: true }; + return { ok: true, map }; } const serverMsg = await invoke("get_app_config_last_message"); if (serverMsg) { @@ -26,7 +18,7 @@ export async function ensureAppConfigReady() { } return { ok: false, - message: "未获取到应用配置,请登录服务端或在「本地配置」中添加 LLM_API_KEY", + message: "未获取到应用配置,请登录服务端或在「本地配置」中添加密钥", }; } catch { return { @@ -36,6 +28,43 @@ export async function ensureAppConfigReady() { } } +/** 一键仿写 / 在线 ASR 等需百炼密钥 */ +export async function ensureAppConfigReady() { + const loaded = await loadAppConfigMap(); + if (!loaded.ok) return loaded; + const apiKey = loaded.map.BAILIAN_API_KEY || loaded.map.DASHSCOPE_API_KEY; + if (!apiKey) { + return { + ok: false, + message: + "缺少 BAILIAN_API_KEY(或 DASHSCOPE_API_KEY),请在配置管理或「本地配置」中设置", + }; + } + return { ok: true }; +} + +/** 撰写文案 / LLM 改写需文本模型配置 */ +export async function ensureLlmConfigReady() { + const loaded = await loadAppConfigMap(); + if (!loaded.ok) return loaded; + const { map } = loaded; + const apiKey = map.LLM_API_KEY || map.API_KEY; + const apiUrl = map.LLM_BASE_URL || map.LLM_API_URL; + if (!apiKey) { + return { + ok: false, + message: "缺少 LLM_API_KEY,请在配置管理或「本地配置」中设置文本模型", + }; + } + if (!apiUrl) { + return { + ok: false, + message: "缺少 LLM_BASE_URL(或 LLM_API_URL),请配置文本模型接口地址", + }; + } + return { ok: true }; +} + const DOUYIN_RE = /douyin\.com|iesdouyin\.com|v\.douyin\.com/i; const DIRECT_VIDEO_RE = /\.(mp4|mov|m4v|webm|mkv|flv|avi)(\?|$)/i; diff --git a/src/stores/workflow.js b/src/stores/workflow.js index a0e93b1..36348b8 100644 --- a/src/stores/workflow.js +++ b/src/stores/workflow.js @@ -8,6 +8,8 @@ import { ensureAppConfigReady, + ensureLlmConfigReady, + validateShareText, DEFAULT_REWRITE_CONFIG, @@ -17,6 +19,7 @@ import { const PIPELINE_SCRIPT = "douyin_pipeline.js"; +const SCRIPT_GENERATE = "script_generate.js"; const NODEJS_EVENT = "nodejs:event"; @@ -61,6 +64,8 @@ export const useWorkflowStore = defineStore("workflow", { videoRewriting: false, + scriptGenerating: false, + /** 流水线进度文案(来自 quickjs:event progress) */ rewriteProgress: "", @@ -315,7 +320,11 @@ export const useWorkflowStore = defineStore("workflow", { }, + async generateScript() { + return executeGenerateScript(this); + + }, resetAll() { @@ -431,7 +440,91 @@ export async function executeVideoRewrite(store) { } } +const DEFAULT_SCRIPT_PROMPT = `请基于以下视频原文案,创作一个新的仿写文案: +原文案:{{content}} + +要求: +1. 保持原文案的核心内容和风格 +2. 适当改变表达方式和用词 +3. 确保新文案通顺自然 +4. 长度约{{count}}字 + +请直接输出仿写后的文案:`; + +/** + * 构建撰写文案提示词(对齐 zhenqianba generateScriptFromVideoContent) + * @param {ReturnType} store + */ +export function buildScriptGeneratePrompt(store) { + const content = String(store.recognizedContent || "").trim(); + const count = store.editWordCount || 300; + let prompt = String(store.rewritePrompt || "").trim(); + if (!prompt) { + prompt = DEFAULT_SCRIPT_PROMPT; + } + prompt = prompt + .replace(/\{\{content\}\}/g, content) + .replace(/\{content\}/g, content) + .replace(/\{\{count\}\}/g, String(count)) + .replace(/\{count\}/g, String(count)); + + const lang = store.editLanguage || ""; + if (lang && lang !== "中文(普通话)") { + prompt = `请用${lang}输出文案。\n\n${prompt}`; + } + return prompt; +} + +/** + * 撰写文案:基于识别的原始内容调用 LLM(对齐 Electron ki) + * @param {ReturnType} store + */ +export async function executeGenerateScript(store) { + const content = String(store.recognizedContent || "").trim(); + if (!content) { + return { ok: false, message: "请先使用智能视频仿写功能识别视频内容" }; + } + + const cfgReady = await ensureLlmConfigReady(); + if (!cfgReady.ok) { + return { ok: false, message: cfgReady.message }; + } + + const userPrompt = buildScriptGeneratePrompt(store); + store.scriptGenerating = true; + + try { + const result = await invoke("run_nodejs_script", { + scriptName: SCRIPT_GENERATE, + params: { + messages: [ + { + role: "system", + content: + "你是专业的短视频文案改写助手。请把用户提供的文案改写,保持原文的风格和主题,但使用不同的表达方式。", + }, + { role: "user", content: userPrompt }, + ], + temperature: 0.8, + max_tokens: 2000, + }, + }); + + if (!result?.success || !result?.content) { + const err = result?.error || "文案仿写失败,请重试"; + return { ok: false, message: String(err) }; + } + + store.scriptContent = String(result.content).trim(); + return { ok: true, message: "文案仿写完成" }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err || "未知错误"); + return { ok: false, message: `文案仿写失败: ${msg}` }; + } finally { + store.scriptGenerating = false; + } +} const UNSUPPORTED_PLATFORM_RULES = [