在上一篇文章中,说明了如何使用 Obsidian + Gemini + Azure TTS 实现笔记翻译、朗读自动化。
今天,我们来优化脚本代码:
- 更新提示词,翻译结果更符合实际需求。
- 增加朗读停顿,针对不同标点符号停顿不同时长。
直接复制以下代码,替换原来的代码即可。
/*
文件名: english_trainer.js
功能: Gemini 翻译 -> Azure TTS (支持语速调节) -> 插入笔记
*/
module.exports = async (params) => {
const { app } = params;
// ================== 核心配置区 (请修改这里) ==================
// 1. Gemini Key (翻译)
const GEMINI_API_KEY = "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; // 替换你的 Key
const GEMINI_MODEL = "gemini-3-flash-preview";
// 2. Azure Key (语音)
const AZURE_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // 替换你的 Key1
const AZURE_REGION = "eastasia"; // 替换你的区域 (如 eastus, japaneast)
// 3. 语音设置
// 推荐: en-US-AndrewNeural (男), en-US-AvaNeural (女)
const AZURE_VOICE = "en-US-AndrewNeural";
// 语速控制: "-15%" (推荐跟读), "0%" (正常), "-30%" (慢速)
const SPEECH_RATE = "-15%";
// Level 1: 长停顿 (句号/问号/感叹号) -> 留出复述整句的时间
const PAUSE_LONG = "3000ms";
// Level 2: 中停顿 (分号/冒号/破折号) -> 逻辑转折,稍作喘息
const PAUSE_MEDIUM = "2000ms";
// Level 3: 短停顿 (逗号) -> 仅用于换气,保持节奏连贯
const PAUSE_SHORT = "1500ms";
// ===========================================================
const editor = app.workspace.activeLeaf.view.editor;
const selection = editor.getSelection();
if (!selection) { new Notice("❌ 请先选中一段中文文本!"); return; }
if (AZURE_KEY.includes("xxxx")) { new Notice("❌ 请先配置脚本中的 API Key"); return; }
new Notice("✨ 正在翻译 (Gemini)...");
try {
// --- 1. Gemini 翻译 ---
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
const geminiResponse = await requestUrl({
url: geminiUrl, method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: `Translate the user input into natural, relaxed English suitable for everyday narration. The translation should sound like how an educated native speaker would casually speak to a camera: smooth, friendly, and easy to follow, but still clear and appropriate for a general audience. Prioritize natural phrasing, rhythm, and flow over literal accuracy. Use light conversational structures and contractions where they feel natural, but avoid slang or overly informal expressions. Output English only. Provide the translated text only. User input: "${selection}"` }] }]
})
});
const translatedText = geminiResponse.json.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
if (!translatedText) throw new Error("翻译结果为空");
// --- 第二步:智能处理文本,插入分级停顿 ---
let processedText = translatedText;
// 1. 处理长停顿:句号(.) 问号(?) 感叹号(!)
// 逻辑不变:后面必须有空格或结尾
processedText = processedText.replace(/([.?!])(?=\s|$)/g, `$1 <break time='${PAUSE_LONG}' />`);
// 2. 处理中停顿:分号(;) 冒号(:)
// 逻辑不变:后面必须有空格
processedText = processedText.replace(/([;:](?=\s))/g, `$1 <break time='${PAUSE_MEDIUM}' />`);
// 3. 【重点修复】处理破折号 (Em Dash) 和双连字符 (--)
// 新逻辑:只要出现 "—" 或 "--",强制替换为停顿,不需要检查空格
// 这里的 replace 逻辑是:把破折号直接替换成 "<break... />",并在前后加个空格以防粘连
processedText = processedText.replace(/—|--/g, ` <break time='${PAUSE_MEDIUM}' /> `);
// 4. 处理 " - " (空格+短横线+空格) 的情况
// 这种通常做插入语使用,也需要中停顿
processedText = processedText.replace(/(\s-\s)/g, ` <break time='${PAUSE_MEDIUM}' /> `);
// 5. 处理短停顿:逗号(,)
// 逻辑不变:后面必须有空格 (防止匹配 10,000)
processedText = processedText.replace(/(,)(?=\s)/g, `$1 <break time='${PAUSE_SHORT}' />`);
// --- 第三步:Azure TTS 生成 ---
new Notice(`🎙️ 生成中 (L:${PAUSE_LONG}, M:${PAUSE_MEDIUM}, S:${PAUSE_SHORT})...`);
const ssml = `
<speak version='1.0' xml:lang='en-US'>
<voice xml:lang='en-US' xml:gender='Male' name='${AZURE_VOICE}'>
<prosody rate='${SPEECH_RATE}'>
${processedText}
</prosody>
</voice>
</speak>`;
const azureUrl = `https://${AZURE_REGION}.tts.speech.microsoft.com/cognitiveservices/v1`;
const audioResponse = await requestUrl({
url: azureUrl,
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': AZURE_KEY,
'Content-Type': 'application/ssml+xml',
'X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3',
'User-Agent': 'ObsidianQuickAdd'
},
body: ssml
});
if (audioResponse.status !== 200) {
throw new Error(`Azure 报错 ${audioResponse.status}: 检查配置`);
}
// --- 第四步:保存与输出 ---
const arrayBuffer = audioResponse.arrayBuffer;
const AUDIO_FOLDER = "English_Audio";
if (!app.vault.getAbstractFileByPath(AUDIO_FOLDER)) await app.vault.createFolder(AUDIO_FOLDER);
const fileName = `${AUDIO_FOLDER}/Speech_${moment().format("YYYYMMDD_HHmmss")}.mp3`;
await app.vault.createBinary(fileName, arrayBuffer);
const output = `
${selection}
🇺🇸 英文
${translatedText}
> [!TIP] 🎧 语音训练 (Azure)
> ![[${fileName}]]
`;
editor.replaceSelection(output);
new Notice("✅ 成功生成!");
} catch (error) {
console.error(error);
new Notice(`❌ 失败: ${error.message}`);
}
};