This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 08:52:38 +08:00
parent 9edf656828
commit 27e83c564c
4 changed files with 182 additions and 20 deletions

View File

@@ -70,11 +70,19 @@ pub async fn transcribe(
.timeout(Duration::from_secs(60)) .timeout(Duration::from_secs(60))
.build()?; .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!({ let submit_body = json!({
"model": model, "model": model,
"input": { "file_urls": [audio_url] }, "input": input,
"parameters": {} "parameters": {
"channel_id": [0],
"enable_itn": false
}
}); });
let submit: Value = client let submit: Value = client
.post(SUBMIT_URL) .post(SUBMIT_URL)
@@ -124,10 +132,15 @@ pub async fn transcribe(
return Err(AsrError::Timeout); return Err(AsrError::Timeout);
} }
// 3) fetch transcription file // 3) fetch transcription file (filetrans: output.result; batch: output.results[0])
let trans_url = poll_resp let trans_url = poll_resp
.pointer("/output/results/0/transcription_url") .pointer("/output/result/transcription_url")
.and_then(Value::as_str) .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}")))? .ok_or_else(|| AsrError::Api(format!("no transcription_url, raw={poll_resp}")))?
.to_string(); .to_string();
let trans_json: Value = client.get(&trans_url).send().await?.json().await?; let trans_json: Value = client.get(&trans_url).send().await?.json().await?;

View File

@@ -1,9 +1,23 @@
<script setup> <script setup>
import { ref } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useWorkflowStore } from "../../stores/workflow.js"; import { useWorkflowStore } from "../../stores/workflow.js";
const workflow = useWorkflowStore(); const workflow = useWorkflowStore();
const { scriptContent, editLanguage, editWordCount } = storeToRefs(workflow); const { scriptContent, editLanguage, editWordCount, scriptGenerating } =
storeToRefs(workflow);
const feedback = ref({ severity: "", message: "" });
async function onGenerateScript() {
feedback.value = { severity: "", message: "" };
const result = await workflow.generateScript();
if (result.ok) {
feedback.value = { severity: "success", message: result.message };
} else {
feedback.value = { severity: "error", message: result.message };
}
}
</script> </script>
<template> <template>
@@ -34,10 +48,23 @@ const { scriptContent, editLanguage, editWordCount } = storeToRefs(workflow);
class="min-w-36 shrink-0 ml-10" class="min-w-36 shrink-0 ml-10"
/> />
</div> </div>
<div class="flex gap-2 pt-1"> <div class="flex flex-wrap items-center gap-2 pt-1">
<Button label="撰写文案" size="small" /> <Button
label="撰写文案"
size="small"
:loading="scriptGenerating"
:disabled="scriptGenerating"
@click="onGenerateScript"
/>
<Button label="AI法务" size="small" severity="secondary" /> <Button label="AI法务" size="small" severity="secondary" />
</div> </div>
<p
v-if="feedback.message"
class="text-sm"
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
>
{{ feedback.message }}
</p>
</div> </div>
</DashboardCard> </DashboardCard>
</template> </template>

View File

@@ -5,20 +5,12 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
/** @returns {Promise<{ ok: true } | { ok: false, message: string }>} */ /** @returns {Promise<{ ok: true, map: Record<string, string> } | { ok: false, message: string }>} */
export async function ensureAppConfigReady() { export async function loadAppConfigMap() {
try { try {
const map = await invoke("get_app_config"); const map = await invoke("get_app_config");
if (map && typeof map === "object" && Object.keys(map).length > 0) { if (map && typeof map === "object" && Object.keys(map).length > 0) {
const apiKey =map.BAILIAN_API_KEY ; return { ok: true, map };
if (!apiKey) {
return {
ok: false,
message:
"缺少 BAILIAN_API_KEY 配置管理 或侧栏「本地配置」中设置",
};
}
return { ok: true };
} }
const serverMsg = await invoke("get_app_config_last_message"); const serverMsg = await invoke("get_app_config_last_message");
if (serverMsg) { if (serverMsg) {
@@ -26,7 +18,7 @@ export async function ensureAppConfigReady() {
} }
return { return {
ok: false, ok: false,
message: "未获取到应用配置,请登录服务端或在「本地配置」中添加 LLM_API_KEY", message: "未获取到应用配置,请登录服务端或在「本地配置」中添加密钥",
}; };
} catch { } catch {
return { 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 DOUYIN_RE = /douyin\.com|iesdouyin\.com|v\.douyin\.com/i;
const DIRECT_VIDEO_RE = /\.(mp4|mov|m4v|webm|mkv|flv|avi)(\?|$)/i; const DIRECT_VIDEO_RE = /\.(mp4|mov|m4v|webm|mkv|flv|avi)(\?|$)/i;

View File

@@ -8,6 +8,8 @@ import {
ensureAppConfigReady, ensureAppConfigReady,
ensureLlmConfigReady,
validateShareText, validateShareText,
DEFAULT_REWRITE_CONFIG, DEFAULT_REWRITE_CONFIG,
@@ -17,6 +19,7 @@ import {
const PIPELINE_SCRIPT = "douyin_pipeline.js"; const PIPELINE_SCRIPT = "douyin_pipeline.js";
const SCRIPT_GENERATE = "script_generate.js";
const NODEJS_EVENT = "nodejs:event"; const NODEJS_EVENT = "nodejs:event";
@@ -61,6 +64,8 @@ export const useWorkflowStore = defineStore("workflow", {
videoRewriting: false, videoRewriting: false,
scriptGenerating: false,
/** 流水线进度文案(来自 quickjs:event progress */ /** 流水线进度文案(来自 quickjs:event progress */
rewriteProgress: "", rewriteProgress: "",
@@ -315,7 +320,11 @@ export const useWorkflowStore = defineStore("workflow", {
}, },
async generateScript() {
return executeGenerateScript(this);
},
resetAll() { resetAll() {
@@ -431,7 +440,91 @@ export async function executeVideoRewrite(store) {
} }
} }
const DEFAULT_SCRIPT_PROMPT = `请基于以下视频原文案,创作一个新的仿写文案:
原文案:{{content}}
要求:
1. 保持原文案的核心内容和风格
2. 适当改变表达方式和用词
3. 确保新文案通顺自然
4. 长度约{{count}}字
请直接输出仿写后的文案:`;
/**
* 构建撰写文案提示词(对齐 zhenqianba generateScriptFromVideoContent
* @param {ReturnType<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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 = [ const UNSUPPORTED_PLATFORM_RULES = [