本地高质量TTS 语音朗读

电脑里有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软件 然后找到语音 点击关于 就知道叫什么了

有没有使用指南,不知道怎么用

如果是不知道怎么用quickadd
deepseek搜
obsidian 如何使用quickadd脚本
如何绑定快捷键
我把代码给你 告诉我每一步如何操作 并且这段代码的功能是什么

如果是不知道xiaoxiao语音如何用
deepseek问
电脑系统如果跳过微软限制 让讲述人里有 xiaoxiao语音