This commit is contained in:
1 parent
0bc0117c89
commit
2084f9083d
10 files changed
+1108
-355
No files matched your search
@@ -95,9 +95,12 @@ object ApiClient {
|
||||
* the shared 30s client, and a re-submitted POST would repeat expensive
|
||||
* provider work after the request may already have reached the server
|
||||
* (the repository also passes `retries = 0`). The returned service reuses
|
||||
* the shared connection pool and the auth/profile interceptor stack, and
|
||||
* applies [readTimeoutMs] to reads and writes with transport retries
|
||||
* disabled (review, PR #1250).
|
||||
* the shared connection pool and the auth/profile interceptor stack. It
|
||||
* applies [readTimeoutMs] to reads and writes and keeps transport-level
|
||||
* retries on: they re-dial and can resend depending on when a failure
|
||||
* struck, so they do not guarantee the absence of duplicate provider
|
||||
* work — the risk is small, not zero. Application-level retry stays off
|
||||
* (review, PR #1250 / #1280).
|
||||
*/
|
||||
fun transcriptionService(readTimeoutMs: Long): HermesApiService {
|
||||
val client =
|
||||
@@ -109,7 +112,16 @@ object ApiClient {
|
||||
.authenticator(TokenRefreshAuthenticator)
|
||||
.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
|
||||
.retryOnConnectionFailure(false)
|
||||
// Transport-level stale-socket rescue stays ON: the shared pool
|
||||
// keeps connections far longer than the dashboard's keep-alive,
|
||||
// so the first STT call after an idle spell can ride a socket
|
||||
// the server just closed ("unexpected end of stream"). OkHttp
|
||||
// re-dials and can resend depending on when the failure struck,
|
||||
// and it may retry across routes — duplicate provider work is
|
||||
// possible, though rare. Timeouts and 5xx are never replayed
|
||||
// (safeApiCall still runs with retries = 0). Risk small, not
|
||||
// zero (review, PR #1280).
|
||||
.retryOnConnectionFailure(true)
|
||||
.build()
|
||||
return Retrofit
|
||||
.Builder()
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.m57.hermescontrol.data.repository
|
||||
|
||||
import com.m57.hermescontrol.data.model.AudioTranscriptionRequest
|
||||
import com.m57.hermescontrol.data.model.AudioTranscriptionResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.HermesApiService
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
import java.util.Base64
|
||||
|
||||
/**
|
||||
@@ -33,6 +37,10 @@ const val VOICE_NOTE_MIME_TYPE = "audio/mp4"
|
||||
* - The STT POST runs on a dedicated long-timeout client with `retries = 0`:
|
||||
* re-submitting after a timeout or 5xx would repeat expensive provider
|
||||
* work although the request may already have reached the server.
|
||||
* - Connect-never-established failures (unreachable route, DNS blip — the
|
||||
* usual suspects across Wi-Fi/cellular switches) get ONE delayed
|
||||
* re-attempt: that request never reached the server, so no provider work
|
||||
* is duplicated (device follow-up, #1247).
|
||||
*
|
||||
* Open for test substitution (mirrors [ChatPersistenceRepository]'s pattern).
|
||||
*/
|
||||
@@ -54,16 +62,23 @@ open class VoiceNoteRepository(
|
||||
)
|
||||
}
|
||||
val dataUrl = "data:$VOICE_NOTE_MIME_TYPE;base64," + Base64.getEncoder().encodeToString(encoded)
|
||||
val result =
|
||||
safeApiCall(retries = 0) {
|
||||
serviceProvider(transcriptionTimeoutMs(dataUrl)).transcribeAudio(
|
||||
AudioTranscriptionRequest(dataUrl = dataUrl, mimeType = VOICE_NOTE_MIME_TYPE),
|
||||
)
|
||||
val service = serviceProvider(transcriptionTimeoutMs(dataUrl))
|
||||
val result = requestTranscription(service, dataUrl)
|
||||
// One bounded re-attempt, and only for connection-never-established
|
||||
// failures — the one class where the request provably never reached
|
||||
// the server, so no provider work is duplicated (device follow-up,
|
||||
// #1247).
|
||||
val finalResult =
|
||||
if (result is NetworkResult.Failure && isConnectNeverEstablished(result.error)) {
|
||||
delay(CONNECT_RETRY_DELAY_MS)
|
||||
requestTranscription(service, dataUrl)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
return when (result) {
|
||||
return when (finalResult) {
|
||||
is NetworkResult.Success -> {
|
||||
NetworkResult.Success(
|
||||
result.data
|
||||
finalResult.data
|
||||
?.transcript
|
||||
.orEmpty()
|
||||
.trim(),
|
||||
@@ -71,11 +86,26 @@ open class VoiceNoteRepository(
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
result
|
||||
finalResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestTranscription(
|
||||
service: HermesApiService,
|
||||
dataUrl: String,
|
||||
): NetworkResult<AudioTranscriptionResponse> =
|
||||
safeApiCall(retries = 0) {
|
||||
service.transcribeAudio(
|
||||
AudioTranscriptionRequest(dataUrl = dataUrl, mimeType = VOICE_NOTE_MIME_TYPE),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isConnectNeverEstablished(error: NetworkError): Boolean {
|
||||
val cause = (error as? NetworkError.Connection)?.cause ?: return false
|
||||
return cause is ConnectException || cause is UnknownHostException
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Floor for STT requests — desktop parity (`AUDIO_TRANSCRIBE_MIN_REQUEST_TIMEOUT_MS`). */
|
||||
const val MIN_REQUEST_TIMEOUT_MS = 180_000L
|
||||
@@ -85,6 +115,9 @@ open class VoiceNoteRepository(
|
||||
|
||||
private const val BASE64_CHARS_PER_TIMEOUT_MS = 10L
|
||||
|
||||
/** One re-attempt window for connection-never-established failures. */
|
||||
private const val CONNECT_RETRY_DELAY_MS = 750L
|
||||
|
||||
/**
|
||||
* Timeout for the blocking STT endpoint, mirroring the desktop's
|
||||
* `audioTranscribeRequestTimeoutMs`: ~0.1ms of budget per base64 char
|
||||
|
||||
Reference in new issue
Block a user