1 line
35 KiB
JSON
1 line
35 KiB
JSON
{"repo": "autsunset/voxengine", "files": {"app/src/main/java/com/voxengine/ui/screens/SettingsScreen.kt": {"5ee087ce1426f9a902ab310a83470a5b3e12ef23": {"t": 1785595185.5619564, "n": 26382, "h": "d7f230354f39908e", "c": "package com.voxengine.ui.screens\n\nimport android.content.Intent\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExposedDropdownMenuBox\nimport androidx.compose.material3.ExposedDropdownMenuDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MenuAnchorType\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Visibility\nimport androidx.compose.material.icons.filled.VisibilityOff\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.PasswordVisualTransformation\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.appcompat.app.AppCompatDelegate\nimport com.voxengine.data.AppDatabase\nimport com.voxengine.data.SettingsRepository\nimport com.voxengine.engine.EngineRegistry\nimport com.voxengine.engine.TTSEngine\nimport com.voxengine.engine.mimo.MiMoEngine\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n// MiMo API 预设 URL\nprivate val MIMO_API_PRESETS = listOf(\n \"按量计费\" to \"https://api.xiaomimimo.com\",\n \"Token Plan (中国区)\" to \"https://token-plan-cn.xiaomimimo.com\",\n \"Token Plan (新加坡)\" to \"https://token-plan-sgp.xiaomimimo.com\",\n \"Token Plan (欧洲)\" to \"https://token-plan-ams.xiaomimimo.com\"\n)\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsScreen() {\n val context = LocalContext.current\n val scope = rememberCoroutineScope()\n val settings = remember { SettingsRepository(context) }\n val db = remember { AppDatabase.getDatabase(context) }\n\n val baseUrl by settings.baseUrl.collectAsState(initial = \"https://api.xiaomimimo.com\")\n val apiKey by settings.apiKey.collectAsState(initial = \"\")\n val defaultVoice by settings.defaultVoice.collectAsState(initial = \"冰糖\")\n val defaultStyle by settings.defaultStyle.collectAsState(initial = \"无\")\n val speed by settings.speed.collectAsState(initial = 1.0f)\n val darkMode by settings.darkMode.collectAsState(initial = false)\n val currentEngineId by settings.currentEngine.collectAsState(initial = \"mimo\")\n val parallelSynthesis by settings.parallelSynthesis.collectAsState(initial = false)\n val ttsConcurrency by settings.ttsConcurrency.collectAsState(initial = 3)\n val defaultTemperature by settings.defaultTemperature.collectAsState(initial = 0.6f)\n\n var baseUrlInput by remember { mutableStateOf(baseUrl) }\n var apiKeyInput by remember { mutableStateOf(apiKey) }\n var styleInput by remember { mutableStateOf(defaultStyle) }\n var voiceExpanded by remember { mutableStateOf(false) }\n var styleExpanded by remember { mutableStateOf(false) }\n var engineExpanded by remember { mutableStateOf(false) }\n var presetExpanded by remember { mutableStateOf(false) }\n var apiKeyVisible by remember { mutableStateOf(false) }\n\n // 同步输入框与 DataStore 值\n LaunchedEffect(baseUrl) { baseUrlInput = baseUrl }\n LaunchedEffect(apiKey) { apiKeyInput = apiKey }\n LaunchedEffect(defaultStyle) { styleInput = defaultStyle }\n\n val activeEngine = EngineRegistry.get(currentEngineId)\n \n // 预设音色\n val presetVoices by produceState(initialValue = emptyList(), activeEngine) {\n value = activeEngine?.getVoices() ?: emptyList()\n }\n \n // 自定义音色(从数据库加载)\n val customVoices by db.voiceDao().getVoiceItemsByEngine(currentEngineId).collectAsState(initial = emptyList())\n \n // 合并所有音色\n val allVoices = remember(presetVoices, customVoices) {\n val presetNames = presetVoices.map { it.name }.toSet()\n val customAsPreset = customVoices.filter { it.name !in presetNames }.map { voice ->\n com.voxengine.engine.VoiceInfo(\n id = \"custom_${voice.id}\",\n name = voice.name,\n description = if (voice.type == \"clone\") \"克隆音色\" else \"设计: ${voice.description}\",\n type = com.voxengine.engine.VoiceType.PRESET,\n engineId = currentEngineId\n )\n }\n presetVoices + customAsPreset\n }\n \n val styles by produceState(initialValue = emptyList<String>(), activeEngine) {\n value = activeEngine?.getStyles() ?: emptyList()\n }\n\n // 当前选中的预设计费模式\n val currentPreset = MIMO_API_PRESETS.find { it.second == baseUrl }?.first ?: \"自定义\"\n val currentPresetDisplay = if (currentPreset == \"按量计费\") \"$currentPreset(限时免费,具体以小米官方为准)\" else currentPreset\n\n var userAgentInput by remember { mutableStateOf(\"\") }\n val userAgent by settings.userAgent.collectAsState(initial = \"openclaw/unknown\")\n LaunchedEffect(userAgent) { userAgentInput = userAgent }\n\n Scaffold(\n topBar = { TopAppBar(title = { Text(\"VoxEngine 设置\") }) }\n ) { padding ->\n Column(\n modifier = Modifier\n .fillMaxSize()\n .verticalScroll(rememberScrollState())\n .padding(padding)\n .padding(16.dp)\n ) {\n\n // 深色模式\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Row(\n modifier = Modifier.fillMaxWidth().padding(16.dp),\n horizontalArrangement = Arrangement.SpaceBetween,\n verticalAlignment = Alignment.CenterVertically\n ) {\n Text(\"深色模式\")\n Switch(\n checked = darkMode,\n onCheckedChange = { enabled ->\n scope.launch {\n settings.updateDarkMode(enabled)\n AppCompatDelegate.setDefaultNightMode(\n if (enabled) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO\n )\n }\n }\n )\n }\n }\n\n // 并行合成模式\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Row(\n modifier = Modifier.fillMaxWidth(),\n horizontalArrangement = Arrangement.SpaceBetween,\n verticalAlignment = Alignment.CenterVertically\n ) {\n Text(\"并行合成\")\n Switch(\n checked = parallelSynthesis,\n onCheckedChange = { enabled ->\n scope.launch { settings.updateParallelSynthesis(enabled) }\n }\n )\n }\n Spacer(Modifier.height(4.dp))\n Text(\n \"将长文本拆成短句并行请求,可减少段间等待时间。\\n默认关闭,使用整段请求模式。\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n if (parallelSynthesis) {\n Spacer(Modifier.height(8.dp))\n Text(\"并发条数: $ttsConcurrency\")\n Slider(\n value = ttsConcurrency.toFloat(),\n onValueChange = { scope.launch { settings.updateTtsConcurrency(it.toInt()) } },\n valueRange = 1f..8f,\n steps = 6\n )\n Text(\n \"同时在途的请求数。调高首字更快,但过高易触发限流(429)。\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n }\n }\n }\n\n // 引擎选择\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Text(\"引擎选择\", style = MaterialTheme.typography.titleMedium)\n Spacer(Modifier.height(8.dp))\n\n ExposedDropdownMenuBox(\n expanded = engineExpanded,\n onExpandedChange = { engineExpanded = it }\n ) {\n OutlinedTextField(\n value = activeEngine?.name ?: currentEngineId,\n onValueChange = {},\n readOnly = true,\n label = { Text(\"TTS 引擎\") },\n trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = engineExpanded) },\n modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable)\n )\n ExposedDropdownMenu(\n expanded = engineExpanded,\n onDismissRequest = { engineExpanded = false }\n ) {\n EngineRegistry.getAll().forEach { engine ->\n DropdownMenuItem(\n text = {\n Column {\n Text(engine.name)\n Text(engine.description, style = MaterialTheme.typography.bodySmall)\n }\n },\n onClick = {\n scope.launch {\n switchEngine(settings, db, engine, defaultVoice, defaultStyle)\n }\n engineExpanded = false\n }\n )\n }\n }\n }\n }\n }\n\n // API 配置\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Text(\"API 配置\", style = MaterialTheme.typography.titleMedium)\n Spacer(Modifier.height(8.dp))\n\n if (currentEngineId != \"mimo\") {\n Text(\n \"Edge TTS 无需 API Key 或 Base URL 配置。\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n } else {\n // MiMo 预设计费模式选择\n Text(\"计费模式\", style = MaterialTheme.typography.bodyMedium)\n Spacer(Modifier.height(4.dp))\n ExposedDropdownMenuBox(\n expanded = presetExpanded,\n onExpandedChange = { presetExpanded = it }\n ) {\n OutlinedTextField(\n value = currentPresetDisplay,\n onValueChange = {},\n readOnly = true,\n label = { Text(\"计费模式\") },\n trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = presetExpanded) },\n modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable)\n )\n ExposedDropdownMenu(\n expanded = presetExpanded,\n onDismissRequest = { presetExpanded = false }\n ) {\n MIMO_API_PRESETS.forEach { (name, url) ->\n DropdownMenuItem(\n text = { Text(name) },\n onClick = {\n baseUrlInput = url\n presetExpanded = false\n }\n )\n }\n }\n }\n Spacer(Modifier.height(4.dp))\n Text(\n \"💡 按量计费使用 sk-xxxxx 格式 API Key(限时免费)\\n💡 Token Plan 使用 tp-xxxxx 格式 API Key\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n Spacer(Modifier.height(8.dp))\n\n OutlinedTextField(\n value = baseUrlInput,\n onValueChange = { baseUrlInput = it },\n label = { Text(\"API Base URL\") },\n modifier = Modifier.fillMaxWidth(),\n singleLine = true\n )\n Spacer(Modifier.height(8.dp))\n\n OutlinedTextField(\n value = apiKeyInput,\n onValueChange = { apiKeyInput = it },\n label = { Text(\"API Key\") },\n modifier = Modifier.fillMaxWidth(),\n singleLine = true,\n visualTransformation = if (apiKeyVisible) VisualTransformation.None else PasswordVisualTransformation(),\n trailingIcon = {\n IconButton(onClick = { apiKeyVisible = !apiKeyVisible }) {\n Icon(\n imageVector = if (apiKeyVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,\n contentDescription = if (apiKeyVisible) \"隐藏\" else \"显示\"\n )\n }\n }\n )\n Spacer(Modifier.height(8.dp))\n\n Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {\n Button(onClick = {\n scope.launch {\n val userAgent = userAgentInput.ifBlank { \"openclaw/unknown\" }\n settings.updateMiMoClientConfig(\n baseUrl = baseUrlInput,\n apiKey = apiKeyInput,\n userAgent = userAgent\n )\n (EngineRegistry.get(\"mimo\") as? MiMoEngine)\n ?.updateClientConfig(baseUrlInput, apiKeyInput, userAgent)\n }\n }) { Text(\"保存 API 配置\") }\n\n OutlinedButton(onClick = {\n scope.launch {\n settings.updateMiMoClientConfig(\n baseUrl = \"https://api.xiaomimimo.com\",\n apiKey = \"\",\n userAgent = \"openclaw/unknown\"\n )\n (EngineRegistry.get(\"mimo\") as? MiMoEngine)\n ?.updateClientConfig(\"https://api.xiaomimimo.com\", \"\", \"openclaw/unknown\")\n }\n }) { Text(\"清除配置\") }\n }\n }\n }\n }\n\n if (currentEngineId == \"mimo\") {\n // 自定义 User-Agent\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Text(\"自定义请求头\", style = MaterialTheme.typography.titleMedium)\n Spacer(Modifier.height(8.dp))\n OutlinedTextField(\n value = userAgentInput,\n onValueChange = { userAgentInput = it },\n label = { Text(\"User-Agent\") },\n modifier = Modifier.fillMaxWidth(),\n singleLine = true\n )\n Spacer(Modifier.height(4.dp))\n Text(\n \"默认使用 openclaw/unknown,用于伪装客户端身份\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n }\n }\n }\n\n // 默认语音\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Text(\"默认语音\", style = MaterialTheme.typography.titleMedium)\n Spacer(Modifier.height(8.dp))\n\n ExposedDropdownMenuBox(\n expanded = voiceExpanded,\n onExpandedChange = { voiceExpanded = it }\n ) {\n OutlinedTextField(\n value = defaultVoice,\n onValueChange = {},\n readOnly = true,\n label = { Text(\"默认音色\") },\n trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = voiceExpanded) },\n modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable)\n )\n ExposedDropdownMenu(\n expanded = voiceExpanded,\n onDismissRequest = { voiceExpanded = false }\n ) {\n // 显示预设音色\n if (presetVoices.isNotEmpty()) {\n DropdownMenuItem(\n text = { \n Text(\"预设音色\", style = MaterialTheme.typography.labelSmall,\n color = MaterialTheme.colorScheme.primary) \n },\n onClick = { /* 分组标题,不处理 */ }\n )\n presetVoices.forEach { voice ->\n DropdownMenuItem(\n text = { Text(\"${voice.name} - ${voice.description}\") },\n onClick = {\n scope.launch { settings.updateDefaultVoice(voice.name) }\n voiceExpanded = false\n }\n )\n }\n }\n \n // 显示自定义音色\n if (customVoices.isNotEmpty()) {\n DropdownMenuItem(\n text = { \n Text(\"自定义音色\", style = MaterialTheme.typography.labelSmall,\n color = MaterialTheme.colorScheme.primary) \n },\n onClick = { /* 分组标题,不处理 */ }\n )\n customVoices.forEach { voice ->\n DropdownMenuItem(\n text = { \n Text(\"${voice.name} - ${if (voice.type == \"clone\") \"克隆\" else \"设计\"}\") \n },\n onClick = {\n scope.launch { settings.updateDefaultVoice(voice.name) }\n voiceExpanded = false\n }\n )\n }\n }\n }\n }\n Spacer(Modifier.height(8.dp))\n\n if (styles.isNotEmpty()) {\n ExposedDropdownMenuBox(\n expanded = styleExpanded,\n onExpandedChange = { styleExpanded = it }\n ) {\n OutlinedTextField(\n value = styleInput,\n onValueChange = { styleInput = it },\n label = { Text(\"默认风格\") },\n placeholder = { Text(\"如:温柔磁性\") },\n trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = styleExpanded) },\n modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryEditable),\n singleLine = true,\n isError = false\n )\n ExposedDropdownMenu(\n expanded = styleExpanded,\n onDismissRequest = {\n styleExpanded = false\n // 关闭菜单时保存输入值\n if (styleInput != defaultStyle) {\n scope.launch { settings.updateDefaultStyle(styleInput) }\n }\n }\n ) {\n styles.forEach { style ->\n DropdownMenuItem(\n text = { Text(style) },\n onClick = {\n styleInput = style\n scope.launch { settings.updateDefaultStyle(style) }\n styleExpanded = false\n }\n )\n }\n }\n }\n Spacer(Modifier.height(8.dp))\n }\n\n Text(\"语速: ${String.format(\"%.1f\", speed)}x\")\n Slider(\n value = speed,\n onValueChange = { scope.launch { settings.updateSpeed(it) } },\n valueRange = 0.5f..2.0f,\n steps = 14\n )\n\n if (currentEngineId == \"mimo\") {\n Spacer(Modifier.height(8.dp))\n Text(\"采样温度: ${String.format(\"%.2f\", defaultTemperature)}\")\n Slider(\n value = defaultTemperature,\n onValueChange = { scope.launch { settings.updateDefaultTemperature(it) } },\n valueRange = 0f..1.5f,\n steps = 14\n )\n Text(\n \"控制合成随机性。值越低越稳定一致,值越高越多样自然。\\n\" +\n \"克隆/听书建议 0.1–0.3 以减少句间风格漂移;预设音色可用默认 0.6。\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n }\n }\n }\n\n // 系统 TTS 设置\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Row(\n modifier = Modifier.fillMaxWidth().padding(16.dp),\n horizontalArrangement = Arrangement.SpaceBetween,\n verticalAlignment = Alignment.CenterVertically\n ) {\n Column(modifier = Modifier.weight(1f)) {\n Text(\"系统 TTS 设置\", style = MaterialTheme.typography.titleMedium)\n Text(\n \"跳转到系统文字转语音设置页面,将 VoxEngine 设为首选引擎\",\n style = MaterialTheme.typography.bodySmall,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n }\n Button(onClick = {\n try {\n val intent = Intent(\"com.android.settings.TTS_SETTINGS\")\n intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n context.startActivity(intent)\n } catch (_: Exception) {\n Toast.makeText(context, \"无法打开系统 TTS 设置\", Toast.LENGTH_SHORT).show()\n }\n }) { Text(\"前往设置\") }\n }\n }\n\n // 使用说明\n Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {\n Column(modifier = Modifier.padding(16.dp)) {\n Text(\"使用说明\", style = MaterialTheme.typography.titleMedium)\n Spacer(Modifier.height(8.dp))\n Text(\n \"1. 在上方设置好 API Key\\n\" +\n \"2. 系统设置 → 语言和输入 → 文字转语音\\n\" +\n \"3. 选择 VoxEngine 为首选引擎\\n\" +\n \"4. 在阅读 APP 中选择系统默认引擎即可\",\n style = MaterialTheme.typography.bodySmall\n )\n }\n }\n }\n }\n}\n\nprivate suspend fun switchEngine(\n settings: SettingsRepository,\n db: AppDatabase,\n engine: TTSEngine,\n currentVoice: String,\n currentStyle: String\n) {\n val presetVoices = withContext(Dispatchers.IO) { engine.getVoices() }\n val customVoices = withContext(Dispatchers.IO) { db.voiceDao().getVoiceItemsByEngine(engine.id).first() }\n val availableVoiceNames = (presetVoices.map { it.name } + customVoices.map { it.name }).distinct()\n val styles = withContext(Dispatchers.IO) { engine.getStyles() }\n\n settings.updateCurrentEngine(engine.id)\n if (availableVoiceNames.isNotEmpty() && currentVoice !in availableVoiceNames) {\n settings.updateDefaultVoice(availableVoiceNames.first())\n }\n if (styles.isEmpty() || currentStyle !in styles) {\n settings.updateDefaultStyle(\"无\")\n }\n}\n"}}, "README.md": {"5ee087ce1426f9a902ab310a83470a5b3e12ef23": {"t": 1785595269.7510393, "n": 3746, "h": "ca8412ef9113918b", "c": "# VoxEngine\n\nAndroid 系统级 TTS 语音合成引擎,支持多引擎切换、音色克隆与设计。注册为系统 TTS 服务后,任意支持系统语音合成的应用(如 Legado 阅读器)均可直接调用。\n\n## 功能特性\n\n- **可插拔引擎架构** — 统一接口设计,当前支持 MiMo TTS 与微软 Edge TTS,可继续扩展更多引擎\n- **预设音色** — 内置多种中英文音色(冰糖、茉莉、苏打、白桦、Mia、Chloe 等),开箱即用\n- **音色语言自动识别** — 系统 TTS 语言按当前默认音色自动上报:英文音色返回 English,日语音色返回 Japanese,其它返回中文\n- **音色克隆** — 上传或录制一段音频样本,精准复刻目标音色\n- **音色设计** — 通过文字描述自动生成定制音色,无需音频文件\n- **风格控制** — 支持情绪、语调、方言、角色扮演等风格标签,一句话切换发音风格\n- **系统 TTS 集成** — 作为 Android TextToSpeechService 运行,所有支持系统 TTS 的应用均可使用\n- **内置阅读书架** — 支持导入本地 TXT 与 EPUB 小说,按章节/目录阅读并保存阅读进度\n- **内置听书** — 阅读页可从当前页或选中段落开始听书,顺序合成并预缓存后续内容\n- **听书稳定性优化** — 预取失败时自动补合成当前段,避免网络抖动导致跳段;音频未播完不会提前保存进度\n- **克隆音色限流设置** — 克隆/设计音色的请求间隔、重试次数和重试等待可在阅读设置中调整\n- **日志查询与导出** — 支持按日期、时间段、级别和关键词查询日志,复制或导出结果\n- **音色导入导出** — 支持将自定义音色导出为 JSON 文件,方便备份与分享\n\n## 图文教程\n\n### 1. 注册 MiMo 平台\n\n前往 [小米 MiMo TTS 平台](https://platform.xiaomimimo.com?ref=S5T7WV) 注册账号,在控制台新建 API Key。\n\nMiMo 提供两种计费模式:\n\n| 计费模式 | API Key 格式 | 说明 |\n|---------|-------------|------|\n| 按量计费 | `sk-xxxxx` | 限时免费,按调用次数计费 |\n| Token Plan | `tp-xxxxx` | 需购买 Token 套餐,中国区/新加坡/欧洲节点可选 |\n\n> 建议新手使用**按量计费**模式(当前限时免费),API Key 以 `sk-` 开头。\n\n\n\n### 2. 复制 API Key\n\n创建完成后复制 API Key。\n\n\n\n### 3. 在 VoxEngine 中填入 API Key\n\n打开 VoxEngine → 设置页面,选择计费模式,填入 API Key,点击「保存 API 配置」。然后选择你喜欢的默认音色和风格。\n\n\n\n### 4. 进入系统 TTS 设置\n\n在 VoxEngine 设置页点击「前往设置」,跳转到系统文字转语音设置页面。\n\n\n\n### 5. 切换首选引擎(第一步)\n\n在系统 TTS 设置中,点击「首选引擎」。\n\n\n\n### 6. 选择 VoxEngine(第二步)\n\n在引擎列表中选择 **VoxEngine**,完成!\n\n\n\n现在任意支持系统 TTS 的应用(如 Legado 阅读器)都可以直接使用 VoxEngine 进行语音合成了。\n\n> 在 Legado 中使用:确保 VoxEngine 已设为系统默认引擎,打开 Legado → 阅读界面 → 点击「朗读」即可。\n\n## 内置阅读与听书\n\nVoxEngine 也内置了简易书架,可直接导入多本本地 TXT 或 EPUB 小说。EPUB 会按照书内目录标题和 spine 正文顺序生成章节,并提取 XHTML 正文用于阅读与听书。阅读页点击屏幕中间会显示顶部/底部菜单,支持目录跳转、左右翻页、从选中段落开始听书、定时停止和播放若干章节后停止。\n\n内置听书采用顺序合成和预缓存逻辑:播放当前内容时会先按顺序预加载当前章节剩余内容,再随每读完一页逐步增加下一章预加载页数。遇到网络抖动或预取失败时,会在当前位置补合成当前段,尽量避免跳段。克隆/设计音色可调请求间隔与重试参数,用于降低 429 限流概率。\n\n## 音色说明\n\n### 预设音色(推荐)\n\n> 预设音色开箱即用,效果最佳。预设音色同样支持自定义风格标签,可以自由搭配语调、情绪、方言等风格。\n\n| 音色 | 描述 |\n|------|------|\n| 冰糖 | 甜美可爱女声 |\n| 茉莉 | 温柔知性女声 |\n| 苏打 | 活力阳光男声 |\n| 白桦 | 沉稳磁性男声 |\n| Mia | 英文女声 |\n| Chloe | 英文女声 |\n| Milo | 英文男声 |\n| Dean | 英文男声 |\n\n### 音色克隆\n\n上传或录制一段参考音频(建议 3-10 秒),MiMo 会根据音频特征克隆出相似的音色。适用于复刻特定角色的声音。\n\n> 自定义音色(克隆/设计)效果取决于输入素材和描述,可能需要多次调试才能达到理想效果。\n\n### 音色设计\n\n通过文字描述生成定制音色,例如:\n- \"温柔磁性的中年男声\"\n- \"活泼可爱的少女音\"\n- \"低沉沙哑的旁白声\"\n\n## 支持的风格\n\n| 类型 | 示例 |\n|------|------|\n| 基础情绪 | 开心、悲伤、愤怒、恐惧、兴奋、平静、冷漠 |\n| 复合情绪 | 怅然、欣慰、无奈、愧疚、释然、动情 |\n| 整体语调 | 温柔、高冷、活泼、严肃、慵懒、深沉、干练 |\n| 音色质感 | 磁性、醇厚、清亮、空灵、甜美、沙哑 |\n| 人设腔调 | 夹子音、御姐音、正太音、大叔音、台湾腔 |\n| 方言 | 粤语、四川话 |\n| 角色扮演 | 孙悟空、林黛玉 |\n| 唱歌 | 唱歌 |\n\n风格可以在设置中选择默认风格。为避免提示词被 TTS 读出,应用不会再把 `(风格)` 拼接进正文文本。系统 TTS 语言会按当前默认音色自动上报,英文内容建议搭配英文音色,日语内容建议搭配 Edge 日语音色使用。\n\n## 日志\n\n日志页支持按日期、时间段、级别和关键词查询,查询结果可复制或导出。应用会自动脱敏音频 base64 数据,避免日志过长或泄露音频内容。\n\n## Token Plan 节点\n\n如果使用 Token Plan,可选择以下节点:\n\n| 节点 | URL |\n|------|-----|\n| 中国区 | `https://token-plan-cn.xiaomimimo.com` |\n| 新加坡 | `https://token-plan-sgp.xiaomimimo.com` |\n| 欧洲 | `https://token-plan-ams.xiaomimimo.com` |\n\n> Token Plan 可能仅限用于编程开发场景,将其接入第三方应用进行语音合成可能违反小米服务条款,导致账号被封禁。建议使用按量计费模式。\n\n## 技术栈\n\n- **语言**: Kotlin\n- **UI**: Jetpack Compose + Material 3\n- **存储**: Room + DataStore\n- **网络**: OkHttp\n- **音频**: Android AudioTrack\n- **最低版本**: Android 8.0 (API 26)\n\n## 构建\n\n```bash\n# Debug 版本\n./gradlew assembleDebug\n\n# Release 版本(需配置签名)\n./gradlew assembleRelease\n```\n\n## 免责声明\n\n本软件为开源项目,仅供学习和个人使用,严禁用于任何违法违规用途。使用本软件即表示您已阅读并同意 [MiMo 用户协议](https://platform.xiaomimimo.com/docs/terms/user-agreement) 和 [MiMo 隐私政策](https://privacy.mi.com/XiaomiMiMoPlatform/zh_CN/)。\n\n## 致谢\n\n- [MiMo TTS](https://platform.xiaomimimo.com?ref=S5T7WV) — 小米 MiMo 语音合成 API\n\n## 许可证\n\n本项目基于 [MIT License](LICENSE) 开源。\n"}}}} |