Node.js 中使用 Windows TTS(文本转语音)功能文档
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 expresschild_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
描述: 执行文本转语音播报
请求参数:
| 参数名 | 类型 | 必需 | 默认值 | 说明 |
|---|---|---|---|---|
str | string | ✅ 是 | - | 要播报的文本内容 |
voice | string | ❌ 否 | null | 语音名称,如 "Microsoft Huihui" |
rate | number | ❌ 否 | 0 | 语速,范围 -10 到 10 |
volume | number | ❌ 否 | 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: 使用浏览器
查看可用语音:
http://localhost:30001/api/System/voices基础播报测试:
http://localhost:30001/api/System/speak?str=你好世界指定语音播报:
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 或类似工具
- 创建 GET 请求
- URL:
http://localhost:30001/api/System/speak - 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=你好世界"
预期结果
- 服务启动成功:控制台显示服务已启动信息
- 语音播报:听到系统播报"你好世界"
- API 响应:返回 JSON 格式的成功响应
故障排查
如果遇到问题,请检查:
- ✅ Node.js 版本 >= 12
- ✅ 已安装 express 和 body-parser
- ✅ Windows 系统(不支持 Linux/Mac)
- ✅ 系统音量未静音
- ✅ 端口 30001 未被占用
常见问题
Q1: 为什么播报没有声音?
可能原因:
- 系统音量被静音或调低
- PowerShell 执行权限不足
- 文本包含特殊字符导致命令解析失败
- Windows 语音服务未启用
解决方法:
- 检查系统音量和应用音量设置
- 以管理员身份运行 Node.js 服务
- 查看服务器控制台的错误日志
- 在 Windows 设置中检查语音服务
Q2: 如何知道系统中有哪些语音可用?
调用 /api/System/voices 接口获取完整列表。
Q3: 指定的语音名称不存在怎么办?
系统会自动使用默认语音,并在控制台输出警告信息。
Q4: 支持哪些语言?
支持 Windows 系统中已安装的所有语音包。常见的中文语音:
Microsoft Huihui- 中文女声Microsoft Yaoyao- 中文女声Microsoft Kangkang- 中文男声
常见的英文语音:
Microsoft Zira- 英文女声Microsoft David- 英文男声
Q5: 如何处理包含特殊字符的文本?
代码已自动处理常见特殊字符的转义,包括:
- 单引号
' - 双引号
" - 美元符号
$ - 反引号
`
如果遇到其他特殊字符导致问题,可以:
- 查看服务器控制台的错误日志
- 对文本进行 URL 编码后再传递
- 扩展转义逻辑
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 功能的完整实现方案。核心要点:
- ✅ 使用
child_process.exec()执行 PowerShell 命令 - ✅ 正确转义特殊字符,确保命令安全执行
- ✅ 支持语音选择、语速和音量控制
- ✅ 完善的错误处理和参数验证
- ✅ 提供查询可用语音的接口
该方案简单高效,无需安装额外的 TTS 库,充分利用 Windows 系统自带功能。
文档版本: 1.0
最后更新: 2024
适用平台: Windows
Node.js 版本: 12+