更新记录
1.0.0(2026-10-19) 下载此版本
首个正式版本,覆盖 App 三端(Android / iOS / HarmonyOS)与 H5 调试端。 全部基于系统原生能力实现,不引入任何三方 SDK。
TTS 语音合成
speak()朗读,支持rate(语速)、pitch(音调)、volume(音量)、voice(音色)、queue(排队)stop()停止、pause()暂停、resume()继续- 三端均支持暂停 / 继续:
- iOS 使用
AVSpeechSynthesizer原生pauseSpeakingAtBoundary/continueSpeaking - Android 采用软件层续读:通过
onRangeStart记录断点,pause()回退到最近句子边界,resume()从该处续读 - 续读时进度回调使用原文绝对位置,进度条不回跳
- iOS 使用
getVoices()获取系统音色列表isAvailable()能力检测openSettings()打开系统 TTS 设置(Android)release()释放引擎- 逐字进度回调
onProgress(Android / iOS)
ASR 语音识别
startRecognize()开始识别,支持engine: 'system' | 'external'external模式采集 16kHz / 16bit / 单声道 PCM,Base64 编码后 POST 到自建接口; 支持url、headers、format、bodyType(json / binary)、resultPath、timeout配置- 中间结果
onPartial、最终结果onResult、状态onStateChange、音量onVolumeChange、结束onEnd stopRecognize()结束并保留结果、cancelRecognize()取消并丢弃isRecognizeAvailable()能力检测releaseRecognize()释放资源
平台实现
- Android:
TextToSpeech+SpeechRecognizer;外部接口模式使用AudioRecord采集 PCM - iOS:
AVSpeechSynthesizer+Speech框架 - HarmonyOS:
@kit.CoreSpeechKit的textToSpeech与speechRecognizer - H5:Web Speech API(仅调试用)
工程化
interface.uts统一三端类型契约,unierror.uts统一 22 个错误码- 各平台自带原生清单:Android
AndroidManifest.xml、iOSInfo.plist、鸿蒙module.json5 - 已通过 HBuilderX CLI
compile app-android编译验证
已知限制
- Android 续读依赖 TTS 引擎的
onRangeStart回调;不提供该回调的引擎(AOSP 未强制要求) 断点始终为 0,续读会从头重读整段 - Android 续读会回退到句首,已念过的小半句会重念一次(避免从词语中间切入)
- HarmonyOS 的
CoreSpeechKit不支持暂停恢复,pause()会退化为stop() - HarmonyOS 的 TTS 不提供逐字进度与音调控制
- Android 外部接口模式的静音断句采用固定阈值,未实现逐帧音量波形
- iOS 的
engine: 'external'暂未内置音频转码链路,会返回提示改用系统识别 - HarmonyOS 暂不支持外部识别接口
平台兼容性
uni-app(5.21)
| Vue2 | Vue2插件版本 | Vue3 | Vue3插件版本 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|
| √ | 1.0.0 | √ | 1.0.0 | × | × | × | × | × | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(5.21)
| Chrome | Safari | Android | Android插件版本 | iOS | iOS插件版本 | 鸿蒙 | 鸿蒙插件版本 | 微信小程序 |
|---|---|---|---|---|---|---|---|---|
| - | - | 9.0 | 1.0.0 | 17 | 1.0.0 | 21 | 1.0.0 | - |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | × | × | √ |
xw-tts
App 三端(Android / iOS / HarmonyOS)原生语音合成(TTS)与语音识别(ASR) UTS 插件。
- 不引入任何三方 SDK,全部基于系统原生能力,无额外体积、无需申请 AppKey
- TTS 支持语速 / 音调 / 音量 / 音色切换、暂停继续、逐字进度回调
- ASR 支持系统识别与外部 HTTP 接口识别两种引擎,后者可对接自建的精准识别服务
- 统一的三端 API 与错误码,业务代码不用写平台判断
目录
平台支持
| 能力 | Android | iOS | HarmonyOS | H5 |
|---|---|---|---|---|
| TTS 朗读 | ✅ TextToSpeech | ✅ AVSpeechSynthesizer | ✅ CoreSpeechKit | ✅ Web Speech API |
| 暂停 / 继续 | ✅ 软件层续读 | ✅ 原生支持 | ⚠️ 退化为停止 | ✅ |
| 逐字进度 | ✅ onRangeStart |
✅ willSpeakRange |
❌ 只有开始/结束 | ✅ onboundary |
| 音色切换 | ✅ | ✅ | ✅(内置音色) | ✅ |
| ASR 系统识别 | ✅ SpeechRecognizer | ✅ Speech 框架 | ✅ CoreSpeechKit | ✅ Webkit |
| ASR 外部接口 | ✅ | ⚠️ 见说明 | ❌ | ❌ |
H5 端仅用于本地调试,正式能力以 App 三端为准。
快速开始
1. 安装插件
在插件市场点击「使用 HBuilderX 导入插件」,或把整个 xw-tts 目录放到项目的 uni_modules 下:
项目根目录
└── uni_modules
└── xw-tts
2. 引入
import {
speak, stop, pause, resume, getVoices, isAvailable, release,
startRecognize, stopRecognize, cancelRecognize, isRecognizeAvailable, releaseRecognize
} from '@/uni_modules/xw-tts'
注意:只能从插件根目录导入,不能写
@/uni_modules/xw-tts/utssdk/xxx。
3. 最简示例
// 朗读
speak({
text: '你好,世界',
onComplete: (res) => console.log('读完了', res),
onError: (err) => console.error(err.errCode, err.errMsg)
})
// 识别
startRecognize({
onResult: (res) => console.log('识别到:', res.text),
onError: (err) => console.error(err.errCode, err.errMsg)
})
TTS 语音合成
speak(options): string
朗读文本,返回本次请求的 requestId。
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
text |
string |
必填 | 要朗读的文本 |
rate |
number |
1.0 |
语速,0.1 ~ 4.0 |
pitch |
number |
1.0 |
音调,0.1 ~ 2.0(鸿蒙忽略) |
volume |
number |
1.0 |
音量,0.0 ~ 1.0 |
voice |
string |
— | 音色 ID,来自 getVoices() |
language |
string |
'zh-CN' |
语言标签,传了 voice 时以 voice 为准 |
queue |
boolean |
false |
true 排队等前一条结束;false 打断当前 |
onStart |
(res) => void |
— | 开始朗读 |
onProgress |
(res) => void |
— | 进度:{ progress, total, percent } |
onComplete |
(res) => void |
— | 结束:{ interrupt, progress } |
onError |
(res) => void |
— | 失败:{ errCode, errMsg, detail } |
speak({
text: '这一段会比较慢,方便听清楚。',
rate: 0.7,
pitch: 1.0,
volume: 1.0,
queue: false,
onStart: (res) => console.log('开始', res.requestId),
: (res) => console.log(`进度 ${res.percent}%`),
onComplete: (res) => {
// interrupt 为 true 表示被 stop() 或新的一次 speak 打断
console.log(res.interrupt ? '被打断' : '正常读完')
},
onError: (err) => uni.showToast({ title: err.errMsg, icon: 'none' })
})
stop(requestId?)
停止朗读。传 requestId 只停指定请求,不传停全部。
被停止的请求会收到一次 onComplete({ interrupt: true })。
pause() / resume()
三端都支持「暂停 → 继续」,但实现方式不同,行为也有差别:
| 平台 | 实现方式 | 续读精度 |
|---|---|---|
| iOS | AVSpeechSynthesizer 原生 pause / continue |
⭐⭐⭐ 精确到词,无缝 |
| Android | 软件层「记录断点 + 截断重读」 | ⭐⭐ 回退到句首,会重念半句话 |
| HarmonyOS | CoreSpeechKit 不支持,退化为 stop |
⭐ 从开头重读 |
Android 的续读原理
Android 的 TextToSpeech 本身没有 pause/continue,所以插件在软件层模拟:
- 朗读过程中,
onRangeStart持续报告「当前念到第几个字符」,插件始终记录这个位置 - 调用
pause()时,从这个位置向前回退到最近的句子边界(。!?;或英文.!?;或换行) - 停掉引擎,把「剩余文本 + 续读起点 + 原参数」存进会话
- 调用
resume()时,从那个句子开头重新朗读剩余部分
speak({ text: '第一句话。第二句话。第三句话。' })
// 念到第二句中间时暂停
pause()
// 从「第二句话」开头重新念,而不是从半个词切入
resume()
为什么回退到句首:Android 的引擎按词而非按字符回调,如果严格从断点字符续读,很可能从一个词的中间开始念,听感很差。回退到句首会重念已念过的小半句,但听起来自然。
进度连续性:onProgress 上报的 progress 是原文中的绝对位置,续读后进度条不会回跳。
已知限制
- 部分 TTS 引擎不提供
onRangeStart(AOSP 只在引擎主动调用SynthesisCallback.rangeStart时才回调)。这种情况下断点位置始终为 0,pause()后再resume()会从头重读整段。Google TTS、多数国产引擎都支持,可实测确认。 - Android
pause()后不会触发onComplete(这是「暂停」而非「结束」)。只有stop()或自然读完才回调onComplete。 - HarmonyOS 的
CoreSpeechKit无此能力,pause()等同stop()。
如果你的业务要求三端行为完全一致且精确,建议用按句切分 +
queue: true的方案自行管理句子下标,这是唯一可在三端做到完全一致的可靠做法。
getVoices(options?)
获取系统可用音色。
getVoices({
language: 'zh', // 可选,按语言前缀过滤
success: (res) => {
// res.voices: Array<{ voiceId, name, language, local, isDefault }>
console.log(res.voices)
},
fail: (err) => console.error(err)
})
Android 注意:系统
TextToSpeech.getVoices()在引擎初始化完成后才有值,且部分机型需要先调一次speak()才会加载语音包。插件内部已做初始化等待,若仍为空,可用openSettings()引导用户下载语音包。
isAvailable(options?)
检测是否有可用 TTS 引擎。
isAvailable({
success: (res) => console.log(res.available, res.voiceCount)
})
openSettings()
跳转到系统 TTS 设置页,引导用户安装 / 切换语音引擎。 仅 Android 有效,iOS / 鸿蒙为空实现。
release()
释放引擎资源,建议在页面 onUnmounted 调用。
ASR 语音识别
startRecognize(options): string
开始识别,返回 requestId。
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
engine |
'system' \| 'external' |
'system' |
识别引擎 |
language |
string |
'zh-CN' |
识别语言 |
maxDuration |
number |
60000 |
最长录音时长(ms) |
silenceTimeout |
number |
10000 |
静音多久自动结束(ms) |
partialResult |
boolean |
true |
是否返回中间结果(边说边出字) |
external |
object |
— | 外部接口配置,见下 |
onPartial |
(res) => void |
— | 中间结果 { text } |
onResult |
(res) => void |
— | 最终结果 { text, engine, duration } |
onStateChange |
(res) => void |
— | 状态 { state }:preparing / listening / recognizing / stopping / end |
onVolumeChange |
(res) => void |
— | 音量 { volume }:0 ~ 100,可画波形 |
onError |
(res) => void |
— | 失败 |
onEnd |
(res) => void |
— | 结束 { success },成功失败都会触发一次 |
startRecognize({
engine: 'system',
language: 'zh-CN',
partialResult: true,
onStateChange: (res) => {
// 用 state 驱动 UI:录音按钮动画、波形等
console.log('状态', res.state)
},
onPartial: (res) => {
// 边说边出字
this.tempText = res.text
},
onResult: (res) => {
this.finalText = res.text
},
: (res) => {
this.volume = res.volume
},
onError: (err) => {
if (err.errCode === 1203) {
uni.showToast({ title: '请先允许麦克风权限', icon: 'none' })
}
},
onEnd: (res) => {
this.recording = false
}
})
stopRecognize(requestId?)
结束识别并保留结果(用户点「说完了」)。最终结果通过 onResult 回调。
cancelRecognize(requestId?)
取消识别并丢弃结果。
isRecognizeAvailable(options?)
isRecognizeAvailable({
engine: 'system',
success: (res) => console.log(res.state === 'available')
})
releaseRecognize()
释放识别引擎。
外部识别接口协议
当 engine: 'external' 时,插件会:
- 用麦克风采集 16kHz / 16bit / 单声道 PCM
- 静音超时或达到
maxDuration后自动停止 - 把音频 Base64 编码后 POST 到你的接口
- 按
resultPath从响应 JSON 中取出文本
配置项
external: {
url: 'https://your-domain.com/asr', // 必填
headers: { 'Authorization': 'Bearer xxx' },
format: 'pcm', // pcm | wav | amr
bodyType: 'json', // json | binary
resultPath: 'data.text', // 从响应里取文本的字段路径
timeout: 15000
}
bodyType: 'json'(默认)请求体
{
"format": "pcm",
"rate": 16000,
"lang": "zh-CN",
"audio": "<Base64 编码的音频>"
}
bodyType: 'binary' 请求体
Content-Type: application/octet-stream,body 直接是音频字节。
服务端响应示例
{ "code": 0, "data": { "text": "今天天气不错" } }
配合 resultPath: 'data.text' 即可取到 今天天气不错。
最小服务端示例(Node.js / Express)
app.post('/asr', express.json({ limit: '20mb' }), async (req, res) => {
const pcm = Buffer.from(req.body.audio, 'base64')
// 转成你所用云厂商要求的格式后调用,例如:
// const text = await callYourAsr(pcm, { sampleRate: 16000, format: 'pcm' })
res.json({ code: 0, data: { text: '识别结果' } })
})
提示:外部识别对精度提升最明显。系统识别在嘈杂环境、专业术语场景下效果有限,把业务词表放到自己的服务端做热词增强是常见做法。
错误码
| 错误码 | 常量名 | 含义 |
|---|---|---|
| 1001 | UNSUPPORTED |
当前平台不支持该能力 |
| 1002 | INVALID_PARAM |
参数不合法 |
| 1003 | NOT_INITIALIZED |
引擎未初始化 |
| 1004 | BUSY |
上次操作尚未结束 |
| 1005 | PERMISSION_DENIED |
用户拒绝授权 |
| 1006 | NETWORK_ERROR |
网络异常 |
| 1007 | TIMEOUT |
请求超时 |
| 1099 | UNKNOWN |
未知错误 |
| 1101 | TTS_TEXT_EMPTY |
朗读文本为空 |
| 1102 | TTS_INIT_FAILED |
引擎初始化失败 |
| 1103 | TTS_NOT_AVAILABLE |
系统无可用合成引擎 |
| 1104 | TTS_VOICE_NOT_FOUND |
音色不存在 |
| 1105 | TTS_SPEAK_FAILED |
朗读失败 |
| 1106 | TTS_LANGUAGE_MISSING |
缺少语音包 |
| 1201 | ASR_INIT_FAILED |
识别引擎初始化失败 |
| 1202 | ASR_NOT_AVAILABLE |
识别服务不可用 |
| 1203 | ASR_PERMISSION_DENIED |
没有麦克风权限 |
| 1204 | ASR_MIC_BUSY |
麦克风被占用 |
| 1205 | ASR_NO_SPEECH |
未识别到内容 |
| 1206 | ASR_MAX_DURATION |
超出最大时长 |
| 1207 | ASR_EXTERNAL_ERROR |
外部接口返回错误 |
| 1208 | ASR_EXTERNAL_NOT_CONFIGURED |
未配置外部接口 |
平台差异
TTS
| 行为 | Android | iOS | HarmonyOS |
|---|---|---|---|
| 底层 | android.speech.tts.TextToSpeech |
AVSpeechSynthesizer |
@kit.CoreSpeechKit textToSpeech |
| 语速映射 | 直接设置 | 0.1~4.0 映射到系统 0~1 |
extraParams.speed |
| 音调 | 原生支持 | pitchMultiplier(0.5~2.0) |
忽略 |
| 暂停继续 | ✅ 软件层续读 | ✅ 原生 | ❌ |
| 逐字进度 | ✅ | ✅ | ❌ |
| 离线 | 取决于系统语音包 | ✅ | 混合(online: 1) |
ASR
| 行为 | Android | iOS | HarmonyOS |
|---|---|---|---|
| 底层 | SpeechRecognizer |
Speech 框架 |
speechRecognizer |
| 中间结果 | ✅ | ✅ | ✅ |
| 音量回调 | ✅ | ⚠️ 未实现 | ⚠️ 未实现 |
| 外部接口 | ✅ | ⚠️ 见下 | ❌ |
iOS 外部接口说明:iOS 上把
AVAudioPCMBuffer逐帧转换成 16k PCM 再 Base64 需要AVAudioConverter,UTS 层处理二进制较重。当前 iOS 端engine: 'external'会返回1001并提示改用系统识别。若必须走自建接口,建议在业务层用uni.getRecorderManager()录音拿到文件,再uni.uploadFile()上传到你的服务端,这样三端逻辑统一且更省心。
权限说明
权限总览
插件遵循「最小权限」原则:语音合成(TTS)不需要任何权限,只有语音识别(ASR)需要麦克风。
| 能力 | 需要的权限 |
|---|---|
| TTS 朗读、暂停、音色、进度 | 无 |
| ASR 系统识别 | 麦克风 + 网络 |
| ASR 外部接口识别 | 麦克风 + 网络 |
三端申请的权限清单
Android
| 权限 | 用途 | 触发时机 |
|---|---|---|
android.permission.RECORD_AUDIO |
语音识别时采集麦克风音频 | 调用 startRecognize() 时动态申请 |
android.permission.INTERNET |
系统识别与外部接口识别需要联网 | 安装时授予 |
android.permission.ACCESS_NETWORK_STATE |
读取网络可用状态,用于识别失败时给出准确错误 | 安装时授予 |
插件同时声明了包可见性 queries(android.speech.RecognitionService 与 android.intent.action.TTS_SERVICE)。
这是 Android 11(API 30)的硬性要求,缺失会导致 isRecognitionAvailable() 误判为不可用。
RECORD_AUDIO是运行时权限,插件内部已封装申请流程: 调用startRecognize()时若未授权,会自动弹出系统授权框, 拒绝后走onError回调,错误码1203 ASR_PERMISSION_DENIED。
iOS
| 键 | 用途 |
|---|---|
NSMicrophoneUsageDescription |
麦克风用途说明,缺失则无法录音 |
NSSpeechRecognitionUsageDescription |
语音识别用途说明,缺失则无法识别 |
插件刻意不声明
UIBackgroundModes: audio。AVAudioSession.Category.playback已能在静音键打开时正常播放 TTS, 声明后台音频只会在上架审核时被要求额外说明后台用途。
HarmonyOS
| 权限 | 用途 | usedScene |
|---|---|---|
ohos.permission.MICROPHONE |
语音识别录音 | inuse(仅使用时) |
ohos.permission.INTERNET |
系统识别与外部接口识别 | inuse(仅使用时) |
权限声明文件位置
插件已内置各端原生清单,导入插件后无需手动编写:
utssdk/app-android/AndroidManifest.xml Android 权限 + queries
utssdk/app-ios/Info.plist iOS 用途说明
utssdk/app-harmony/module.json5 鸿蒙权限 + resources 文案
接入方仍需做的事
Android
RECORD_AUDIO 最终以项目的 manifest.json 为准,需补充:
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
]
}
}
}
iOS
若项目自己的 manifest.json 也配置了 infoPlist,两处会合并。保持文案语义一致,否则审核可能被质疑用途描述与实际行为不符:
"app-plus": {
"distribute": {
"ios": {
"privacyDescription": {
"NSMicrophoneUsageDescription": "需要使用麦克风进行语音识别",
"NSSpeechRecognitionUsageDescription": "需要语音识别能力将你的语音转换为文字"
}
}
}
}
HarmonyOS
插件 module.json5 已自动合并,无需额外配置。
用户拒绝授权时怎么处理
startRecognize({
onError(err) {
if (err.errCode === 1203) {
// 用户拒绝了麦克风权限
uni.showModal({
title: '需要麦克风权限',
content: '语音识别需要使用麦克风,是否前往设置开启?',
success(res) {
if (res.confirm) {
// 各端跳设置的写法不同,按需实现
}
}
})
}
}
})
数据与隐私
- TTS:文本交给系统语音引擎在本地合成,插件不上传任何文本
- ASR 系统识别:音频由系统语音服务处理(部分引擎会走厂商云端,取决于系统实现)
- ASR 外部接口:音频会 POST 到开发者自己配置的 URL,插件不内建服务器、不内置统计或上报逻辑
- 插件不采集设备标识、不申请通讯录/位置/存储等无关权限
打包配置
自定义基座
UTS 插件在真机运行时需要打自定义基座(HBuilderX:运行 → 运行到手机或模拟器 → 制作自定义调试基座)。标准基座不含本插件。
常见问题
Q1:getVoices() 返回空数组
- Android:系统未安装 TTS 引擎或中文语音包。用
openSettings()引导用户去系统设置下载;也可以在speak()成功后再次调用getVoices()。 - iOS:首次启动时系统音色列表可能尚未加载,稍等再取。
Q2:Android 上朗读没有声音
排查顺序:
- 手机媒体音量是否为 0(插件用的是
STREAM_MUSIC) - 系统是否装了 TTS 引擎(部分国产 ROM 默认没有)
- 调
isAvailable()看available是否为true - 调
speak()看是否返回1102 / 1106错误码
Q3:识别一直返回 1205(未识别到内容)
- 检查麦克风权限是否真的授予了(
ASR_PERMISSION_DENIED是1203,1205通常是权限正常但没说话 / 环境太吵) - 外部接口模式下常见于音频太短(不足 300ms)或格式与服务端要求不匹配
Q4:朗读和识别同时用会互相干扰
麦克风和扬声器会互相影响(回声)。建议:
- 朗读期间不要开启识别
- 或者用外部接口模式,并在服务端做回声消除(AEC)
Q5:如何做「边播边听」的实时对话
推荐链路:
识别(系统) → 拿到文本 → 请求大模型 → 文本分句 → speak({ queue: true })
要点:按句切分再 speak,不要一次喂很长的文本,这样能显著降低首句延迟。
Q6:长文本朗读中途要支持暂停
直接调 pause() / resume() 即可,三端都有支持(Android 是软件层续读,见 pause / resume)。
如果发现 Android 上续读总是从头开始,多半是当前系统 TTS 引擎不回调 onRangeStart。这时改用「按句切分 + queue: true」的方案,自己维护句子下标,三端行为完全一致。
Q7:Android 上暂停后继续,会重念一点内容?
这是预期行为。Android 引擎按词回调,为了不从半个词切入,resume() 会回退到最近的句子开头,因此已念过的小半句会重念一次。如果这个体验不可接受,用分句方案。
许可证
MIT

收藏人数:
https://atomgit.com/xw-uniapp-plugins/xw-tts
下载插件并导入HBuilder
赞赏(0)
下载 0
赞赏 0
下载 12694408
赞赏 1956
赞赏
京公网安备:11010802035340号