ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

零成本打造安卓AI提词器:Jetpack Compose与语音识别实战

零成本打造安卓AI提词器:Jetpack Compose与语音识别实战 1. 项目背景与核心概念在视频创作尤其是口播、知识分享、直播等场景中提词器是一个至关重要的工具。它能帮助创作者流畅地表达避免忘词和卡顿从而提升内容质量和录制效率。然而专业的硬件提词器价格昂贵动辄数千元对于个人创作者或小型团队来说是一笔不小的开销。而软件提词器虽然选择多样但要么功能单一要么操作复杂要么存在广告干扰体验并不完美。本项目旨在解决这个痛点利用一部普通的安卓手机零成本打造一个功能强大、体验流畅的私人专属提词器。我们不仅会实现基础的滚动字幕功能还会融入AI能力实现语音识别实时转字幕、智能语速匹配等进阶特性让提词过程更加智能和自然。整个项目将基于Android平台开发使用Kotlin语言结合Jetpack Compose构建现代化UI并集成百度语音识别等AI服务最终形成一个完整可安装的APK。通过这个实战项目你将系统掌握Android应用开发、Compose UI、第三方SDK集成、多媒体处理等核心技能。2. 环境准备与版本说明在开始编码之前请确保你的开发环境已就绪。以下是我开发时使用的环境你可以根据实际情况进行微调。操作系统Windows 10/11, macOS Monterey及以上或 Ubuntu 20.04及以上均可。集成开发环境 (IDE)Android Studio 推荐使用最新稳定版如 Android Studio Flamingo | 2022.2.1 或更高。它是官方支持的Android开发工具对Compose和Kotlin的支持最好。开发语言与框架Kotlin: 版本 1.8.0 或更高。Kotlin已是Android首推开发语言其简洁语法非常适合本项目。Android SDK: API Level 33 (Android 13) 作为targetSdkVersionminSdkVersion设置为 API 24 (Android 7.0)以覆盖绝大多数设备。Jetpack Compose:BOM版本2023.06.01或更高。我们将使用Compose来构建所有用户界面这是Android现代UI开发的未来。第三方服务百度AI开放平台-语音识别: 用于实现语音实时转文字功能。你需要前往百度AI开放平台注册账号并创建应用以获取API Key和Secret Key。项目依赖管理使用Gradle作为构建系统这是Android Studio的默认配置。版本兼容性提示本文的核心代码和配置思路具有通用性。如果未来Compose或相关库有重大更新部分API可能需要调整但整体架构和实现逻辑保持不变。重点在于理解每一步的原理。3. 核心功能与原理拆解我们的“手机提词器”核心功能可以拆解为以下几个模块理解其原理是编码的基础。3.1 文本滚动显示引擎这是提词器的核心。原理是控制一个文本控件在屏幕上沿垂直方向匀速移动。关键参数滚动速度 通常以“像素/秒”或“行/秒”来衡量需要允许用户动态调节。字体与颜色 为了清晰易读需要支持大字体、高对比度颜色如白字黑底或黑字白底。暂停/继续 必须能随时中断和恢复滚动以适应录制时的节奏。实现思路 在Compose中我们可以通过Animatable或animate*AsState动画API来持续改变文本的垂直偏移量(offsetY)结合LaunchedEffect来驱动动画循环从而实现平滑滚动。3.2 语音识别集成为了让提词器更智能我们引入语音识别实现“所说即所得”的提词稿生成。工作流程权限申请 需要获取RECORD_AUDIO录音权限。音频采集 使用Android的AudioRecord或更高阶的MediaRecorder捕获麦克风输入。调用云端API 将采集到的音频数据通常需要转换为PCM格式发送给百度语音识别REST API。结果解析与显示 接收API返回的JSON格式识别结果提取出文字片段并实时追加到提词文本框中。难点与优化网络延迟 云端识别必然有延迟UI上需要良好的加载状态提示。识别精度 受环境噪音、口音影响需要引导用户在安静环境下使用并可能提供简单的文本编辑功能进行后期修正。3.3 智能语速匹配进阶基础滚动是匀速的但人说话有快有慢。智能语速匹配旨在分析识别出的文本动态微调滚动速度。简单策略根据标点符号如句号、逗号短暂减缓滚动速度模拟自然停顿。检测到长难句时稍微提升滚动速度以避免等待。实现考虑 这是一个优化项初期可以提供一个手动调节速度的滑块后期再引入基于规则的简单智能调节。3.4 用户界面与交互一个直观易用的UI至关重要。核心界面主提词界面 占据大部分屏幕的滚动文本区域背景可调。控制面板 悬浮或侧边栏包含开始/暂停、速度调节、字体大小、颜色切换、语音识别开关等按钮。文本编辑界面 用于初始粘贴或修改提词稿。4. 完整实战从零构建应用让我们开始动手编码。我们将创建一个名为TelePrompter的新Android项目选择Empty Activity模板并确保语言为KotlinUI框架为Jetpack Compose。4.1 项目结构与依赖配置首先配置项目的build.gradle.kts文件。1. 项目级build.gradle.kts(project/build.gradle.kts):主要配置仓库和插件版本。plugins { id(com.android.application) version 8.1.0 apply false id(org.jetbrains.kotlin.android) version 1.8.0 apply false }2. 模块级build.gradle.kts(project/app/build.gradle.kts):这是配置的核心添加所有必要的依赖。plugins { id(com.android.application) id(org.jetbrains.kotlin.android) } android { namespace “com.yourcompany.teleprompter compileSdk 33 defaultConfig { applicationId “com.yourcompany.teleprompter minSdk 24 targetSdk 33 versionCode 1 versionName “1.0 testInstrumentationRunner “androidx.test.runner.AndroidJUnitRunner vectorDrawables { useSupportLibrary true } } buildTypes { release { isMinifyEnabled false proguardFiles( getDefaultProguardFile(“proguard-android-optimize.txt), “proguard-rules.pro ) } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget “1.8 } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion “1.4.3 } packaging { resources { excludes “/META-INF/{AL2.0,LGPL2.1} } } } dependencies { // Android 核心库 implementation(“androidx.core:core-ktx:1.10.1) implementation(“androidx.lifecycle:lifecycle-runtime-ktx:2.6.1) implementation(“androidx.activity:activity-compose:1.7.2) implementation(platform(“androidx.compose:compose-bom:2023.06.01)) implementation(“androidx.compose.ui:ui) implementation(“androidx.compose.ui:ui-graphics) implementation(“androidx.compose.ui:ui-tooling-preview) implementation(“androidx.compose.material3:material3) testImplementation(“junit:junit:4.13.2) androidTestImplementation(“androidx.test.ext:junit:1.1.5) androidTestImplementation(“androidx.test.espresso:espresso-core:3.5.1) androidTestImplementation(platform(“androidx.compose:compose-bom:2023.06.01)) androidTestImplementation(“androidx.compose.ui:ui-test-junit4) debugImplementation(“androidx.compose.ui:ui-tooling) debugImplementation(“androidx.compose.ui:ui-test-manifest) // 视图模型用于管理UI状态 implementation(“androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1) // 网络请求 (用于调用百度语音识别API) implementation(“com.squareup.retrofit2:retrofit:2.9.0) implementation(“com.squareup.retrofit2:converter-gson:2.9.0) implementation(“com.squareup.okhttp3:logging-interceptor:4.11.0) // 权限请求 implementation(“com.google.accompanist:accompanist-permissions:0.31.5-beta) }注意com.google.accompanist:accompanist-permissions库用于简化Compose中的权限请求但请注意其可能已迁移至Android官方库请根据你使用的Compose版本查阅最新文档。3. 百度语音识别依赖百度官方通常提供aar包或Maven依赖。假设我们使用网络请求的方式 Retrofit依赖已添加。你需要将百度语音识别SDK如果有的aar文件放入app/libs/目录并在build.gradle.kts的dependencies块中添加implementation(fileTree(mapOf(“dir” to “libs”, “include” to listOf(“*.jar”, “*.aar”))))或者如果百度提供了Maven仓库则添加对应的implementation语句。4.2 实现基础提词滚动界面我们首先构建最核心的滚动文本显示功能。1. 定义数据状态 (ui/TeleprompterState.kt):使用ViewModel和StateFlow/MutableStateFlow来管理应用状态这是Compose推荐的做法。import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class TeleprompterViewModel : ViewModel() { // 提词文本内容 private val _scriptText MutableStateFlow(“欢迎使用手机提词器请在此输入或粘贴您的讲稿。\n第二行测试文本。”) val scriptText: StateFlowString _scriptText.asStateFlow() // 滚动位置像素偏移量 private val _scrollOffset MutableStateFlow(0f) val scrollOffset: StateFlowFloat _scrollOffset.asStateFlow() // 滚动速度像素/秒 private val _scrollSpeed MutableStateFlow(60f) val scrollSpeed: StateFlowFloat _scrollSpeed.asStateFlow() // 是否正在滚动 private val _isScrolling MutableStateFlow(false) val isScrolling: StateFlowBoolean _isScrolling.asStateFlow() // 字体大小SP private val _fontSize MutableStateFlow(24f) val fontSize: StateFlowFloat _fontSize.asStateFlow() // 文本颜色 private val _textColor MutableStateFlow(androidx.compose.ui.graphics.Color.White) val textColor: StateFlowandroidx.compose.ui.graphics.Color _textColor.asStateFlow() // 背景颜色 private val _backgroundColor MutableStateFlow(androidx.compose.ui.graphics.Color.Black) val backgroundColor: StateFlowandroidx.compose.ui.graphics.Color _backgroundColor.asStateFlow() fun updateScript(newText: String) { _scriptText.value newText } fun setScrollSpeed(speed: Float) { _scrollSpeed.value speed } fun setFontSize(size: Float) { _fontSize.value size } fun toggleScrolling() { _isScrolling.value !_isScrolling.value if (_isScrolling.value) { startAutoScroll() } } fun resetScroll() { _scrollOffset.value 0f _isScrolling.value false } private fun startAutoScroll() { viewModelScope.launch { while (isScrolling.value) { // 根据速度和时间增量更新偏移量 // 这是一个简化的逻辑实际应该基于时间差计算 _scrollOffset.value _scrollSpeed.value / 60 // 假设60FPS kotlinx.coroutines.delay(16) // 约60FPS的延迟 } } } }2. 创建主提词界面 (ui/TeleprompterScreen.kt):import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel Composable fun TeleprompterScreen(viewModel: TeleprompterViewModel viewModel()) { val script by viewModel.scriptText.collectAsState() val scrollOffset by viewModel.scrollOffset.collectAsState() val scrollSpeed by viewModel.scrollSpeed.collectAsState() val isScrolling by viewModel.isScrolling.collectAsState() val fontSize by viewModel.fontSize.collectAsState() val textColor by viewModel.textColor.collectAsState() val backgroundColor by viewModel.backgroundColor.collectAsState() // 使用动画使滚动更平滑可选 val animatedOffset by animateFloatAsState( targetValue scrollOffset, animationSpec if (isScrolling) tween(durationMillis 16) else snap() ) Box( modifier Modifier .fillMaxSize() .background(backgroundColor) .clipToBounds() // 确保文本超出部分被裁剪 ) { // 滚动的文本内容 Text( text script, color textColor, fontSize fontSize.sp, fontWeight FontWeight.Normal, modifier Modifier .fillMaxWidth() .align(Alignment.TopCenter) .padding(horizontal 32.dp) .offset(y animatedOffset.dp) // 应用垂直偏移实现滚动 ) // 控制面板 (可以设计为悬浮按钮或底部栏) ControlPanel( isScrolling isScrolling, scrollSpeed scrollSpeed, onSpeedChange { viewModel.setScrollSpeed(it) }, onToggleScroll { viewModel.toggleScrolling() }, onReset { viewModel.resetScroll() }, modifier Modifier.align(Alignment.BottomCenter) ) } } Composable fun ControlPanel( isScrolling: Boolean, scrollSpeed: Float, onSpeedChange: (Float) - Unit, onToggleScroll: () - Unit, onReset: () - Unit, modifier: Modifier Modifier ) { Column( modifier modifier .fillMaxWidth() .background(Color.DarkGray.copy(alpha 0.8f)) .padding(16.dp), horizontalAlignment Alignment.CenterHorizontally ) { Text(“滚动速度: ${“%.1f”.format(scrollSpeed)}“, color Color.White) Slider( value scrollSpeed, onValueChange onSpeedChange, valueRange 20f..200f, modifier Modifier.fillMaxWidth() ) Button(onClick onToggleScroll) { Text(if (isScrolling) “暂停” else “开始”) } Button(onClick onReset, modifier Modifier.padding(top 8.dp)) { Text(“重置到顶部”) } } }3. 在MainActivity中调用 (MainActivity.kt):import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.yourcompany.teleprompter.ui.TeleprompterScreen import com.yourcompany.teleprompter.ui.theme.TeleprompterTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { TeleprompterTheme { Surface( modifier Modifier.fillMaxSize(), color MaterialTheme.colorScheme.background ) { TeleprompterScreen() } } } } }现在运行应用你应该能看到一个黑色背景、白色文字的界面点击“开始”按钮文本会向上滚动。速度可以通过滑块调节。4.3 集成语音识别功能这是一个进阶功能。由于涉及网络权限和异步操作代码会复杂一些。这里给出核心步骤和代码框架。1. 权限处理 (utils/PermissionUtils.kt或直接在ViewModel中):使用accompanist-permissions或Android原生API请求录音权限。// 在 ViewModel 或一个专门类中管理权限状态 Composable fun RequestRecordAudioPermission() { val permissionState rememberPermissionState(android.Manifest.permission.RECORD_AUDIO) if (!permissionState.status.isGranted) { LaunchedEffect(permissionState) { permissionState.launchPermissionRequest() } } // 根据 permissionState.status 更新UI或逻辑 }2. 百度语音识别服务封装 (service/BaiduAsrService.kt):这是一个简化版的Retrofit服务接口和调用类。import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Body import retrofit2.http.Header import retrofit2.http.POST import java.util.concurrent.TimeUnit // 百度语音识别API返回的数据结构根据官方文档定义 data class AsrResponse( val result: ListString?, val err_no: Int, val err_msg: String, val sn: String, val corpus_no: String? ) // 请求体结构 data class AsrRequest( val format: String “pcm”, val rate: Int 16000, val channel: Int 1, val cuid: String, val token: String, val speech: String, // Base64编码的音频数据 val len: Int // 音频数据长度 ) interface BaiduAsrApi { POST(“/v2/asr) suspend fun recognize( Header(“Content-Type”) contentType: String “application/json”, Body request: AsrRequest ): AsrResponse } object BaiduAsrService { private const val BASE_URL “https://vop.baidu.com private val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() private val retrofit Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() val api: BaiduAsrApi retrofit.create(BaiduAsrApi::class.java) // 获取Access Token (需要你的 API Key 和 Secret Key) suspend fun getAccessToken(apiKey: String, secretKey: String): String? { // 实现获取Token的逻辑通常需要另一个网络请求 // 返回获取到的 token或 null return null } }3. 在ViewModel中集成语音识别逻辑:扩展TeleprompterViewModel添加音频录制、识别和文本追加功能。这部分涉及AudioRecord的使用和协程管理代码较长核心思路是在toggleVoiceRecognition函数中开始或停止录音。录音时将音频数据放入缓冲区定期如每1秒发送到BaiduAsrService进行识别。收到识别结果后将文本追加到_scriptText.value的末尾。4.4 完善功能与UI文本编辑界面 创建一个新的Composable屏幕使用TextField或BasicTextField来编辑scriptText。设置界面 允许用户修改字体大小、颜色主题如白底黑字、黑底黄字等。本地存储 使用DataStore或SharedPreferences保存用户设置和最近的文稿。导出功能 可以将最终的文稿含语音识别添加的内容保存为文本文件。5. 常见问题与排查思路在开发和测试过程中你可能会遇到以下问题问题现象可能原因排查与解决思路应用安装失败设备不满足minSdkVersion要求签名冲突。检查设备Android版本在Android Studio中执行Build - Clean Project再Rebuild。文本不滚动或滚动卡顿ViewModel中的滚动逻辑有误动画刷新率太低。检查startAutoScroll协程是否正常启动确保scrollOffset的更新能触发UI重组考虑使用withFrameMillis进行更精确的基于时间的动画。语音识别无反应未授予录音权限网络问题百度API配置错误。1. 检查应用权限设置。2. 检查网络连接。3. 确认百度AI应用的API Key、Secret Key正确且服务“语音识别”已开通。4. 查看Logcat中的网络请求和错误日志。识别结果不准或延迟高环境噪音大音频格式或参数不匹配。1. 确保在安静环境下测试。2. 检查AsrRequest中的format、rate、channel是否与录音参数严格一致。3. 考虑使用VAD语音活动检测来只在说话时发送音频减少无效请求。UI布局错乱或预览不显示Compose版本不兼容预览函数参数问题。1. 同步Gradle依赖。2. 确保Preview注解的函数提供了默认参数或使用PreviewParameter。3. 尝试File - Invalidate Caches and Restart。控制面板遮挡文本布局层级或对齐方式问题。调整ControlPanel的Modifier.align位置或使用BoxWithConstraints动态计算布局。6. 最佳实践与工程建议将一个小想法做成一个健壮的应用需要关注更多工程细节。状态管理规范化始终坚持使用ViewModelStateFlow/MutableStateFlow或LiveData来管理UI状态。避免在Composable函数中直接持有可变状态。对于复杂状态考虑使用sealed class来定义状态使状态变化更清晰。示例sealed interface RecognitionState { object Idle : RecognitionState object Recording : RecognitionState data class Recognizing(val progress: Int) : RecognitionState data class Error(val message: String) : RecognitionState data class Result(val text: String) : RecognitionState }网络请求与错误处理将所有网络操作如语音识别放在ViewModel或Repository层使用协程的try-catch进行包装。在UI层收集状态并根据不同的状态加载中、成功、失败显示相应的界面如加载框、错误提示Snackbar。合理设置超时时间并考虑重试机制。性能优化滚动性能 确保滚动文本的TextComposable 不会在每一帧都进行昂贵的重组。如果文稿极长考虑使用LazyColumn或其他虚拟化列表但滚动逻辑会变得更复杂。对于本项目分段渲染文本可能是一个优化方向。内存管理 语音识别时音频数据缓冲区要及时清理避免内存泄漏。在ViewModel的onCleared方法中取消所有协程。用户体验优化手势支持 为提词界面添加双击暂停、上下滑动微调滚动位置等功能。多种颜色主题 提供几套预设的颜色方案如白底黑字、黑底绿字、蓝底黄字适应不同拍摄环境。倒计时功能 在开始滚动前提供3秒倒计时让创作者做好准备。镜像模式 对于前置摄像头拍摄提供文本水平镜像翻转的选项。代码结构与可维护性遵循单一职责原则将语音识别、文件存储、设置管理等逻辑抽离到独立的Repository或UseCase类中。使用依赖注入如Hilt来管理ViewModel、Repository、Service的实例使代码更可测试。为关键业务逻辑编写单元测试。发布前检查隐私政策 因为应用需要录音权限必须在应用内提供隐私政策链接说明音频数据的使用方式仅用于本地识别或上传至百度服务器。混淆配置 在proguard-rules.pro中添加Retrofit、Gson等库的混淆保留规则。图标与名称 设计一个专业的应用图标和名称。通过这个项目你不仅得到了一个零成本的强大提词器更完成了一次完整的Android现代应用开发实战。从UI构建、状态管理、第三方SDK集成到性能调优和用户体验打磨每一个环节都蕴含着宝贵的工程经验。你可以在此基础上继续扩展比如加入AI文稿润色、多语言翻译提词、与云存储同步文稿等功能让它真正成为一个独一无二的创作利器。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进