Node.js 中使用 Windows TTS(文本转语音)功能文档

lishihuan大约 13 分钟

Node.js 中使用 Windows TTS(文本转语音)功能文档

目录


概述

本文档介绍如何在 Node.js Express 服务中集成 Windows 系统的 TTS(Text-to-Speech)功能,实现文本语音播报。该方案使用 Windows 自带的 System.Speech.Synthesis 组件,通过 PowerShell 命令调用,无需安装额外的第三方库。

功能特性

  • ✅ 文本转语音播报
  • ✅ 支持多种语音音色选择(男声/女声)
  • ✅ 可调节语速(-10 到 10)
  • ✅ 可调节音量(0 到 100)
  • ✅ 自动处理特殊字符转义
  • ✅ 完善的错误处理机制

实现原理

技术栈

  • Node.js: 使用 child_process.exec() 执行系统命令
  • PowerShell: 调用 Windows 的 System.Speech.Synthesis 组件
  • Express: 提供 HTTP API 接口

工作流程

客户端请求 → Express 路由 → 参数验证 → 文本转义 → PowerShell 命令构建 → 
执行系统命令 → 调用 Windows TTS → 返回结果

核心代码

1. 基础依赖

const express = require('express');
const { exec } = require('child_process');

说明

  • express: Web 框架,需要安装 npm install express
  • child_process: Node.js 内置模块,无需安装

2. 核心 speak 函数

