拓冰建站拓冰建站
首页 / 资讯中心 / 正文

OpenHuman 的 Tauri v2 iOS 插件 tauri-plugin-ptt 实战:推按即讲录音识别与 TTS 语音合成

OpenHuman 的 Tauri v2 iOS 插件 tauri-plugin-ptt 实战推按即讲录音识别与 TTS 语音合成【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhumanOpenHuman 是一个面向 Mac、Windows 与 Linux 的开源个人 AI 应用其移动端语音交互依赖一个独立的 Tauri v2 插件tauri-plugin-ptt实现推按即讲Push-to-talk 语音合成TTS能力。本文以该插件在仓库中的官方文档为主体结合 Rust 命令层、Swift 原生实现与 JS 绑定源码系统讲解它的命令/事件契约、iOS 权限配置、底层音频会话管理原理以及真机测试要点帮助读者在自研 Tauri v2 移动应用中原样落地这套按住说话、松开上屏、语音朗读的完整链路。插件定位为 Tauri v2 补齐 iOS 语音能力tauri-plugin-ptt是仓库内位于 packages/tauri-plugin-ptt 的独立 Tauri v2 插件目标平台明确为iOS。它包装了三套系统框架AVAudioEngine—— 麦克风音频采集引擎Speech.frameworkSFSpeechRecognizer—— 语音转文字STTAVSpeechSynthesizer—— 文字转语音TTS。插件对外暴露 5 个命令、5 类异步事件和一套错误码前端通过plugin:ptt|command形式调用。插件对桌面端Mac/Windows/Linux做了隔离降级所有命令在非 iOS 目标上统一返回NotSupported错误因此桌面构建完全不受影响见 src/lib.rs 中PttHandle的 stub 分支。整体架构JS → Rust → Swift 三层调用链README 给出了一张清晰的架构图结合源码可还原完整链路JS (MascotScreen) ↓ invoke / listen Rust (commands.rs) ↓ PluginHandle::run_mobile_plugin Swift (PTTPlugin.swift) ↓ PTTRecorder — AVAudioEngine SFSpeechRecognizer PTTSpeaker — AVSpeechSynthesizer AudioSessionManager — AVAudioSession lifecycle notifications三层各司其职JS 层guest-js/index.ts 通过invoke(plugin:ptt|name)调用命令通过listen(ptt://event)订阅事件Rust 层src/commands.rs 定义 5 个#[command]经tauri::generate_handler!注册src/mobile.rs 用tauri::ios_plugin_binding!生成 Swift↔Rust 的 FFI 胶水每个命令经PluginHandle::run_mobile_plugin将载荷序列化为 JSON调用PTTPlugin上对应的objc funcSwift 层ios/Sources/tauri-plugin-ptt/PTTPlugin.swift 是 Tauri 插件类聚合三个组件PTTRecorder录音识别、PTTSpeaker合成、AudioSessionManager会话生命周期与系统通知。值得注意的是 Rust 命令命名snake_case到 Swift 方法camelCase的自动映射start_listening→startListeningcancel_speech→cancelSpeech等Swift 侧注释明确要求命令名必须与 commands.rs 一致。命令契约CommandsCommand描述start_listening激活AVAudioEngineSFSpeechRecognizer。部分识别结果以事件流形式持续到达。stop_listening停用录音会话并返回最终识别文本。speak入队一条AVSpeechSynthesizer语音合成任务。cancel_speech立即停止当前合成任务。list_voices列出所有AVSpeechSynthesisVoice.speechVoices()语音。前端调用 APIJS 绑定位于 guest-js/index.ts对应关系如下// 开始录音部分转写通过事件异步到达 await startListening(); // 停止录音返回最终文本 { text, isFinal } const result: TranscriptEvent await stopListening(); // 合成语音voiceId 可选rate 为 0.5–2.0 的倍率默认 1.0 正常语速 await speak(Hello from OpenHuman, { voiceId: com.apple.voice.compact.en-US.Samantha, rate: 1.2, }); // 立即停止合成 await cancelSpeech(); // 枚举设备语音 [{ id, name, lang }] const voices: VoiceInfo[] await listVoices();命令参数在 Rust 侧定义于 src/models.rsSpeakRequest携带text、voice_id、rate三个字段其中rate的取值范围注释为0.5慢~ 2.0快默认1.0TranscriptResult返回text与恒为true的is_final仅在stop_listening返回时成立。命令的 Swift 侧实现要点在 PTTPlugin.swift 中startListening异步执行recorder.startListening()成功则invoke.resolve()失败则invoke.reject(...)并额外向事件总线发出ptt://error权限拒绝或音频错误stopListening同步取出最终文本先触发ptt://transcript-final事件再返回TranscriptResult(text:isFinal: true)speak用invoke.parseArgs(SpeakArgs.self)解析参数后交给PTTSpeaker.speaklistVoices将 Swift 字典映射为VoiceInfoPayload数组返回。事件契约Events所有事件经 Tauri 事件总线发往 main 目标前端用listen订阅EventPayload描述ptt://transcript-partial{ text: string }录音过程中实时返回的部分转写结果ptt://transcript-final{ text: string }stop_listening之后的最终结果ptt://tts-started{ utteranceId: string }合成开始ptt://tts-ended{ utteranceId: string; finished: boolean }合成结束finished: false表示被取消ptt://error{ code: string; message: string }异步错误权限、中断等JS 侧提供了 5 个订阅函数均返回UnlistenFn以便在组件卸载时取消订阅const unlistenPartial await onTranscriptPartial(text { // 实时更新按住说话期间的转写预览 }); const unlistenFinal await onTranscriptFinal(text { // 松手后把最终文本发送到会话 }); const unlistenStarted await onTtsStarted(id { /* 开始朗读 */ }); const unlistenEnded await onTtsEnded((id, finished) { // finished false 表示被 cancelSpeech 打断 }); const unlistenErr await onError(err { // err: { code, message } });事件在 Swift 侧如何触发PTTPlugin.load(webview:)中把闭包回调接线到triggerrecorder.onPartialTranscript→ 触发ptt://transcript-partialrecorder.onError→ 触发ptt://errorspeaker.onStarted/speaker.onEnded→ 分别触发ptt://tts-started/ptt://tts-ended。PTTSpeaker通过AVSpeechSynthesizerDelegate回调上报生命周期didStart上报(uid, true)didFinish上报(uid, true)didCancel上报(uid, false)。utteranceId 由UUID().uuidString生成并记录在currentUtteranceId由于插件同一时刻只入队一条合成任务委托回调读取最近一次设置的 id 即可保证对应关系正确见 PTTSpeaker.swift。错误码Error codesCode触发场景permission_denied麦克风或语音识别权限被拒绝interrupted电话或系统音频打断了录音会话route_changed录音过程中蓝牙耳机断开audio_errorAVAudioEngine失败recognition_errorSFSpeechRecognizer转写失败错误码的产生路径在 Swift 侧分两条同步返回Rust 侧Error枚举见 src/error.rs包含MicrophonePermissionDenied、SpeechPermissionDenied、AlreadyRecording、NotRecording、AudioEngine、SpeechRecognizer、Tts等变体通过自定义Serialize实现把错误字符串化后返回给 JS异步事件PTTPlugin.emitPermissionOrAudioError把PTTRecorder.RecorderError.microphonePermissionDenied/speechPermissionDenied统一映射为permission_denied其余映射为audio_error并携带localizedDescription。此外PTTRecorder的识别任务回调里做了专门的错误过滤kAFAssistantErrorDomain下 code 209用户取消与 1110无语音输入被视为正常结束不触发recognition_error只有其他错误才上报见 PTTRecorder.swift。iOS 权限配置Info.plist首次调用startListening时系统会弹出权限对话框因此必须在 Info.plist 中声明两个用途描述keyNSMicrophoneUsageDescription/key stringUsed for push-to-talk voice messages./string keyNSSpeechRecognitionUsageDescription/key stringUsed to transcribe your voice to text./string权限请求逻辑在PTTRecorder.requestPermissions()中同时兼容新旧 iOS APIiOS 17 使用AVAudioApplication.requestRecordPermission更早版本回退到AVAudioSession.sharedInstance().requestRecordPermission语音识别统一通过SFSpeechRecognizer.requestAuthorization状态必须为.authorized才继续。录音识别与语音合成的底层实现PTTRecorder单会话式的录音 STT 管线PTTRecorder.swift 遵循一次startListening创建一个识别任务stopListening完整拆除的单会话模型任务绝不跨会话残留。关键细节识别请求shouldReportPartialResults true实时回传部分转写requiresOnDeviceRecognition false即允许走网络识别通过engine.inputNode.installTap(onBus:0, bufferSize:1024, ...)把音频缓冲持续append到SFSpeechAudioBufferRecognitionRequest全程不落盘stopListening()先调用request.endAudio()与task.finish()让识别器基于已缓冲内容完成收尾再摘除 tap、停止引擎、解激活音频会话并返回镜像的latestTranscriptforceStop()用于应用退到后台或会话被中断的强停场景task.cancel()后直接清理不等待最终结果识别任务回调会持续更新latestTranscript供停止时读取——因为SFSpeechRecognitionTask本身不暴露result属性。PTTSpeaker语速映射与取消语义PTTSpeaker.swift 对AVSpeechSynthesizer做薄封装未指定voiceId时使用设备当前语言的语音AVSpeechSynthesisVoice(language: Locale.current...)语速映射调用方传入的归一化倍率0.5–2.0JS 侧1.0 正常会被先clamp到[0.1, 2.0]再按AVSpeechUtteranceDefaultSpeechRate * clamped换算到 AVFoundation 的[0,1]语速刻度AVFoundation 默认速率0.5对应调用方的1.0cancel()使用stopSpeaking(at: .immediate)随后委托回调didCancel上报finished: false前端据此区分读完与被打断。AudioSessionManager录音/播放共享单一会话AudioSessionManager.swift 以单例形式集中管理AVAudioSession让录音与播放共享一套 category 配置避免蓝牙场景下反复切换 category 引发爆音activateForRecording()设置category: .playAndRecord、mode: .spokenAudiooptions 包含.defaultToSpeaker、.allowBluetooth、.allowBluetoothA2DP—— 这正是 README 测试清单中iPhone 离开耳朵时默认走扬声器外放的实现基础deactivate()以.notifyOthersOnDeactivation释放会话并通知其他应用恢复音频startObserving注册AVAudioSession.interruptionNotification电话/系统音频抢占与routeChangeNotification蓝牙设备插拔两个系统通知回调交给PTTPlugin统一处理。中断与路由变更的优雅降级PTTPlugin对两类系统扰动做了兜底见 PTTPlugin.swift 的handleInterruption/handleRouteChange/appDidBackground电话中断先stopListening()取得最终文本并触发ptt://transcript-final再发出ptt://errorcode: interrupted蓝牙断开仅在.oldDeviceUnavailable且正在录音时同样先收尾再发route_changed错误退到后台应用didEnterBackground时若正在录音则停止并产出最终转写同时speaker.cancel()释放音频会话。权限系统Tauri 能力capabilities/permissions作为 Tauri v2 插件命令调用受权限系统管控。permissions/autogenerated/reference.md 为 5 个命令各生成了一对allow/deny权限标识例如ptt:allow-start-listening/ptt:deny-start-listeningptt:allow-stop-listening/ptt:deny-stop-listeningptt:allow-speak/ptt:deny-speakptt:allow-cancel-speech/ptt:deny-cancel-speechptt:allow-list-voices/ptt:deny-list-voices在移动端 Tauri 应用的 capability 文件仓库中见 src-tauri-mobile/capabilities里为对应窗口授予所需权限即可未授权的命令调用会被拒绝。桌面端降级no-op stub插件在非 iOS 平台上不引入任何原生依赖src/lib.rs 中的PttHandleR通过#[cfg(target_os ios)]条件编译——iOS 上持有PttMobileR其他平台退化为PhantomDatafn(R) - R占位。占位类型特意选用函数指针fn(R) - R而非PhantomDataR以保证结构体在R不满足Send Sync时依然满足 Taurimanage()的Send Sync static约束。5 个方法在非 iOS 分支统一返回Error::NotSupported并打印 warn 日志因此桌面构建可以安全引用该插件而不影响主流程。手动测试清单Manual testing checklistSwift 原生层无法在 CI 中做单元测试需要 iOS 工具链与模拟器因此官方文档要求按下列清单在真机或模拟器上逐项验收首次调用startListening时弹出权限对话框说话过程中部分转写实时更新停止后最终转写与内容一致按住按钮录音、松开停止聊天消息携带转写文本发出iPhone 离开耳朵时TTS 默认通过扬声器外放蓝牙耳机音频路由正确录音中断开耳机能优雅停止录音过程中应用退到后台能产出最终转写并干净停止电话打断时发出ptt://errorcode为interruptedTTS 播放中调用cancelSpeech收到tts-ended且finished: falselistVoices返回非空的AVSpeechSynthesisVoice列表。其中第 4 条与第 5 条分别由上文介绍的.defaultToSpeaker会话选项与routeChangeNotification监听保证第 8 条由didCancel委托回调保证。工程结构与构建配置插件按 Tauri v2 移动插件标准布局组织srcRust 侧lib.rs/commands.rs/mobile.rs/models.rs/error.rsguest-jsJS 绑定源码与 Vitest 单测index.test.ts 用 mock 的invoke/listen验证每个函数调用了正确的命令名、参数结构与事件名ios/Sources/tauri-plugin-pttPTTPlugin.swift/PTTRecorder.swift/PTTSpeaker.swift/AudioSessionManager.swiftpermissions权限标识、schema.json与自动生成的reference.md。构建配置方面Cargo.toml 声明crate-type [cdylib, rlib]并依赖tauri 2、serde、thiserroriOS 目标无需额外 Rust 依赖桥接走ios_plugin_binding!路径Package.swift 要求 iOS 16 并静态链接Tauri框架package.json 以tauri-plugin-ptt-api作为 JS 包名peer 依赖tauri-apps/api 2.0.0。小结tauri-plugin-ptt给出了一个在 Tauri v2 iOS 应用中落地推按即讲与语音合成的完整范式Rust 命令层负责跨端统一契约并在桌面端优雅降级Swift 层以AVAudioEngine SFSpeechRecognizer与AVSpeechSynthesizer承接系统能力AudioSessionManager统一管理会话与系统通知最后通过 5 类事件把实时转写、合成生命周期与异步错误流回传给前端。阅读 packages/tauri-plugin-ptt/README.md 可快速掌握契约全貌深入对应源码则可复用到自研移动插件的权限申请、中断降级与事件桥接设计中。【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门