@webgal-tools/voice
Version:
WebGAL GPT-SoVITS语音合成应用
587 lines (578 loc) • 28.6 kB
JavaScript
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createMistral } from '@ai-sdk/mistral';
import { createCohere } from '@ai-sdk/cohere';
import { createOllama } from 'ollama-ai-provider';
import { logger } from '@webgal-tools/logger';
import path from 'node:path';
// 导出新的接口和实现
export * from './interface.js';
export * from './implementations.js';
export * from './factory.js';
/**
* 角色语言特色配置存储
*/
const characterStyles = new Map();
/**
* 统一翻译服务类
*/
export class TranslateService {
modelCache = new Map();
currentIndexMaps;
/**
* 获取或创建AI模型实例
*/
getModel(config) {
// 包含api_key在缓存键中,确保api_key变更时会创建新的模型实例
const cacheKey = `${config.model_type}:${config.base_url}:${config.model_name}:${config.api_key || 'no-key'}`;
if (this.modelCache.has(cacheKey)) {
const timestamp = new Date().toISOString();
logger.info(`[${timestamp}] 🔄 使用缓存的翻译模型: ${config.model_type}`);
return this.modelCache.get(cacheKey);
}
let model;
switch (config.model_type) {
case 'ollama':
const ollamaProvider = createOllama({
baseURL: config.base_url,
});
model = ollamaProvider(config.model_name);
break;
case 'openai':
if (!config.api_key) {
throw new Error('OpenAI 需要提供 api_key');
}
const openaiProvider = createOpenAI({
baseURL: config.base_url !== 'https://api.openai.com/v1' ? config.base_url : undefined,
apiKey: config.api_key,
});
model = openaiProvider(config.model_name);
break;
case 'anthropic':
if (!config.api_key) {
throw new Error('Anthropic 需要提供 api_key');
}
const anthropicProvider = createAnthropic({
baseURL: config.base_url !== 'https://api.anthropic.com' ? config.base_url : undefined,
apiKey: config.api_key,
});
model = anthropicProvider(config.model_name);
break;
case 'google':
if (!config.api_key) {
throw new Error('Google 需要提供 api_key');
}
const googleProvider = createGoogleGenerativeAI({
baseURL: config.base_url !== 'https://generativelanguage.googleapis.com/v1beta' ? config.base_url : undefined,
apiKey: config.api_key,
});
model = googleProvider(config.model_name);
break;
case 'mistral':
if (!config.api_key) {
throw new Error('Mistral 需要提供 api_key');
}
const mistralProvider = createMistral({
baseURL: config.base_url !== 'https://api.mistral.ai/v1' ? config.base_url : undefined,
apiKey: config.api_key,
});
model = mistralProvider(config.model_name);
break;
case 'cohere':
if (!config.api_key) {
throw new Error('Cohere 需要提供 api_key');
}
const cohereProvider = createCohere({
baseURL: config.base_url !== 'https://api.cohere.ai/v1' ? config.base_url : undefined,
apiKey: config.api_key,
});
model = cohereProvider(config.model_name);
break;
case 'custom':
// 对于自定义供应商,尝试使用通用的OpenAI兼容格式
logger.info(`🔍 使用自定义模型: ${config.model_name}`);
logger.info(`🔍 使用自定义模型: ${config.base_url}`);
logger.debug(`🔍 使用自定义模型: ${config.api_key}`);
const customProvider = createOpenAI({
baseURL: config.base_url,
apiKey: config.api_key || 'dummy-key',
});
model = customProvider(config.model_name);
break;
default:
throw new Error(`不支持的模型类型: ${config.model_type}`);
}
const timestamp = new Date().toISOString();
logger.info(`[${timestamp}] ✅ 创建新的翻译模型: ${config.model_type} (API Key: ${config.api_key ? '已设置' : '未设置'})`);
this.modelCache.set(cacheKey, model);
return model;
}
/**
* 获取角色的语言特色
*/
getCharacterStyle(character) {
return characterStyles.get(character) || '保持角色原有的语言风格和语气';
}
/**
* 获取通用翻译规则
* 提取两个提示词中共同的翻译规则部分
*/
getCommonTranslationRules(targetLanguage) {
return `
- **全部翻译**:无论是口癖、拟声词、专有名词等,都必须翻译为目标语言的表达方式,不允许保留原文或夹杂原文。
- 错误示例:"嘿嘿嘿" -> "嘿嘿嘿"
- 正确示例:"嘿嘿嘿" -> "ふふふ" 或 "ほほほ" 或 "hehehe"
- 错误示例:"老大" -> "老大"
- 正确示例:"老大" -> "ボス" 或 "Boss"
- 如果你确实不会翻译,并且这些词汇无关语义,那么你还可以将这些词汇移除,但必须保证不改变原文的意义
- **疑问句处理**:疑问句翻译时要注意保持疑问语气,可以使用目标语言的疑问词、语调或语气词。
- 正确示例:"你要去哪里?" -> "どこに行くの?"(日语中加了疑问语气词"の")
- 正确示例:"真的吗?" -> "本当?" 或 "Really?"(保持疑问语气)
- 错误示例:"你要去哪里?" -> "どこに行く"(缺少疑问语气)
- **感叹句处理**:感叹句翻译时要保持强烈的情感表达,可以使用感叹词、强调语气或感叹号。
- 正确示例:"太棒了!" -> "すごい!" 或 "Amazing!"
- 正确示例:"好痛!" -> "痛い!" 或 "Ouch!"
- 错误示例:"太棒了!" -> "すごい"(缺少感叹语气)
- **语气词适配**:根据目标语言的特点,适当调整语气词的使用。
- 日语:可以使用"ね"、"よ"、"わ"、"さ"等语气词
- 英语:可以使用"you know"、"well"、"oh"等
- 中文:可以使用"呢"、"吧"、"啊"等
`;
}
/**
* 构建翻译提示词
*/
buildTranslatePrompt(character, text, targetLanguage, context, globalPrompt, characterPrompt) {
const characterStyle = this.getCharacterStyle(character);
let prompt = `你是一位专业的游戏翻译专家,任务是将游戏对话精准地翻译成${targetLanguage}。
## 翻译目标
将 <待翻译文本> 的内容翻译成${targetLanguage}。
## 核心翻译准则
1. **纯净输出**:只返回翻译后的文本,不包含任何原文、解释、注释或额外符号。
- 错误示例:\`你好!(Hello!)\`
- 正确示例:\`Hello!\`
2. **忠实原文**:保持原文的语气、情感和风格。不要添加或删减信息。
- 原文:"你好!"
- 错误翻译:"你好吗?"
- 正确翻译:"Hello!"
3. **流畅自然**:译文需符合${targetLanguage}的语言习惯。
4. **遵循角色设定**:严格遵守提供的角色信息和语言风格。
5. **参考示例**:
- 用户提示: 若叶(わかば)睦(むつみ)
- 用户目标: 若叶同学?
- 分析: 这里直接叫同学的姓,说明并不是很亲近的人,应该使用さん来保证礼貌
- 最终翻译: わかばさん?
${this.getCommonTranslationRules(targetLanguage)}
7. **适度润色**:可以选择性地添加目标语言常用的语气词或语气助词,以提升译文的自然度和口语感,但必须保证不改变原文的意义。
- 正确示例:"快点!" -> "早くね!"(日语中加了语气词"ね"更自然)
- 错误示例:"快点!" -> "你最好快点,不然我就生气了!"(添加了原文没有的威胁语气,改变了原意)
11. **标点符号使用**:根据语气和情感使用合适的标点符号来调制语气,增强表达效果。
- **疑问句**:使用问号"?"或"?",表示疑问、困惑、惊讶等
- **感叹句**:使用感叹号"!"或"!",表示强烈情感、惊讶、愤怒、兴奋等
- **省略号**:使用"..."或"…",表示犹豫、思考、未完待续等
- **破折号**:使用"—"或"-",表示转折、强调、停顿等
- **正确示例**:
- "真的吗?" → "本当?"(疑问)
- "太棒了!" → "すごい!"(兴奋)
- "那个..." → "あの..."(犹豫)
- "哼—" → "ふん—"(不屑)
- **错误示例**:
- "真的吗?" → "本当"(缺少问号,失去疑问语气)
- "太棒了!" → "すごい"(缺少感叹号,失去兴奋感)
## 背景信息
- **当前说话角色**: ${character}
- **角色语言风格**: ${characterStyle}
${context ? `- **对话上下文**: \n${context}\n` : ''}
${globalPrompt ? `
## 全局翻译指南
${globalPrompt}
` : ''}
## 待翻译文本
${text}
`;
logger.debug(prompt);
return prompt;
}
/**
* 构建模型选择提示词
*/
buildModelSelectionPrompt(character, text, targetLanguage, scannedFiles, context, globalPrompt, characterPrompt) {
const characterStyle = this.getCharacterStyle(character);
// 创建模型组索引映射
const modelGroupIndexMap = new Map();
const refAudioIndexMap = new Map();
// 按basename分组GPT和SoVITS模型
const modelGroups = new Map();
// 遍历GPT文件,寻找匹配的SoVITS文件
for (const gptFile of scannedFiles.gpt_files) {
const gptBasename = path.basename(gptFile, path.extname(gptFile));
// 寻找对应的SoVITS文件
const matchingSovits = scannedFiles.sovits_files.find(sovitsFile => {
const sovitsBasename = path.basename(sovitsFile, path.extname(sovitsFile));
return gptBasename === sovitsBasename;
});
if (matchingSovits) {
modelGroups.set(gptBasename, {
gpt: gptFile,
sovits: matchingSovits
});
}
}
// 为未匹配的GPT文件创建单独的组
for (const gptFile of scannedFiles.gpt_files) {
const gptBasename = path.basename(gptFile, path.extname(gptFile));
if (!modelGroups.has(gptBasename)) {
modelGroups.set(gptBasename, {
gpt: gptFile,
sovits: scannedFiles.sovits_files[0] || '' // 使用第一个SoVITS文件作为默认
});
}
}
// 为未匹配的SoVITS文件创建单独的组
for (const sovitsFile of scannedFiles.sovits_files) {
const sovitsBasename = path.basename(sovitsFile, path.extname(sovitsFile));
if (!modelGroups.has(sovitsBasename)) {
modelGroups.set(sovitsBasename, {
gpt: scannedFiles.gpt_files[0] || '', // 使用第一个GPT文件作为默认
sovits: sovitsFile
});
}
}
// 创建模型组列表
const modelGroupsList = Array.from(modelGroups.entries()).map(([basename, files], index) => {
const groupIndex = index + 1;
modelGroupIndexMap.set(groupIndex, files);
return `${groupIndex}. \`${basename}\` (GPT: ${path.basename(files.gpt)}, SoVITS: ${path.basename(files.sovits)})`;
}).join('\n');
// 为参考音频文件创建索引
const refAudioFilesList = scannedFiles.ref_audio_files.map((f, index) => {
const fileIndex = index + 1;
refAudioIndexMap.set(fileIndex, f);
return `${fileIndex}. \`${f}\``;
}).join('\n');
// 存储索引映射到实例中,供后续使用
this.currentIndexMaps = {
modelGroups: modelGroupIndexMap,
refAudio: refAudioIndexMap
};
let prompt = `你是一位专业的AI语音生成助手。你的任务是分析一段游戏对话,将其翻译成${targetLanguage},然后根据对话内容和情感,从提供的文件列表中选择最合适的语音模型来生成音频。
## 任务流程
1. **分析**: 理解 <待处理文本> 的内容、上下文和情感。
2. **翻译**: 将文本翻译成${targetLanguage}。
3. **选择**: 从 <可用模型文件> 列表中,为翻译后的文本选择最匹配的模型组索引号和参考音频索引号。
4. **输出**: 以一个完整的JSON对象的形式返回结果,不要有任何其他多余的文字。
## 可用模型文件
### 模型组 (GPT + SoVITS)
${modelGroupsList || '无'}
### 参考音频
${refAudioFilesList || '无'}
## 背景信息
- **当前说话角色**: ${character}
- **角色语言风格**: ${characterStyle}
${context ? `- **对话上下文**: \n${context}\n` : ''}
${globalPrompt ? `
## 全局翻译与选择指南
${globalPrompt}
` : ''}
## 模型选择逻辑
- **情感匹配**: 分析文本情感(如:开心、悲伤、愤怒、惊讶、中性、紧张、温柔、兴奋等),选择模型组名称中最能体现该情感的模型。
- **内容匹配**: 如果模型组名称包含场景或状态信息,请匹配文本内容。
${this.getCommonTranslationRules(targetLanguage)}
- **备用方案**: 如果没有明显匹配项,选择一个看起来最通用、最中性的模型组。
- **示例分析**:
- **日常对话**: "要一起吃午饭吗?" -> 情绪: 中性, 日常。可选择通用模型或带有 "normal"、"neutral" 字样的模型组。
- **悲伤情绪**: "我的外卖被偷了..." -> 情绪: 悲伤, 沮丧。应选择带有 "sad"、"sorrow"、"cry" 等字样的模型组。
- **愤怒情绪**: "谁敢这么欺负咱们老大!" -> 情绪: 愤怒, 激动。应选择带有 "angry"、"rage"、"mad" 等字样的模型组。
- **开心情绪**: "哇!我中奖了!" -> 情绪: 开心, 兴奋。应选择带有 "happy"、"joy"、"excited" 等字样的模型组。
- **惊讶情绪**: "什么?你说的是真的吗?" -> 情绪: 惊讶, 震惊。应选择带有 "surprised"、"shock"、"amazed" 等字样的模型组。
- **温柔情绪**: "别担心,一切都会好起来的。" -> 情绪: 温柔, 安慰。应选择带有 "gentle"、"soft"、"comfort" 等字样的模型组。
- **紧张情绪**: "快跑!后面有人追来了!" -> 情绪: 紧张, 恐惧。应选择带有 "nervous"、"fear"、"panic" 等字样的模型组。
- **害羞情绪**: "那个...我可以和你一起吗?" -> 情绪: 害羞, 腼腆。应选择带有 "shy"、"timid"、"bashful" 等字样的模型组。
- **傲慢情绪**: "哼,就凭你也配?" -> 情绪: 傲慢, 轻蔑。应选择带有 "arrogant"、"proud"、"contempt" 等字样的模型组。
- **疑惑情绪**: "咦?这是怎么回事?" -> 情绪: 疑惑, 困惑。应选择带有 "confused"、"puzzled"、"wonder" 等字样的模型组。
## 待处理文本
"${text}"
## 输出格式必须为json
{
"model_group_index": 选择的模型组索引号(数字),
"ref_audio_index": 选择的参考音频索引号(数字),
"translated_text": "翻译后的文本",
"emotion": "对所分析文本情绪的简短描述(例如:开心)"
}
`;
logger.debug(prompt);
return prompt;
}
/**
* 智能路径匹配函数
* 处理AI返回的路径与扫描结果路径的匹配问题
*/
findBestMatchingPath(aiPath, scannedPaths) {
if (!aiPath || scannedPaths.length === 0) {
return null;
}
// 标准化路径分隔符
const normalizedAiPath = aiPath.replace(/\\/g, '/').toLowerCase();
const normalizedScannedPaths = scannedPaths.map(p => p.replace(/\\/g, '/').toLowerCase());
// 1. 精确匹配
const exactMatch = normalizedScannedPaths.find(p => p === normalizedAiPath);
if (exactMatch) {
return scannedPaths[normalizedScannedPaths.indexOf(exactMatch)];
}
// 2. 文件名匹配(忽略路径)
const aiFileName = path.basename(normalizedAiPath);
const fileNameMatch = normalizedScannedPaths.find(p => path.basename(p) === aiFileName);
if (fileNameMatch) {
return scannedPaths[normalizedScannedPaths.indexOf(fileNameMatch)];
}
// 3. 模糊匹配(包含关系)
const fuzzyMatches = normalizedScannedPaths.filter(p => p.includes(aiFileName) || aiFileName.includes(path.basename(p)));
if (fuzzyMatches.length > 0) {
// 选择最长的匹配(通常更具体)
const bestMatch = fuzzyMatches.reduce((a, b) => a.length > b.length ? a : b);
return scannedPaths[normalizedScannedPaths.indexOf(bestMatch)];
}
// 4. 部分路径匹配
const aiPathParts = normalizedAiPath.split('/').filter(p => p.length > 0);
const pathMatches = normalizedScannedPaths.filter(p => {
const scannedParts = p.split('/').filter(sp => sp.length > 0);
return aiPathParts.some(part => scannedParts.includes(part));
});
if (pathMatches.length > 0) {
// 选择匹配度最高的
const bestMatch = pathMatches.reduce((a, b) => {
const aScore = aiPathParts.filter(part => a.includes(part)).length;
const bScore = aiPathParts.filter(part => b.includes(part)).length;
return aScore > bScore ? a : b;
});
return scannedPaths[normalizedScannedPaths.indexOf(bestMatch)];
}
return null;
}
/**
* 模型选择和翻译
*/
async selectModelAndTranslate(character, speech, targetLanguage, scannedFiles, config, characterConfig, context) {
if (!targetLanguage) {
throw new Error('未提供翻译目标语言');
}
try {
const model = this.getModel(config);
const prompt = this.buildModelSelectionPrompt(character, speech, targetLanguage, scannedFiles, context, config.additional_prompt, characterConfig?.prompt);
const result = await generateText({
model,
prompt,
temperature: config.temperature ?? 0.3, // 使用配置的温度参数,默认0.3
maxTokens: config.max_tokens ?? 512, // 使用配置的最大token数,默认512
});
logger.debug('输出token: ', result.usage.completionTokens);
let responseText = result.text.trim();
if (!responseText) {
throw new Error('模型选择结果为空');
}
logger.debug(responseText);
// 提取JSON内容
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('响应中没有找到有效的JSON格式');
}
const jsonText = jsonMatch[0];
let selectionResult;
try {
selectionResult = JSON.parse(jsonText);
}
catch (parseError) {
throw new Error(`JSON解析失败: ${parseError}`);
}
// 验证响应结构
if (!selectionResult.model_group_index || !selectionResult.ref_audio_index ||
!selectionResult.translated_text || !selectionResult.emotion) {
throw new Error('响应JSON缺少必要的字段');
}
// 检查索引映射是否存在
if (!this.currentIndexMaps) {
throw new Error('索引映射未初始化');
}
// 使用索引映射获取模型组和参考音频路径
const modelGroup = this.currentIndexMaps.modelGroups.get(selectionResult.model_group_index);
const refAudioPath = this.currentIndexMaps.refAudio.get(selectionResult.ref_audio_index);
if (!modelGroup || !refAudioPath) {
throw new Error('无法通过索引找到对应的文件路径');
}
// 使用智能路径匹配验证选择的文件
const matchedGpt = this.findBestMatchingPath(modelGroup.gpt, scannedFiles.gpt_files);
const matchedSovits = this.findBestMatchingPath(modelGroup.sovits, scannedFiles.sovits_files);
const matchedRefAudio = this.findBestMatchingPath(refAudioPath, scannedFiles.ref_audio_files);
// 构建最终结果
const finalResult = {
gpt: matchedGpt || scannedFiles.gpt_files[0] || '',
sovits: matchedSovits || scannedFiles.sovits_files[0] || '',
ref_audio: matchedRefAudio || scannedFiles.ref_audio_files[0] || '',
translated_text: selectionResult.translated_text,
emotion: selectionResult.emotion
};
// 记录匹配信息
if (matchedGpt && matchedGpt !== modelGroup.gpt) {
logger.info(`GPT路径匹配: "${modelGroup.gpt}" -> "${matchedGpt}"`);
}
if (matchedSovits && matchedSovits !== modelGroup.sovits) {
logger.info(`SoVITS路径匹配: "${modelGroup.sovits}" -> "${matchedSovits}"`);
}
if (matchedRefAudio && matchedRefAudio !== refAudioPath) {
logger.info(`参考音频路径匹配: "${refAudioPath}" -> "${matchedRefAudio}"`);
}
if (!matchedGpt) {
logger.warn(`无法匹配GPT文件: ${modelGroup.gpt},使用第一个可用文件`);
}
if (!matchedSovits) {
logger.warn(`无法匹配SoVITS文件: ${modelGroup.sovits},使用第一个可用文件`);
}
if (!matchedRefAudio) {
logger.warn(`无法匹配参考音频文件: ${refAudioPath},使用第一个可用文件`);
}
logger.info(`[模型选择] ${character}: "${speech}" -> 情绪:${finalResult.emotion}, 翻译:"${finalResult.translated_text}"`);
return finalResult;
}
catch (error) {
logger.error(`模型选择失败 [${config.model_type}] ${character}:`, error);
// 回退方案:使用默认选择
const fallbackResult = {
gpt: scannedFiles.gpt_files[0] || '',
sovits: scannedFiles.sovits_files[0] || '',
ref_audio: scannedFiles.ref_audio_files[0] || '',
translated_text: speech, // 回退到原文
emotion: 'neutral'
};
// 检查回退方案是否有效
if (!fallbackResult.gpt || !fallbackResult.sovits || !fallbackResult.ref_audio) {
logger.error(`回退方案无效:缺少必要的模型文件`);
logger.error(`可用GPT文件: ${scannedFiles.gpt_files.length}个`);
logger.error(`可用SoVITS文件: ${scannedFiles.sovits_files.length}个`);
logger.error(`可用参考音频文件: ${scannedFiles.ref_audio_files.length}个`);
throw new Error('没有可用的模型文件进行回退');
}
logger.warn(`使用回退方案: ${JSON.stringify(fallbackResult)}`);
return fallbackResult;
}
}
/**
* 翻译文本
*/
async translate(character, speech, targetLanguage, config, characterConfig, context) {
if (!targetLanguage) {
throw new Error('未提供翻译目标语言');
}
try {
const model = this.getModel(config);
const prompt = this.buildTranslatePrompt(character, speech, targetLanguage, context, config.additional_prompt, characterConfig?.prompt);
const result = await generateText({
model,
prompt,
temperature: config.temperature ?? 0.3, // 使用配置的温度参数,默认0.3
maxTokens: config.max_tokens ?? 1000, // 使用配置的最大token数,默认1000
});
let translatedText = result.text.trim();
if (!translatedText) {
throw new Error('翻译结果为空');
}
// 清理响应内容
this.cleanupTranslationResult(translatedText);
// 移除可能的思考标签内容
if (translatedText.includes('<think>')) {
translatedText = (translatedText.split('</think>')[1] ?? '').trim();
}
// 移除开头和结尾的引号
if ((translatedText.startsWith('"') && translatedText.endsWith('"')) ||
(translatedText.startsWith('"') && translatedText.endsWith('"'))) {
translatedText = translatedText.slice(1, -1);
}
const hasAdditionalInfo = context || config.additional_prompt || characterConfig?.prompt;
if (hasAdditionalInfo) {
logger.info(`[翻译+增强] ${character}: "${speech}" -> "${translatedText}"`);
}
else {
logger.info(`[翻译] ${character}: "${speech}" -> "${translatedText}"`);
}
return translatedText;
}
catch (error) {
logger.error(`翻译失败 [${config.model_type}] ${character}:`, error);
logger.error(`回退到原文: "${speech}"`);
return speech;
}
}
/**
* 清理翻译结果
*/
cleanupTranslationResult(text) {
return text.trim()
.replace(/^["'""]|["'""]$/g, '') // 移除首尾引号
.replace(/\n+/g, ' ') // 将多个换行符替换为单个空格
.trim();
}
/**
* 检查服务可用性
*/
async checkAvailability(config) {
try {
const model = this.getModel(config);
// 发送一个简单的测试请求
const result = await generateText({
model,
prompt: '请回复"测试成功"',
maxTokens: 100,
});
logger.info(`${config.model_type} 测试消息: ${result.text.trim()}`);
return result.text.length > 2;
}
catch (error) {
logger.error(`服务可用性检查失败 [${config.model_type}]:`, error);
return false;
}
}
/**
* 清理模型缓存
*/
clearCache() {
const cacheSize = this.modelCache.size;
const timestamp = new Date().toISOString();
logger.info(`[${timestamp}] 🧹 清理翻译模型缓存,共清理 ${cacheSize} 个缓存模型`);
this.modelCache.clear();
}
/**
* 设置角色语言特色
*/
setCharacterStyle(character, style) {
characterStyles.set(character, style);
}
/**
* 获取所有角色样式
*/
getAllCharacterStyles() {
return new Map(characterStyles);
}
/**
* 移除角色样式
*/
removeCharacterStyle(character) {
characterStyles.delete(character);
}
}
// 创建单例实例
const translateService = new TranslateService();
// 导出便利函数以保持向后兼容
export async function translate(character, speech, targetLanguage, config, context) {
return translateService.translate(character, speech, targetLanguage, config, undefined, context);
}
export function setCharacterStyle(character, style) {
translateService.setCharacterStyle(character, style);
}
// 已废弃:使用 translateService.checkAvailability() 替代
export async function checkTranslatorService(config) {
return translateService.checkAvailability(config);
}
// 导出服务实例和类型
export { translateService };
//# sourceMappingURL=index.js.map