function speak(text, voiceName = null, rate = 0, volume = 100) {
    // 检查文本是否为空
    if (!text || typeof text !== 'string') {
        console.error('播报文本为空或无效');
        return;
    }

    // 转义 PowerShell 中的特殊字符
    const escapedText = text
        .replace(/'/g, "''")  // 单引号转义为双单引号
        .replace(/"/g, '`"')  // 双引号转义
        .replace(/\$/g, '`$') // $ 符号转义
        .replace(/`/g, '``'); // 反引号转义
    
    // 构建 PowerShell 命令
    let cmd = `PowerShell -Command "Add-Type -AssemblyName System.Speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;`;
    
    // 如果指定了语音名称,尝试选择该语音
    if (voiceName) {
        const escapedVoiceName = voiceName.replace(/'/g, "''").replace(/"/g, '`"').replace(/\$/g, '`$').replace(/`/g, '``');
        cmd += ` try { $speak.SelectVoice('${escapedVoiceName}'); } catch { Write-Host '语音 ${escapedVoiceName} 不可用,使用默认语音'; };`;
    }
    
    // 设置语速(-10 到 10,0 为正常速度)
    cmd += ` $speak.Rate = ${rate};`;
    
    // 设置音量(0 到 100)
    cmd += ` $speak.Volume = ${volume};`;
    
    // 执行播报
    cmd += ` $speak.Speak('${escapedText}');"`;
    
    exec(cmd, (error, stdout, stderr) => {
        if (error) {
            console.error('播报执行错误:', error);
            console.error('错误输出:', stderr);
            return;
        }
        if (stderr) {
            console.warn('播报警告:', stderr);
        }
        console.log('播报成功:', text, voiceName ? `(语音: ${voiceName})` : '(默认语音)');
    });
}

3. 关键点说明

文本转义的重要性

PowerShell 命令中的特殊字符必须正确转义,否则会导致命令执行失败:

字符转义方式原因
' (单引号)'' (双单引号)PowerShell 单引号字符串中的转义方式
" (双引号)`" (反引号+双引号)PowerShell 双引号字符串中的转义方式
$ (美元符号)`$ (反引号+美元符号)防止被当作变量
` (反引号) `` (双反引号)PowerShell 转义字符本身

PowerShell 命令结构

Add-Type -AssemblyName System.Speech;
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;
$speak.SelectVoice('语音名称');  # 可选
$speak.Rate = 0;                # 语速
$speak.Volume = 100;            # 音量
$speak.Speak('文本内容');

API 接口

1. 获取可用语音列表

接口: GET /api/System/voices

描述: 获取系统中所有可用的语音列表

请求参数: 无

响应示例:

{
  "success": true,
  "voices": [
    "Microsoft Huihui",
    "Microsoft Yaoyao",
    "Microsoft Kangkang",
    "Microsoft Zira",
    "Microsoft David"
  ],
  "count": 5
}

代码实现:

app.get('/api/System/voices', (req, res) => {
    const cmd = `PowerShell -Command "Add-Type -AssemblyName System.Speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $voices = $speak.GetInstalledVoices(); $voices | ForEach-Object { $_.VoiceInfo.Name }"`;
    
    exec(cmd, (error, stdout, stderr) => {
        if (error) {
            console.error('获取语音列表错误:', error);
            return res.status(500).json({ 
                success: false, 
                message: '获取语音列表失败',
                error: stderr 
            });
        }
        
        // 解析 PowerShell 输出的语音列表
        const voices = stdout
            .split('\n')
            .map(v => v.trim())
            .filter(v => v.length > 0);
        
        res.json({ 
            success: true, 
            voices: voices,
            count: voices.length
        });
    });
});

2. 文本播报接口

接口: GET /api/System/speak

描述: 执行文本转语音播报

请求参数:

参数名类型必需默认值说明
strstring✅ 是-要播报的文本内容
voicestring❌ 否null语音名称,如 "Microsoft Huihui"
ratenumber❌ 否0语速,范围 -10 到 10
volumenumber❌ 否100音量,范围 0 到 100

响应示例:

{
  "success": true,
  "message": "播报请求已发送",
  "voice": "Microsoft Huihui",
  "rate": 0,
  "volume": 100
}

错误响应:

{
  "success": false,
  "message": "缺少参数 str"
}

代码实现:

app.get('/api/System/speak', (req, res) => {
    const text = req.query.str;
    const voice = req.query.voice || null;
    const rate = parseInt(req.query.rate) || 0;
    const volume = parseInt(req.query.volume) || 100;

    if (!text) {
        return res.status(400).json({ 
            success: false, 
            message: '缺少参数 str' 
        });
    }

    // 验证参数范围
    if (rate < -10 || rate > 10) {
        return res.status(400).json({ 
            success: false, 
            message: '语速参数 rate 必须在 -10 到 10 之间' 
        });
    }

    if (volume < 0 || volume > 100) {
        return res.status(400).json({ 
            success: false, 
            message: '音量参数 volume 必须在 0 到 100 之间' 
        });
    }

    speak(text, voice, rate, volume);

    res.json({ 
        success: true, 
        message: '播报请求已发送',
        voice: voice || '默认',
        rate: rate,
        volume: volume
    });
});

使用示例

示例 1: 基础播报

# 使用默认语音播报
curl "http://localhost:30001/api/System/speak?str=你好世界"

示例 2: 指定语音播报

# 使用女声播报
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui"

# 使用男声播报
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Kangkang"

示例 3: 调整语速和音量

# 快速播报(语速 +3)
curl "http://localhost:30001/api/System/speak?str=你好世界&rate=3"

# 慢速播报(语速 -2)
curl "http://localhost:30001/api/System/speak?str=你好世界&rate=-2"

# 调整音量到 80%
curl "http://localhost:30001/api/System/speak?str=你好世界&volume=80"

# 组合使用
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui&rate=2&volume=90"

示例 4: 获取可用语音列表

curl "http://localhost:30001/api/System/voices"

示例 5: JavaScript/前端调用

// 获取语音列表
fetch('http://localhost:30001/api/System/voices')
    .then(res => res.json())
    .then(data => {
        console.log('可用语音:', data.voices);
    });

// 播报文本
fetch('http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui&rate=0&volume=100')
    .then(res => res.json())
    .then(data => {
        console.log('播报结果:', data);
    });

完整 Demo

本节提供一个完整的、可直接运行的 Demo,方便快速创建和测试 TTS 功能。

快速开始

1. 创建项目目录

mkdir tts-demo
cd tts-demo

2. 初始化项目

npm init -y

3. 安装依赖

npm install express body-parser

4. 创建文件

创建以下两个文件:

package.json

{
  "name": "tts-demo",
  "version": "1.0.0",
  "description": "Windows TTS 文本转语音 Demo",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": ["tts", "speech", "windows"],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "body-parser": "^2.2.1"
  }
}

server.js

// 文件名: server.js
// Windows TTS 文本转语音服务

const express = require('express');
const bodyParser = require('body-parser');
const { exec } = require('child_process');

const app = express();
const PORT = 30001;

// TTS 播报函数
function speak(text, voiceName = null, rate = 0, volume = 100) {
    // 检查文本是否为空
    if (!text || typeof text !== 'string') {
        console.error('播报文本为空或无效');
        return;
    }

    // 转义 PowerShell 中的特殊字符
    const escapedText = text
        .replace(/'/g, "''")  // 单引号转义为双单引号
        .replace(/"/g, '`"')  // 双引号转义
        .replace(/\$/g, '`$') // $ 符号转义
        .replace(/`/g, '``'); // 反引号转义
    
    // 构建 PowerShell 命令
    let cmd = `PowerShell -Command "Add-Type -AssemblyName System.Speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;`;
    
    // 如果指定了语音名称,尝试选择该语音
    if (voiceName) {
        const escapedVoiceName = voiceName.replace(/'/g, "''").replace(/"/g, '`"').replace(/\$/g, '`$').replace(/`/g, '``');
        cmd += ` try { $speak.SelectVoice('${escapedVoiceName}'); } catch { Write-Host '语音 ${escapedVoiceName} 不可用,使用默认语音'; };`;
    }
    
    // 设置语速(-10 到 10,0 为正常速度)
    cmd += ` $speak.Rate = ${rate};`;
    
    // 设置音量(0 到 100)
    cmd += ` $speak.Volume = ${volume};`;
    
    // 执行播报
    cmd += ` $speak.Speak('${escapedText}');"`;
    
    exec(cmd, (error, stdout, stderr) => {
        if (error) {
            console.error('播报执行错误:', error);
            console.error('错误输出:', stderr);
            return;
        }
        if (stderr) {
            console.warn('播报警告:', stderr);
        }
        console.log('播报成功:', text, voiceName ? `(语音: ${voiceName})` : '(默认语音)');
    });
}

// 解析 JSON 请求体
app.use(bodyParser.json());

// 获取可用的语音列表
app.get('/api/System/voices', (req, res) => {
    const cmd = `PowerShell -Command "Add-Type -AssemblyName System.Speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $voices = $speak.GetInstalledVoices(); $voices | ForEach-Object { $_.VoiceInfo.Name }"`;
    
    exec(cmd, (error, stdout, stderr) => {
        if (error) {
            console.error('获取语音列表错误:', error);
            return res.status(500).json({ 
                success: false, 
                message: '获取语音列表失败',
                error: stderr 
            });
        }
        
        // 解析 PowerShell 输出的语音列表
        const voices = stdout
            .split('\n')
            .map(v => v.trim())
            .filter(v => v.length > 0);
        
        res.json({ 
            success: true, 
            voices: voices,
            count: voices.length
        });
    });
});

// 文本播报接口
app.get('/api/System/speak', (req, res) => {
    console.log('收到 speak 请求', req.query);
    const text = req.query.str;
    const voice = req.query.voice || null;  // 可选:语音名称,如 "Microsoft Huihui"
    const rate = parseInt(req.query.rate) || 0;  // 可选:语速 -10 到 10,默认 0
    const volume = parseInt(req.query.volume) || 100;  // 可选:音量 0 到 100,默认 100

    if (!text) {
        return res.status(400).json({ 
            success: false, 
            message: '缺少参数 str' 
        });
    }

    // 验证参数范围
    if (rate < -10 || rate > 10) {
        return res.status(400).json({ 
            success: false, 
            message: '语速参数 rate 必须在 -10 到 10 之间' 
        });
    }

    if (volume < 0 || volume > 100) {
        return res.status(400).json({ 
            success: false, 
            message: '音量参数 volume 必须在 0 到 100 之间' 
        });
    }

    speak(text, voice, rate, volume);

    res.json({ 
        success: true, 
        message: '播报请求已发送',
        voice: voice || '默认',
        rate: rate,
        volume: volume
    });
});

// 启动服务
app.listen(PORT, () => {
    console.log(`TTS 服务已启动,监听端口 ${PORT}`);
    console.log(`访问 http://localhost:${PORT}/api/System/voices 查看可用语音`);
    console.log(`访问 http://localhost:${PORT}/api/System/speak?str=你好 进行测试`);
});

5. 启动服务

npm start

或者:

node server.js

看到以下输出表示启动成功:

TTS 服务已启动,监听端口 30001
访问 http://localhost:30001/api/System/voices 查看可用语音
访问 http://localhost:30001/api/System/speak?str=你好 进行测试

测试 Demo

方法 1: 使用浏览器

  1. 查看可用语音

    http://localhost:30001/api/System/voices
    
  2. 基础播报测试

    http://localhost:30001/api/System/speak?str=你好世界
    
  3. 指定语音播报

    http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui
    

方法 2: 使用 curl(PowerShell)

# 查看可用语音
curl http://localhost:30001/api/System/voices

# 基础播报
curl "http://localhost:30001/api/System/speak?str=你好世界"

# 指定女声播报
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui"

# 指定男声播报
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Kangkang"

# 调整语速和音量
curl "http://localhost:30001/api/System/speak?str=你好世界&voice=Microsoft%20Huihui&rate=2&volume=90"

方法 3: 使用 Postman 或类似工具

  1. 创建 GET 请求
  2. URL: http://localhost:30001/api/System/speak
  3. Query 参数:
    • str: 你好世界
    • voice: Microsoft Huihui (可选)
    • rate: 0 (可选)
    • volume: 100 (可选)

方法 4: 使用 JavaScript 测试

创建 test.html 文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TTS 测试</title>
</head>
<body>
    <h1>TTS 文本转语音测试</h1>
    
    <div>
        <label>文本内容:</label>
        <input type="text" id="textInput" value="你好世界" placeholder="输入要播报的文本">
    </div>
    
    <div>
        <label>语音选择:</label>
        <select id="voiceSelect">
            <option value="">默认语音</option>
        </select>
        <button onclick="loadVoices()">刷新语音列表</button>
    </div>
    
    <div>
        <label>语速:</label>
        <input type="range" id="rateInput" min="-10" max="10" value="0">
        <span id="rateValue">0</span>
    </div>
    
    <div>
        <label>音量:</label>
        <input type="range" id="volumeInput" min="0" max="100" value="100">
        <span id="volumeValue">100</span>
    </div>
    
    <div>
        <button onclick="speak()">播报</button>
    </div>
    
    <div id="result"></div>

    <script>
        const API_BASE = 'http://localhost:30001';
        
        // 加载语音列表
        async function loadVoices() {
            try {
                const response = await fetch(`${API_BASE}/api/System/voices`);
                const data = await response.json();
                
                const select = document.getElementById('voiceSelect');
                select.innerHTML = '<option value="">默认语音</option>';
                
                data.voices.forEach(voice => {
                    const option = document.createElement('option');
                    option.value = voice;
                    option.textContent = voice;
                    select.appendChild(option);
                });
            } catch (error) {
                console.error('加载语音列表失败:', error);
            }
        }
        
        // 播报文本
        async function speak() {
            const text = document.getElementById('textInput').value;
            const voice = document.getElementById('voiceSelect').value;
            const rate = document.getElementById('rateInput').value;
            const volume = document.getElementById('volumeInput').value;
            
            if (!text) {
                alert('请输入要播报的文本');
                return;
            }
            
            const params = new URLSearchParams({
                str: text,
                rate: rate,
                volume: volume
            });
            
            if (voice) {
                params.append('voice', voice);
            }
            
            try {
                const response = await fetch(`${API_BASE}/api/System/speak?${params}`);
                const data = await response.json();
                
                document.getElementById('result').innerHTML = 
                    `<p>${data.success ? '✅ 播报成功' : '❌ 播报失败'}</p>
                     <pre>${JSON.stringify(data, null, 2)}</pre>`;
            } catch (error) {
                console.error('播报失败:', error);
                document.getElementById('result').innerHTML = 
                    `<p>❌ 请求失败: ${error.message}</p>`;
            }
        }
        
        // 更新显示值
        document.getElementById('rateInput').addEventListener('input', (e) => {
            document.getElementById('rateValue').textContent = e.target.value;
        });
        
        document.getElementById('volumeInput').addEventListener('input', (e) => {
            document.getElementById('volumeValue').textContent = e.target.value;
        });
        
        // 页面加载时获取语音列表
        loadVoices();
    </script>
</body>
</html>

在浏览器中打开 test.html 即可进行可视化测试。

项目结构

tts-demo/
├── package.json          # 项目配置文件
├── server.js             # 服务器主文件
├── test.html            # 前端测试页面(可选)
└── node_modules/        # 依赖包(npm install 后生成)

完整命令清单

# 1. 创建项目
mkdir tts-demo && cd tts-demo

# 2. 初始化项目
npm init -y

# 3. 安装依赖
npm install express body-parser

# 4. 创建 server.js(复制上面的代码)

# 5. 启动服务
npm start

# 6. 测试(在另一个终端)
curl "http://localhost:30001/api/System/speak?str=你好世界"

预期结果

  1. 服务启动成功:控制台显示服务已启动信息
  2. 语音播报:听到系统播报"你好世界"
  3. API 响应:返回 JSON 格式的成功响应

故障排查

如果遇到问题,请检查:

  1. ✅ Node.js 版本 >= 12
  2. ✅ 已安装 express 和 body-parser
  3. ✅ Windows 系统(不支持 Linux/Mac)
  4. ✅ 系统音量未静音
  5. ✅ 端口 30001 未被占用

常见问题

Q1: 为什么播报没有声音?

可能原因

  1. 系统音量被静音或调低
  2. PowerShell 执行权限不足
  3. 文本包含特殊字符导致命令解析失败
  4. Windows 语音服务未启用

解决方法

  • 检查系统音量和应用音量设置
  • 以管理员身份运行 Node.js 服务
  • 查看服务器控制台的错误日志
  • 在 Windows 设置中检查语音服务

Q2: 如何知道系统中有哪些语音可用?

调用 /api/System/voices 接口获取完整列表。

Q3: 指定的语音名称不存在怎么办?

系统会自动使用默认语音,并在控制台输出警告信息。

Q4: 支持哪些语言?

支持 Windows 系统中已安装的所有语音包。常见的中文语音:

  • Microsoft Huihui - 中文女声
  • Microsoft Yaoyao - 中文女声
  • Microsoft Kangkang - 中文男声

常见的英文语音:

  • Microsoft Zira - 英文女声
  • Microsoft David - 英文男声

Q5: 如何处理包含特殊字符的文本?

代码已自动处理常见特殊字符的转义,包括:

  • 单引号 '
  • 双引号 "
  • 美元符号 $
  • 反引号 `

如果遇到其他特殊字符导致问题,可以:

  1. 查看服务器控制台的错误日志
  2. 对文本进行 URL 编码后再传递
  3. 扩展转义逻辑

Q6: 语速和音量的取值范围是什么?

  • 语速 (rate): -10 到 10
    • -10: 最慢
    • 0: 正常速度(默认)
    • 10: 最快
  • 音量 (volume): 0 到 100
    • 0: 静音
    • 100: 最大音量(默认)

注意事项

1. 平台限制

⚠️ 仅支持 Windows 系统,因为使用了 Windows 特有的 System.Speech.Synthesis 组件。

2. 安全性

  • ✅ 已实现文本转义,防止命令注入攻击
  • ✅ 参数验证,防止无效输入
  • ⚠️ 建议在生产环境中添加请求频率限制

3. 性能考虑

  • exec() 是异步非阻塞的,不会阻塞 Node.js 事件循环
  • 多个播报请求会并发执行
  • 如果需要串行执行,需要自行实现队列机制

4. 错误处理

  • 所有错误都会记录到控制台
  • API 返回错误状态码和错误信息
  • PowerShell 执行失败不会导致 Node.js 服务崩溃

5. 依赖要求

必需依赖(需要安装):

{
  "dependencies": {
    "express": "^4.18.2",
    "body-parser": "^2.2.1"
  }
}

内置模块(无需安装):

  • child_process - Node.js 核心模块

6. 完整代码示例

完整可运行的 server.js 文件请参考项目中的 server.js 文件。


扩展功能建议

1. 添加语音队列

如果需要串行播报,避免多个语音同时播放:

const speechQueue = [];
let isSpeaking = false;

function speakWithQueue(text, voiceName, rate, volume) {
    speechQueue.push({ text, voiceName, rate, volume });
    processQueue();
}

function processQueue() {
    if (isSpeaking || speechQueue.length === 0) return;
    
    isSpeaking = true;
    const { text, voiceName, rate, volume } = speechQueue.shift();
    
    speak(text, voiceName, rate, volume);
    
    // 假设播报需要 2 秒(实际需要根据文本长度计算)
    setTimeout(() => {
        isSpeaking = false;
        processQueue();
    }, 2000);
}

2. 支持 POST 请求

app.post('/api/System/speak', (req, res) => {
    const { str, voice, rate, volume } = req.body;
    // ... 处理逻辑
});

3. 添加语音缓存

对于重复的文本,可以缓存语音文件,提高响应速度。

4. 支持 SSML

如果需要更高级的语音控制(如停顿、重音等),可以考虑使用 SSML(Speech Synthesis Markup Language)。


总结

本文档介绍了在 Node.js 中使用 Windows TTS 功能的完整实现方案。核心要点:

  1. ✅ 使用 child_process.exec() 执行 PowerShell 命令
  2. ✅ 正确转义特殊字符,确保命令安全执行
  3. ✅ 支持语音选择、语速和音量控制
  4. ✅ 完善的错误处理和参数验证
  5. ✅ 提供查询可用语音的接口

该方案简单高效,无需安装额外的 TTS 库,充分利用 Windows 系统自带功能。


文档版本: 1.0
最后更新: 2024
适用平台: Windows
Node.js 版本: 12+