RNOH x HarmonyOS Core Speech Kit TTS:商品卖点语音播报真机实践
本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第 3 篇。上一篇我们完成了 RNOH TurboModule 最小实践,证明 React Native 页面可以调用 HarmonyOS 原生 ArkTS 模块。本篇在这个基础上接入第一个真实系统能力:通过 HarmonyOS Core Speech Kit 完成商品卖点 TTS 语音播报。
先来看看真机运行效果如下(PS:为了能跑 Kit,博主下了血本入手了时团的真机!):



📋 全文摘要
本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第3篇,详细介绍了如何在React Native应用中接入HarmonyOS Core Speech Kit实现TTS(文本转语音)功能。主要内容包括:
🎯 核心目标
- 在RNOH(React Native on HarmonyOS)项目中接入HarmonyOS原生TTS能力
- 实现商品卖点语音播报功能
- 构建完整的React Native ↔ HarmonyOS TurboModule通信链路
🏗️ 技术架构
- React Native侧:声明NativeTTSModule,实现TTS页面组件
- C++层:TurboModule wrapper桥接RN与ArkTS
- ArkTS侧:接入Core Speech Kit,实现TTS引擎管理
- 生命周期管理:处理播放完成、停止播放和资源清理
📁 工程结构
- 完整的模块化目录组织
- RN页面、TurboModule、ArkTS模块分离
- 配置文件和依赖管理
🔧 关键实现
- RN页面:TTS控制界面,支持开始/停止播报、状态显示
- TurboModule:C++ wrapper实现RN与ArkTS通信
- ArkTS模块:Core Speech Kit的封装与调用
- 生命周期:正确处理播放完成事件和资源释放
🚀 真机运行
- 详细的真机部署步骤
- 设备配置和权限设置
- 运行效果验证
⚠️ 常见问题排查
TTSModule找不到的解决方案- 引擎状态显示异常处理
- 播放完成事件监听问题
- 多次点击播报的处理策略
- 页面闪动问题修复
📝 总结与展望
- 成功验证RNOH调用HarmonyOS系统能力的技术路径
- 为后续OCR、图像识别等能力接入奠定基础
- 下一篇预告:RNOH x HarmonyOS OCR商品包装/物流面单识别
本文通过完整的代码示例和详细的步骤说明,为开发者提供了在React Native应用中集成HarmonyOS TTS能力的实战指南,涵盖了从工程搭建到问题排查的全流程。
1. 本篇要实现什么?
本篇目标是实现一个 React Native 页面调用 HarmonyOS Core Speech Kit 进行语音播报 的完整闭环。
React Native 页面输入商品卖点
↓
点击“开始播报”
↓
调用 RNOH TurboModule
↓
C++ TurboModule wrapper 转发到 ArkTS
↓
ArkTS TTSModule 调用 Core Speech Kit textToSpeech
↓
系统完成文字转语音播放
↓
播放状态通过事件回传 React Native 页面
最终页面提供这些能力:
1. 展示 TTS 引擎是否可用
2. 输入商品卖点文案
3. 开始播报
4. 停止播报
5. 显示播放中、播放完成、已停止、播放失败等状态
6. 播报期间禁止重复点击开始播报
2. 为什么第一个系统能力先选 TTS?
在 HarmonyOS 创新能力接入里,TTS 很适合作为第一篇系统能力实践。
- 交互直观:点击按钮后能直接听到声音,真机效果明显。
- 链路清晰:RN 传文本,ArkTS 调系统能力,结果和状态回传 RN。
- 复杂度适中:相比 OCR、图片选择、端侧 AI 推理,TTS 更容易聚焦 TurboModule 注册、事件回传和系统 Kit 调用。
在本系列的统一 Demo —— RN Harmony SKU Assistant 中,我们把它设计成:
商品卖点语音播报工具。
这既能展示鸿蒙语音能力,也能和后续 OCR、端侧 AI、商品资料管理等文章串起来。
3. 当前工程目录结构
本篇实现不是单一 ArkTS 文件就能完成。当前 RNOH 工程需要 JS spec、RN 页面、C++ wrapper、Package 注册和 ArkTS 模块共同配合。
RNHarmonySkuAssistant
├── App.tsx
├── src
│ ├── native
│ │ └── NativeTTSModule.ts
│ └── pages
│ ├── SkuTurboModulePage.tsx
│ └── TTSPage.tsx
└── harmony
└── entry
└── src
└── main
├── cpp
│ ├── CMakeLists.txt
│ ├── PackageProvider.cpp
│ ├── TTSPackage.h
│ └── turbomodule
│ ├── TTSModule.cpp
│ └── TTSModule.h
└── ets
├── GeneratedPackage.ets
└── turbomodule
└── TTSModule.ets
关键文件说明:
| 文件 | 作用 |
|---|---|
src/native/NativeTTSModule.ts |
RN 侧 TurboModule 类型声明 |
src/pages/TTSPage.tsx |
TTS Demo 页面与事件监听 |
harmony/entry/src/main/cpp/turbomodule/TTSModule.* |
C++ TurboModule 方法映射 |
harmony/entry/src/main/cpp/TTSPackage.h |
C++ Package 工厂,按模块名创建 TTSModule |
harmony/entry/src/main/cpp/PackageProvider.cpp |
把 TTSPackage 加入 RNOH Package 列表 |
harmony/entry/src/main/ets/GeneratedPackage.ets |
ArkTS TurboModule 工厂注册 |
harmony/entry/src/main/ets/turbomodule/TTSModule.ets |
Core Speech Kit TTS 真实调用 |
4. RN 侧声明 NativeTTSModule
src/native/NativeTTSModule.ts 负责声明 RN 可以调用的原生能力:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
speak(text: string): Promise<string>;
stop(): Promise<string>;
isAvailable(): Promise<boolean>;
addListener(eventName: string): void;
removeListeners(count: number): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('TTSModule');
这里有两个容易踩坑的点。
第一,模块名必须是同一个:
RN 侧:TurboModuleRegistry.getEnforcing<Spec>('TTSModule')
C++ 侧:if (name == "TTSModule")
ArkTS 侧:static readonly NAME = 'TTSModule'
第二,RN 的 NativeEventEmitter 会检查原生模块是否提供 addListener 和 removeListeners。即使 ArkTS 侧不需要做额外订阅管理,也要把这两个方法暴露出来,否则会出现事件监听相关警告或异常。
5. React Native 页面实现
src/pages/TTSPage.tsx 负责页面交互和状态展示。
页面初始化时先检查 TTS 引擎是否可用:
useEffect(() => {
const checkAvailability = async () => {
try {
const available = await TTSModule.isAvailable();
setIsAvailable(available);
} catch {
setIsAvailable(false);
}
};
checkAvailability();
}, []);
状态变化不靠 speak() 的 Promise 猜测,而是监听原生事件:
const ttsEventEmitter = new NativeEventEmitter(TTSModule);
const subscription = ttsEventEmitter.addListener(
'TTSStatus',
(event: { type: string; errorMessage?: string }) => {
switch (event.type) {
case 'start':
setStatus('播放中');
setSpeaking(true);
break;
case 'complete':
setStatus('播放完成');
setSpeaking(false);
break;
case 'stop':
setStatus('已停止');
setSpeaking(false);
break;
case 'error':
setStatus(`播放失败: ${event.errorMessage ?? '未知错误'}`);
setSpeaking(false);
break;
}
},
);
开始播报时做两个保护:
const handleSpeak = async () => {
const content = text.trim();
if (!content) {
Alert.alert('提示', '请输入需要播报的文字');
return;
}
if (speaking) {
Alert.alert('提示', '当前正在播报,请先停止后再开始新的播报');
return;
}
setStatus('准备播报');
await TTSModule.speak(content);
};
这样可以避免连续多次点击“开始播报”导致多个播报请求叠加。真实业务里也可以选择“新播报自动 stop 上一次”,但 Demo 里采用更保守的方式:播报中禁用开始按钮。
6. App.tsx 增加 Demo 入口
为了保留上一篇 TurboModule 最小实践,本篇没有把 App.tsx 直接替换成 TTS 页面,而是增加了 Demo 列表入口:
// App.tsx 节选
type DemoKey = 'list' | 'sku' | 'tts';
export default function App() {
const [currentDemo, setCurrentDemo] = useState<DemoKey>('list');
if (currentDemo === 'sku') {
return <SkuTurboModulePage onBack={() => setCurrentDemo('list')} />;
}
if (currentDemo === 'tts') {
return <TTSPage onBack={() => setCurrentDemo('list')} />;
}
return (
<SafeAreaView style={styles.safeArea}>
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>RNOH 能力 Demo</Text>
<DemoCard
title="TurboModule 最小实践"
desc="调用 ArkTS 模块,验证 RN 到原生的基础桥接。"
tag="基础链路"
onPress={() => setCurrentDemo('sku')}
/>
<DemoCard
title="HarmonyOS TTS 语音播报"
desc="调用 Core Speech Kit,将商品卖点文案转换为语音。"
tag="系统能力"
onPress={() => setCurrentDemo('tts')}
/>
</ScrollView>
</SafeAreaView>
);
}
后续我们会继续扩展:OCR、端侧 AI、Form Kit 等能力
7. C++ TurboModule wrapper
RNOH 的 TurboModule 调用链里,C++ wrapper 是很关键的一层。如果只写 ArkTS TTSModule.ets,RN 侧仍然可能报:
TurboModuleRegistry.getEnforcing(...): 'TTSModule' could not be found
本篇在 harmony/entry/src/main/cpp/turbomodule/TTSModule.cpp 中声明方法映射:
TTSModule::TTSModule(const ArkTSTurboModule::Context ctx, const std::string name)
: ArkTSTurboModule(ctx, name) {
methodMap_ = {
ARK_ASYNC_METHOD_METADATA(speak, 1),
ARK_ASYNC_METHOD_METADATA(stop, 0),
ARK_ASYNC_METHOD_METADATA(isAvailable, 0),
ARK_SCHEDULE_METHOD_METADATA(addListener, 1),
ARK_SCHEDULE_METHOD_METADATA(removeListeners, 1),
};
}
再通过 TTSPackage.h 按模块名创建 C++ TurboModule:
SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
if (name == "TTSModule") {
return std::make_shared<TTSModule>(ctx, name);
}
return nullptr;
}
最后在 PackageProvider.cpp 加入:
packages.emplace_back(std::make_unique<TTSPackage>(ctx));
并在 CMakeLists.txt 编译 ./turbomodule/TTSModule.cpp。这几处少任意一处,都可能导致 RN 找不到模块。
8. ArkTS 侧接入 Core Speech Kit
harmony/entry/src/main/ets/turbomodule/TTSModule.ets 使用 @kit.CoreSpeechKit:
import { textToSpeech } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
引擎采用懒加载:首次 isAvailable() 或 speak() 时创建。
const initParams: textToSpeech.CreateEngineParams = {
language: 'zh-CN',
person: 0,
online: 1,
extraParams: {
style: 'interaction-broadcast',
locate: 'CN',
name: 'RNOHTTSEngine',
},
};
textToSpeech.createEngine(initParams, (err, engine) => {
if (!err && engine) {
this.ttsEngine = engine;
this.engineReady = true;
resolve();
} else {
this.engineReady = false;
reject(`TTS 引擎创建失败: code=${err?.code}, message=${err?.message}`);
}
});
播报时设置监听器,并通过 RNOH 的 device event 通知 RN:
private emitTTSStatus(payload: TTSStatusPayload): void {
this.ctx.rnInstance.emitDeviceEvent('TTSStatus', payload);
}
const speakListener: textToSpeech.SpeakListener = {
onStart: (utteranceId: string): void => {
this.clearPlaybackPollTimer();
this.emitTTSStatus({ type: 'start', utteranceId });
},
onComplete: (utteranceId: string): void => {
this.waitForPlaybackComplete(utteranceId);
},
onStop: (utteranceId: string): void => {
this.clearPlaybackPollTimer();
this.emitTTSStatus({ type: 'stop', utteranceId });
},
onError: (utteranceId: string, errorCode: number, errorMessage: string): void => {
this.clearPlaybackPollTimer();
this.emitTTSStatus({
type: 'error',
utteranceId,
errorCode,
errorMessage,
});
},
};
9. 播放完成为什么不能只看 onComplete?
这次真机调试里有一个关键问题:onComplete 回调触发时,扬声器实际可能还在播放。
也就是说,onComplete 更接近“合成请求完成 / 输出完成”的信号,不一定等于用户听到的声音已经结束。如果 RN 在这个回调里立刻显示“播放完成”,页面状态就会早于真实播放。
当前实现采用更稳的方式:收到 onComplete 后继续轮询 ttsEngine.isBusy(),等引擎不忙时再向 RN 发 complete。
private waitForPlaybackComplete(utteranceId: string): void {
this.clearPlaybackPollTimer();
this.playbackPollTimer = setInterval(() => {
if (!this.ttsEngine) {
this.clearPlaybackPollTimer();
this.emitTTSStatus({ type: 'complete', utteranceId });
return;
}
if (!this.ttsEngine.isBusy()) {
this.clearPlaybackPollTimer();
this.emitTTSStatus({ type: 'complete', utteranceId });
}
}, 200);
}
这不是估算播报时长,而是使用引擎状态判断播放是否仍在进行。相比按文本长度估算时间,它对语速、设备性能、语音合成策略更友好。
10. 停止播放与生命周期清理
停止播报时,需要同时处理两件事:
1. 清掉播放完成轮询定时器
2. 调用 TTS 引擎 stop()
async stop(): Promise<string> {
if (!this.ttsEngine) {
return '引擎未初始化,无需停止';
}
this.clearPlaybackPollTimer();
this.ttsEngine.stop();
return '已停止播报';
}
模块销毁时也要释放资源:
__onDestroy__() {
this.clearPlaybackPollTimer();
if (this.ttsEngine) {
this.ttsEngine.shutdown();
this.ttsEngine = null;
this.engineReady = false;
}
}
这一步可以避免页面销毁、热更新或应用退出后留下无意义的引擎状态。
11. 真机运行步骤
本篇在真机上验证,建议按下面顺序排查:
1. 启动 Metro
2. 重新编译 HarmonyOS 工程
3. 安装到真机
4. 打开设备音量
5. 进入 “HarmonyOS TTS 语音播报” Demo
6. 确认页面显示 “TTS 引擎可用”
7. 点击 “开始播报”
8. 听到系统语音播报
9. 点击 “停止播报” 验证停止链路
本次工程验证命令:
npm test -- --runInBand
hvigorw assembleApp --no-daemon
其中 hvigorw assembleApp 已可完成构建。构建过程中仍可能看到 RNOH 或 Metro 的提示日志,例如 8081 端口已被占用、部分 RNOH 编译 warning,这类信息需要结合实际输出判断是否影响安装运行。
12. 常见问题与排查
问题 1:TTSModule 找不到
现象:
TurboModuleRegistry.getEnforcing(...): 'TTSModule' could not be found
优先检查:
1. RN 侧模块名是否为 TTSModule
2. C++ TTSModule.cpp 是否声明 speak / stop / isAvailable / addListener / removeListeners
3. TTSPackage.h 是否按 name == "TTSModule" 创建模块
4. PackageProvider.cpp 是否加入 TTSPackage
5. CMakeLists.txt 是否编译 turbomodule/TTSModule.cpp
6. GeneratedPackage.ets 是否注册 ArkTS TTSModule
7. 是否重新编译安装,而不是只刷新 Metro Bundle
本篇最容易漏的是 C++ Package 和 CMake 注册。ArkTS 文件存在,不代表 RN 一定能找到 TurboModule。
问题 2:页面显示 TTS 引擎不可用,但实际能播报
如果 isAvailable() 只是读取一个未初始化状态,就可能出现“页面显示不可用,点击后又能播报”的矛盾。
当前实现里,isAvailable() 会主动尝试初始化引擎:
isAvailable()
↓
如果已有可用引擎,返回 true
↓
否则调用 initEngine()
↓
初始化成功返回 true,失败返回 false
这样页面展示的“可用/不可用”和真实播报能力就能保持一致。
问题 3:点击开始播报后页面短暂闪动
这通常不是 TTS 引擎问题,而是 RN 页面状态在“准备播报”和原生事件回调之间快速切换。
当前实现里:
点击按钮:准备播报
onStart:播放中
onComplete + isBusy false:播放完成
onStop:已停止
如果页面文案或状态块高度变化明显,就会造成视觉闪动。解决方式是保持状态区域尺寸稳定,并避免把临时状态写成大标题。
问题 4:播放还没结束就显示播放完成
不要按文本长度估算播报时长,也不要在 speak() Promise resolve 后直接显示完成。
当前做法是:
onComplete
↓
轮询 ttsEngine.isBusy()
↓
引擎空闲后 emit complete
↓
RN 显示播放完成
问题 5:多次点击开始播报会播放多次吗?
如果不做保护,连续点击确实可能发起多个播报请求。当前 Demo 在 RN 侧用 speaking 锁住按钮:
speaking === true 时:
- “开始播报”按钮禁用
- 文案显示“正在播报”
- 如果仍触发 handleSpeak,会提示先停止当前播报
真实业务里也可以改成“开始新播报前自动 stop 上一次”,但要明确产品语义。
13. 本篇小结
本篇完成了 RNOH 接入 HarmonyOS Core Speech Kit TTS 的真机实践:
React Native TTSPage
↓
NativeTTSModule.speak(text)
↓
C++ ArkTSTurboModule wrapper
↓
ArkTS TTSModule
↓
Core Speech Kit textToSpeech
↓
系统语音播报
↓
TTSStatus 事件回传 RN
它的重点不只是“能发声”,而是把 RNOH 系统能力接入中几个高频问题都跑了一遍:
1. TurboModule 模块名一致性
2. C++ Package 注册
3. ArkTS 工厂注册
4. NativeEventEmitter 事件回传
5. 真机 TTS 引擎可用性检查
6. 播放完成状态不能靠时长估算
7. 连续点击的播报请求控制
这套链路跑通后,后续接 OCR、端侧 AI、图片选择、系统通知等能力时,工程结构就有了一个可复用的模板。
14. 下一篇预告
下一篇建议进入视觉能力:
RNOH x HarmonyOS OCR:商品包装 / 物流面单识别接入方案
下一篇会实现:
RN 页面选择图片
↓
调用 ArkTS 图片选择与 OCR 能力
↓
识别商品包装 / 物流面单文字
↓
结果回显到 RN 页面
相比 TTS,OCR 会多出图片选择、文件路径、图片权限、识别结果结构化等问题,会更接近真实业务中的系统能力接入。
更多推荐



所有评论(0)