Android: fix upload lifecycle and JNI UTF-8 abort (M3a hardening)
- upload ran as a detached coroutine while cleanup() stopped the
service, and onDestroy's scope.cancel() killed it before any status
appeared; the upload is now part of finishRecording itself and the
service stays foreground ("Saving recording…") until it completes
- JNI getTextSegment used NewStringUTF, which ABORTS THE PROCESS on
invalid UTF-8 — whisper can emit garbled bytes on noisy audio (seen
live: SIGABRT with illegal start byte 0x8d); whisper_jni.cpp now
decodes UTF-8 to UTF-16 itself, replacing bad sequences with U+FFFD
- validated on device end-to-end: record -> local final pass -> automatic
upload -> library entry with all four files (verified server-side)
This commit is contained in:
@@ -266,7 +266,12 @@ class RecorderService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Final pass + outputs, after the WAV is safely closed. */
|
||||
/**
|
||||
* Final pass + upload + outputs, after the WAV is safely closed.
|
||||
* The service stays foreground ("Saving recording…") until the
|
||||
* upload is done — stopping earlier would cancel the upload
|
||||
* coroutine via onDestroy's scope.cancel().
|
||||
*/
|
||||
private suspend fun finishRecording(file: File, durationMs: Long, sampleRate: Int) {
|
||||
// let the live loop drain its current window first
|
||||
liveJob?.cancel()
|
||||
@@ -276,54 +281,43 @@ class RecorderService : Service() {
|
||||
val cfg = session
|
||||
val transcriber: Transcriber? =
|
||||
cfg?.serverUrl?.let { RemoteWhisperEngine(it) } ?: cfg?.finalEngine
|
||||
|
||||
var outputs: List<File> = emptyList()
|
||||
if (transcriber == null) {
|
||||
_state.value = RecordingState.Finished(file, durationMs, sampleRate)
|
||||
maybeUpload(file, emptyList(), durationMs, cfg)
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
_state.value = RecordingState.Finalizing(file, durationMs)
|
||||
updateNotification("Transcribing meeting…")
|
||||
try {
|
||||
val t0 = SystemClock.elapsedRealtime()
|
||||
val samples = withContext(Dispatchers.IO) {
|
||||
file.inputStream().use { WavReader.read(it) }
|
||||
} else {
|
||||
_state.value = RecordingState.Finalizing(file, durationMs)
|
||||
updateNotification("Transcribing meeting…")
|
||||
try {
|
||||
val t0 = SystemClock.elapsedRealtime()
|
||||
val samples = withContext(Dispatchers.IO) {
|
||||
file.inputStream().use { WavReader.read(it) }
|
||||
}
|
||||
val segments = transcriber.transcribe(
|
||||
samples,
|
||||
WhisperEngine.DEFAULT_THREADS,
|
||||
beamSize = 5,
|
||||
language = cfg?.language,
|
||||
)
|
||||
Log.i(TAG, "final: ${segments.size} segments in " +
|
||||
"${SystemClock.elapsedRealtime() - t0} ms")
|
||||
outputs = TranscriptFiles.writeAll(file, durationMs, segments)
|
||||
_state.value = RecordingState.Finished(file, durationMs, sampleRate, segments, outputs)
|
||||
} catch (e: Exception) {
|
||||
_state.value = RecordingState.Finished(
|
||||
file, durationMs, sampleRate, error = e.message,
|
||||
)
|
||||
}
|
||||
val segments = transcriber.transcribe(
|
||||
samples,
|
||||
WhisperEngine.DEFAULT_THREADS,
|
||||
beamSize = 5,
|
||||
language = cfg?.language,
|
||||
)
|
||||
Log.i(TAG, "final: ${segments.size} segments in " +
|
||||
"${SystemClock.elapsedRealtime() - t0} ms")
|
||||
val outputs = TranscriptFiles.writeAll(file, durationMs, segments)
|
||||
_state.value = RecordingState.Finished(file, durationMs, sampleRate, segments, outputs)
|
||||
maybeUpload(file, outputs, durationMs, cfg)
|
||||
} catch (e: Exception) {
|
||||
_state.value = RecordingState.Finished(
|
||||
file, durationMs, sampleRate, error = e.message,
|
||||
)
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
/** Uploads the bundle to the recording library in the background. */
|
||||
private fun maybeUpload(
|
||||
file: File,
|
||||
outputs: List<File>,
|
||||
durationMs: Long,
|
||||
cfg: SessionConfig?,
|
||||
) {
|
||||
val url = cfg?.storageUrl?.trim()
|
||||
if (url.isNullOrBlank()) return
|
||||
scope.launch {
|
||||
val storageUrl = cfg?.storageUrl?.trim()
|
||||
if (!storageUrl.isNullOrBlank()) {
|
||||
updateNotification("Saving recording…")
|
||||
_uploadState.value = UploadState.Uploading(file)
|
||||
try {
|
||||
val id = withContext(Dispatchers.IO) {
|
||||
StorageClient.upload(
|
||||
baseUrl = url,
|
||||
baseUrl = storageUrl,
|
||||
wav = file,
|
||||
txt = outputs.firstOrNull { it.name.endsWith(".txt") },
|
||||
srt = outputs.firstOrNull { it.name.endsWith(".srt") },
|
||||
@@ -342,6 +336,7 @@ class RecorderService : Service() {
|
||||
_uploadState.value = UploadState.Error(e.message ?: "upload failed")
|
||||
}
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
|
||||
Reference in New Issue
Block a user