电脑里有xiaoxiao语音的 或者什么其他高质量语音的
可以用 quick add插件 执行以下脚本,来朗读 obsidian的内容
/*
Obsidian TTS 逐句朗读脚本 (Windows版) - V7.0 完美续读版
新增功能:停止时自动将光标归位,实现“断点续读”功能。
核心特性:1.5倍速 + 智能净化 + 极速流式 + 可暂停/续读
*/
module.exports = async (params) => {
const { app, quickAddApi, obsidian } = params;
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const os = require('os');
// ================= 配置区域 =================
const VOICE_NAME = "Xiaoxiao"; /* 语音名称 */
const SPEED_RATE = 1.1; /* 语速 */
// ===========================================
// --- 全局状态 ---
if (!window.obsidian_tts) {
window.obsidian_tts = { isReading: false, process: null, resolver: null };
}
// ============================================================
// 🛑 停止逻辑 (包含光标归位)
// ============================================================
if (window.obsidian_tts.isReading) {
new Notice("⏸️ 已暂停 (再次点击继续)");
window.obsidian_tts.isReading = false;
// 1. 杀掉进程
if (window.obsidian_tts.process) {
window.obsidian_tts.process.kill();
window.obsidian_tts.process = null;
}
// 2. 结束等待
if (window.obsidian_tts.resolver) window.obsidian_tts.resolver();
// 3. 【核心修改】光标归位逻辑
const activeView = app.workspace.getActiveViewOfType(obsidian.MarkdownView);
if (activeView) {
const editor = activeView.editor;
// 如果当前有选中(因为朗读时是高亮选中的),就把光标移到选区开头
if (editor.somethingSelected()) {
const cursorStart = editor.getCursor("from");
editor.setCursor(cursorStart); // 取消选中,光标闪烁在句子开头
}
}
return;
}
const view = app.workspace.getActiveViewOfType(obsidian.MarkdownView);
if (!view) { new Notice("请先打开文档"); return; }
const editor = view.editor;
// --- 获取范围 ---
let startPos = editor.getCursor("from");
let endPos = { line: editor.lineCount() - 1, ch: editor.getLine(editor.lineCount() - 1).length };
// 逻辑:
// 如果你有手动选中的文本,就只读选中的。
// 如果没有选中(或者上次暂停后光标归位了),就从光标读到最后。
if (editor.somethingSelected()) {
startPos = editor.getCursor("from");
endPos = editor.getCursor("to");
editor.setSelection(startPos); // 先取消选中,避免干扰
}
const fullText = editor.getRange(startPos, endPos);
if (!fullText.trim()) { new Notice("🏁 内容为空或已读完"); return; }
// --- 拆句 ---
const sentenceRegex = /[^。!?.!?\n\r]+[。!?.!?\n\r]*/g;
let match;
const sentences = [];
while ((match = sentenceRegex.exec(fullText)) !== null) {
if (match[0].trim()) sentences.push({ text: match[0], index: match.index, length: match[0].length });
}
if (sentences.length === 0) { new Notice("无内容"); return; }
window.obsidian_tts.isReading = true;
new Notice(`▶️ 继续朗读 (${sentences.length} 句)`);
const tempFile = path.join(os.tmpdir(), "obsidian_tts_stream.txt");
// --- 净化函数 ---
function cleanMarkdown(text) {
return text
.replace(/#+\s/g, "")
.replace(/\*\*/g, "")
.replace(/==/g, "")
.replace(/`{1,3}/g, "")
.replace(/\[\[(?:[^|\]]*\|)?([^\]]+)\]\]/g, "$1")
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
.replace(/^>\s/gm, "")
.replace(/!\[.*?\]\(.*?\)/g, "")
.replace(/[-*+] /g, "")
.replace(/\s+/g, " ");
}
// --- PowerShell 初始化 ---
const psInitScript = `
$Host.UI.RawUI.WindowTitle = 'ObsidianTTS_Stream';
Add-Type -AssemblyName System.Speech;
$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;
$v = $s.GetInstalledVoices() | Where-Object { $_.VoiceInfo.Name -like '*${VOICE_NAME}*' } | Select-Object -First 1;
if ($v) { $s.SelectVoice($v.VoiceInfo.Name) };
function Read-Text {
$path = '${tempFile}';
if (Test-Path $path) {
try {
$txt = Get-Content -LiteralPath $path -Encoding UTF8 | Out-String;
if (-not [string]::IsNullOrWhiteSpace($txt)) {
$safeTxt = [System.Security.SecurityElement]::Escape($txt);
$ssml = "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'><prosody rate='${SPEED_RATE}'>$safeTxt</prosody></speak>";
$s.SpeakSsml($ssml);
}
} catch {}
}
Write-Output "|||DONE|||";
}
`;
const ps = spawn('powershell', ['-Command', '-'], { windowsHide: true });
window.obsidian_tts.process = ps;
ps.stdout.on('data', (data) => {
if (data.toString().includes("|||DONE|||")) {
if (window.obsidian_tts.resolver) {
window.obsidian_tts.resolver();
window.obsidian_tts.resolver = null;
}
}
});
ps.stdin.write(psInitScript + "\n");
const waitForAudio = () => {
return new Promise((resolve) => {
window.obsidian_tts.resolver = resolve;
});
};
// --- 循环朗读 ---
try {
for (let i = 0; i < sentences.length; i++) {
if (!window.obsidian_tts.isReading) break;
const sent = sentences[i];
// 1. 高亮
const absStartIndex = editor.posToOffset(startPos) + sent.index;
const absEndIndex = absStartIndex + sent.length;
const highlightStart = editor.offsetToPos(absStartIndex);
const highlightEnd = editor.offsetToPos(absEndIndex);
editor.setSelection(highlightStart, highlightEnd);
editor.scrollIntoView({ from: highlightStart, to: highlightEnd }, true);
// 2. 净化
const pureText = cleanMarkdown(sent.text);
if (!pureText.trim()) continue;
// 3. 写入
fs.writeFileSync(tempFile, pureText, { encoding: 'utf8' });
// 4. 发令
ps.stdin.write("Read-Text\n");
// 5. 等待
await waitForAudio();
}
} catch (e) {
console.error(e);
} finally {
// 如果是正常读完(而不是被暂停打断),也把光标归位,体验更好
if (window.obsidian_tts.isReading) {
window.obsidian_tts.isReading = false;
// 正常读完后,光标停在最后一句的后面
// 你也可以选择在这里加逻辑,让它回到第一句,或者保持在最后
}
if (window.obsidian_tts.process) {
window.obsidian_tts.process.kill();
window.obsidian_tts.process = null;
}
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
// 只有当正常读完时才提示结束,暂停时不提示
if (!window.obsidian_tts.isReading) {
// new Notice("✅ 朗读结束");
}
}
};
语音可以改成你的本地语音
不知道本地语音叫什么名字的
下载Balabolka软件 然后找到语音 点击关于 就知道叫什么了