feat(ui,stage-ui,stage-pages,i18n): transcription confidence filter (#1148)
This commit is contained in:
@@ -404,6 +404,14 @@ pages:
|
||||
section:
|
||||
provider-selection:
|
||||
description: Select the suitable speech recognition provider
|
||||
confidence-threshold:
|
||||
title: Confidence Threshold
|
||||
description: Filter out low-confidence transcriptions to reduce Whisper hallucinations. Values closer to 0 are more strict; drag to the leftmost to disable. Only effective for providers supporting Whisper API (e.g., OpenAI, Groq).
|
||||
disabled: Disabled
|
||||
verbose-json-note: >-
|
||||
Note: If your provider does not support verbose_json responses, this setting will have no effect.
|
||||
verbose-json-unsupported: >-
|
||||
Your provider did not return verbose_json segments. Confidence filtering had no effect on the last transcription.
|
||||
memory-long-term:
|
||||
description: Long-term memory specific settings and management
|
||||
title: Long-Term Memory
|
||||
|
||||
@@ -388,6 +388,12 @@ pages:
|
||||
section:
|
||||
provider-selection:
|
||||
description: 选择合适的语音转文本的服务来源
|
||||
confidence-threshold:
|
||||
title: 置信度阈值
|
||||
description: 过滤低置信度的转录结果,避免 Whisper 幻觉噪音。值越接近 0 越严格,拖到最左为禁用。仅对支持 Whisper API 的服务商有效(如 OpenAI、Groq)。
|
||||
disabled: 已禁用
|
||||
verbose-json-note: 注意:如果你的服务商不支持 verbose_json 响应,此设置将不会生效。
|
||||
verbose-json-unsupported: 你的服务商未返回 verbose_json 片段,上次转录的置信度过滤未生效。
|
||||
memory-long-term:
|
||||
description: 长期记忆
|
||||
title: 长期记忆
|
||||
|
||||
@@ -379,6 +379,12 @@ pages:
|
||||
section:
|
||||
provider-selection:
|
||||
description: 選擇合適的語音辨識提供者
|
||||
confidence-threshold:
|
||||
title: 置信度閾值
|
||||
description: 過濾低置信度的轉錄結果,避免 Whisper 幻覺噪音。值越接近 0 越嚴格,拖到最左為禁用。僅對支援 Whisper API 的服務商有效(如 OpenAI、Groq)。
|
||||
disabled: 已停用
|
||||
verbose-json-note: 注意:如果你的服務商不支援 verbose_json 響應,此設定將不會生效。
|
||||
verbose-json-unsupported: 你的服務商未返回 verbose_json 片段,上次轉錄的置信度過濾未生效。
|
||||
memory-long-term:
|
||||
description: 長期記憶
|
||||
title: 長期記憶
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Alert, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple
|
||||
import { useAnalytics, useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { useVAD } from '@proj-airi/stage-ui/stores/ai/models/vad'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { CONFIDENCE_THRESHOLD_DISABLED, useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { Button, FieldCheckbox, FieldCombobox, FieldInput, FieldRange } from '@proj-airi/ui'
|
||||
@@ -28,6 +28,8 @@ const {
|
||||
activeCustomModelName,
|
||||
autoSendEnabled,
|
||||
autoSendDelay,
|
||||
confidenceThreshold,
|
||||
verboseJsonNotSupported,
|
||||
} = storeToRefs(hearingStore)
|
||||
const providersStore = useProvidersStore()
|
||||
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
@@ -656,6 +658,32 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confidence threshold (only for non-streaming providers) -->
|
||||
<div v-if="!supportsStreamInput" class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-500">
|
||||
{{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
{{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.description') }}
|
||||
</div>
|
||||
</div>
|
||||
<FieldRange
|
||||
v-model="confidenceThreshold"
|
||||
:min="CONFIDENCE_THRESHOLD_DISABLED"
|
||||
:max="0"
|
||||
:step="0.1"
|
||||
:format-value="value => value <= CONFIDENCE_THRESHOLD_DISABLED ? t('settings.pages.modules.hearing.sections.section.confidence-threshold.disabled') : value.toFixed(1)"
|
||||
/>
|
||||
<div v-if="confidenceThreshold > CONFIDENCE_THRESHOLD_DISABLED" class="mt-2 text-xs text-neutral-400 dark:text-neutral-500">
|
||||
{{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.verbose-json-note') }}
|
||||
</div>
|
||||
<div v-if="verboseJsonNotSupported" class="mt-2 flex items-center gap-1.5 text-xs text-amber-500 dark:text-amber-400">
|
||||
<div i-solar:warning-circle-line-duotone class="shrink-0" />
|
||||
{{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.verbose-json-unsupported') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-send settings -->
|
||||
<div class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
|
||||
<div class="mb-4">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { filterTranscriptionByConfidence } from './hearing'
|
||||
|
||||
describe('filterTranscriptionByConfidence', () => {
|
||||
const segments = [
|
||||
{ text: 'Hello ', avg_logprob: -0.3 },
|
||||
{ text: 'world ', avg_logprob: -1.2 },
|
||||
{ text: 'gibberish', avg_logprob: -2.5 },
|
||||
]
|
||||
|
||||
it('keeps all segments when threshold is very low', () => {
|
||||
expect(filterTranscriptionByConfidence(segments, -3)).toBe('Hello world gibberish')
|
||||
})
|
||||
|
||||
it('filters out low-confidence segments', () => {
|
||||
expect(filterTranscriptionByConfidence(segments, -1)).toBe('Hello')
|
||||
})
|
||||
|
||||
it('filters out all segments when threshold is 0', () => {
|
||||
expect(filterTranscriptionByConfidence(segments, 0)).toBe('')
|
||||
})
|
||||
|
||||
it('returns empty string for empty segments', () => {
|
||||
expect(filterTranscriptionByConfidence([], -1)).toBe('')
|
||||
})
|
||||
|
||||
it('trims whitespace from result', () => {
|
||||
expect(filterTranscriptionByConfidence([{ text: ' hello ', avg_logprob: -0.5 }], -1)).toBe('hello')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||
import { refManualReset } from '@vueuse/core'
|
||||
import { generateTranscription } from '@xsai/generate-transcription'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import vadWorkletUrl from '../../workers/vad/process.worklet?worker&url'
|
||||
|
||||
@@ -76,6 +76,19 @@ interface HearingTranscriptionInvokeOptions {
|
||||
providerOptions?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const CONFIDENCE_THRESHOLD_DISABLED = -3
|
||||
|
||||
export function filterTranscriptionByConfidence(
|
||||
segments: Array<{ text?: string, avg_logprob?: number }>,
|
||||
threshold: number,
|
||||
): string {
|
||||
if (!segments.some(s => s?.avg_logprob != null && s?.text != null)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return segments.filter(s => (s?.avg_logprob ?? -Infinity) >= threshold).map(s => s?.text ?? '').join('').trim()
|
||||
}
|
||||
|
||||
const STREAM_TRANSCRIPTION_EXECUTORS: Record<string, StreamTranscription> = {
|
||||
'aliyun-nls-transcription': streamAliyunTranscription,
|
||||
// Web Speech API is handled specially in transcribeForMediaStream since it works directly with MediaStream
|
||||
@@ -92,6 +105,12 @@ export const useHearingStore = defineStore('hearing-store', () => {
|
||||
const transcriptionModelSearchQuery = refManualReset<string>('')
|
||||
const autoSendEnabled = useLocalStorageManualReset<boolean>('settings/hearing/auto-send-enabled', false)
|
||||
const autoSendDelay = useLocalStorageManualReset<number>('settings/hearing/auto-send-delay', 2000) // Default 2 seconds
|
||||
const confidenceThreshold = useLocalStorageManualReset<number>('settings/hearing/confidence-threshold', CONFIDENCE_THRESHOLD_DISABLED)
|
||||
const verboseJsonNotSupported = ref(false)
|
||||
|
||||
watch(activeTranscriptionProvider, () => {
|
||||
verboseJsonNotSupported.value = false
|
||||
})
|
||||
|
||||
// Computed properties
|
||||
const availableProvidersMetadata = computed(() => allAudioTranscriptionProvidersMetadata.value)
|
||||
@@ -154,6 +173,7 @@ export const useHearingStore = defineStore('hearing-store', () => {
|
||||
transcriptionModelSearchQuery.reset()
|
||||
autoSendEnabled.reset()
|
||||
autoSendDelay.reset()
|
||||
confidenceThreshold.reset()
|
||||
}
|
||||
|
||||
async function transcription(
|
||||
@@ -217,12 +237,28 @@ export const useHearingStore = defineStore('hearing-store', () => {
|
||||
throw new Error('File input is required for transcription.')
|
||||
}
|
||||
|
||||
const useVerboseJson = !format && confidenceThreshold.value > CONFIDENCE_THRESHOLD_DISABLED
|
||||
const response = await generateTranscription({
|
||||
...provider.transcription(model, options?.providerOptions),
|
||||
file: normalizedInput.file,
|
||||
responseFormat: format,
|
||||
responseFormat: useVerboseJson ? 'verbose_json' : format,
|
||||
})
|
||||
|
||||
if (useVerboseJson) {
|
||||
if (response.segments) {
|
||||
verboseJsonNotSupported.value = false
|
||||
return {
|
||||
mode: 'generate',
|
||||
...response,
|
||||
text: filterTranscriptionByConfidence(response.segments, confidenceThreshold.value),
|
||||
}
|
||||
}
|
||||
else {
|
||||
verboseJsonNotSupported.value = true
|
||||
console.warn('[Hearing] Confidence filter is enabled but the provider did not return verbose_json segments. Filtering has no effect.')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'generate',
|
||||
...response,
|
||||
@@ -237,6 +273,8 @@ export const useHearingStore = defineStore('hearing-store', () => {
|
||||
transcriptionModelSearchQuery,
|
||||
autoSendEnabled,
|
||||
autoSendDelay,
|
||||
confidenceThreshold,
|
||||
verboseJsonNotSupported,
|
||||
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
|
||||
@@ -36,9 +36,9 @@ const modelValue = defineModel<number>({ required: true })
|
||||
<div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
|
||||
<Range
|
||||
v-model="modelValue"
|
||||
:min="min || 0"
|
||||
:max="max || 1"
|
||||
:step="step || 0.01"
|
||||
:min="min ?? 0"
|
||||
:max="max ?? 1"
|
||||
:step="step ?? 0.01"
|
||||
:class="['w-full']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user