1
This commit is contained in:
35
src/services/audioDb.js
Normal file
35
src/services/audioDb.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
function ensureTauri() {
|
||||
if (!isTauri()) {
|
||||
throw new Error("语音历史需在 Tauri 桌面端使用");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ id: number, name: string, filePath: string, createdAt: string }} GeneratedAudioRecord
|
||||
*/
|
||||
|
||||
/** @returns {Promise<GeneratedAudioRecord[]>} */
|
||||
export async function listGeneratedAudios() {
|
||||
ensureTauri();
|
||||
return invoke("list_generated_audios");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name 可为空,空时由后端填入当前时间
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<GeneratedAudioRecord>}
|
||||
*/
|
||||
export async function insertGeneratedAudio(name, filePath) {
|
||||
ensureTauri();
|
||||
return invoke("insert_generated_audio", { name, filePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
export async function deleteGeneratedAudio(id) {
|
||||
ensureTauri();
|
||||
return invoke("delete_generated_audio", { id });
|
||||
}
|
||||
89
src/services/avatarDb.js
Normal file
89
src/services/avatarDb.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "aiclient_video_templates";
|
||||
|
||||
function ensureTauri() {
|
||||
if (!isTauri()) {
|
||||
throw new Error("形象管理需在 Tauri 桌面端使用");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ id: number, name: string, filePath: string, createdAt: string }} AvatarRecord
|
||||
*/
|
||||
|
||||
/** @returns {Promise<AvatarRecord[]>} */
|
||||
export async function listAvatars() {
|
||||
ensureTauri();
|
||||
return invoke("list_avatars");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<AvatarRecord>}
|
||||
*/
|
||||
export async function insertAvatar(name, filePath) {
|
||||
ensureTauri();
|
||||
return invoke("insert_avatar", { name, filePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {string} name
|
||||
*/
|
||||
export async function updateAvatarName(id, name) {
|
||||
ensureTauri();
|
||||
return invoke("update_avatar_name", { id, name });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
export async function deleteAvatar(id) {
|
||||
ensureTauri();
|
||||
return invoke("delete_avatar", { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number | null} [excludeId]
|
||||
*/
|
||||
export async function avatarNameExists(name, excludeId = null) {
|
||||
ensureTauri();
|
||||
return invoke("avatar_name_exists", {
|
||||
name,
|
||||
excludeId: excludeId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
/** 将旧版 localStorage 数据迁入 SQLite(仅当库为空且存在 legacy 数据时) */
|
||||
export async function migrateLegacyAvatarsIfNeeded() {
|
||||
ensureTauri();
|
||||
const current = await listAvatars();
|
||||
if (current.length > 0) return;
|
||||
|
||||
let legacy = [];
|
||||
try {
|
||||
const raw = localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
legacy = Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
} catch {
|
||||
legacy = [];
|
||||
}
|
||||
if (!legacy.length) return;
|
||||
|
||||
for (const item of legacy) {
|
||||
const name = String(item.name || "").trim();
|
||||
const filePath = String(item.video || item.filePath || "").trim();
|
||||
if (!name || !filePath) continue;
|
||||
try {
|
||||
await insertAvatar(name, filePath);
|
||||
} catch {
|
||||
/* 重名等跳过 */
|
||||
}
|
||||
}
|
||||
localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
}
|
||||
32
src/services/avatarVideo.js
Normal file
32
src/services/avatarVideo.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
/**
|
||||
* @returns {Promise<string | null>}
|
||||
*/
|
||||
export async function pickVideoFile() {
|
||||
if (!isTauri()) {
|
||||
throw new Error("请在 Tauri 桌面端选择视频文件");
|
||||
}
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "视频",
|
||||
extensions: ["mp4", "mov", "webm", "mkv", "m4v", "avi"],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected == null) return null;
|
||||
if (Array.isArray(selected)) return selected[0] || null;
|
||||
return String(selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sourcePath
|
||||
* @returns {Promise<string>} 应用内保存路径
|
||||
*/
|
||||
export async function importAvatarVideo(sourcePath) {
|
||||
return invoke("import_avatar_video", { sourcePath });
|
||||
}
|
||||
|
||||
12
src/services/localVideo.js
Normal file
12
src/services/localVideo.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { convertFileSrc, isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
/**
|
||||
* 本地视频文件转为 WebView 可播放地址
|
||||
* @param {string} filePath
|
||||
*/
|
||||
export function localVideoToPlayableUrl(filePath) {
|
||||
if (!isTauri()) {
|
||||
throw new Error("请在 Tauri 桌面端预览视频");
|
||||
}
|
||||
return convertFileSrc(filePath);
|
||||
}
|
||||
@@ -1,99 +1,96 @@
|
||||
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;
|
||||
}
|
||||
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";
|
||||
|
||||
import { insertGeneratedAudio } from "./audioDb.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,
|
||||
|
||||
Reference in New Issue
Block a user