11
This commit is contained in:
@@ -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?;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user