- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
导读
本文基于 Operit 仓库中的speech_services_settings_ui_20260822设计文档,系统讲解语音服务设置页从"单页双长表单"重构为"TTS/STT 分页 + 紧凑档案管理 + 可展开低频配置"的完整过程。你将掌握页面重排的行为约束(不破坏已发布配置数据与 Provider 契约)、TTS/STT 页面结构与各 Compose 组件实现、档案生命周期与自动保存机制,以及SpeechServiceProfilesPreferences与旧SpeechServicesPreferences双 DataStore 投影的底层原理,可直接对照 SpeechServicesSettingsScreen.kt 与 SpeechServiceProfilesPreferences.kt 逐行验证。
一、重构背景:单页长表单的问题
重构前的设置页把 TTS、STT、配置档案、供应商参数、清理规则和说明内容连续堆叠在同一条LazyColumn长列表中。TTS 供应商切换后,低频 JSON 参数(如 Headers、响应管道)和常用播放参数(语速、音调)没有清晰层级,用户需要长距离滚动才能完成一次配置(见 speech_services_settings_ui_20260822/index.md 的"原本状况")。
本次重排的目标是:在不改变已发布配置数据、路由、自动保存和 Provider 参数契约的前提下,将页面压缩为 TTS/STT 分页、单行配置档案管理、紧凑播放参数和可展开的低频设置。作用域严格限定为SpeechServicesSettingsScreen.kt的 Compose 页面结构,SpeechServiceProfilesPreferences.kt及 Provider 层数据模型与运行时契约一律不动。
二、已发布行为约束:重排的"铁律"
原文档 01_ui_relayout.md 明确了四条不可逾越的约束,这是本次重构与普通 UI 改版最大的区别:
| 约束 | 含义 | 源码对应 |
|---|---|---|
| 不改变档案存储 | SpeechServiceProfilesPreferences的 JSON、档案 ID、当前档案选择和删除约束保持原样 | SpeechServiceProfilesPreferences.kt 中TtsProfile/SttProfile数据结构、tts_profiles/stt_profiles等 key 均未变动 |
| 不改变旧投影 | 旧SpeechServicesPreferences投影保持原样,现有 Provider 继续读取相同字段 | saveTtsSettings/saveSttSettings签名与写入 key 未变,见 SpeechServicesPreferences.kt |
| 不增加第二套保存机制 | 页面继续使用现有自动保存流程,不引入手动"保存"按钮 | 自动保存由LaunchedEffect+ 防抖实现(见下文第五节) |
| 不删除已存在字段 | 所有已存在的 Provider 配置字段全部保留 | TTS 侧TtsHttpConfig的urlTemplate、apiKey、headers、httpMethod、requestBody、contentType、localeTag、voiceId、modelName、responsePipeline十个字段,以及VitsTtsPackageConfig、SttHttpConfig均原样保留 |
这套约束的根本原因在于:仓库已经发布了基于旧speech_services_preferencesDataStore 的版本,迁移到档案存储后,旧 DataStore 仍作为运行时投影被现有 Provider 和旧版本读取接口消费(见 speech_service_profiles.md)。任何字段或保存路径的改动都会破坏已发布安装的兼容性。
三、新页面结构:五大组成模块
原文档给出了重排后的页面结构蓝图,源码实现可逐条对应:
1. 页面内 Tab:TTS / STT
由 SpeechServicesSettingsScreen.kt 中的SpeechServicesModeTabs实现:使用 Material3 的TabRow承载两个Tab,selectedTabIndex为 0 表示 TTS、1 表示 STT。页面主体通过if (selectedTabIndex == 0) item(key = "tts-settings")(L458)与if (selectedTabIndex == 1) item(key = "stt-settings")(L2101)条件渲染,这正是完成标准中"不再同时渲染 TTS 和 STT 两个完整长表单"的实现方式——同一时刻LazyColumn中只有一个完整配置区。
2. 当前 Tab 的配置档案紧凑管理栏
SpeechProfileManagementBar(L2523-L2564)根据当前 Tab 决定展示 TTS 还是 STT 档案,内部委托给SpeechProfileSelector(L2566-L2681)。该组件提供:
- 当前档案展示:整行可点击的
Surface,显示"当前档案"标签与档案名,点击展开DropdownMenu列出全部档案; - 新建(Add 图标):弹出
SpeechProfileNameDialog(L2683-L2714)输入名称,确认后调用createTtsProfile/createSttProfile; - 重命名(Edit 图标):弹出同名对话框,确认后调用
updateTtsProfile(activeTtsProfile.copy(name = name)); - 切换:在下拉菜单点击任意档案条目触发
onSelect,调用selectTtsProfile/selectSttProfile; - 删除非当前档案:下拉菜单中,非当前档案条目右侧显示 Delete 图标,点击后弹出
SpeechProfileDeleteDialog(L2716-L2736)确认;当前档案不显示删除按钮(if (profile.id != activeProfileId))。
3. 当前 Provider 与播放参数集中展示
TTS 引擎选择使用ExposedDropdownMenuBox下拉框,枚举值来自 VoiceServiceFactory.kt 的VoiceServiceType:SIMPLE_TTS、HTTP_TTS、OPENAI_WS_TTS、SILICONFLOW_TTS、MINIMAX_TTS、MIMO_TTS、DOUBAO_TTS、OPENAI_TTS、VITS_TTS共九种。选择 Doubao 时页面自动预填默认端点 URL、默认音色与application/jsonContent-Type(见 L528-L532)。
语速与音调合并为同一"播放参数"区域的两个Slider,取值范围均为0.5f..2.0f、steps = 5,实时显示当前数值——重排后这两个高频参数始终可见,不再被长表单淹没。
4. 清理正则与 HTTP 请求参数作为可展开区域
- TTS 清理规则(
ttsCleanerExpanded状态 +AnimatedVisibility):标题行显示当前正则数量(N),展开后可逐条编辑、删除、新增(Add按钮),并提供模板下拉菜单,内置五个常用正则模板:\*[^*]+\*(星号包裹)、\*\*[^*]+\*\*(双星号包裹)、\([^)]+\)(英文括号)、([^)]+)(中文括号)、<[^>]+>(XML 标签)。默认清理列表见 SpeechServicesPreferences.kt(中英文括号两个正则)。 - HTTP 高级参数(
ttsHttpAdvancedExpanded状态):仅当引擎为HTTP_TTS时展示,包含 Headers(JSON 文本域,实时校验)、HTTP 方法(GET/POST 下拉)、Content-Type、POST 请求体模板(支持{text}占位符)以及响应管道responsePipeline(多行 JSON,逐字校验并给出错误提示)。
5. 测试入口与说明
TTS Tab 底部保留"测试 TTS"按钮(L2297-L2310),通过onNavigateToTextToSpeech跳转测试页;STT Tab 保留对应引擎参数与说明。两种 Tab 共用底部说明区,根据selectedTabIndex切换显示 TTS/STT 描述文本。
四、数据契约:档案存储与旧投影的双轨结构
页面重排不改数据层,但理解数据契约才能理解 UI 行为。档案存储使用独立的speech_service_profilesDataStore(版本号 currentVersion = 1,含 schema 迁移),由 SpeechServiceProfilesPreferences.kt 独占读写:
- TtsProfile:
id、name、serviceType、httpConfig(完整TtsHttpConfig)、vitsConfig、cleanerRegexs、speechRate、pitch、createdAt、updatedAt; - SttProfile:
id、name、serviceType、httpConfig(SttHttpConfig)、createdAt、updatedAt; - DataStore 顶层 key:
tts_profiles、stt_profiles、current_tts_profile_id、current_stt_profile_id。
旧speech_services_preferencesDataStore 则保留为"当前档案投影":projectTtsProfile/projectSttProfile(L346-L362)在创建、更新、切换档案后调用旧存储的saveTtsSettings/saveSttSettings,把当前档案的字段原样写回旧 key(tts_service_type、tts_http_config、tts_cleaner_regexs、tts_speech_rate、tts_pitch、stt_service_type、stt_http_config)。现有 Provider 与旧版本读取接口继续从旧 DataStore 消费,而新代码一律通过档案存储读写——这就是"切换和保存会更新旧偏好投影并重置对应服务实例"的落地方式。
首次访问新档案存储时,migratePreferencesFromVersionZero会把旧 TTS/STT 配置分别封装为固定 ID(legacy-tts-profile/legacy-stt-profile)的档案并写回,同时设置当前档案 ID;新安装用户因旧 key 缺失而直接获得默认档案:系统 TTS(SIMPLE_TTS,语速/音调默认1.0f)与本地 Sherpa STT(SHERPA_NCNN),见 SpeechServicesPreferences.kt。
五、自动保存流程:防抖 + 变更检测 + 校验门控
页面"无保存按钮"依赖自动保存流程,其实现位于 SpeechServicesSettingsScreen.kt:
- 变更检测:
hasPendingChanges逐字段对比输入 state 与当前档案值,覆盖 TTS 全部字段(服务类型、URL、API Key、Headers、方法、Body、Content-Type、语言、音色、模型、响应管道、VITS 路径/说话人/选项、清理正则、语速、音调)与 STT 全部字段(服务类型、端点、API Key、模型); - 防抖:
LaunchedEffect监听所有输入字段,延迟500ms后再次检查hasPendingChanges,避免每次按键都触发磁盘写入; - JSON 校验门控:当引擎为
HTTP_TTS且 Headers/响应管道 JSON 非法、或引擎为VITS_TTS且 options JSON 非法时,直接return@LaunchedEffect阻断保存,配合输入框的isError红色提示; - 写入与重置:依次调用
updateTtsProfile/updateSttProfile,随后调用VoiceServiceFactory.resetInstance()与SpeechServiceFactory.resetInstance(),使已创建的 Provider 实例在下次使用时按新配置重建; - 异常反馈:捕获异常后通过
AppLogger.e记录,并将错误信息写入ttsProfileError/sttProfileError,由SnackbarHostState.showSnackbar弹出提示。
值得注意:Tab 切换不参与自动保存(selectedTabIndex不在LaunchedEffect的监听列表内),且所有输入 state 均为remember(数据源)初始化、常驻 Composable 作用域,因此切换 Tab 不会丢失任一侧的编辑状态,这是完成标准的第一条。
六、档案生命周期:创建、更新、切换、删除的源码约束
SpeechServiceProfilesPreferences.kt 的四个核心方法与页面操作一一对应:
- 创建(
createTtsProfile/createSttProfile):以当前档案为模板copy出新档案,分配新UUID.randomUUID(),写入列表并立即设为当前档案,随后投影到旧存储; - 更新(
updateTtsProfile/updateSttProfile):名称经requireProfileName去空白并校验非空,cleanerRegexs过滤空串,speechRate/pitch经requirePositive校验必须 > 0;保留原createdAt,只更新updatedAt;仅当被更新档案是当前档案时才触发投影; - 切换(
selectTtsProfile/selectSttProfile):只写当前档案 ID 并投影,不改动任何档案内容; - 删除(
deleteTtsProfile/deleteSttProfile):先check(preferences[CURRENT_TTS_PROFILE_ID] != id),当前档案删除直接抛异常,UI 层通过错误捕获转为 Snackbar 提示;随后从列表过滤目标 ID。
底层数据通过ttsProfilesFlow/sttProfilesFlow等 Flow 对外暴露,UI 使用collectAsState(initial = emptyList())订阅;当前档案缺失时页面显示CircularProgressIndicator,不会用隐式默认档案掩盖数据损坏(见架构文档 speech_service_profiles.md 的生命周期约束)。
七、Provider 如何消费档案:工厂调用链
档案不仅是设置页的数据源,也是运行时语音服务的配置来源。VoiceServiceFactory.kt 的createVoiceService通过runBlocking { profiles.getCurrentTtsProfile() }读取当前档案,再按serviceType分派到对应 Provider:SIMPLE_TTS用SimpleVoiceProvider(携带localeTag/voiceId),其余引擎依次映射到 HTTP、OpenAI Realtime、硅基流动、MiniMax、MiMo、豆包、OpenAI、VITS 等实现。STT 侧由 SpeechServiceFactory.kt 以同样方式消费。这也解释了为什么设置页保存后必须resetInstance():单例工厂持有已构建的 Provider 实例,只有重置才能让下一次朗读/识别使用新档案。
仓库中SpeechServiceProfilesPreferences的消费方还包括聊天朗读(ChatViewModel.kt)、工具箱 TTS 页面(TextToSpeechScreen.kt)、悬浮窗全屏页(FloatingFullscreenScreen.kt)以及内置软件设置工具(StandardSoftwareSettingsModifyTools.kt),说明档案存储是全局语音配置的唯一入口。
八、完成标准与验收要点
原文档的四条完成标准,均可对照实现逐项验收:
- 切换 Tab 不丢失编辑状态:输入 state 使用
remember(数据源)初始化并常驻,selectedTabIndex不触发自动保存,切回 Tab 时字段原样保留; - 切换、创建、重命名、删除档案行为不变:四类操作全部走
SpeechServiceProfilesPreferences原方法,UI 仅改变入口形态(下拉菜单 + 对话框),底层check约束与投影逻辑未动; - 每种已存在 TTS Provider 仍可访问原有字段:九种
VoiceServiceType全部保留,TtsHttpConfig十字段、VitsTtsPackageConfig、SttHttpConfig逐一在编辑区可用; - 页面不再同时渲染 TTS 和 STT 两个完整长表单:通过
selectedTabIndex条件渲染,同一时刻仅渲染一个配置卡片(tts-settings或stt-settingsitem)。
相关源码路径索引
- 设置页实现:app/src/main/java/com/ai/assistance/operit/ui/features/settings/screens/SpeechServicesSettingsScreen.kt(共 2736 行,含 Tab、档案管理栏、TTS/STT 卡片、对话框与自动保存)
- 档案存储:app/src/main/java/com/ai/assistance/operit/data/preferences/SpeechServiceProfilesPreferences.kt
- 旧投影存储:app/src/main/java/com/ai/assistance/operit/data/preferences/SpeechServicesPreferences.kt
- 语音工厂:app/src/main/java/com/ai/assistance/operit/api/voice/VoiceServiceFactory.kt、app/src/main/java/com/ai/assistance/operit/api/speech/SpeechServiceFactory.kt
- 架构文档:docs/doc-src/architecture/speech_service_profiles.md、docs/TODO/speech_service_profiles_20260810/01_migration_contract.md
- 本主题设计文档:docs/TODO/speech_services_settings_ui_20260822/index.md、docs/TODO/speech_services_settings_ui_20260822/01_ui_relayout.md
- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
相关推荐
Xberg 页面级抽取与页面标记配置实战:extract_pages / insert_page_markers 契约解析与实现原理
Xberg 页面级抽取与页面标记配置实战:extract_pages / insert_page_markers 契约解析与实现原理 本篇技术指南围绕 Xber
后端AI 应用NLPLiveKit Agents 集成 Telnyx 语音服务:livekit-plugins-telnyx 的 STT 与 TTS 实战指南
LiveKit Agents 集成 Telnyx 语音服务:livekit plugins telnyx 的 STT 与 TTS 实战指南 livekit pl
AI Agent人工智能语音AI 应用多模态AIRI 语音输入输出配置实战:TTS 语音合成与 ASR/STT 语音识别完整指南
AIRI 语音输入输出配置实战:TTS 语音合成与 ASR/STT 语音识别完整指南 AIRI 的语音能力分为语音合成(TTS,将 AI 回复朗读出来)与语音识
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考