package com.sherpaonnxofflinestt.managers import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.sherpaonnxofflinestt.utils.STTLogger /** * Manages all React Native event emissions for STT module */ class EventEmissionManager(private val reactContext: ReactApplicationContext) { /** * Send transcript update event (partial or final) with performance metrics */ fun sendTranscriptUpdate( text: String, isFinal: Boolean, startTime: Long, endTime: Long, speakerId: Int = -1, processingTimeMs: Long = 0, confidence: Float = 0f ) { if (!reactContext.hasActiveReactInstance()) return val audioDurationMs = endTime - startTime val rtfx = if (processingTimeMs > 0) audioDurationMs.toFloat() / processingTimeMs else 0f val params = Arguments.createMap().apply { putString("text", text) putBoolean("isFinal", isFinal) putDouble("startTime", startTime.toDouble() / 1000) putDouble("endTime", endTime.toDouble() / 1000) putDouble("processingTime", processingTimeMs.toDouble() / 1000) putDouble("audioDuration", audioDurationMs.toDouble() / 1000) putDouble("rtfx", rtfx.toDouble()) putDouble("confidence", confidence.toDouble()) if (speakerId > 0) { putInt("speakerId", speakerId) } } emit("TranscriptUpdate", params) } /** * Send streaming transcript update with volatile/confirmed state */ fun sendStreamingUpdate( volatileText: String, confirmedText: String, fullText: String, isFinal: Boolean, confidence: Float, processingTimeMs: Long, audioDurationMs: Long ) { if (!reactContext.hasActiveReactInstance()) return val rtfx = if (processingTimeMs > 0) audioDurationMs.toFloat() / processingTimeMs else 0f val params = Arguments.createMap().apply { putString("volatile", volatileText) putString("confirmed", confirmedText) putString("fullText", fullText) putBoolean("isFinal", isFinal) putDouble("confidence", confidence.toDouble()) putDouble("processingTime", processingTimeMs.toDouble() / 1000) putDouble("rtfx", rtfx.toDouble()) } emit("StreamingTranscriptUpdate", params) } /** * Send VAD state update event */ fun sendVADUpdate( state: String, probability: Float, speechMs: Int, silenceMs: Int ) { if (!reactContext.hasActiveReactInstance()) return val params = Arguments.createMap().apply { putString("state", state) putDouble("speechProbability", probability.toDouble()) putInt("speechDurationMs", speechMs) putInt("silenceDurationMs", silenceMs) } emit("VADUpdate", params) } /** * Send speaker update event (diarization) * @param speakerId The speaker's ID * @param status The speaker's status ("pending" or "confirmed") * @param justConfirmed Whether the speaker was just confirmed * @param totalSpeakers Total number of tracked speakers * @param confidence Match confidence (0-1), 0 for new speakers * @param embeddingQuality Embedding quality score (0-1) */ fun sendSpeakerUpdate( speakerId: Int, status: String, justConfirmed: Boolean, totalSpeakers: Int, confidence: Float = 0f, embeddingQuality: Float = 0f ) { if (!reactContext.hasActiveReactInstance()) return val params = Arguments.createMap().apply { putInt("speakerId", speakerId) putString("status", status) putBoolean("justConfirmed", justConfirmed) putInt("totalSpeakers", totalSpeakers) putDouble("confidence", confidence.toDouble()) putDouble("embeddingQuality", embeddingQuality.toDouble()) } emit("SpeakerUpdate", params) } /** * Send multi-speaker detection event when multiple speakers are detected in a single segment * @param speakers List of detected speaker IDs * @param speakerWindows List of (timestampMs, speakerId) pairs showing speaker at each window */ fun sendMultiSpeakerDetected( speakers: List, speakerWindows: List> ) { if (!reactContext.hasActiveReactInstance()) return val speakersArray = Arguments.createArray() speakers.forEach { speakersArray.pushInt(it) } val windowsArray = Arguments.createArray() speakerWindows.forEach { (timestampMs, speakerId) -> val windowMap = Arguments.createMap().apply { putInt("timestampMs", timestampMs) putInt("speakerId", speakerId) } windowsArray.pushMap(windowMap) } val params = Arguments.createMap().apply { putArray("speakers", speakersArray) putArray("windows", windowsArray) putInt("speakerCount", speakers.size) } emit("MultiSpeakerDetected", params) } /** * Send error event */ fun sendError(code: String, message: String) { if (!reactContext.hasActiveReactInstance()) return val params = Arguments.createMap().apply { putString("code", code) putString("message", message) } emit("STTError", params) } /** * Create a result map for accumulating results with performance metrics */ fun createResultMap( text: String, isFinal: Boolean, startTime: Long, endTime: Long, speakerId: Int = -1, processingTimeMs: Long = 0, confidence: Float = 0f ): WritableMap { val audioDurationMs = endTime - startTime val rtfx = if (processingTimeMs > 0) audioDurationMs.toFloat() / processingTimeMs else 0f return Arguments.createMap().apply { putString("text", text) putBoolean("isFinal", isFinal) putDouble("startTime", startTime.toDouble() / 1000) putDouble("endTime", endTime.toDouble() / 1000) putDouble("processingTime", processingTimeMs.toDouble() / 1000) putDouble("audioDuration", audioDurationMs.toDouble() / 1000) putDouble("rtfx", rtfx.toDouble()) putDouble("confidence", confidence.toDouble()) if (speakerId > 0) { putInt("speakerId", speakerId) } } } private fun emit(eventName: String, params: WritableMap) { if (!reactContext.hasActiveReactInstance()) { STTLogger.Events.w("[EMIT] No active catalyst instance, dropping event: $eventName") return } STTLogger.Events.d("[EMIT] Sending event: $eventName") reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } }