1
This commit is contained in:
@@ -274,6 +274,80 @@ async function fetchJson(url, options = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchArrayBuffer(url, options = {}) {
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
||||||
|
}
|
||||||
|
return Buffer.from(await res.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QWEN_TTS_URL =
|
||||||
|
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation";
|
||||||
|
|
||||||
|
/** 对齐 Electron Ka:根据 voiceId 选择 TTS 模型 */
|
||||||
|
function resolveTtsModel(voiceId) {
|
||||||
|
const v = String(voiceId || "").trim();
|
||||||
|
if (!v || v === "Cherry") return "qwen3-tts-flash";
|
||||||
|
if (v.startsWith("qwen-tts-vc-")) return "qwen3-tts-vc-2026-01-22";
|
||||||
|
return "qwen3-tts-flash";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通义千问 TTS 合成(对齐 Electron synthesizeOneSegment)
|
||||||
|
* @param {{ apiKey: string, text: string, voice: string, model?: string, instruction?: string, outputPath: string }} opts
|
||||||
|
*/
|
||||||
|
async function qwenTtsSynthesize(opts) {
|
||||||
|
const apiKey = opts.apiKey;
|
||||||
|
const text = String(opts.text || "").trim();
|
||||||
|
const voice = String(opts.voice || "").trim();
|
||||||
|
const outputPath = opts.outputPath;
|
||||||
|
const model = opts.model || resolveTtsModel(voice);
|
||||||
|
const instruction = String(opts.instruction || "").trim();
|
||||||
|
|
||||||
|
if (!apiKey) return { success: false, error: "缺少 DashScope apiKey" };
|
||||||
|
if (!text) return { success: false, error: "缺少合成文本" };
|
||||||
|
if (!voice) return { success: false, error: "缺少 voice" };
|
||||||
|
if (!outputPath) return { success: false, error: "缺少 outputPath" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchJson(QWEN_TTS_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
input: {
|
||||||
|
text,
|
||||||
|
voice,
|
||||||
|
...(instruction
|
||||||
|
? { instructions: instruction, optimize_instructions: true }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioUrl = data?.output?.audio?.url;
|
||||||
|
if (!audioUrl) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: data?.message || data?.code || "未获取到音频 URL",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = await fetchArrayBuffer(audioUrl);
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
fs.writeFileSync(outputPath, buf);
|
||||||
|
log("info", "qwen tts saved", { outputPath, voice, model });
|
||||||
|
return { success: true, audioPath: outputPath };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e.message || String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function aliyunAsrFiletrans(audioUrl, opts) {
|
async function aliyunAsrFiletrans(audioUrl, opts) {
|
||||||
const apiKey = opts.apiKey || opts.api_key;
|
const apiKey = opts.apiKey || opts.api_key;
|
||||||
const model = opts.model || "qwen3-asr-flash-filetrans";
|
const model = opts.model || "qwen3-asr-flash-filetrans";
|
||||||
@@ -454,4 +528,6 @@ module.exports = {
|
|||||||
aliyunAsrFiletrans,
|
aliyunAsrFiletrans,
|
||||||
localAsr,
|
localAsr,
|
||||||
openaiChat,
|
openaiChat,
|
||||||
|
resolveTtsModel,
|
||||||
|
qwenTtsSynthesize,
|
||||||
};
|
};
|
||||||
|
|||||||
74
scripts/nodejs/voice_preview.js
Normal file
74
scripts/nodejs/voice_preview.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色试听(对齐 Electron aliyun:cosyvoice:synthesize + getPreCachedVoicePath)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
const pipelineNative = require("./pipeline_native.js");
|
||||||
|
const desktopConfig = require("./desktop_config.js");
|
||||||
|
|
||||||
|
const CACHE_DIR = path.join(os.tmpdir(), "aiclient-voice-preview-cache");
|
||||||
|
|
||||||
|
function buildPreviewText(title, customText) {
|
||||||
|
if (customText && String(customText).trim()) {
|
||||||
|
return String(customText).trim();
|
||||||
|
}
|
||||||
|
const shortName = String(title || "")
|
||||||
|
.split("(")[0]
|
||||||
|
.trim();
|
||||||
|
return `你好,我是${shortName || "我"},很高兴为您服务。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__nodejsMain = async function (params) {
|
||||||
|
const p = params || {};
|
||||||
|
const apiKey =
|
||||||
|
desktopConfig.get("BAILIAN_API_KEY") ||
|
||||||
|
desktopConfig.get("DASHSCOPE_API_KEY");
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "缺少 BAILIAN_API_KEY(或 DASHSCOPE_API_KEY),请在配置中设置",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const voice = String(p.voice || "").trim();
|
||||||
|
if (!voice) {
|
||||||
|
return { success: false, error: "缺少 voice 参数" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = p.cacheKey ? String(p.cacheKey).trim() : "";
|
||||||
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
let outputPath;
|
||||||
|
if (cacheKey) {
|
||||||
|
outputPath = path.join(CACHE_DIR, `${cacheKey.replace(/[^\w.-]/g, "_")}.wav`);
|
||||||
|
if (fs.existsSync(outputPath)) {
|
||||||
|
return { success: true, audioPath: outputPath, cached: true };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = path.join(
|
||||||
|
CACHE_DIR,
|
||||||
|
`preview-${Date.now()}-${Math.random().toString(36).slice(2)}.wav`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = buildPreviewText(p.title, p.text);
|
||||||
|
const result = await pipelineNative.qwenTtsSynthesize({
|
||||||
|
apiKey,
|
||||||
|
text,
|
||||||
|
voice,
|
||||||
|
model: p.model,
|
||||||
|
instruction:
|
||||||
|
p.instruction ||
|
||||||
|
"请用自然、清晰、适合短视频配音的风格朗读。",
|
||||||
|
outputPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { ...result, cached: false };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user