Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
local.properties
.kotlin
ktlint
ktlint.bat
ktlint.bat
whisper/build/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "llama.cpp"]
path = llama.cpp
url = https://github.com/ggerganov/llama.cpp
[submodule "whisper/src/main/jni/whisper.cpp"]
path = whisper/src/main/jni/whisper.cpp
url = https://github.com/ggml-org/whisper.cpp.git
7 changes: 6 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
kotlin("plugin.serialization") version "2.1.0"
kotlin("plugin.serialization") version "2.0.0"
}

android {
Expand Down Expand Up @@ -92,12 +92,17 @@ dependencies {

implementation(project(":smollm"))
implementation(project(":hf-model-hub-api"))
implementation(project(":whisper"))

// Android Wave Recorder for speech-to-text
implementation("com.github.squti:Android-Wave-Recorder:2.1.0")

// Koin: dependency injection
implementation(libs.koin.android)
implementation(libs.koin.annotations)
implementation(libs.koin.androidx.compose)
implementation(libs.androidx.ui.text.google.fonts)
implementation(libs.androidx.compose.foundation)
ksp(libs.koin.ksp.compiler)

// compose-markdown: Markdown rendering in Compose
Expand Down
13 changes: 12 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,16 @@ plugins {
alias(libs.plugins.android.library) apply false
id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
kotlin("plugin.serialization") version "2.1.0" apply false
kotlin("plugin.serialization") version "2.0.0" apply false
}

subprojects {
plugins.withType<org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin> {
extensions.configure<org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension> {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.AZUL)
}
}
}
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Enable JVM toolchain auto-provisioning
org.gradle.java.installations.auto-download=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ uiTextGoogleFonts = "1.7.7"
composeIcons = "1.1.1"
appcompat = "1.6.1"
material = "1.10.0"
foundation = "1.10.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand Down Expand Up @@ -45,6 +46,7 @@ androidx-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text
composeIcons-feather = { module = "br.com.devsrsouza.compose.icons:feather", version.ref = "composeIcons" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down
2 changes: 1 addition & 1 deletion hf-model-hub-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
kotlin("plugin.serialization") version "2.1.0"
kotlin("plugin.serialization") version "2.0.0"
}

val ktorVersion = "3.0.2"
Expand Down
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ pluginManagement {
gradlePluginPortal()
}
}

plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
Expand All @@ -30,3 +34,4 @@ rootProject.name = "SmolChat Android"
include(":app")
include(":smollm")
include(":hf-model-hub-api")
include(":whisper")
58 changes: 58 additions & 0 deletions whisper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}

android {
namespace = "com.whispercpp"
compileSdk = 35

defaultConfig {
minSdk = 26

ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
}
externalNativeBuild {
cmake {
arguments("-DCMAKE_BUILD_TYPE=Release")
cppFlags("-ffile-prefix-map=${projectDir}=.")
cFlags("-ffile-prefix-map=${projectDir}=.")
}
}
}

buildTypes {
release {
isMinifyEnabled = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

externalNativeBuild {
cmake {
path = file("src/main/jni/whisper/CMakeLists.txt")
}
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

ndkVersion = "27.2.12479018"
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
}
4 changes: 4 additions & 0 deletions whisper/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
212 changes: 212 additions & 0 deletions whisper/src/main/java/com/whispercpp/whisper/LibWhisper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package com.whispercpp.whisper

import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import java.util.concurrent.Executors

private const val LOG_TAG = "LibWhisper"


class WhisperContext private constructor(private var ptr: Long) {
// Meet Whisper C++ constraint: Don't access from more than one thread at a time.
val scope: CoroutineScope = CoroutineScope(
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
fun stopTranscription(){
WhisperLib.stopTranscription()
}


suspend fun transcribeData(
data: FloatArray,
language: String,
printTimestamp: Boolean = true,
callback: WhisperCallback
): String = withContext(scope.coroutineContext) {
require(ptr != 0L)

try {
val numThreads = WhisperCpuConfig.preferredThreadCount
Log.d(LOG_TAG, "Selecting $numThreads threads")

WhisperLib.fullTranscribe(ptr, numThreads, data, language, callback)

val textCount = WhisperLib.getTextSegmentCount(ptr)
return@withContext buildString {
for (i in 0 until textCount) {
if (printTimestamp) {
val textTimestamp = "[${toTimestamp(WhisperLib.getTextSegmentT0(ptr, i))} --> ${
toTimestamp(WhisperLib.getTextSegmentT1(ptr, i))
}]"
val textSegment = WhisperLib.getTextSegment(ptr, i)
append("$textTimestamp: $textSegment\n")
} else {
append(WhisperLib.getTextSegment(ptr, i))
}
}
}

} catch (e: Exception) {
Log.e(LOG_TAG, "Error during transcription", e)
return@withContext ""
}
}

suspend fun benchMemory(nthreads: Int): String = withContext(scope.coroutineContext) {
return@withContext WhisperLib.benchMemcpy(nthreads)
}

suspend fun benchGgmlMulMat(nthreads: Int): String = withContext(scope.coroutineContext) {
return@withContext WhisperLib.benchGgmlMulMat(nthreads)
}

suspend fun release() = withContext(scope.coroutineContext) {
if (ptr != 0L) {
WhisperLib.freeContext(ptr)
ptr = 0
}
}

protected fun finalize() {
runBlocking {
release()
}
}

companion object {
fun createContextFromFile(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)
}

fun createContextFromInputStream(stream: InputStream): WhisperContext {
val ptr = WhisperLib.initContextFromInputStream(stream)

if (ptr == 0L) {
throw java.lang.RuntimeException("Couldn't create context from input stream")
}
return WhisperContext(ptr)
}

fun createContextFromAsset(assetManager: AssetManager, assetPath: String): WhisperContext {
val ptr = WhisperLib.initContextFromAsset(assetManager, assetPath)

if (ptr == 0L) {
throw java.lang.RuntimeException("Couldn't create context from asset $assetPath")
}
return WhisperContext(ptr)
}

fun getSystemInfo(): String {
return WhisperLib.getSystemInfo()
}
}
}

