├── settings.gradle ├── app ├── proguard.cfg ├── src │ └── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── styles.xml │ │ │ ├── bools.xml │ │ │ ├── keys.xml │ │ │ ├── defaults.xml │ │ │ └── strings.xml │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── xml │ │ │ ├── recognition_service_ws.xml │ │ │ ├── locales_config.xml │ │ │ └── preferences_server_ws.xml │ │ ├── layout │ │ │ ├── list_item_server_ip.xml │ │ │ ├── about.xml │ │ │ └── recognition_service_ws_url.xml │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ ├── drawable │ │ │ ├── ic_menu_info.xml │ │ │ ├── ic_menu_help.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── menu │ │ │ └── preferences_header.xml │ │ ├── values-et │ │ │ └── strings.xml │ │ └── layout-watch │ │ │ └── recognition_service_ws_url.xml │ │ ├── java │ │ └── ee │ │ │ └── ioc │ │ │ └── phon │ │ │ └── android │ │ │ └── k6neleservice │ │ │ ├── activity │ │ │ ├── AboutActivity.kt │ │ │ ├── PreferencesRecognitionServiceWs.java │ │ │ └── RecognitionServiceWsUrlActivity.java │ │ │ ├── Log.kt │ │ │ ├── utils │ │ │ ├── Utils.kt │ │ │ └── QueryUtils.java │ │ │ ├── receiver │ │ │ └── GetLanguageDetailsReceiver.kt │ │ │ ├── Caller.java │ │ │ ├── service │ │ │ ├── WebSocketResponse.java │ │ │ └── WebSocketRecognitionService.java │ │ │ └── ChunkedWebRecSessionBuilder.java │ │ └── AndroidManifest.xml └── build.gradle ├── .gitmodules ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── docs └── server │ ├── start.sh │ ├── pip_freeze.txt │ └── README.md ├── .github └── workflows │ └── android.yml ├── privacy_policy.rst ├── README.md ├── gradlew └── LICENSE.txt /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':speechutils:app' 2 | -------------------------------------------------------------------------------- /app/proguard.cfg: -------------------------------------------------------------------------------- 1 | -printconfiguration r8-config.txt 2 | -printusage usage.txt 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "speechutils"] 2 | path = speechutils 3 | url = git@github.com:Kaljurand/speechutils.git 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffa000 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaljurand/K6nele-service/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFC400 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.defaults.buildfeatures.buildconfig=true 2 | android.enableJetifier=true 3 | android.nonFinalResIds=false 4 | android.nonTransitiveRClass=false 5 | android.useAndroidX=true 6 | org.gradle.jvmargs=-Xmx2560m 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/recognition_service_ws.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 16 20:59:58 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_server_ip.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 8 | false 9 | 10 | -------------------------------------------------------------------------------- /docs/server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ~/myapps/whisper-fastapi/.venv/bin/activate 3 | export LD_LIBRARY_PATH=`python3 -c 'import os; import nvidia.cublas.lib; import nvidia.cudnn.lib; print(os.path.dirname(nvidia.cublas.lib.__file__) + ":" + os.path.dirname(nvidia.cudnn.lib.__file__))'` 4 | whisper=~/myapps/whisper-fastapi/whisper_fastapi.py 5 | python $whisper --host 0.0.0.0 --port 3001 --model whisper-medium-et.ct2 6 | #python $whisper --host 0.0.0.0 --port 3001 --model large-v3 & 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/keys.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | keyAppInfo 4 | keyWsServer 5 | keyLocales 6 | keyWsAudioCues 7 | keyWsAudioFormat 8 | keyWsAutoStopAfterPause 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_help.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/preferences_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | 5 | push: 6 | branches: [ master ] 7 | 8 | pull_request: 9 | branches: [ master ] 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | 20 | - name: Checkout repository and submodules 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: recursive 24 | 25 | - name: Set up Java 17 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | 31 | - name: Build with Gradle 32 | run: ./gradlew build 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | ws://bark.phon.ioc.ee:82/dev/duplex-speech-api/ws/speech 10 | ws://bark.phon.ioc.ee:82/dev/duplex-speech-api/ws/speech 11 | wss://bark.phon.ioc.ee:8443/dev/duplex-speech-api/ws/speech 12 | 13 | et-EE 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/activity/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package ee.ioc.phon.android.k6neleservice.activity 2 | 3 | import android.os.Bundle 4 | import android.text.Html 5 | import android.text.method.LinkMovementMethod 6 | import androidx.appcompat.app.AppCompatActivity 7 | import ee.ioc.phon.android.k6neleservice.R 8 | import ee.ioc.phon.android.k6neleservice.databinding.AboutBinding 9 | import ee.ioc.phon.android.k6neleservice.utils.Utils 10 | 11 | class AboutActivity : AppCompatActivity() { 12 | public override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val binding = AboutBinding.inflate(layoutInflater) 15 | setContentView(binding.root) 16 | val ab = supportActionBar 17 | if (ab != null) { 18 | ab.setTitle(R.string.labelApp) 19 | ab.subtitle = "v" + Utils.getVersionName(this) 20 | } 21 | binding.tvAbout.movementMethod = LinkMovementMethod.getInstance() 22 | val about = String.format( 23 | getString(R.string.tvAbout), 24 | getString(R.string.labelApp) 25 | ) 26 | binding.tvAbout.text = Html.fromHtml(about) 27 | } 28 | } -------------------------------------------------------------------------------- /docs/server/pip_freeze.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.6.2.post1 3 | av==13.1.0 4 | certifi==2024.8.30 5 | cffi==1.17.1 6 | charset-normalizer==3.4.0 7 | click==8.1.7 8 | coloredlogs==15.0.1 9 | ctranslate2==4.5.0 10 | fastapi==0.115.5 11 | faster-whisper @ git+https://github.com/heimoshuiyu/faster-whisper@a759f5f48f5ef5b79461a6461966eafe9df088a9 12 | filelock==3.16.1 13 | flatbuffers==24.3.25 14 | fsspec==2024.10.0 15 | h11==0.14.0 16 | httptools==0.6.4 17 | huggingface-hub==0.26.3 18 | humanfriendly==10.0 19 | idna==3.10 20 | mpmath==1.3.0 21 | numpy==2.1.3 22 | nvidia-cublas-cu12==12.6.4.1 23 | nvidia-cudnn-cu12==9.5.1.17 24 | onnxruntime==1.20.1 25 | opencc==1.1.9 26 | packaging==24.2 27 | prometheus-client==0.21.0 28 | prometheus-fastapi-instrumentator==7.0.0 29 | protobuf==5.29.0 30 | pycparser==2.22 31 | pydantic==2.10.2 32 | pydantic-core==2.27.1 33 | pydub==0.25.1 34 | python-dotenv==1.0.1 35 | python-multipart==0.0.18 36 | pyyaml==6.0.2 37 | requests==2.32.3 38 | setuptools==75.6.0 39 | sniffio==1.3.1 40 | sounddevice==0.5.1 41 | starlette==0.41.3 42 | sympy==1.13.3 43 | tokenizers==0.21.0 44 | tqdm==4.67.1 45 | typing-extensions==4.12.2 46 | urllib3==2.2.3 47 | uvicorn==0.32.1 48 | uvloop==0.21.0 49 | watchfiles==1.0.0 50 | websockets==14.1 51 | whisper-ctranslate2==0.4.9 52 | -------------------------------------------------------------------------------- /privacy_policy.rst: -------------------------------------------------------------------------------- 1 | Privacy policy 2 | ============== 3 | 4 | Kõnele service performs the recording and transcribing of audio. The start of the 5 | recording and transcribing depends on the caller of the service (e.g. when a 6 | microphone button is pressed in its user interface). 7 | 8 | The service itself can stop the recording after a longer pause in the input audio (depending on the 9 | settings). 10 | The service can be configured to signal the beginning and end of the recording with audio cues. 11 | 12 | For the recording, Kõnele service requires the microphone permission. 13 | For the transcribing, Kõnele service requires network access to a speech recognition server. The network connection is not encrypted. The connection is established when the recording is started and closed when the recording is stopped and the final transcription has been received. 14 | 15 | Kõnele service has a single corresponding recognition server, whose web address is visible and changeable in the Kõnele service settings. The server runs independently of Kõnele service and is covered by its own privacy policy. The privacy policy of the default server is available at http://phon.ioc.ee. The default server is based on free and open source software (available at https://github.com/alumae/kaldi-gstreamer-server) allowing the user to install it in a local private network. 16 | 17 | Apart from using a third-party speech recognition server as described above, Kõnele service does not collect nor share any user data. The source code of all Kõnele service components and their dependencies is open and available at https://github.com/Kaljurand/K6nele-service. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kõnele service 2 | ============== 3 | 4 | Kõnele service is an Android app that offers a speech-to-text service to other apps, in particular to Kõnele (). 5 | It implements Android's [SpeechRecognizer](http://developer.android.com/reference/android/speech/SpeechRecognizer.html) interface to 6 | an open source speech recognition server software . 7 | 8 | [Get it on Google Play](https://play.google.com/store/apps/details?id=ee.ioc.phon.android.k6neleservice) 11 | 12 | Building the APK from source 13 | ---------------------------- 14 | 15 | Clone the source code including the `speechutils` submodule: 16 | 17 | git clone --recursive git@github.com:Kaljurand/K6nele-service.git 18 | 19 | 20 | Point to the Android SDK directory by setting the environment variable 21 | `ANDROID_HOME`, e.g. 22 | 23 | ANDROID_HOME=${HOME}/myapps/android-sdk/ 24 | 25 | In order to change your build environment create the file `gradle.properties` 26 | at a location pointed to by the environment variable `GRADLE_USER_HOME`. 27 | This will extend and override the definitions found in the `gradle.properties` 28 | that is part of the release. 29 | 30 | Build the app 31 | 32 | ./gradlew assemble 33 | 34 | If you have access to a release keystore then add these lines to the extended `gradle.properties`: 35 | 36 | storeFilename= 37 | storePassword= 38 | keyAlias= 39 | keyPassword= 40 | 41 | The (signed and unsigned) APKs will be generated into `app/build/outputs/apk/`. 42 | -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/Log.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014, Institute of Cybernetics at Tallinn University of Technology 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ee.ioc.phon.android.k6neleservice 17 | 18 | import android.util.Log 19 | 20 | object Log { 21 | @JvmField 22 | val DEBUG = BuildConfig.DEBUG 23 | private const val LOG_TAG = "k6nele-service" 24 | private const val NULL = "NULL" 25 | 26 | @JvmStatic 27 | fun i(msg: String?) { 28 | if (DEBUG) Log.i(LOG_TAG, msg ?: NULL) 29 | } 30 | 31 | @JvmStatic 32 | fun i(msgs: List) { 33 | if (DEBUG) { 34 | for (msg in msgs) { 35 | Log.i(LOG_TAG, msg ?: NULL) 36 | } 37 | } 38 | } 39 | 40 | @JvmStatic 41 | fun e(msg: String?) { 42 | if (DEBUG) Log.e(LOG_TAG, msg ?: NULL) 43 | } 44 | 45 | @JvmStatic 46 | fun e(msg: String?, throwable: Throwable?) { 47 | if (DEBUG) Log.e(LOG_TAG, msg ?: NULL, throwable) 48 | } 49 | 50 | @JvmStatic 51 | fun i(tag: String?, msg: String?) { 52 | if (DEBUG) Log.i(tag, msg ?: NULL) 53 | } 54 | 55 | @JvmStatic 56 | fun e(tag: String?, msg: String?) { 57 | if (DEBUG) Log.e(tag, msg ?: NULL) 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2021, Institute of Cybernetics at Tallinn University of Technology 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ee.ioc.phon.android.k6neleservice.utils 17 | 18 | import android.content.Context 19 | import android.content.pm.PackageInfo 20 | import android.content.pm.PackageManager 21 | import android.os.Build 22 | import ee.ioc.phon.android.k6neleservice.Log.e 23 | 24 | /** 25 | * Some useful static methods. 26 | * 27 | * @author Kaarel Kaljurand 28 | */ 29 | object Utils { 30 | @JvmStatic 31 | fun getVersionName(c: Context): String { 32 | val info = getPackageInfo(c) ?: return "?.?.?" 33 | val versionName = info.versionName ?: return "?.?.?" 34 | return versionName 35 | } 36 | 37 | private fun getPackageInfo(c: Context): PackageInfo? { 38 | val manager = c.packageManager 39 | try { 40 | return manager.getPackageInfo(c.packageName, 0) 41 | } catch (e: PackageManager.NameNotFoundException) { 42 | e("Couldn't find package information in PackageManager: $e") 43 | } 44 | return null 45 | } 46 | 47 | @JvmStatic 48 | fun makeUserAgentComment(tag: String, versionName: String, caller: String): String { 49 | return "${tag}/${versionName}; ${Build.MANUFACTURER}/${Build.DEVICE}/${Build.DISPLAY}; $caller" 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/receiver/GetLanguageDetailsReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2023, Institute of Cybernetics at Tallinn University of Technology 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ee.ioc.phon.android.k6neleservice.receiver 17 | 18 | import android.content.BroadcastReceiver 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.os.Bundle 22 | import android.speech.RecognizerIntent 23 | import androidx.preference.PreferenceManager 24 | import ee.ioc.phon.android.k6neleservice.R 25 | import ee.ioc.phon.android.speechutils.utils.PreferenceUtils 26 | 27 | class GetLanguageDetailsReceiver : BroadcastReceiver() { 28 | override fun onReceive(context: Context, intent: Intent) { 29 | // TODO: not sure how we are supposed to behave in the case, where 30 | // another recognizer has already responded to the broadcast and filled 31 | // in its values. 32 | // val resultExtras = getResultExtras(true) 33 | // if (!resultExtras.isEmpty()) { ... } 34 | val prefs = PreferenceManager.getDefaultSharedPreferences(context) 35 | val localesAsStr = PreferenceUtils.getPrefString( 36 | prefs, 37 | context.resources, 38 | R.string.keyLocales, 39 | R.string.defaultLocales 40 | ); 41 | 42 | val langs = localesAsStr.split(",").map { it.trim() }.filter { it.isNotEmpty() } 43 | if (langs.isNotEmpty()) { 44 | val extras = Bundle() 45 | extras.putString(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, langs[0]) 46 | extras.putStringArrayList(RecognizerIntent.EXTRA_SUPPORTED_LANGUAGES, ArrayList(langs)) 47 | setResultExtras(extras) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences_server_ws.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 12 | 13 | 14 | 19 | 22 | 23 | 24 | 32 | 33 | 39 | 45 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values-et/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Süsteemsed seaded 5 | Lubade, salvestusruumi jms seadistamine 6 | 7 | Info 8 | Kõnele teenus 9 | 10 | Serveri toetatud lokaaled 11 | Komadega eraldatud nimekiri keeltest/regioonidest, mida server toetab, nt \"et-EE, vro-EE, en-US\" 12 | 13 | Autostopp pärast pausi 14 | Lõpeta lindistamine pärast paarisekundist pausi 15 | 16 | Helisignaalid 17 | Anna lindistamise algusest ja lõpust märku lühikese 18 | helisignaaliga 19 | Loobu 20 | Vaikeserver 1 21 | Vaikeserver 2 22 | 23 | Info 24 | Kasutusjuhend 25 | 26 | 27 | <p><b>%1$s</b> on kõnetuvastusteenus Androidi rakendustele, eelkõige <a href="https://kaljurand.github.io/K6nele/">Kõnele</a> rakendusele.</p> 28 | 29 | <p>Vaikimisi kasutab teenus TTÜ Küberneetika Instituudi foneetika ja kõnetehnoloogia labori eesti keele 30 | kõnetuvastusserverit, vt <a href="http://phon.ioc.ee">http://phon.ioc.ee</a>.</p> 31 | 32 | <p>Teenus väljastab tuvastustulemuse juba rääkimise ajal ja ei sea 33 | kõnesisendi pikkusele mingit piirangut.</p> 34 | 35 | <p>Põhjalikum 36 | dokumentatsioon ja lähtekood on 37 | <a 38 | href="https://github.com/Kaljurand/K6nele-service">%1$s veebilehel</a>. 39 | Kasutajaandmete kaitse kohta lugege <a href="https://github.com/Kaljurand/K6nele-service/blob/master/privacy_policy.rst">siit</a>.</p></p> 40 | 41 | VIGA: võrgu IP aadress pole määratud 42 | 43 | ⚠️ Hetkel puudub luba heli salvestada, mis on selle teenuse jaoks hädavajalik, 44 | palun andke see luba. 45 | 46 | VIGA: serveri aadressi valimine luhtus 47 | 48 | %d vaba slott 49 | %d vaba slotti 50 | 51 | Kaldi GStreamer serveri põhine teenus. Serveriaadressi on võimalik muuta. Vaikeserver toetab eesti keelt ja kiiret/täpset tuvastust. 52 | @string/labelRecognitionServiceWs 53 | Serveriaadress 54 | 55 | %1$s\n(Mitte-raw formaadi kasutamine vähendab võrguliiklust 2 korda. Kõikidel seadmetel ei pruugi toimida.) 56 | Audioformaat 57 | 58 | Rakenda 59 | Otsi 60 | Otsi (koht)võrgust servereid, et asendada teenuse aadress mõne leitud serveri aadressiga. 61 | 62 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | dependencies { 5 | implementation project(':speechutils:app') 6 | implementation 'com.koushikdutta.async:androidasync:3.1.0' 7 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24' 8 | implementation 'androidx.appcompat:appcompat:1.7.0' 9 | implementation 'androidx.preference:preference-ktx:1.2.1' 10 | implementation 'androidx.recyclerview:recyclerview:1.3.2' 11 | // implementation 'androidx.activity:activity-ktx:1.8.2' 12 | implementation 'com.google.android.material:material:1.12.0' 13 | } 14 | 15 | android { 16 | compileSdk rootProject.compileSdk 17 | 18 | // API level 7: MediaRecorder.AudioSource.VOICE_RECOGNITION 19 | // API level 8: android.speech.SpeechRecognizer and android.speech.RecognitionService 20 | // API level 14: @android:style/Theme.DeviceDefault 21 | defaultConfig { 22 | applicationId 'ee.ioc.phon.android.k6neleservice' 23 | minSdkVersion 23 24 | targetSdkVersion 35 25 | versionCode 210 26 | versionName '0.2.10' 27 | vectorDrawables.useSupportLibrary = true 28 | // Keep only en and et resources 29 | resourceConfigurations += ['en', 'et'] 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_11 34 | targetCompatibility JavaVersion.VERSION_11 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '11' 39 | } 40 | packagingOptions { 41 | resources { 42 | excludes += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'] 43 | } 44 | } 45 | 46 | 47 | signingConfigs { 48 | release {} 49 | } 50 | 51 | buildTypes { 52 | release { 53 | minifyEnabled true 54 | shrinkResources true 55 | proguardFile getDefaultProguardFile('proguard-android-optimize.txt') 56 | proguardFile 'proguard.cfg' 57 | signingConfig signingConfigs.release 58 | } 59 | } 60 | 61 | buildFeatures { 62 | viewBinding true 63 | } 64 | 65 | lint { 66 | // TODO: in the future check for Kotlin-Java interop 67 | //check 'Interoperability' 68 | disable 'ResourceType', 'AppLinkUrlError', 'EllipsizeMaxLines', 'RtlSymmetry', 'Autofill' 69 | } 70 | namespace 'ee.ioc.phon.android.k6neleservice' 71 | 72 | } 73 | 74 | 75 | if (project.hasProperty('storeFilename') && project.hasProperty('storePassword') && project.hasProperty('keyAlias') && project.hasProperty('keyPassword')) { 76 | android.signingConfigs.release.storeFile = file(storeFilename) 77 | android.signingConfigs.release.storePassword = storePassword 78 | android.signingConfigs.release.keyAlias = keyAlias 79 | android.signingConfigs.release.keyPassword = keyPassword 80 | } else { 81 | println "WARNING: The release will not be signed" 82 | android.buildTypes.release.signingConfig = null 83 | } 84 | 85 | 86 | tasks.register('deploy') { 87 | doLast { 88 | description 'Copy the APK and the ProGuard mapping file to the deploy directory' 89 | 90 | def deploy_dir = System.getenv('APK_DEPLOY_DIR') 91 | 92 | def version = android.defaultConfig.versionName 93 | 94 | def name = 'K6nele-service' 95 | 96 | def outputs = 'build/outputs/' 97 | def apk1 = outputs + 'apk/release/app-release.apk' 98 | def apk2 = "${deploy_dir}${name}-${version}.apk" 99 | def mapping1 = outputs + 'mapping/release/mapping.txt' 100 | def mapping2 = "${deploy_dir}${name}-mapping-${version}.txt" 101 | 102 | exec { 103 | commandLine 'cp', '--verbose', apk1, apk2 104 | } 105 | 106 | exec { 107 | commandLine 'cp', '--verbose', mapping1, mapping2 108 | } 109 | 110 | exec { 111 | commandLine 'ls', '-l', deploy_dir 112 | } 113 | 114 | println "adb uninstall ${android.defaultConfig.applicationId}" 115 | println "adb install ${apk2}" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kõnele service 4 | speech-to-text service for Android apps 5 | 10.0.0.2 6 | 7 | %1$s 8 | https://github.com/Kaljurand/K6nele-service 9 | 10 | App info 11 | View and change permissions, clear cache, etc. 12 | 13 | About Kõnele service 14 | Kõnele service 15 | @string/labelRecognitionServiceWs 16 | 17 | Service based on the Kaldi GStreamer server. The server URL can be changed. The default server supports Estonian and fast/accurate recognition. 18 | 19 | Server locales 20 | Comma-separated list of locales supported by the server, e.g. \"et-EE, vro-EE, en-US\" 21 | Auto stop after pause 22 | Stop recording after a few seconds of pause 23 | 24 | 25 | Audio format 26 | %1$s\n(Using a non-raw format results in 2x less network traffic. Might not work on all devices.) 27 | Play audio cues 28 | Beep before and after recording 29 | Server URL 30 | 31 | 32 | %d slot available 33 | %d slots available 34 | 35 | %1$s 36 | 37 | Cancel 38 | Apply 39 | Scan 40 | Default 1 41 | Default 2 42 | 43 | About 44 | Help 45 | 46 | <p>%1$s is an app that offers a speech-to-text service to other apps, in particular to <a href="https://kaljurand.github.io/K6nele/">Kõnele</a>. 47 | <p>Service based on the Kaldi GStreamer server. The server URL can be changed. The default server supports Estonian and fast/accurate recognition. 48 | <p>For more information, see <a href="http://phon.ioc.ee">http://phon.ioc.ee</a>. 49 | <p>See also the <a href="https://github.com/Kaljurand/K6nele-service/blob/master/privacy_policy.rst">privacy policy</a>. 50 | 51 | ERROR: Failed to obtain the server URL 52 | 53 | ERROR: Network IP address undefined 54 | 55 | Scan the (local) network and click through the found servers to try to replace the service address. 56 | 57 | ⚠️ There is currently no microphone permission. Please grant this permission, because recording audio is essential for this service. 58 | 59 | audio/x-raw 60 | 61 | 62 | raw 63 | FLAC 64 | 65 | 66 | 67 | audio/x-raw 68 | audio/x-flac 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/utils/QueryUtils.java: -------------------------------------------------------------------------------- 1 | package ee.ioc.phon.android.k6neleservice.utils; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.util.Pair; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.URLEncoder; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import ee.ioc.phon.android.k6neleservice.ChunkedWebRecSessionBuilder; 13 | import ee.ioc.phon.android.k6neleservice.Log; 14 | import ee.ioc.phon.android.speechutils.Extras; 15 | 16 | public final class QueryUtils { 17 | private static final String QUERY_STRING_MARKER = "?"; 18 | private static final String PARAMETER_SEPARATOR = "&"; 19 | private static final String NAME_VALUE_SEPARATOR = "="; 20 | 21 | private QueryUtils() { 22 | } 23 | 24 | public static String combine(String server, String part) { 25 | return server + (server.indexOf(QUERY_STRING_MARKER) > 0 ? PARAMETER_SEPARATOR : QUERY_STRING_MARKER) + part; 26 | } 27 | 28 | /** 29 | * Extracts the editor info, and uses 30 | * ChunkedWebRecSessionBuilder to extract some additional extras. 31 | * TODO: unify this better 32 | */ 33 | public static List> getQueryParams(Intent intent, ChunkedWebRecSessionBuilder builder) { 34 | if (Log.DEBUG) Log.i(builder.toStringArrayList()); 35 | List> list = new ArrayList<>(); 36 | flattenBundle("editorInfo_", list, intent.getBundleExtra(Extras.EXTRA_EDITOR_INFO)); 37 | listAdd(list, "lang", builder.getLang()); 38 | listAdd(list, "lm", toString(builder.getGrammarUrl())); 39 | listAdd(list, "output-lang", builder.getGrammarTargetLang()); 40 | listAdd(list, "user-agent", builder.getUserAgentComment()); 41 | listAdd(list, "calling-package", builder.getCaller()); 42 | listAdd(list, "user-id", builder.getDeviceId()); 43 | listAdd(list, "partial", "" + builder.isPartialResults()); 44 | return list; 45 | } 46 | 47 | private static boolean listAdd(List> list, String key, String value) { 48 | if (value == null || value.length() == 0) { 49 | return false; 50 | } 51 | return list.add(new Pair<>(key, value)); 52 | } 53 | 54 | private static void flattenBundle(String prefix, List> list, Bundle bundle) { 55 | if (bundle != null) { 56 | for (String key : bundle.keySet()) { 57 | Object value = bundle.get(key); 58 | if (value != null) { 59 | if (value instanceof Bundle) { 60 | flattenBundle(prefix + key + "_", list, (Bundle) value); 61 | } else { 62 | listAdd(list, prefix + key, toString(value)); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | // TODO: replace by a built-in 70 | private static String toString(Object obj) { 71 | if (obj == null) { 72 | return null; 73 | } 74 | return obj.toString(); 75 | } 76 | 77 | /** 78 | * Returns a String that is suitable for use as an application/x-www-form-urlencoded 79 | * list of parameters in an HTTP PUT or HTTP POST. 80 | *

