11
This commit is contained in:
@@ -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?;
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -34,10 +48,23 @@ const { scriptContent, editLanguage, editWordCount } = storeToRefs(workflow);
|
||||
class="min-w-36 shrink-0 ml-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 pt-1">
|
||||
<Button label="撰写文案" size="small" />
|
||||
<div class="flex flex-wrap items-center gap-2 pt-1">
|
||||
<Button
|
||||
label="撰写文案"
|
||||
size="small"
|
||||
:loading="scriptGenerating"
|
||||
:disabled="scriptGenerating"
|
||||
@click="onGenerateScript"
|
||||
/>
|
||||
<Button label="AI法务" size="small" severity="secondary" />
|
||||
</div>
|
||||
<p
|
||||
v-if="feedback.message"
|
||||
class="text-sm"
|
||||
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</template>
|
||||
|
||||
@@ -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<string, string> } | { 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;
|
||||
|
||||
|
||||
@@ -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<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 = [
|
||||
|
||||
|
||||
Reference in New Issue
Block a user