stt(phone): полный текст по клику — whisper на всём буфере, VAD только для промежуточных фраз
This commit is contained in:
@@ -29,6 +29,11 @@ internal fun s16leToFloat(pcm: ByteArray): FloatArray {
|
||||
* Стриминговое распознавание: куски PCM → VAD-фразы (Silero, окна по 512) →
|
||||
* Whisper (в отдельном потоке) → колбэк [onPhrase].
|
||||
*
|
||||
* Весь PCM сессии дополнительно накапливается без зависимости от VAD и в
|
||||
* [finish] (по клику) распознаётся одним вызовом (хвост 30 с, если больше),
|
||||
* перезаписывая VAD-текст; VAD-фразы остаются только промежуточными
|
||||
* ([onPhrase]).
|
||||
*
|
||||
* Потокобезопасен: [accept]/[finish]/[reset] синхронизированы; распознавание
|
||||
* фраз идёт в однопоточном executor-е (не блокирует приём аудио).
|
||||
*
|
||||
@@ -45,6 +50,12 @@ class SttStreamer(
|
||||
companion object {
|
||||
private const val SILENCE_TIMEOUT_MS = 30_000L
|
||||
private const val WINDOW_SIZE = 512
|
||||
|
||||
/** Частота сэмплирования PCM: 16 кГц, s16le, mono. */
|
||||
private const val SAMPLE_RATE = 16_000
|
||||
|
||||
/** Максимум PCM для распознания по клику: 30 с (дольше — последние 30 с). */
|
||||
private val MAX_FULL_BUFFER_SAMPLES = SAMPLE_RATE * 30
|
||||
}
|
||||
|
||||
private val lock = java.util.concurrent.locks.ReentrantLock()
|
||||
@@ -79,6 +90,12 @@ class SttStreamer(
|
||||
/** Недозаполненные сэмплы (< 512) для следующего окна VAD. */
|
||||
private var pending = FloatArray(0)
|
||||
|
||||
/**
|
||||
* Весь PCM сессии (FloatArray @16 кГц) — не зависит от VAD-сегментации:
|
||||
* [finish] прогоняет его через распознаватель одним вызовом.
|
||||
*/
|
||||
private var sessionPcm: FloatArray = FloatArray(0)
|
||||
|
||||
/** Последняя поставленная в executor задача — finish ждёт её завершения. */
|
||||
private var lastFuture: Future<*>? = null
|
||||
|
||||
@@ -103,6 +120,7 @@ class SttStreamer(
|
||||
lock.withLock {
|
||||
if (deadlineMs == 0L) deadlineMs = System.currentTimeMillis() + SILENCE_TIMEOUT_MS
|
||||
val floats = s16leToFloat(pcm)
|
||||
sessionPcm = sessionPcm + floats
|
||||
var i = 0
|
||||
|
||||
if (pending.isNotEmpty()) {
|
||||
@@ -134,8 +152,11 @@ class SttStreamer(
|
||||
|
||||
/**
|
||||
* Завершить сессию (клик): выбросить остаток буфера, дождаться всех
|
||||
* распознаваний и вернуть полный текст. Буферы НЕ сбрасывает — после
|
||||
* вызова нужно [reset] (состояние сессии снимает вызывающий).
|
||||
* распознаваний VAD, затем распознать весь буфер сессии одним вызовом
|
||||
* (хвост 30 с, если длиннее) — [full] перезаписывается результатом
|
||||
* (не конкатенация с VAD-фразами). Вернуть полный текст.
|
||||
* Буферы НЕ сбрасывает — после вызова нужно [reset] (состояние
|
||||
* сессии снимает вызывающий).
|
||||
*/
|
||||
fun finish(): String {
|
||||
lock.withLock {
|
||||
@@ -148,9 +169,40 @@ class SttStreamer(
|
||||
}
|
||||
val last = lastFuture
|
||||
if (last != null) runCatching { last.get(30, TimeUnit.SECONDS) }
|
||||
recognizeFullBufferLocked()
|
||||
return full
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Распознать весь буфер сессии [sessionPcm] одним вызовом: целиком,
|
||||
* если ≤ 30 с, иначе последние 30 с (команда перед кликом точно влезает).
|
||||
* Результат перезаписывает [full] — VAD-фразы идут только в [onPhrase].
|
||||
* Вызывать с удержанным [lock].
|
||||
*/
|
||||
private fun recognizeFullBufferLocked() {
|
||||
val buffer = sessionPcm
|
||||
if (buffer.isEmpty()) return
|
||||
val chunk = if (buffer.size > MAX_FULL_BUFFER_SAMPLES) {
|
||||
buffer.copyOfRange(buffer.size - MAX_FULL_BUFFER_SAMPLES, buffer.size)
|
||||
} else buffer
|
||||
val sessionId = session
|
||||
val task = executor.submit {
|
||||
if (sessionId != session) return@submit
|
||||
val result = runCatching { stt.recognize(chunk) }
|
||||
if (result.isSuccess) {
|
||||
full = result.getOrThrow()
|
||||
log(
|
||||
"stt",
|
||||
"полный буфер: ${chunk.size} сэмплов (${chunk.size / SAMPLE_RATE.toFloat()}с) → '$full'",
|
||||
)
|
||||
} else {
|
||||
log("stt", "полный буфер: ошибка распознавания: ${result.exceptionOrNull()?.message}")
|
||||
}
|
||||
}
|
||||
lastFuture = task
|
||||
runCatching { task.get(30, TimeUnit.SECONDS) }
|
||||
}
|
||||
/** Отмена/сброс сессии: текст не выводится, таймер тишины снимается. */
|
||||
fun reset() {
|
||||
lock.withLock { resetLocked() }
|
||||
@@ -179,6 +231,7 @@ class SttStreamer(
|
||||
session++
|
||||
full = ""
|
||||
pending = FloatArray(0)
|
||||
sessionPcm = FloatArray(0)
|
||||
deadlineMs = 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user