81 | * Modification of org.apache.http.client.utils.URLEncodedUtils#format 82 | * 83 | * @param parameters The parameters to include. 84 | * @param encoding The encoding to use. 85 | */ 86 | public static String encodeKeyValuePairs( 87 | final List> parameters, 88 | final String encoding) throws UnsupportedEncodingException { 89 | final StringBuilder result = new StringBuilder(); 90 | for (final Pair parameter : parameters) { 91 | final String encodedName = URLEncoder.encode(parameter.first, encoding); 92 | final String value = parameter.second; 93 | final String encodedValue = value != null ? URLEncoder.encode(value, encoding) : ""; 94 | if (result.length() > 0) 95 | result.append(PARAMETER_SEPARATOR); 96 | result.append(encodedName); 97 | result.append(NAME_VALUE_SEPARATOR); 98 | result.append(encodedValue); 99 | } 100 | return result.toString(); 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/ee/ioc/phon/android/k6neleservice/Caller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2013, Institute of Cybernetics at Tallinn University of Technology 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ee.ioc.phon.android.k6neleservice; 18 | 19 | import android.app.PendingIntent; 20 | import android.os.Bundle; 21 | import android.speech.RecognizerIntent; 22 | 23 | import ee.ioc.phon.android.speechutils.utils.BundleUtils; 24 | 25 | /** 26 | *

