fix(stage-ui): expose silence duration and correct threshold display (#1774)
This commit is contained in:
@@ -77,9 +77,14 @@ const testStatusMessage = ref<string>('')
|
||||
const testStreamWasStarted = ref(false) // Track if we started the stream for testing
|
||||
|
||||
const useVADThreshold = ref(0.6) // 0.1 - 0.9
|
||||
const useVADMinSilenceDurationMs = ref(800)
|
||||
const useVADModel = ref(true) // Toggle between VAD and volume-based detection
|
||||
const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!stream.value)
|
||||
|
||||
function formatVADThreshold(value: number) {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
async function handleSpeechStart() {
|
||||
if (shouldUseStreamInput.value && stream.value) {
|
||||
// Use both callbacks to support incremental updates and final transcript replacement.
|
||||
@@ -119,6 +124,7 @@ const {
|
||||
loading: loadingVAD,
|
||||
} = useVAD(workletUrl, {
|
||||
threshold: useVADThreshold,
|
||||
minSilenceDurationMs: useVADMinSilenceDurationMs,
|
||||
onSpeechStart: () => {
|
||||
void handleSpeechStart()
|
||||
},
|
||||
@@ -771,7 +777,17 @@ onUnmounted(() => {
|
||||
:min="0.1"
|
||||
:max="0.9"
|
||||
:step="0.05"
|
||||
:format-value="value => `${(value * 100).toFixed(0)}%`"
|
||||
:format-value="formatVADThreshold"
|
||||
/>
|
||||
|
||||
<FieldRange
|
||||
v-model="useVADMinSilenceDurationMs"
|
||||
label="Pause Before Stop"
|
||||
description="How long silence must last before speech is considered finished"
|
||||
:min="200"
|
||||
:max="1500"
|
||||
:step="50"
|
||||
:format-value="value => `${value} ms`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -845,6 +861,7 @@ onUnmounted(() => {
|
||||
active-legend-label="Voice detected"
|
||||
inactive-legend-label="Silence"
|
||||
threshold-label="Speech threshold"
|
||||
:format-threshold="formatVADThreshold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ interface Props {
|
||||
showActiveIndicator?: boolean // Show active state indicator
|
||||
showLegend?: boolean // Show legend
|
||||
formatValue?: (value: number) => string // Custom value formatter
|
||||
formatThreshold?: (value: number) => string // Custom threshold formatter
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -361,7 +362,7 @@ const dataAreaPath = computed(() => {
|
||||
{{ inactiveLegendLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="threshold !== null" class="text-nowrap">{{ thresholdLabel }}: {{ (threshold * 100).toFixed(0) }}%</span>
|
||||
<span v-if="threshold !== null" class="text-nowrap">{{ thresholdLabel }}: {{ formatThreshold ? formatThreshold(threshold) : `${(threshold * 100).toFixed(0)}%` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolveVADConfig } from './vad'
|
||||
|
||||
describe('resolveVADConfig', () => {
|
||||
it('uses safer defaults for threshold and silence duration', () => {
|
||||
expect(resolveVADConfig()).toEqual({
|
||||
speechThreshold: 0.6,
|
||||
exitThreshold: 0.18,
|
||||
minSilenceDurationMs: 800,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves explicit threshold and silence duration values', () => {
|
||||
expect(resolveVADConfig(0.45, 650)).toEqual({
|
||||
speechThreshold: 0.45,
|
||||
exitThreshold: 0.135,
|
||||
minSilenceDurationMs: 650,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BaseVADConfig } from '../../../libs/audio/vad'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { merge } from '@moeru/std'
|
||||
@@ -7,14 +8,29 @@ import { createVAD, createVADStates } from '../../../workers/vad'
|
||||
|
||||
interface UseVADOptions {
|
||||
threshold?: MaybeRefOrGetter<number>
|
||||
minSilenceDurationMs?: MaybeRefOrGetter<number>
|
||||
|
||||
onSpeechStart?: () => void
|
||||
onSpeechEnd?: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_VAD_THRESHOLD = 0.6
|
||||
const DEFAULT_VAD_MIN_SILENCE_DURATION_MS = 800
|
||||
|
||||
export function resolveVADConfig(threshold?: number, minSilenceDurationMs?: number): Pick<BaseVADConfig, 'speechThreshold' | 'exitThreshold' | 'minSilenceDurationMs'> {
|
||||
const resolvedThreshold = threshold ?? DEFAULT_VAD_THRESHOLD
|
||||
|
||||
return {
|
||||
speechThreshold: resolvedThreshold,
|
||||
exitThreshold: resolvedThreshold * 0.3,
|
||||
minSilenceDurationMs: minSilenceDurationMs ?? DEFAULT_VAD_MIN_SILENCE_DURATION_MS,
|
||||
}
|
||||
}
|
||||
|
||||
export function useVAD(workerUrl: string, options?: UseVADOptions) {
|
||||
const defaultOptions: UseVADOptions = {
|
||||
threshold: ref(0.6),
|
||||
threshold: ref(DEFAULT_VAD_THRESHOLD),
|
||||
minSilenceDurationMs: ref(DEFAULT_VAD_MIN_SILENCE_DURATION_MS),
|
||||
}
|
||||
|
||||
options = merge(defaultOptions, options)
|
||||
@@ -32,6 +48,7 @@ export function useVAD(workerUrl: string, options?: UseVADOptions) {
|
||||
const loading = ref(false)
|
||||
|
||||
const threshold = toRef(options.threshold)
|
||||
const minSilenceDurationMs = toRef(options.minSilenceDurationMs)
|
||||
|
||||
async function init() {
|
||||
if (loaded.value || loading.value || manager.value)
|
||||
@@ -41,11 +58,11 @@ export function useVAD(workerUrl: string, options?: UseVADOptions) {
|
||||
inferenceError.value = ''
|
||||
|
||||
try {
|
||||
const vadConfig = resolveVADConfig(threshold.value, minSilenceDurationMs.value)
|
||||
|
||||
vad.value = await createVAD({
|
||||
sampleRate: 16000,
|
||||
speechThreshold: threshold.value,
|
||||
exitThreshold: (threshold.value ?? 0.6) * 0.3,
|
||||
minSilenceDurationMs: 400,
|
||||
...vadConfig,
|
||||
})
|
||||
|
||||
// Set up event handlers
|
||||
@@ -119,11 +136,17 @@ export function useVAD(workerUrl: string, options?: UseVADOptions) {
|
||||
}
|
||||
|
||||
watch(threshold, (newVal) => {
|
||||
if (vad.value && newVal) {
|
||||
if (vad.value && newVal !== undefined) {
|
||||
vad.value.updateConfig({ speechThreshold: newVal, exitThreshold: newVal * 0.3 })
|
||||
}
|
||||
})
|
||||
|
||||
watch(minSilenceDurationMs, (newVal) => {
|
||||
if (vad.value && newVal !== undefined) {
|
||||
vad.value.updateConfig({ minSilenceDurationMs: newVal })
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
isSpeech,
|
||||
isSpeechProb,
|
||||
@@ -132,6 +155,7 @@ export function useVAD(workerUrl: string, options?: UseVADOptions) {
|
||||
loading,
|
||||
inferenceError,
|
||||
threshold,
|
||||
minSilenceDurationMs,
|
||||
|
||||
init,
|
||||
start,
|
||||
|
||||
Reference in New Issue
Block a user