mirror of
https://github.com/ggml-org/whisper.cpp.git
synced 2026-10-04 05:21:27 +02:00
Add Android sample (#277)
* Add Android sample * Use main project C files * Stop existing playback before starting new playback * Make text scrollable * Stop playback when starting to record * Remove extra var
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.whispercppdemo
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import com.whispercppdemo.ui.main.MainScreen
|
||||
import com.whispercppdemo.ui.main.MainScreenViewModel
|
||||
import com.whispercppdemo.ui.theme.WhisperCppDemoTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val viewModel: MainScreenViewModel by viewModels { MainScreenViewModel.factory() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
WhisperCppDemoTheme {
|
||||
MainScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.whispercppdemo.media
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
fun decodeWaveFile(file: File): FloatArray {
|
||||
val baos = ByteArrayOutputStream()
|
||||
file.inputStream().use { it.copyTo(baos) }
|
||||
val buffer = ByteBuffer.wrap(baos.toByteArray())
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
buffer.position(44)
|
||||
val shortBuffer = buffer.asShortBuffer()
|
||||
val shortArray = ShortArray(shortBuffer.limit())
|
||||
shortBuffer.get(shortArray)
|
||||
return FloatArray(shortArray.size) { index ->
|
||||
(shortArray[index] / 32767.0f).coerceIn(-1f..1f)
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeWaveFile(file: File, data: ShortArray) {
|
||||
file.outputStream().use {
|
||||
it.write(headerBytes(data.size * 2))
|
||||
val buffer = ByteBuffer.allocate(data.size * 2)
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
buffer.asShortBuffer().put(data)
|
||||
val bytes = ByteArray(buffer.limit())
|
||||
buffer.get(bytes)
|
||||
it.write(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun headerBytes(totalLength: Int): ByteArray {
|
||||
require(totalLength >= 44)
|
||||
ByteBuffer.allocate(44).apply {
|
||||
order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
put('R'.code.toByte())
|
||||
put('I'.code.toByte())
|
||||
put('F'.code.toByte())
|
||||
put('F'.code.toByte())
|
||||
|
||||
putInt(totalLength - 8)
|
||||
|
||||
put('W'.code.toByte())
|
||||
put('A'.code.toByte())
|
||||
put('V'.code.toByte())
|
||||
put('E'.code.toByte())
|
||||
|
||||
put('f'.code.toByte())
|
||||
put('m'.code.toByte())
|
||||
put('t'.code.toByte())
|
||||
put(' '.code.toByte())
|
||||
|
||||
putInt(16)
|
||||
putShort(1.toShort())
|
||||
putShort(1.toShort())
|
||||
putInt(16000)
|
||||
putInt(32000)
|
||||
putShort(2.toShort())
|
||||
putShort(16.toShort())
|
||||
|
||||
put('d'.code.toByte())
|
||||
put('a'.code.toByte())
|
||||
put('t'.code.toByte())
|
||||
put('a'.code.toByte())
|
||||
|
||||
putInt(totalLength - 44)
|
||||
position(0)
|
||||
}.also {
|
||||
val bytes = ByteArray(it.limit())
|
||||
it.get(bytes)
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.whispercppdemo.recorder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import com.whispercppdemo.media.encodeWaveFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class Recorder {
|
||||
private val scope: CoroutineScope = CoroutineScope(
|
||||
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
)
|
||||
private var recorder: AudioRecordThread? = null
|
||||
|
||||
suspend fun startRecording(outputFile: File, onError: (Exception) -> Unit) = withContext(scope.coroutineContext) {
|
||||
recorder = AudioRecordThread(outputFile, onError)
|
||||
recorder?.start()
|
||||
}
|
||||
|
||||
suspend fun stopRecording() = withContext(scope.coroutineContext) {
|
||||
recorder?.stopRecording()
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
recorder?.join()
|
||||
recorder = null
|
||||
}
|
||||
}
|
||||
|
||||
private class AudioRecordThread(
|
||||
private val outputFile: File,
|
||||
private val onError: (Exception) -> Unit
|
||||
) :
|
||||
Thread("AudioRecorder") {
|
||||
private var quit = AtomicBoolean(false)
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun run() {
|
||||
try {
|
||||
val bufferSize = AudioRecord.getMinBufferSize(
|
||||
16000,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT
|
||||
) * 4
|
||||
val buffer = ShortArray(bufferSize / 2)
|
||||
|
||||
val audioRecord = AudioRecord(
|
||||
MediaRecorder.AudioSource.MIC,
|
||||
16000,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize
|
||||
)
|
||||
|
||||
try {
|
||||
audioRecord.startRecording()
|
||||
|
||||
val allData = mutableListOf<Short>()
|
||||
|
||||
while (!quit.get()) {
|
||||
val read = audioRecord.read(buffer, 0, buffer.size)
|
||||
if (read > 0) {
|
||||
for (i in 0 until read) {
|
||||
allData.add(buffer[i])
|
||||
}
|
||||
} else {
|
||||
throw java.lang.RuntimeException("audioRecord.read returned $read")
|
||||
}
|
||||
}
|
||||
|
||||
audioRecord.stop()
|
||||
encodeWaveFile(outputFile, allData.toShortArray())
|
||||
} finally {
|
||||
audioRecord.release()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
quit.set(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.whispercppdemo.ui.main
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.whispercppdemo.R
|
||||
|
||||
@Composable
|
||||
fun MainScreen(viewModel: MainScreenViewModel) {
|
||||
MainScreen(
|
||||
canTranscribe = viewModel.canTranscribe,
|
||||
isRecording = viewModel.isRecording,
|
||||
messageLog = viewModel.dataLog,
|
||||
onTranscribeSampleTapped = viewModel::transcribeSample,
|
||||
onRecordTapped = viewModel::toggleRecord
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MainScreen(
|
||||
canTranscribe: Boolean,
|
||||
isRecording: Boolean,
|
||||
messageLog: String,
|
||||
onTranscribeSampleTapped: () -> Unit,
|
||||
onRecordTapped: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) }
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
TranscribeSampleButton(enabled = canTranscribe, onClick = onTranscribeSampleTapped)
|
||||
RecordButton(
|
||||
enabled = canTranscribe,
|
||||
isRecording = isRecording,
|
||||
onClick = onRecordTapped
|
||||
)
|
||||
}
|
||||
MessageLog(messageLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageLog(log: String) {
|
||||
Text(modifier = Modifier.verticalScroll(rememberScrollState()), text = log)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TranscribeSampleButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
Button(onClick = onClick, enabled = enabled) {
|
||||
Text("Transcribe sample")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
private fun RecordButton(enabled: Boolean, isRecording: Boolean, onClick: () -> Unit) {
|
||||
val micPermissionState = rememberPermissionState(
|
||||
permission = android.Manifest.permission.RECORD_AUDIO,
|
||||
onPermissionResult = { granted ->
|
||||
if (granted) {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
)
|
||||
Button(onClick = {
|
||||
if (micPermissionState.status.isGranted) {
|
||||
onClick()
|
||||
} else {
|
||||
micPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}, enabled = enabled) {
|
||||
Text(
|
||||
if (isRecording) {
|
||||
"Stop recording"
|
||||
} else {
|
||||
"Start recording"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package com.whispercppdemo.ui.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.whispercppdemo.media.decodeWaveFile
|
||||
import com.whispercppdemo.recorder.Recorder
|
||||
import com.whispercppdemo.whisper.WhisperContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
private const val LOG_TAG = "MainScreenViewModel"
|
||||
|
||||
class MainScreenViewModel(private val application: Application) : ViewModel() {
|
||||
var canTranscribe by mutableStateOf(false)
|
||||
private set
|
||||
var dataLog by mutableStateOf("")
|
||||
private set
|
||||
var isRecording by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val modelsPath = File(application.filesDir, "models")
|
||||
private val samplesPath = File(application.filesDir, "samples")
|
||||
private var recorder: Recorder = Recorder()
|
||||
private var whisperContext: WhisperContext? = null
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var recordedFile: File? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadData() {
|
||||
printMessage("Loading data...\n")
|
||||
try {
|
||||
copyAssets()
|
||||
loadBaseModel()
|
||||
canTranscribe = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, e)
|
||||
printMessage("${e.localizedMessage}\n")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun printMessage(msg: String) = withContext(Dispatchers.Main) {
|
||||
dataLog += msg
|
||||
}
|
||||
|
||||
private suspend fun copyAssets() = withContext(Dispatchers.IO) {
|
||||
modelsPath.mkdirs()
|
||||
samplesPath.mkdirs()
|
||||
application.copyData("models", modelsPath, ::printMessage)
|
||||
application.copyData("samples", samplesPath, ::printMessage)
|
||||
printMessage("All data copied to working directory.\n")
|
||||
}
|
||||
|
||||
private suspend fun loadBaseModel() = withContext(Dispatchers.IO) {
|
||||
printMessage("Loading model...\n")
|
||||
val firstModel = modelsPath.listFiles()!!.first()
|
||||
whisperContext = WhisperContext.createContext(firstModel.absolutePath)
|
||||
printMessage("Loaded model ${firstModel.name}.\n")
|
||||
}
|
||||
|
||||
fun transcribeSample() = viewModelScope.launch {
|
||||
transcribeAudio(getFirstSample())
|
||||
}
|
||||
|
||||
private suspend fun getFirstSample(): File = withContext(Dispatchers.IO) {
|
||||
samplesPath.listFiles()!!.first()
|
||||
}
|
||||
|
||||
private suspend fun readAudioSamples(file: File): FloatArray = withContext(Dispatchers.IO) {
|
||||
stopPlayback()
|
||||
startPlayback(file)
|
||||
return@withContext decodeWaveFile(file)
|
||||
}
|
||||
|
||||
private suspend fun stopPlayback() = withContext(Dispatchers.Main) {
|
||||
mediaPlayer?.stop()
|
||||
mediaPlayer?.release()
|
||||
mediaPlayer = null
|
||||
}
|
||||
|
||||
private suspend fun startPlayback(file: File) = withContext(Dispatchers.Main) {
|
||||
mediaPlayer = MediaPlayer.create(application, file.absolutePath.toUri())
|
||||
mediaPlayer?.start()
|
||||
}
|
||||
|
||||
private suspend fun transcribeAudio(file: File) {
|
||||
if (!canTranscribe) {
|
||||
return
|
||||
}
|
||||
|
||||
canTranscribe = false
|
||||
|
||||
try {
|
||||
printMessage("Reading wave samples...\n")
|
||||
val data = readAudioSamples(file)
|
||||
printMessage("Transcribing data...\n")
|
||||
val text = whisperContext?.transcribeData(data)
|
||||
printMessage("Done: $text\n")
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, e)
|
||||
printMessage("${e.localizedMessage}\n")
|
||||
}
|
||||
|
||||
canTranscribe = true
|
||||
}
|
||||
|
||||
fun toggleRecord() = viewModelScope.launch {
|
||||
try {
|
||||
if (isRecording) {
|
||||
recorder.stopRecording()
|
||||
isRecording = false
|
||||
recordedFile?.let { transcribeAudio(it) }
|
||||
} else {
|
||||
stopPlayback()
|
||||
val file = getTempFileForRecording()
|
||||
recorder.startRecording(file) { e ->
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
printMessage("${e.localizedMessage}\n")
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
}
|
||||
isRecording = true
|
||||
recordedFile = file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, e)
|
||||
printMessage("${e.localizedMessage}\n")
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTempFileForRecording() = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("recording", "wav")
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
runBlocking {
|
||||
whisperContext?.release()
|
||||
whisperContext = null
|
||||
stopPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun factory() = viewModelFactory {
|
||||
initializer {
|
||||
val application =
|
||||
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
|
||||
MainScreenViewModel(application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Context.copyData(
|
||||
assetDirName: String,
|
||||
destDir: File,
|
||||
printMessage: suspend (String) -> Unit
|
||||
) = withContext(Dispatchers.IO) {
|
||||
assets.list(assetDirName)?.forEach { name ->
|
||||
val assetPath = "$assetDirName/$name"
|
||||
Log.v(LOG_TAG, "Processing $assetPath...")
|
||||
val destination = File(destDir, name)
|
||||
Log.v(LOG_TAG, "Copying $assetPath to $destination...")
|
||||
printMessage("Copying $name...\n")
|
||||
assets.open(assetPath).use { input ->
|
||||
destination.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
Log.v(LOG_TAG, "Copied $assetPath to $destination")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.whispercppdemo.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.whispercppdemo.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun WhisperCppDemoTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
|
||||
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.whispercppdemo.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.whispercppdemo.whisper
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class WhisperContext private constructor(private var ptr: Long) {
|
||||
// Meet Whisper C++ constraint: Don't access from more than one thread at a time.
|
||||
private val scope: CoroutineScope = CoroutineScope(
|
||||
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
)
|
||||
|
||||
suspend fun transcribeData(data: FloatArray): String = withContext(scope.coroutineContext) {
|
||||
require(ptr != 0L)
|
||||
WhisperLib.fullTranscribe(ptr, data)
|
||||
val textCount = WhisperLib.getTextSegmentCount(ptr)
|
||||
return@withContext buildString {
|
||||
for (i in 0 until textCount) {
|
||||
append(WhisperLib.getTextSegment(ptr, i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun release() = withContext(scope.coroutineContext) {
|
||||
if (ptr != 0L) {
|
||||
WhisperLib.freeContext(ptr)
|
||||
ptr = 0
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
runBlocking {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createContext(filePath: String): WhisperContext {
|
||||
val ptr = WhisperLib.initContext(filePath)
|
||||
if (ptr == 0L) {
|
||||
throw java.lang.RuntimeException("Couldn't create context with path $filePath")
|
||||
}
|
||||
return WhisperContext(ptr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WhisperLib {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("whisper")
|
||||
}
|
||||
|
||||
// JNI methods
|
||||
external fun initContext(modelPath: String): Long
|
||||
external fun freeContext(contextPtr: Long)
|
||||
external fun fullTranscribe(contextPtr: Long, audioData: FloatArray)
|
||||
external fun getTextSegmentCount(contextPtr: Long): Int
|
||||
external fun getTextSegment(contextPtr: Long, index: Int): String
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user