Description of the caller that receives the transcription. 27 | * If the extras specify a pending intent (I've never encountered such an app though), 28 | * then the pending intent's target package's name is returned.

29 | * 30 | *

Otherwise we use EXTRA_CALLING_PACKAGE because there does not seem to be a way to 31 | * find out which Activity called us, i.e. this does not work:

32 | * 33 | *
 34 |  * ComponentName callingActivity = getCallingActivity();
 35 |  * if (callingActivity != null) {
 36 |  *     return callingActivity.getPackageName();
 37 |  * }
 38 |  * 
39 | * 40 | *

The above tries to detect the "primary caller" (e.g. a keyboard app). We also 41 | * look for the "secondary caller" (e.g. an app in which the keyboard is used). 42 | * by parsing the extras looking for another package name, e.g. included in the 43 | * android.speech.extras.RECOGNITION_CONTEXT extra which some keyboard 44 | * apps set.

45 | * 46 | *

The caller description can be obtained in two ways. First a string in the form 47 | * "1st-caller/2nd-caller" which can be used as a User-Agent string. Examples:

48 | * 49 | *
    50 | *
  • VoiceIME/com.google.android.apps.plus (standard keyboard in Google Plus app)
  • 51 | *
  • SwypeIME/com.timsu.astrid
  • 52 | *
  • mobi.mgeek.TunnyBrowser/null
  • 53 | *
  • null/null (if no caller-identifying info was found in the extras)
  • 54 | *
55 | * 56 | *

Secondly, we try to determine which caller string is more informative so 57 | * that it can be used in the Apps-database, for counting, and server/grammar assignment. 58 | * There should be no difference in terms of grammar assigning if speech recognition 59 | * in the app is used via the keyboard or via a dedicated speech input button.

60 | */ 61 | public class Caller { 62 | 63 | private static final String KEY_PACKAGE_NAME = "packageName"; 64 | 65 | private final String mPrimaryCaller; 66 | private final String mSecondaryCaller; 67 | 68 | public Caller(PendingIntent pendingIntent, Bundle bundle) { 69 | if (pendingIntent == null) { 70 | mPrimaryCaller = bundle.getString(RecognizerIntent.EXTRA_CALLING_PACKAGE); 71 | } else { 72 | mPrimaryCaller = pendingIntent.getTargetPackage(); 73 | } 74 | mSecondaryCaller = getPackageName(bundle); 75 | } 76 | 77 | 78 | public String getActualCaller() { 79 | if (mSecondaryCaller == null) { 80 | if (mPrimaryCaller == null) { 81 | return "null"; 82 | } 83 | return mPrimaryCaller; 84 | } 85 | return mSecondaryCaller; 86 | } 87 | 88 | 89 | public String toString() { 90 | return mPrimaryCaller + "/" + mSecondaryCaller; 91 | } 92 | 93 | 94 | /** 95 | *

Traverses the given bundle (which can contain other bundles) 96 | * looking for the key "packageName". 97 | * Returns its corresponding value if finds it.

98 | * 99 | * @param bundle bundle (e.g. intent extras) 100 | * @return package name possibly hidden deep into the given bundle 101 | */ 102 | private static String getPackageName(Bundle bundle) { 103 | Object obj = BundleUtils.getBundleValue(bundle, KEY_PACKAGE_NAME); 104 | if (obj instanceof String) { 105 | return (String) obj; 106 | } 107 | return null; 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/recognition_service_ws_url.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 27 | 28 | 35 | 36 |