package com.sherpaonnxofflinestt.managers /** * StreamingStateManager - Volatile/Confirmed text state machine * * NOTE: This logic is duplicated in iOS (SherpaOnnxOfflineStt.swift, StreamingStateManager class). * Any changes here MUST be mirrored in iOS to maintain platform parity. */ /** * Data class for streaming transcript updates */ data class StreamingUpdate( val volatile: String, val confirmed: String, val fullText: String, val confidence: Float, val processingTimeMs: Long, val audioDurationMs: Long ) /** * Manages two-tier streaming transcript state (volatile/confirmed) * * - Volatile text: Current hypothesis that may change with more audio * - Confirmed text: Stable text that won't change (high confidence, sufficient context) * * This provides smoother UX by showing stable confirmed text while volatile portions update. */ class StreamingStateManager { private var volatileText: String = "" private var confirmedText: String = "" private var totalContextMs: Long = 0 private var lastProcessingTimeMs: Long = 0 // Configuration var confirmationThreshold: Float = 0.85f var minContextMs: Long = 5000L // 5 seconds of context before confirming /** * Update the streaming state with new transcription result * * @param newText The latest transcription hypothesis * @param confidence Confidence score (0-1) * @param audioLengthMs Duration of audio processed in this update * @param processingTimeMs Time taken to process this update * @return StreamingUpdate with current state */ fun updateState( newText: String, confidence: Float, audioLengthMs: Long, processingTimeMs: Long ): StreamingUpdate { totalContextMs += audioLengthMs lastProcessingTimeMs = processingTimeMs // Determine if we should confirm current volatile text val shouldConfirm = confidence >= confirmationThreshold && totalContextMs >= minContextMs && volatileText.isNotEmpty() if (shouldConfirm) { // Move volatile text to confirmed confirmedText = if (confirmedText.isEmpty()) { volatileText } else { "$confirmedText $volatileText" } volatileText = newText // Reset context counter after confirmation totalContextMs = 0 } else { volatileText = newText } val fullText = if (confirmedText.isEmpty()) { volatileText } else if (volatileText.isEmpty()) { confirmedText } else { "$confirmedText $volatileText" } return StreamingUpdate( volatile = volatileText, confirmed = confirmedText, fullText = fullText.trim(), confidence = confidence, processingTimeMs = processingTimeMs, audioDurationMs = audioLengthMs ) } /** * Force confirm current volatile text (e.g., on endpoint detection) * * @param processingTimeMs Time taken for final processing * @return StreamingUpdate with confirmed state */ fun confirmCurrent(processingTimeMs: Long): StreamingUpdate { if (volatileText.isNotEmpty()) { confirmedText = if (confirmedText.isEmpty()) { volatileText } else { "$confirmedText $volatileText" } volatileText = "" } return StreamingUpdate( volatile = "", confirmed = confirmedText, fullText = confirmedText.trim(), confidence = 1.0f, processingTimeMs = processingTimeMs, audioDurationMs = 0 ) } /** * Get current confirmed text */ fun getConfirmedText(): String = confirmedText /** * Get current volatile text */ fun getVolatileText(): String = volatileText /** * Get full text (confirmed + volatile) */ fun getFullText(): String { return if (confirmedText.isEmpty()) { volatileText } else if (volatileText.isEmpty()) { confirmedText } else { "$confirmedText $volatileText" }.trim() } /** * Get total context duration in milliseconds */ fun getTotalContextMs(): Long = totalContextMs /** * Reset all state */ fun reset() { volatileText = "" confirmedText = "" totalContextMs = 0 lastProcessingTimeMs = 0 } }