news 2026/9/28 8:24:24

Operit 语音服务设置页 UI 重排实战:TTS/STT 分页、档案管理与可展开配置的实现与数据契约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Operit 语音服务设置页 UI 重排实战:TTS/STT 分页、档案管理与可展开配置的实现与数据契约
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

导读

本文基于 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:

  1. 变更检测:hasPendingChanges逐字段对比输入 state 与当前档案值,覆盖 TTS 全部字段(服务类型、URL、API Key、Headers、方法、Body、Content-Type、语言、音色、模型、响应管道、VITS 路径/说话人/选项、清理正则、语速、音调)与 STT 全部字段(服务类型、端点、API Key、模型);
  2. 防抖:LaunchedEffect监听所有输入字段,延迟500ms后再次检查hasPendingChanges,避免每次按键都触发磁盘写入;
  3. JSON 校验门控:当引擎为HTTP_TTS且 Headers/响应管道 JSON 非法、或引擎为VITS_TTS且 options JSON 非法时,直接return@LaunchedEffect阻断保存,配合输入框的isError红色提示;
  4. 写入与重置:依次调用updateTtsProfile/updateSttProfile,随后调用VoiceServiceFactory.resetInstance()与SpeechServiceFactory.resetInstance(),使已创建的 Provider 实例在下次使用时按新配置重建;
  5. 异常反馈:捕获异常后通过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),说明档案存储是全局语音配置的唯一入口。

八、完成标准与验收要点

原文档的四条完成标准,均可对照实现逐项验收:

  1. 切换 Tab 不丢失编辑状态:输入 state 使用remember(数据源)初始化并常驻,selectedTabIndex不触发自动保存,切回 Tab 时字段原样保留;
  2. 切换、创建、重命名、删除档案行为不变:四类操作全部走SpeechServiceProfilesPreferences原方法,UI 仅改变入口形态(下拉菜单 + 对话框),底层check约束与投影逻辑未动;
  3. 每种已存在 TTS Provider 仍可访问原有字段:九种VoiceServiceType全部保留,TtsHttpConfig十字段、VitsTtsPackageConfig、SttHttpConfig逐一在编辑区可用;
  4. 页面不再同时渲染 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

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:swagger-codegen 生成的 Eiffel 客户端 API 参考:FAKECLASSNAMETAGS123_API 的 test_classname 端点全解析
下一篇:如何让Mac上的重要窗口永远保持在最上层?Topit窗口置顶工具带来全新解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 8:23:01

海外动态IP:跨境电商防关联与稳定运营的关键技术

做海外市场这几年&#xff0c;我身边很多做跨境电商、独立站投放、海外社媒运营的朋友&#xff0c;都问过我同一个问题&#xff1a;为什么我的账号总是被限制&#xff1f;为什么店铺刚起来就封号&#xff1f;为什么同一个团队操作多个店铺&#xff0c;其中一个出事&#xff0c;…

作者头像 李华
网站建设 2026/9/28 8:22:22

FAST Element 数据绑定的运行时核心:深入解析 BindingBehavior 类

前端UI组件 【免费下载链接】fast The adaptive interface system for modern web experiences. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/fa/fast 点击查看 免费下载 导读 BindingBehavior 是 FAST Element&#xff08;microsoft/fast-element&#xff09;模板…

作者头像 李华
网站建设 2026/9/28 8:22:10

React脚手架与Hooks实战:从工程化配置到高复用封装

React脚手架及Hooks钩子&#xff0c;这两块东西在圈子里聊的人很多&#xff0c;但大多数讨论都停在了“脚手架怎么搭、Hooks怎么用”的演示层面&#xff0c;真正拿到生产环境、放进团队协作里&#xff0c;你会发现差得不是一星半点。我这篇是这个系列的第三篇&#xff0c;前两篇…

作者头像 李华