Recently Written · git

subrep-android

Subrep on Android: live captions of the sound of the phone, made on the phone by Whisper, shared by link.

git clone https://github.com/equwal/subrep-android

Log | Files | Refs


app/src/main/kotlin/com/honjimaku/subrep/Models.kt (3127 bytes)

1 package com.honjimaku.subrep
2 
3 import android.content.Context
4 import okhttp3.OkHttpClient
5 import okhttp3.Request
6 import java.io.File
7 
8 /**
9  * The speech models. Each is a whisper.cpp file from the ggerganov/whisper.cpp repository on
10  * Hugging Face, in 8-bit, which is what a phone runs fastest.
11  */
12 enum class Model(val id: String, val label: String, val about: String) {
13     TINY("tiny-q8_0", "tiny", "43 MB. The fastest. Rough words."),
14     BASE("base-q8_0", "base", "82 MB. Keeps up on most phones. Fair words."),
15     SMALL("small-q8_0", "small", "264 MB. Good words. Slow on a phone: the captions come late.");
16 
17     val fileName: String get() = "ggml-$id.bin"
18     val url: String get() = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/$fileName"
19 
20     fun file(context: Context): File = File(File(context.filesDir, "models"), fileName)
21 
22     fun downloaded(context: Context): Boolean = file(context).isFile
23 
24     companion object {
25         fun byId(id: String): Model = entries.firstOrNull { it.id == id } ?: BASE
26     }
27 }
28 
29 /** Downloads a model to its file. Runs on the thread of the caller. */
30 class Downloader(private val http: OkHttpClient) {
31 
32     /**
33      * Downloads [model] into its file under [context]. [onProgress] gets the bytes so far and
34      * the total (-1 when unknown). Returns null when done, else the reason it failed.
35      */
36     fun download(context: Context, model: Model, onProgress: (Long, Long) -> Unit): String? {
37         val target = model.file(context)
38         target.parentFile?.mkdirs()
39         // The whole file goes to a temporary name first: a half file must never pass as a model.
40         val temp = File(target.path + ".part")
41         try {
42             http.newCall(Request.Builder().url(model.url).build()).execute().use { response ->
43                 if (!response.isSuccessful) return "HTTP ${response.code}"
44                 val body = response.body ?: return "empty answer"
45                 val total = body.contentLength()
46                 var done = 0L
47                 var lastReport = 0L
48                 body.byteStream().use { input ->
49                     temp.outputStream().use { output ->
50                         val buffer = ByteArray(64 * 1024)
51                         while (true) {
52                             val n = input.read(buffer)
53                             if (n < 0) break
54                             output.write(buffer, 0, n)
55                             done += n
56                             if (done - lastReport >= 1024 * 1024) {
57                                 lastReport = done
58                                 onProgress(done, total)
59                             }
60                         }
61                     }
62                 }
63                 if (total > 0 && done != total) return "the download stopped at $done of $total bytes"
64                 if (!temp.renameTo(target)) return "cannot write ${target.name}"
65                 onProgress(done, total)
66                 return null
67             }
68         } catch (e: Exception) {
69             return "${e.javaClass.simpleName}: ${e.message}"
70         } finally {
71             temp.delete()
72         }
73     }
74 }