fix(stage-ui): expose silence duration and correct threshold display (#1774)

This commit is contained in:
Sambhram
2026-05-06 17:02:30 +08:00
committed by GitHub
parent abc4341525
commit 33cd61111f
4 changed files with 70 additions and 7 deletions
@@ -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,
})
})
})
+29 -5
View File
@@ -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,