private class WhisperLib {
companion object {
init {
Log.d(LOG_TAG, "Primary ABI: ${Build.SUPPORTED_ABIS[0]}")
var loadVfpv4 = false
var loadV8fp16 = false
if (isArmEabiV7a()) {
// armeabi-v7a needs runtime detection support
val cpuInfo = cpuInfo()
cpuInfo?.let {
Log.d(LOG_TAG, "CPU info: $cpuInfo")
if (cpuInfo.contains("vfpv4")) {
Log.d(LOG_TAG, "CPU supports vfpv4")
loadVfpv4 = true
}
}
} else if (isArmEabiV8a()) {
// ARMv8.2a needs runtime detection support
val cpuInfo = cpuInfo()
cpuInfo?.let {
Log.d(LOG_TAG, "CPU info: $cpuInfo")
if (cpuInfo.contains("fphp")) {
Log.d(LOG_TAG, "CPU supports fp16 arithmetic")
loadV8fp16 = true
}
}
}

if (loadVfpv4) {
Log.d(LOG_TAG, "Loading libwhisper_vfpv4.so")
System.loadLibrary("whisper_vfpv4")
} else if (loadV8fp16) {
Log.d(LOG_TAG, "Loading libwhisper_v8fp16_va.so")
System.loadLibrary("whisper_v8fp16_va")
} else {
Log.d(LOG_TAG, "Loading libwhisper.so")
System.loadLibrary("whisper")
}
}

// JNI methods
external fun initContextFromInputStream(inputStream: InputStream): Long
external fun initContextFromAsset(assetManager: AssetManager, assetPath: String): Long
external fun initContext(modelPath: String): Long
external fun freeContext(contextPtr: Long)
external fun stopTranscription()
external fun fullTranscribe(
contextPtr: Long,
numThreads: Int,
audioData: FloatArray,
language: String,
callback: WhisperCallback
)

external fun getTextSegmentCount(contextPtr: Long): Int
external fun getTextSegment(contextPtr: Long, index: Int): String
external fun getTextSegmentT0(contextPtr: Long, index: Int): Long
external fun getTextSegmentT1(contextPtr: Long, index: Int): Long
external fun getSystemInfo(): String
external fun benchMemcpy(nthread: Int): String
external fun benchGgmlMulMat(nthread: Int): String
}
}

// 500 -> 00:05.000
// 6000 -> 01:00.000
private fun toTimestamp(t: Long, comma: Boolean = false): String {
var msec = t * 10
val hr = msec / (1000 * 60 * 60)
msec -= hr * (1000 * 60 * 60)
val min = msec / (1000 * 60)
msec -= min * (1000 * 60)
val sec = msec / 1000
msec -= sec * 1000

val delimiter = if (comma) "," else "."
return String.format("%02d:%02d:%02d%s%03d", hr, min, sec, delimiter, msec)
}

private fun isArmEabiV7a(): Boolean {
return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a")
}

private fun isArmEabiV8a(): Boolean {
return Build.SUPPORTED_ABIS[0].equals("arm64-v8a")
}

private fun cpuInfo(): String? {
return try {
File("/proc/cpuinfo").inputStream().bufferedReader().use {
it.readText()
}
} catch (e: Exception) {
Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e)
null
}
}
Loading