diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml
index fa975984d..1e48dbf04 100644
--- a/packages/i18n/src/locales/en/settings.yaml
+++ b/packages/i18n/src/locales/en/settings.yaml
@@ -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
diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml
index 286e88350..c7684467b 100644
--- a/packages/i18n/src/locales/zh-Hans/settings.yaml
+++ b/packages/i18n/src/locales/zh-Hans/settings.yaml
@@ -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: 长期记忆
diff --git a/packages/i18n/src/locales/zh-Hant/settings.yaml b/packages/i18n/src/locales/zh-Hant/settings.yaml
index 3de11ac64..fcc6345fa 100644
--- a/packages/i18n/src/locales/zh-Hant/settings.yaml
+++ b/packages/i18n/src/locales/zh-Hant/settings.yaml
@@ -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: 長期記憶
diff --git a/packages/stage-pages/src/pages/settings/modules/hearing.vue b/packages/stage-pages/src/pages/settings/modules/hearing.vue
index 5ab8c939c..3937b2aa8 100644
--- a/packages/stage-pages/src/pages/settings/modules/hearing.vue
+++ b/packages/stage-pages/src/pages/settings/modules/hearing.vue
@@ -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(() => {
+
+
+
+
+ {{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.title') }}
+
+
+ {{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.description') }}
+
+
+
+
+ {{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.verbose-json-note') }}
+
+
+
+ {{ t('settings.pages.modules.hearing.sections.section.confidence-threshold.verbose-json-unsupported') }}
+
+
+
diff --git a/packages/stage-ui/src/stores/modules/hearing.test.ts b/packages/stage-ui/src/stores/modules/hearing.test.ts
new file mode 100644
index 000000000..654717208
--- /dev/null
+++ b/packages/stage-ui/src/stores/modules/hearing.test.ts
@@ -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')
+ })
+})
diff --git a/packages/stage-ui/src/stores/modules/hearing.ts b/packages/stage-ui/src/stores/modules/hearing.ts
index 9898dbcae..554c7999d 100644
--- a/packages/stage-ui/src/stores/modules/hearing.ts
+++ b/packages/stage-ui/src/stores/modules/hearing.ts
@@ -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
}
+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 = {
'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('')
const autoSendEnabled = useLocalStorageManualReset('settings/hearing/auto-send-enabled', false)
const autoSendDelay = useLocalStorageManualReset('settings/hearing/auto-send-delay', 2000) // Default 2 seconds
+ const confidenceThreshold = useLocalStorageManualReset('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,
diff --git a/packages/ui/src/components/form/field/field-range.vue b/packages/ui/src/components/form/field/field-range.vue
index bc6776328..b9e1bd6aa 100644
--- a/packages/ui/src/components/form/field/field-range.vue
+++ b/packages/ui/src/components/form/field/field-range.vue
@@ -36,9 +36,9 @@ const modelValue = defineModel({ required: true })