diff --git a/.github/workflows/apk-speaker-identification.yaml b/.github/workflows/apk-speaker-identification.yaml new file mode 100644 index 0000000000..7112f057a4 --- /dev/null +++ b/.github/workflows/apk-speaker-identification.yaml @@ -0,0 +1,125 @@ +name: apk-speaker-identification + +on: + push: + branches: + - apk + tags: + - '*' + + workflow_dispatch: + +concurrency: + group: apk-speaker-identification-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + apk_tts: + if: github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa' + runs-on: ${{ matrix.os }} + name: apk for tts ${{ matrix.index }}/${{ matrix.total }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + total: ["10"] + index: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # https://github.com/actions/setup-java + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '21' + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ matrix.os }}-android + + - name: Display NDK HOME + shell: bash + run: | + echo "ANDROID_NDK_LATEST_HOME: ${ANDROID_NDK_LATEST_HOME}" + ls -lh ${ANDROID_NDK_LATEST_HOME} + + - name: Install Python dependencies + shell: bash + run: | + python3 -m pip install --upgrade pip jinja2 + + - name: Generate build script + shell: bash + run: | + cd scripts/apk + + total=${{ matrix.total }} + index=${{ matrix.index }} + + ./generate-speaker-identification-apk-script.py --total $total --index $index + + chmod +x build-apk-speaker-identification.sh + mv -v ./build-apk-speaker-identification.sh ../.. + + - name: build APK + shell: bash + run: | + export CMAKE_CXX_COMPILER_LAUNCHER=ccache + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + cmake --version + + export ANDROID_NDK=$ANDROID_NDK_LATEST_HOME + ./build-apk-speaker-identification.sh + + - name: Display APK + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + # - name: Release + # uses: svenstaro/upload-release-action@v2 + # with: + # file_glob: true + # file: ./apks/*.apk + # overwrite: true + # repo_name: k2-fsa/sherpa-onnx + # repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }} + # tag: speaker-recongition-models + + - name: Publish to huggingface + if: true + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + uses: nick-fields/retry@v2 + with: + max_attempts: 20 + timeout_seconds: 200 + shell: bash + command: | + git config --global user.email "csukuangfj@gmail.com" + git config --global user.name "Fangjun Kuang" + + rm -rf huggingface + export GIT_LFS_SKIP_SMUDGE=1 + + git clone https://huggingface.co/csukuangfj/sherpa-onnx-apk huggingface + cd huggingface + git fetch + git pull + git merge -m "merge remote" --ff origin main + + mkdir -p speaker-identification + cp -v ../apks/*.apk ./speaker-identification/ + git status + git lfs track "*.apk" + git add . + git commit -m "add more apks" + git push https://csukuangfj:$HF_TOKEN@huggingface.co/csukuangfj/sherpa-onnx-apk main diff --git a/CMakeLists.txt b/CMakeLists.txt index 833786fbe2..a4446e8817 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.7") +set(SHERPA_ONNX_VERSION "1.9.8") # Disable warning about # diff --git a/android/SherpaOnnxSpeakerIdentification/.gitignore b/android/SherpaOnnxSpeakerIdentification/.gitignore new file mode 100644 index 0000000000..aa724b7707 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/SherpaOnnxSpeakerIdentification/app/.gitignore b/android/SherpaOnnxSpeakerIdentification/app/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/build.gradle.kts b/android/SherpaOnnxSpeakerIdentification/app/build.gradle.kts new file mode 100644 index 0000000000..31e118e013 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.k2fsa.sherpa.onnx.speaker.identification" + compileSdk = 34 + + defaultConfig { + applicationId = "com.k2fsa.sherpa.onnx.speaker.identification" + minSdk = 21 + targetSdk = 34 + 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.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.08.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.navigation:navigation-compose:2.7.6") + 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.08.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/proguard-rules.pro b/android/SherpaOnnxSpeakerIdentification/app/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/speaker/identification/ExampleInstrumentedTest.kt b/android/SherpaOnnxSpeakerIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/speaker/identification/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..c02bca48fd --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/speaker/identification/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.k2fsa.sherpa.onnx.speaker.identification + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.k2fsa.sherpa.onnx.speaker.identification", appContext.packageName) + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/AndroidManifest.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..136cb78cd0 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/assets/.gitkeep b/android/SherpaOnnxSpeakerIdentification/app/src/main/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/BarItem.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/BarItem.kt new file mode 100644 index 0000000000..7c3a56ddad --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/BarItem.kt @@ -0,0 +1,13 @@ +package com.k2fsa.sherpa.onnx.speaker.identification + +import androidx.compose.ui.graphics.vector.ImageVector + +data class BarItem ( + val title: String, + + // see https://www.composables.com/icons + // and + // https://developer.android.com/reference/kotlin/androidx/compose/material/icons/filled/package-summary + val image: ImageVector, + val route: String, +) \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/MainActivity.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/MainActivity.kt new file mode 100644 index 0000000000..262f1973c7 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/MainActivity.kt @@ -0,0 +1,179 @@ +package com.k2fsa.sherpa.onnx.speaker.identification + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.app.ActivityCompat +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.k2fsa.sherpa.onnx.SpeakerRecognition +import com.k2fsa.sherpa.onnx.speaker.identification.screens.HelpScreen +import com.k2fsa.sherpa.onnx.speaker.identification.screens.HomeScreen +import com.k2fsa.sherpa.onnx.speaker.identification.screens.RegisterScreen +import com.k2fsa.sherpa.onnx.speaker.identification.screens.ViewScreen +import com.k2fsa.sherpa.onnx.speaker.identification.ui.theme.SherpaOnnxSpeakerIdentificationTheme + +const val TAG = "sherpa-onnx-speaker" +private const val REQUEST_RECORD_AUDIO_PERMISSION = 200 + +class MainActivity : ComponentActivity() { + private val permissions: Array = arrayOf(Manifest.permission.RECORD_AUDIO) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SherpaOnnxSpeakerIdentificationTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainScreen() + } + } + } + + ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION) + + SpeakerRecognition.initExtractor(this.assets) + } + + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + val permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { + grantResults[0] == PackageManager.PERMISSION_GRANTED + } else { + false + } + + if (!permissionToRecordAccepted) { + Log.e(TAG, "Audio record is disallowed") + Toast.makeText( + this, + "This App needs access to the microphone", + Toast.LENGTH_SHORT + ) + .show() + finish() + } + + Log.i(TAG, "Audio record is permitted") + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen(modifier: Modifier = Modifier) { + val navController = rememberNavController() + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Next-gen Kaldi: Speaker Identification", + fontWeight = FontWeight.Bold, + ) + }, + ) + }, + content = { padding -> + Column(Modifier.padding(padding)) { + NavigationHost(navController = navController) + + } + }, + bottomBar = { + BottomNavigationBar(navController = navController) + } + ) +} + +@Composable +fun NavigationHost(navController: NavHostController) { + NavHost(navController = navController, startDestination = NavRoutes.Home.route) { + composable(NavRoutes.Home.route) { + HomeScreen() + } + + composable(NavRoutes.Register.route) { + RegisterScreen() + } + + composable(NavRoutes.View.route) { + ViewScreen() + } + + composable(NavRoutes.Help.route) { + HelpScreen() + } + } +} + +@Composable +fun BottomNavigationBar(navController: NavHostController) { + NavigationBar { + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route + + NavBarItems.BarItems.forEach { navItem -> + NavigationBarItem(selected = currentRoute == navItem.route, + onClick = { + navController.navigate(navItem.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + Icon(imageVector = navItem.image, contentDescription = navItem.title) + }, label = { + Text(text = navItem.title) + }) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MainScreenPreview() { + SherpaOnnxSpeakerIdentificationTheme { + MainScreen() + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavBarItems.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavBarItems.kt new file mode 100644 index 0000000000..36ce5f5c3c --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavBarItems.kt @@ -0,0 +1,33 @@ +package com.k2fsa.sherpa.onnx.speaker.identification + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Info + + +object NavBarItems { + val BarItems = listOf( + BarItem( + title = "Home", + image = Icons.Filled.Home, + route = "home", + ), + BarItem( + title = "Register", + image = Icons.Filled.Add, + route = "register", + ), + BarItem( + title = "View", + image = Icons.Filled.AccountCircle, + route = "view", + ), + BarItem( + title = "Help", + image = Icons.Filled.Info, + route = "help", + ), + ) +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavRoutes.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavRoutes.kt new file mode 100644 index 0000000000..1183966451 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/NavRoutes.kt @@ -0,0 +1,8 @@ +package com.k2fsa.sherpa.onnx.speaker.identification + +sealed class NavRoutes(val route: String) { + object Home: NavRoutes("home") + object Register: NavRoutes("register") + object View: NavRoutes("view") + object Help: NavRoutes("help") +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/Speaker.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/Speaker.kt new file mode 100644 index 0000000000..e3dc62fa17 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/Speaker.kt @@ -0,0 +1,189 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager +import android.util.Log +import com.k2fsa.sherpa.onnx.speaker.identification.TAG + + +data class SpeakerEmbeddingExtractorConfig( + val model: String, + var numThreads: Int = 1, + var debug: Boolean = false, + var provider: String = "cpu", +) + +class SpeakerEmbeddingExtractorStream(var ptr: Long) { + fun acceptWaveform(samples: FloatArray, sampleRate: Int) = + acceptWaveform(ptr, samples, sampleRate) + + fun inputFinished() = inputFinished(ptr) + + protected fun finalize() { + delete(ptr) + ptr = 0 + } + + private external fun myTest(ptr: Long, v: Array) + + fun release() = finalize() + private external fun acceptWaveform(ptr: Long, samples: FloatArray, sampleRate: Int) + + private external fun inputFinished(ptr: Long) + + private external fun delete(ptr: Long) + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} + +class SpeakerEmbeddingExtractor( + assetManager: AssetManager? = null, + config: SpeakerEmbeddingExtractorConfig, +) { + private var ptr: Long + + init { + ptr = if (assetManager != null) { + new(assetManager, config) + } else { + newFromFile(config) + } + } + + protected fun finalize() { + delete(ptr) + ptr = 0 + } + + fun release() = finalize() + + fun createStream(): SpeakerEmbeddingExtractorStream { + val p = createStream(ptr) + return SpeakerEmbeddingExtractorStream(p) + } + + fun isReady(stream: SpeakerEmbeddingExtractorStream) = isReady(ptr, stream.ptr) + fun compute(stream: SpeakerEmbeddingExtractorStream) = compute(ptr, stream.ptr) + fun dim() = dim(ptr) + + private external fun new( + assetManager: AssetManager, + config: SpeakerEmbeddingExtractorConfig, + ): Long + + private external fun newFromFile( + config: SpeakerEmbeddingExtractorConfig, + ): Long + + private external fun delete(ptr: Long) + + private external fun createStream(ptr: Long): Long + + private external fun isReady(ptr: Long, streamPtr: Long): Boolean + + private external fun compute(ptr: Long, streamPtr: Long): FloatArray + + private external fun dim(ptr: Long): Int + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} + +class SpeakerEmbeddingManager(val dim: Int) { + private var ptr: Long + + init { + ptr = new(dim) + } + + protected fun finalize() { + delete(ptr) + ptr = 0 + } + + fun release() = finalize() + fun add(name: String, embedding: FloatArray) = add(ptr, name, embedding) + fun add(name: String, embedding: Array) = addList(ptr, name, embedding) + fun remove(name: String) = remove(ptr, name) + fun search(embedding: FloatArray, threshold: Float) = search(ptr, embedding, threshold) + fun verify(name: String, embedding: FloatArray, threshold: Float) = + verify(ptr, name, embedding, threshold) + + fun contains(name: String) = contains(ptr, name) + fun numSpeakers() = numSpeakers(ptr) + + fun allSpeakerNames() = allSpeakerNames(ptr) + + private external fun new(dim: Int): Long + private external fun delete(ptr: Long): Unit + private external fun add(ptr: Long, name: String, embedding: FloatArray): Boolean + private external fun addList(ptr: Long, name: String, embedding: Array): Boolean + private external fun remove(ptr: Long, name: String): Boolean + private external fun search(ptr: Long, embedding: FloatArray, threshold: Float): String + private external fun verify( + ptr: Long, + name: String, + embedding: FloatArray, + threshold: Float + ): Boolean + + private external fun contains(ptr: Long, name: String): Boolean + private external fun numSpeakers(ptr: Long): Int + + private external fun allSpeakerNames(ptr: Long): Array + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} + +// Please download the model file from +// https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models +// and put it inside the assets directory. +// +// Please don't put it in a subdirectory of assets +private val modelName = "3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx" + +object SpeakerRecognition { + var _extractor: SpeakerEmbeddingExtractor? = null + var _manager: SpeakerEmbeddingManager? = null + + val extractor: SpeakerEmbeddingExtractor + get() { + return _extractor!! + } + + val manager: SpeakerEmbeddingManager + get() { + return _manager!! + } + + fun initExtractor(assetManager: AssetManager? = null) { + synchronized(this) { + if (_extractor != null) { + return + } + Log.i(TAG, "Initializing speaker embedding extractor") + + _extractor = SpeakerEmbeddingExtractor( + assetManager = assetManager, + config = SpeakerEmbeddingExtractorConfig( + model = modelName, + numThreads = 2, + debug = false, + provider = "cpu", + ) + ) + + _manager = SpeakerEmbeddingManager(dim = _extractor!!.dim()) + } + } +} diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Help.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Help.kt new file mode 100644 index 0000000000..cb2bdc1105 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Help.kt @@ -0,0 +1,29 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun HelpScreen() { + Box(modifier= Modifier.fillMaxSize()) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text("Please see http://github.com/k2-fsa/sherpa-onnx ") + Spacer(modifier = Modifier.height(16.dp)) + Text("https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models") + Spacer(modifier = Modifier.height(16.dp)) + Text("https://k2-fsa.github.io/sherpa/social-groups.html") + Spacer(modifier = Modifier.height(16.dp)) + Text("Everything is open-sourced!") + } + } +} diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Home.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Home.kt new file mode 100644 index 0000000000..ddaaa0e3a7 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Home.kt @@ -0,0 +1,228 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.screens + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat +import com.k2fsa.sherpa.onnx.SpeakerRecognition +import com.k2fsa.sherpa.onnx.speaker.identification.R +import com.k2fsa.sherpa.onnx.speaker.identification.TAG +import kotlin.concurrent.thread + +private var audioRecord: AudioRecord? = null +private var sampleList: MutableList? = null + +private val clearedResult = "-cleared-" +@Composable +fun HomeScreen() { + val activity = LocalContext.current as Activity + var threshold by remember { + mutableStateOf(0.5F) + } + + var detectedName by remember { + mutableStateOf(clearedResult) + } + + var isStarted by remember { mutableStateOf(false) } + val onRecordingButtonClick: () -> Unit = { + isStarted = !isStarted + + if (isStarted) { + if (ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "Recording is not allowed") + } else { + // recording is allowed + val audioSource = MediaRecorder.AudioSource.MIC + val channelConfig = AudioFormat.CHANNEL_IN_MONO + val audioFormat = AudioFormat.ENCODING_PCM_16BIT + val numBytes = + AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) + + audioRecord = AudioRecord( + audioSource, + sampleRateInHz, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + numBytes * 2 // a sample has two bytes as we are using 16-bit PCM + ) + + sampleList = null + detectedName = clearedResult + + // recording is started here + thread(true) { + Log.i(TAG, "processing samples") + + val interval = 0.1 // i.e., 100 ms + val bufferSize = (interval * sampleRateInHz).toInt() // in samples + val buffer = ShortArray(bufferSize) + audioRecord?.let { + it.startRecording() + + while (isStarted) { + val ret = audioRecord?.read(buffer, 0, buffer.size) + ret?.let { n -> + val samples = FloatArray(n) { buffer[it] / 32768.0f } + if (sampleList == null) { + sampleList = mutableListOf(samples) + } else { + sampleList?.add(samples) + } + } + } + } + + Log.i(TAG, "Home: Recording is stopped. ${sampleList?.count()}") + } + } + } else { + // recording is stopped here + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + + sampleList?.let { + val stream = SpeakerRecognition.extractor.createStream() + for (samples in it) { + stream.acceptWaveform(samples = samples, sampleRate = sampleRateInHz) + } + stream.inputFinished() + if (SpeakerRecognition.extractor.isReady(stream)) { + val embedding = SpeakerRecognition.extractor.compute(stream) + detectedName = SpeakerRecognition.manager.search( + embedding = embedding, + threshold = threshold, + ) + } + } + } + } + + val onThresholdChange = { newValue: Float -> + threshold = newValue + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HomeThresholdRow( + threshold = threshold, + onValueChange = onThresholdChange, + ) + HomeButtonRow( + isStarted = isStarted, + onRecordingButtonClick = onRecordingButtonClick, + onClearButtonClick = { + detectedName = clearedResult + }, + ) + + Spacer(modifier = Modifier.height(48.dp)) + + if(detectedName == clearedResult) { + // do nothing + } else if (detectedName.length > 0) { + Text( + text = "Speaker: ${detectedName}", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + ) + } else { + Text( + text = "Unknown speaker", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + ) + } + } + } +} + +@SuppressLint("UnrememberedMutableState") +@Composable +private fun HomeButtonRow( + modifier: Modifier = Modifier, + isStarted: Boolean, + onRecordingButtonClick: () -> Unit, + onClearButtonClick: () -> Unit, +) { + val numSpeakers: Int by mutableStateOf(SpeakerRecognition.manager.numSpeakers()) + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Button( + enabled = numSpeakers > 0, + onClick = onRecordingButtonClick + ) { + Text(text = stringResource(if (isStarted) R.string.stop else R.string.start)) + } + + Spacer(modifier = Modifier.width(24.dp)) + + Button(onClick = onClearButtonClick) { + Text(text = stringResource(id = R.string.clear)) + } + } +} + +@Composable +fun HomeThresholdRow( + modifier: Modifier = Modifier, + threshold: Float, + onValueChange: (Float) -> Unit, +) { + Column(modifier = Modifier) { + Text( + text = "Threshold: " + String.format("%.2f", threshold), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = modifier.padding(bottom = 8.dp, top = 8.dp), + ) + Slider( + value = threshold, + onValueChange = onValueChange, + valueRange = 0.1F..1.0F, + modifier = modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Register.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Register.kt new file mode 100644 index 0000000000..7ac895d173 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/Register.kt @@ -0,0 +1,254 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.screens + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat +import com.k2fsa.sherpa.onnx.SpeakerRecognition +import com.k2fsa.sherpa.onnx.speaker.identification.R +import com.k2fsa.sherpa.onnx.speaker.identification.TAG +import kotlin.concurrent.thread + +private var audioRecord: AudioRecord? = null + +private var sampleList: MutableList? = null + +private var embeddingList: MutableList? = null + +val sampleRateInHz = 16000 + +@SuppressLint("UnrememberedMutableState") +@Preview +@Composable +fun RegisterScreen(modifier: Modifier = Modifier) { + val activity = LocalContext.current as Activity + + var firstTime by remember { mutableStateOf(true) } + if (firstTime) { + firstTime = false + // clear states + embeddingList = null + } + + val numberAudio: Int by mutableStateOf(embeddingList?.count() ?: 0) + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + var speakerName by remember { mutableStateOf("") } + val onSpeakerNameChange = { newName: String -> speakerName = newName } + + var isStarted by remember { mutableStateOf(false) } + val onRecordingButtonClick: () -> Unit = { + isStarted = !isStarted + + if (isStarted) { + if (ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "Recording is not allowed") + } else { + // recording is allowed + val audioSource = MediaRecorder.AudioSource.MIC + val channelConfig = AudioFormat.CHANNEL_IN_MONO + val audioFormat = AudioFormat.ENCODING_PCM_16BIT + val numBytes = + AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) + + audioRecord = AudioRecord( + audioSource, + sampleRateInHz, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + numBytes * 2 // a sample has two bytes as we are using 16-bit PCM + ) + + sampleList = null + + // recording is started here + thread(true) { + Log.i(TAG, "processing samples") + + val interval = 0.1 // i.e., 100 ms + val bufferSize = (interval * sampleRateInHz).toInt() // in samples + val buffer = ShortArray(bufferSize) + audioRecord?.let { + it.startRecording() + + while (isStarted) { + val ret = audioRecord?.read(buffer, 0, buffer.size) + ret?.let { n -> + val samples = FloatArray(n) { buffer[it] / 32768.0f } + if (sampleList == null) { + sampleList = mutableListOf(samples) + } else { + sampleList?.add(samples) + } + } + } + } + + Log.i(TAG, "Recording is stopped. ${sampleList?.count()}") + + } + } + } else { + // recording is stopped here + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + + sampleList?.let { + val stream = SpeakerRecognition.extractor.createStream() + for (samples in it) { + stream.acceptWaveform(samples=samples, sampleRate=sampleRateInHz) + } + stream.inputFinished() + if(SpeakerRecognition.extractor.isReady(stream)) { + val embedding = SpeakerRecognition.extractor.compute(stream) + if(embeddingList == null) { + embeddingList = mutableListOf(embedding) + } else { + embeddingList?.add(embedding) + } + } + } + } + } + + val onAddButtonClick: () -> Unit = { + if(speakerName.isEmpty() || speakerName.isBlank()) { + Toast.makeText( + activity, + "please input a speaker name", + Toast.LENGTH_SHORT + ).show() + } else if(SpeakerRecognition.manager.contains(speakerName.trim())) { + Toast.makeText( + activity, + "A speaker with $speakerName already exists. Please choose a new name", + Toast.LENGTH_SHORT + ).show() + } else { + val ok = SpeakerRecognition.manager.add(speakerName.trim(), embedding = embeddingList!!.toTypedArray()) + if(ok) { + Log.i(TAG, "Added ${speakerName.trim()} successfully") + Toast.makeText( + activity, + "Added ${speakerName.trim()}", + Toast.LENGTH_SHORT + ).show() + + embeddingList = null + sampleList = null + speakerName = "" + firstTime = true + } else { + Log.i(TAG, "Failed to add ${speakerName.trim()}") + Toast.makeText( + activity, + "Failed to add ${speakerName.trim()}", + Toast.LENGTH_SHORT + ).show() + } + } + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SpeakerNameRow(speakerName = speakerName, onValueChange = onSpeakerNameChange) + Text( + "Number of recordings: ${numberAudio}", + modifier = modifier.padding(24.dp), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + RegisterSpeakerButtonRow( + modifier, + isStarted = isStarted, + onRecordingButtonClick = onRecordingButtonClick, + onAddButtonClick = onAddButtonClick, + ) + } + } +} + +@Composable +fun SpeakerNameRow( + modifier: Modifier = Modifier, + speakerName: String, + onValueChange: (String) -> Unit +) { + OutlinedTextField( + value = speakerName, + onValueChange = onValueChange, + label = { + Text("Please input the speaker name") + }, + singleLine = true, + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + ) +} + +@SuppressLint("UnrememberedMutableState") +@Composable +fun RegisterSpeakerButtonRow( + modifier: Modifier = Modifier, + isStarted: Boolean, + onRecordingButtonClick: () -> Unit, + onAddButtonClick: () -> Unit, +) { + val numberAudio: Int by mutableStateOf(embeddingList?.count() ?: 0) + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Button(onClick = onRecordingButtonClick) { + Text(text = stringResource(if (isStarted) R.string.stop else R.string.start)) + } + + Spacer(modifier = Modifier.width(24.dp)) + + Button( + enabled = numberAudio > 0, + onClick = onAddButtonClick, + ) { + Text(text = stringResource(id = R.string.add)) + } + } +} diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/View.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/View.kt new file mode 100644 index 0000000000..3d70ce20ed --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/screens/View.kt @@ -0,0 +1,113 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.screens + +import android.annotation.SuppressLint +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.k2fsa.sherpa.onnx.SpeakerRecognition + +class SpeakerName(val name: String) { + val nameState = mutableStateOf(name) + val checked = mutableStateOf(false) + + fun onCheckedChange(newValue: Boolean) { + checked.value = newValue + } +} + +@SuppressLint("UnrememberedMutableState") +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ViewScreen() { + val allSpeakerNames = SpeakerRecognition.manager.allSpeakerNames() + val allSpeakerNameList = remember { + MutableList( + allSpeakerNames.size + ) { + SpeakerName(allSpeakerNames[it]) + }.toMutableStateList() + } + + var enabled by remember { + mutableStateOf(SpeakerRecognition.manager.numSpeakers() > 0) + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button( + enabled = enabled, + onClick = { + val toRemove: MutableList = mutableListOf() + for (s in allSpeakerNameList) { + if (s.checked.value) { + SpeakerRecognition.manager.remove(s.name) + toRemove.add(s) + } + } + allSpeakerNameList.removeAll(toRemove) + enabled = SpeakerRecognition.manager.numSpeakers() > 0 + }) { + Text("Delete selected") + } + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(allSpeakerNameList) { s: SpeakerName -> + ViewRow(speakerName = s) + } + } + } + } +} + +@Composable +fun ViewRow( + modifier: Modifier = Modifier, + speakerName: SpeakerName +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + color = MaterialTheme.colorScheme.inversePrimary, + ) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = speakerName.name, + modifier = modifier.weight(1.0F), + ) + Checkbox(checked = speakerName.checked.value, + onCheckedChange = { speakerName.onCheckedChange(it) } + ) + } + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Color.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Color.kt new file mode 100644 index 0000000000..29c357b2cc --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.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) \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Theme.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Theme.kt new file mode 100644 index 0000000000..62d9438d75 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.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.WindowCompat + +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 SherpaOnnxSpeakerIdentificationTheme( + 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 { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Type.kt b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Type.kt new file mode 100644 index 0000000000..1d2bd1d976 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/speaker/identification/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.k2fsa.sherpa.onnx.speaker.identification.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 + ) + */ +) \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/arm64-v8a/.gitkeep b/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/arm64-v8a/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/armeabi-v7a/.gitkeep b/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/armeabi-v7a/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/x86/.gitkeep b/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/x86/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/x86_64/.gitkeep b/android/SherpaOnnxSpeakerIdentification/app/src/main/jniLibs/x86_64/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..2b068d1146 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable/ic_launcher_background.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..07d5da9cbf --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000..c209e78ecd Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..b2dfe3d1ba Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000..4f0f1d64e5 Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..62b611da08 Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000..948a3070fe Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..1b9a6956b3 Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..28d4b77f9f Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9287f50836 Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..aa7d6427e6 Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9126ae37cb Binary files /dev/null and b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/colors.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..f8c6127d32 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/strings.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..0766efd7d1 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + Speaker Identification + Start recording + Stop recording + Add speaker + Clear result + \ No newline at end of file diff --git a/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/themes.xml b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..b4482787f1 --- /dev/null +++ b/android/SherpaOnnxSpeakerIdentification/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +