@mentra/bluetooth-sdk 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.stt
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.konovalov.vad.silero.Vad
|
|
6
|
+
import com.konovalov.vad.silero.VadSilero
|
|
7
|
+
import com.konovalov.vad.silero.config.FrameSize
|
|
8
|
+
import com.konovalov.vad.silero.config.Mode
|
|
9
|
+
import com.konovalov.vad.silero.config.SampleRate
|
|
10
|
+
import java.nio.ByteBuffer
|
|
11
|
+
import java.nio.ByteOrder
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* VadGateSpeechPolicy handles Voice Activity Detection using Silero VAD.
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Dynamic VAD check throttling (adjusts frequency based on silence duration)
|
|
18
|
+
* - 8-second silence timeout before declaring "not speaking"
|
|
19
|
+
* - Bypass modes for debugging and PCM streaming
|
|
20
|
+
* - 512-sample frame size (matches Sherpa-ONNX requirements)
|
|
21
|
+
*
|
|
22
|
+
* Ported from android_core VadGateSpeechPolicy.java
|
|
23
|
+
*/
|
|
24
|
+
class VadGateSpeechPolicy(private val context: Context) {
|
|
25
|
+
companion object {
|
|
26
|
+
private const val TAG = "VadGateSpeechPolicy"
|
|
27
|
+
|
|
28
|
+
// Total required silence duration
|
|
29
|
+
private const val REQUIRED_SILENCE_DURATION_MS = 8000L
|
|
30
|
+
|
|
31
|
+
// Dynamic VAD check intervals
|
|
32
|
+
private const val INITIAL_SILENCE_VAD_INTERVAL_MS = 50L // Check frequently at first
|
|
33
|
+
private const val MEDIUM_SILENCE_VAD_INTERVAL_MS = 100L // Medium frequency after some time
|
|
34
|
+
private const val LONG_SILENCE_VAD_INTERVAL_MS =
|
|
35
|
+
200L // Less frequent after extended silence
|
|
36
|
+
|
|
37
|
+
// Thresholds for switching intervals
|
|
38
|
+
private const val MEDIUM_SILENCE_THRESHOLD_MS =
|
|
39
|
+
20000L // Switch to medium interval after 20s
|
|
40
|
+
private const val LONG_SILENCE_THRESHOLD_MS = 60000L // Switch to long interval after 60s
|
|
41
|
+
|
|
42
|
+
private const val FRAME_SIZE = 512
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private var vad: VadSilero? = null
|
|
46
|
+
private var isCurrentlySpeech = false
|
|
47
|
+
private var bypassVadForDebugging = false
|
|
48
|
+
private var bypassVadForPCM = false
|
|
49
|
+
|
|
50
|
+
/** Notified whenever the speech/silence state actually flips. */
|
|
51
|
+
var onSpeechStateChanged: ((Boolean) -> Unit)? = null
|
|
52
|
+
|
|
53
|
+
// Timestamp of the last detected speech
|
|
54
|
+
private var lastSpeechDetectedTime = 0L
|
|
55
|
+
// Throttle timer for silence VAD checks
|
|
56
|
+
private var lastVadCheckTime = 0L
|
|
57
|
+
|
|
58
|
+
/** Initialize the Silero VAD model */
|
|
59
|
+
fun init(blockSizeSamples: Int) {
|
|
60
|
+
startVad()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun startVad() {
|
|
64
|
+
try {
|
|
65
|
+
// Set internal silence duration very low; external logic manages the full required
|
|
66
|
+
// silence duration
|
|
67
|
+
vad =
|
|
68
|
+
Vad.builder()
|
|
69
|
+
.setContext(context)
|
|
70
|
+
.setSampleRate(SampleRate.SAMPLE_RATE_16K)
|
|
71
|
+
.setFrameSize(FrameSize.FRAME_SIZE_512)
|
|
72
|
+
.setMode(Mode.NORMAL)
|
|
73
|
+
.setSilenceDurationMs(50)
|
|
74
|
+
.setSpeechDurationMs(50)
|
|
75
|
+
.build()
|
|
76
|
+
|
|
77
|
+
Log.d(TAG, "VAD initialized successfully")
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
Log.e(TAG, "Failed to initialize VAD", e)
|
|
80
|
+
vad = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Check if audio should be passed to the recognizer based on VAD state */
|
|
85
|
+
fun shouldPassAudioToRecognizer(): Boolean {
|
|
86
|
+
// CRITICAL: Handle VAD null case
|
|
87
|
+
if (vad == null) {
|
|
88
|
+
return bypassVadForDebugging || bypassVadForPCM || true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return bypassVadForDebugging || bypassVadForPCM || isCurrentlySpeech
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Process audio bytes through VAD to detect speech */
|
|
95
|
+
fun processAudioBytes(bytes: ByteArray, offset: Int, length: Int) {
|
|
96
|
+
val now = System.currentTimeMillis()
|
|
97
|
+
|
|
98
|
+
// If in speech state and it hasn't been 8 seconds since the last speech was detected, skip
|
|
99
|
+
// processing
|
|
100
|
+
if (isCurrentlySpeech && (now - lastSpeechDetectedTime < REQUIRED_SILENCE_DURATION_MS)) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Calculate silence duration
|
|
105
|
+
val silenceDuration = now - lastSpeechDetectedTime
|
|
106
|
+
|
|
107
|
+
// Determine which VAD interval to use based on silence duration
|
|
108
|
+
val currentVadInterval =
|
|
109
|
+
when {
|
|
110
|
+
silenceDuration < MEDIUM_SILENCE_THRESHOLD_MS -> INITIAL_SILENCE_VAD_INTERVAL_MS
|
|
111
|
+
silenceDuration < LONG_SILENCE_THRESHOLD_MS -> MEDIUM_SILENCE_VAD_INTERVAL_MS
|
|
112
|
+
else -> LONG_SILENCE_VAD_INTERVAL_MS
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// During silence, throttle VAD checks based on the dynamic interval
|
|
116
|
+
if (!isCurrentlySpeech && (now - lastVadCheckTime < currentVadInterval)) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
lastVadCheckTime = now
|
|
120
|
+
|
|
121
|
+
val audioBytesFull = bytesToShort(bytes)
|
|
122
|
+
val totalSamples = audioBytesFull.size
|
|
123
|
+
|
|
124
|
+
if (totalSamples % FRAME_SIZE != 0) {
|
|
125
|
+
Log.e(
|
|
126
|
+
TAG,
|
|
127
|
+
"Invalid audio frame size: $totalSamples samples. Needs to be multiple of $FRAME_SIZE."
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
val currentVad =
|
|
133
|
+
vad
|
|
134
|
+
?: run {
|
|
135
|
+
Log.w(TAG, "VAD not initialized, skipping audio processing")
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
var previousSpeechState = isCurrentlySpeech
|
|
140
|
+
|
|
141
|
+
// Process each 512-sample frame
|
|
142
|
+
val numFrames = totalSamples / FRAME_SIZE
|
|
143
|
+
for (i in 0 until numFrames) {
|
|
144
|
+
val currentTime = System.currentTimeMillis()
|
|
145
|
+
val startIdx = i * FRAME_SIZE
|
|
146
|
+
val audioFrame = audioBytesFull.copyOfRange(startIdx, startIdx + FRAME_SIZE)
|
|
147
|
+
val detectedSpeech = currentVad.isSpeech(audioFrame)
|
|
148
|
+
|
|
149
|
+
if (detectedSpeech) {
|
|
150
|
+
isCurrentlySpeech = true
|
|
151
|
+
// Update the last speech detection timestamp
|
|
152
|
+
lastSpeechDetectedTime = currentTime
|
|
153
|
+
} else {
|
|
154
|
+
// If no speech detected, and 8 seconds have elapsed since the last speech, mark as
|
|
155
|
+
// silence
|
|
156
|
+
if (currentTime - lastSpeechDetectedTime >= REQUIRED_SILENCE_DURATION_MS) {
|
|
157
|
+
isCurrentlySpeech = false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isCurrentlySpeech != previousSpeechState) {
|
|
162
|
+
Log.d(
|
|
163
|
+
TAG,
|
|
164
|
+
"Speech detection changed to: ${if (isCurrentlySpeech) "SPEECH" else "SILENCE"}"
|
|
165
|
+
)
|
|
166
|
+
previousSpeechState = isCurrentlySpeech
|
|
167
|
+
onSpeechStateChanged?.invoke(isCurrentlySpeech)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Convert byte array (PCM16LE) to short array */
|
|
173
|
+
private fun bytesToShort(bytes: ByteArray): ShortArray {
|
|
174
|
+
val shorts = ShortArray(bytes.size / 2)
|
|
175
|
+
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts)
|
|
176
|
+
return shorts
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Stop VAD and release resources */
|
|
180
|
+
fun stop() {
|
|
181
|
+
try {
|
|
182
|
+
vad?.close()
|
|
183
|
+
vad = null
|
|
184
|
+
Log.d(TAG, "VAD stopped and resources released")
|
|
185
|
+
} catch (e: Exception) {
|
|
186
|
+
Log.e(TAG, "Error stopping VAD", e)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Reset VAD state */
|
|
191
|
+
fun reset() {
|
|
192
|
+
val wasSpeaking = isCurrentlySpeech
|
|
193
|
+
isCurrentlySpeech = false
|
|
194
|
+
lastSpeechDetectedTime = 0
|
|
195
|
+
lastVadCheckTime = 0
|
|
196
|
+
if (wasSpeaking) onSpeechStateChanged?.invoke(false)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Change bypass VAD for debugging state */
|
|
200
|
+
fun changeBypassVadForDebugging(bypass: Boolean) {
|
|
201
|
+
bypassVadForDebugging = bypass
|
|
202
|
+
Log.d(TAG, "Bypass VAD for debugging: $bypass")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Change bypass VAD for PCM streaming state */
|
|
206
|
+
fun changeBypassVadForPCM(bypass: Boolean) {
|
|
207
|
+
Log.d(TAG, "VAD PCM Bypass State Change: $bypassVadForPCM -> $bypass")
|
|
208
|
+
bypassVadForPCM = bypass
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Handle microphone state changes */
|
|
212
|
+
fun microphoneStateChanged(state: Boolean) {
|
|
213
|
+
if (!state) {
|
|
214
|
+
// Microphone turned off: force immediate silence
|
|
215
|
+
val wasSpeaking = isCurrentlySpeech
|
|
216
|
+
isCurrentlySpeech = false
|
|
217
|
+
lastSpeechDetectedTime = 0
|
|
218
|
+
lastVadCheckTime = 0
|
|
219
|
+
|
|
220
|
+
// Optionally flush the VAD's internal state by processing a silent frame
|
|
221
|
+
vad?.let { vadInstance ->
|
|
222
|
+
val silentFrame = ShortArray(FRAME_SIZE)
|
|
223
|
+
vadInstance.isSpeech(silentFrame)
|
|
224
|
+
}
|
|
225
|
+
Log.d(TAG, "Microphone turned off; forced state to SILENCE.")
|
|
226
|
+
if (wasSpeaking) onSpeechStateChanged?.invoke(false)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package com.mentra.core.tts
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import com.k2fsa.sherpa.onnx.GenerationConfig
|
|
6
|
+
import com.k2fsa.sherpa.onnx.OfflineTts
|
|
7
|
+
import com.k2fsa.sherpa.onnx.OfflineTtsConfig
|
|
8
|
+
import com.k2fsa.sherpa.onnx.OfflineTtsModelConfig
|
|
9
|
+
import com.k2fsa.sherpa.onnx.OfflineTtsSupertonicModelConfig
|
|
10
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
11
|
+
import java.io.File
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Utilities for offline Sherpa-ONNX Supertonic 3 TTS model management.
|
|
15
|
+
*
|
|
16
|
+
* Supertonic 3 is a single multilingual model bundle (31 languages) with 10
|
|
17
|
+
* preset voice styles packed into a single voice.bin. Speaker IDs are indexed
|
|
18
|
+
* alphabetically by JSON filename, so the order is F1, F2, F3, F4, F5, M1, M2,
|
|
19
|
+
* M3, M4, M5 (sids 0..9). We default to F1 (sid 0) for a female voice.
|
|
20
|
+
*/
|
|
21
|
+
object TTSTools {
|
|
22
|
+
private const val PREFS_NAME = "MentraPrefs"
|
|
23
|
+
private const val KEY_TTS_MODEL_PATH = "TTSModelPath"
|
|
24
|
+
private const val KEY_TTS_MODEL_LANGUAGE = "TTSModelLanguageCode"
|
|
25
|
+
|
|
26
|
+
private const val FILE_DURATION_PREDICTOR = "duration_predictor.int8.onnx"
|
|
27
|
+
private const val FILE_TEXT_ENCODER = "text_encoder.int8.onnx"
|
|
28
|
+
private const val FILE_VECTOR_ESTIMATOR = "vector_estimator.int8.onnx"
|
|
29
|
+
private const val FILE_VOCODER = "vocoder.int8.onnx"
|
|
30
|
+
private const val FILE_TTS_JSON = "tts.json"
|
|
31
|
+
private const val FILE_UNICODE_INDEXER = "unicode_indexer.bin"
|
|
32
|
+
private const val FILE_VOICE_STYLE = "voice.bin"
|
|
33
|
+
|
|
34
|
+
private val REQUIRED_FILES = listOf(
|
|
35
|
+
FILE_DURATION_PREDICTOR,
|
|
36
|
+
FILE_TEXT_ENCODER,
|
|
37
|
+
FILE_VECTOR_ESTIMATOR,
|
|
38
|
+
FILE_VOCODER,
|
|
39
|
+
FILE_TTS_JSON,
|
|
40
|
+
FILE_UNICODE_INDEXER,
|
|
41
|
+
FILE_VOICE_STYLE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
fun setTtsModelDetails(context: Context, path: String, languageCode: String) {
|
|
45
|
+
val prefs = getPrefs(context)
|
|
46
|
+
prefs.edit().apply {
|
|
47
|
+
putString(KEY_TTS_MODEL_PATH, path)
|
|
48
|
+
putString(KEY_TTS_MODEL_LANGUAGE, languageCode)
|
|
49
|
+
apply()
|
|
50
|
+
}
|
|
51
|
+
Bridge.log("TTS model details saved: path=$path, language=$languageCode")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun getTtsModelPath(context: Context): String {
|
|
55
|
+
val prefs = getPrefs(context)
|
|
56
|
+
return prefs.getString(KEY_TTS_MODEL_PATH, "") ?: ""
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun getTtsModelLanguage(context: Context): String {
|
|
60
|
+
val prefs = getPrefs(context)
|
|
61
|
+
return prefs.getString(KEY_TTS_MODEL_LANGUAGE, "en-US") ?: "en-US"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun checkTTSModelAvailable(context: Context): Boolean {
|
|
65
|
+
val modelPath = getTtsModelPath(context)
|
|
66
|
+
if (modelPath.isEmpty()) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
return validateTTSModel(modelPath)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun validateTTSModel(path: String): Boolean {
|
|
73
|
+
val modelDir = File(path)
|
|
74
|
+
if (!modelDir.exists() || !modelDir.isDirectory) {
|
|
75
|
+
Bridge.log("TTS model path does not exist or is not a directory: $path")
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (name in REQUIRED_FILES) {
|
|
80
|
+
val file = File(modelDir, name)
|
|
81
|
+
if (!file.exists() || !file.canRead() || file.length() == 0L) {
|
|
82
|
+
Bridge.log("TTS model missing required file '$name' at: $path")
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private val SUPPORTED_LANGS = setOf(
|
|
91
|
+
"en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et",
|
|
92
|
+
"fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl",
|
|
93
|
+
"pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/** Convert a BCP-47 tag like "en-US" or "fr-FR" to a Supertonic 2-letter code. */
|
|
97
|
+
private fun languageTagToSupertonicLang(tag: String?): String {
|
|
98
|
+
if (tag.isNullOrBlank()) return "en"
|
|
99
|
+
val primary = tag.substringBefore('-').lowercase()
|
|
100
|
+
return if (primary in SUPPORTED_LANGS) primary else "en"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun generateTtsAudio(
|
|
104
|
+
context: Context,
|
|
105
|
+
text: String,
|
|
106
|
+
modelPath: String,
|
|
107
|
+
outputPath: String,
|
|
108
|
+
speakerId: Int,
|
|
109
|
+
speed: Float,
|
|
110
|
+
): Boolean {
|
|
111
|
+
val languageTag = getTtsModelLanguage(context)
|
|
112
|
+
if (text.isBlank()) {
|
|
113
|
+
Bridge.log("TTS_ERROR: text is empty")
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
if (!validateTTSModel(modelPath)) {
|
|
117
|
+
Bridge.log("TTS_ERROR: model is invalid: $modelPath")
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val modelDir = File(modelPath)
|
|
122
|
+
val outputFile = File(outputPath)
|
|
123
|
+
outputFile.parentFile?.mkdirs()
|
|
124
|
+
|
|
125
|
+
var tts: OfflineTts? = null
|
|
126
|
+
return try {
|
|
127
|
+
val supertonic = OfflineTtsSupertonicModelConfig(
|
|
128
|
+
durationPredictor = File(modelDir, FILE_DURATION_PREDICTOR).absolutePath,
|
|
129
|
+
textEncoder = File(modelDir, FILE_TEXT_ENCODER).absolutePath,
|
|
130
|
+
vectorEstimator = File(modelDir, FILE_VECTOR_ESTIMATOR).absolutePath,
|
|
131
|
+
vocoder = File(modelDir, FILE_VOCODER).absolutePath,
|
|
132
|
+
ttsJson = File(modelDir, FILE_TTS_JSON).absolutePath,
|
|
133
|
+
unicodeIndexer = File(modelDir, FILE_UNICODE_INDEXER).absolutePath,
|
|
134
|
+
voiceStyle = File(modelDir, FILE_VOICE_STYLE).absolutePath,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
val modelConfig = OfflineTtsModelConfig(
|
|
138
|
+
supertonic = supertonic,
|
|
139
|
+
numThreads = 2,
|
|
140
|
+
provider = "cpu",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
val config = OfflineTtsConfig(
|
|
144
|
+
model = modelConfig,
|
|
145
|
+
maxNumSentences = 1,
|
|
146
|
+
silenceScale = 0.2f,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
tts = OfflineTts(null, config)
|
|
150
|
+
|
|
151
|
+
// sid out of range falls back to F1 (sid 0); clamp 0..9.
|
|
152
|
+
val sid = speakerId.coerceIn(0, tts.numSpeakers() - 1)
|
|
153
|
+
val clampedSpeed = speed.coerceIn(0.5f, 2.0f)
|
|
154
|
+
val lang = languageTagToSupertonicLang(languageTag)
|
|
155
|
+
val genConfig = GenerationConfig(
|
|
156
|
+
sid = sid,
|
|
157
|
+
speed = clampedSpeed,
|
|
158
|
+
extra = mapOf("lang" to lang),
|
|
159
|
+
)
|
|
160
|
+
val audio = tts.generateWithConfig(text = text, config = genConfig)
|
|
161
|
+
val saved = audio.save(outputFile.absolutePath)
|
|
162
|
+
Bridge.log("TTS generated ${outputFile.absolutePath}: saved=$saved sid=$sid lang=$lang")
|
|
163
|
+
saved
|
|
164
|
+
} catch (e: Exception) {
|
|
165
|
+
Bridge.log("TTS_ERROR: ${e.javaClass.simpleName}: ${e.message}")
|
|
166
|
+
e.printStackTrace()
|
|
167
|
+
false
|
|
168
|
+
} finally {
|
|
169
|
+
try {
|
|
170
|
+
tts?.release()
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
Bridge.log("TTS release failed: ${e.message}")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun getPrefs(context: Context): SharedPreferences {
|
|
178
|
+
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
179
|
+
}
|
|
180
|
+
}
|