This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 14:12:57 +08:00
parent 6cc305f5e8
commit 4771397301
7 changed files with 292 additions and 27 deletions

View File

@@ -0,0 +1,20 @@
import { invoke, isTauri } from "@tauri-apps/api/core";
export function mimeFromAudioPath(filePath) {
const lower = String(filePath || "").toLowerCase();
if (lower.endsWith(".mp3")) return "audio/mpeg";
if (lower.endsWith(".ogg")) return "audio/ogg";
return "audio/wav";
}
/**
* @param {string} filePath
*/
export async function localAudioToPlayableUrl(filePath) {
if (!isTauri()) {
throw new Error("请在 Tauri 桌面端使用音频播放功能");
}
const base64 = await invoke("read_local_file_base64", { path: filePath });
const mime = mimeFromAudioPath(filePath);
return `data:${mime};base64,${base64}`;
}

View File

@@ -0,0 +1,99 @@
import { invoke } from "@tauri-apps/api/core";
import { ensureAppConfigReady } from "../config/videoPipeline.js";
import { resolveAliyunVoiceId } from "./soundPromptStorage.js";
import { getVoiceDisplayLabel } from "../utils/voiceLabel.js";
import { buildTtsInstruction, buildTtsLanguageType } from "../utils/ttsOptions.js";
import { localAudioToPlayableUrl } from "./localAudio.js";
const VOICE_GENERATE_SCRIPT = "voice_generate.js";
/**
* 生成语音(对齐 Electron generateAudio CosyVoice
* @param {ReturnType<typeof import('../stores/workflow.js').useWorkflowStore>} store
*/
export async function executeGenerateSpeech(store) {
const text = String(store.scriptContent || "").trim();
if (!text) {
return { ok: false, message: "请先在「视频文案编辑」中填写文案内容" };
}
if (!store.selectedVoiceId) {
return { ok: false, message: "请先选择音色" };
}
const cfg = await ensureAppConfigReady();
if (!cfg.ok) {
return { ok: false, message: cfg.message };
}
const voice = resolveAliyunVoiceId(store.selectedVoiceId);
const instruction = buildTtsInstruction(
store.voiceLanguage,
store.selectedVoiceId,
store.voiceEmotion,
store.speechRate,
);
const languageType = buildTtsLanguageType(store.voiceLanguage);
store.speechGenerating = true;
try {
const result = await invoke("run_nodejs_script", {
scriptName: VOICE_GENERATE_SCRIPT,
params: {
text,
voice,
instruction,
languageType: languageType || null,
format: "mp3",
},
});
if (!result?.success || !result?.audioPath) {
return {
ok: false,
message: String(result?.error || "语音生成失败,请重试"),
};
}
const audioSrc = await localAudioToPlayableUrl(result.audioPath);
const label = `${getVoiceDisplayLabel(store.selectedVoiceId)} · ${formatHistoryTime()}`;
const entry = {
id: `audio_${Date.now()}`,
label,
audioPath: result.audioPath,
audioSrc,
createdAt: Date.now(),
};
store.generatedAudioPath = result.audioPath;
store.generatedAudioSrc = audioSrc;
store.currentAudioId = entry.id;
store.audioHistory = [entry, ...store.audioHistory].slice(0, 50);
return { ok: true, message: "语音生成完成" };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err || "未知错误");
return { ok: false, message: `语音生成失败: ${msg}` };
} finally {
store.speechGenerating = false;
}
}
function formatHistoryTime() {
const d = new Date();
const p = (n) => String(n).padStart(2, "0");
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
/**
* @param {ReturnType<typeof import('../stores/workflow.js').useWorkflowStore>} store
* @param {string} audioId
*/
export function selectAudioHistory(store, audioId) {
const item = store.audioHistory.find((h) => h.id === audioId);
if (!item) return;
store.currentAudioId = item.id;
store.generatedAudioPath = item.audioPath;
store.generatedAudioSrc = item.audioSrc;
}

View File

@@ -1,32 +1,13 @@
import { invoke, isTauri } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/core";
import { ensureAppConfigReady } from "../config/videoPipeline.js";
import { resolveAliyunVoiceId } from "./soundPromptStorage.js";
import { localAudioToPlayableUrl } from "./localAudio.js";
const VOICE_PREVIEW_SCRIPT = "voice_preview.js";
/** @type {Map<string, string>} cacheKey → data:audio URL */
const previewUrlCache = new Map();
function mimeFromPath(filePath) {
const lower = String(filePath || "").toLowerCase();
if (lower.endsWith(".mp3")) return "audio/mpeg";
if (lower.endsWith(".ogg")) return "audio/ogg";
return "audio/wav";
}
/**
* 将 Node 写入的本地音频转为可在 WebView 播放的 URL避免 asset.localhost 不可用)
* @param {string} filePath
*/
async function localAudioToPlayableUrl(filePath) {
if (!isTauri()) {
throw new Error("请在 Tauri 桌面端使用试听功能");
}
const base64 = await invoke("read_local_file_base64", { path: filePath });
const mime = mimeFromPath(filePath);
return `data:${mime};base64,${base64}`;
}
/**
* 音色试听
* @param {{ listKey: string, title: string, aliyunVoiceId?: string, selectedVoiceId?: string }} opts