mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
This commit is contained in:
@@ -102,6 +102,7 @@ const VoiceDebugPanel = () => {
|
||||
min_duration_secs: settings.min_duration_secs,
|
||||
silence_threshold: settings.silence_threshold,
|
||||
custom_dictionary: settings.custom_dictionary,
|
||||
always_on_enabled: settings.always_on_enabled,
|
||||
});
|
||||
setNotice(t('voice.debug.settingsSaved'));
|
||||
await loadData(true);
|
||||
@@ -203,6 +204,37 @@ const VoiceDebugPanel = () => {
|
||||
|
||||
{settings && (
|
||||
<>
|
||||
{/* Always-on listening (Phase 2) — opt-in, privacy-sensitive. */}
|
||||
<div className="flex items-start justify-between gap-3 rounded-md border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<span className="text-xs font-medium text-stone-700 dark:text-neutral-200">
|
||||
{t('voice.debug.alwaysOn')}
|
||||
</span>
|
||||
<p className="text-[11px] text-stone-400 dark:text-neutral-500 mt-0.5">
|
||||
{t('voice.debug.alwaysOnDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.always_on_enabled}
|
||||
aria-label={t('voice.debug.alwaysOn')}
|
||||
data-testid="voice-always-on-toggle"
|
||||
onClick={() => updateSetting('always_on_enabled', !settings.always_on_enabled)}
|
||||
className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${
|
||||
settings.always_on_enabled
|
||||
? 'bg-primary-500'
|
||||
: 'bg-stone-300 dark:bg-neutral-600'
|
||||
}`}>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`inline-block h-3 w-3 transform rounded-full bg-white shadow transition-transform ${
|
||||
settings.always_on_enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs font-medium text-stone-600 dark:text-neutral-300">
|
||||
{t('voice.debug.minimumRecordingSeconds')}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../../services/api/voiceSettingsApi';
|
||||
import {
|
||||
openhumanGetVoiceServerSettings,
|
||||
openhumanUpdateVoiceServerSettings,
|
||||
openhumanVoiceSetProviders,
|
||||
openhumanVoiceStatus,
|
||||
type VoiceProvidersSnapshot,
|
||||
@@ -485,6 +486,57 @@ const VoicePanel = ({ embedded = false }: VoicePanelProps = {}) => {
|
||||
)}
|
||||
|
||||
<div className={embedded ? 'space-y-4' : 'p-4 space-y-4'}>
|
||||
{/* ─── Always-on listening (Phase 2) ──────────────────────────── */}
|
||||
{settings && (
|
||||
<section className="space-y-3">
|
||||
<div className="bg-stone-50 dark:bg-neutral-800/60 rounded-lg border border-stone-200 dark:border-neutral-800 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('voice.debug.alwaysOn')}
|
||||
</h3>
|
||||
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-1">
|
||||
{t('voice.debug.alwaysOnDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.always_on_enabled}
|
||||
aria-label={t('voice.debug.alwaysOn')}
|
||||
data-testid="voice-always-on-toggle"
|
||||
onClick={async () => {
|
||||
const next = !settings.always_on_enabled;
|
||||
setSettings(current =>
|
||||
current ? { ...current, always_on_enabled: next } : current
|
||||
);
|
||||
try {
|
||||
await openhumanUpdateVoiceServerSettings({ always_on_enabled: next });
|
||||
} catch (err) {
|
||||
// Revert on failure so the UI reflects the persisted value.
|
||||
setSettings(current =>
|
||||
current ? { ...current, always_on_enabled: !next } : current
|
||||
);
|
||||
console.error('[VoicePanel] failed to toggle always-on', err);
|
||||
}
|
||||
}}
|
||||
className={`relative mt-0.5 inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${
|
||||
settings.always_on_enabled
|
||||
? 'bg-primary-500'
|
||||
: 'bg-stone-300 dark:bg-neutral-600'
|
||||
}`}>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`inline-block h-3 w-3 transform rounded-full bg-white shadow transition-transform ${
|
||||
settings.always_on_enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ─── Section 1: Voice Provider Chips ─────────────────────────── */}
|
||||
<section className="space-y-3">
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
openhumanGetVoiceServerSettings,
|
||||
openhumanUpdateVoiceServerSettings,
|
||||
openhumanVoiceServerStatus,
|
||||
openhumanVoiceStatus,
|
||||
type VoiceServerSettings,
|
||||
type VoiceServerStatus,
|
||||
type VoiceStatus,
|
||||
} from '../../../../utils/tauriCommands';
|
||||
import type { ConfigSnapshot } from '../../../../utils/tauriCommands/config';
|
||||
import VoiceDebugPanel from '../VoiceDebugPanel';
|
||||
|
||||
// Key-passthrough i18n + trivial chrome so we can render the panel standalone.
|
||||
vi.mock('../../../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock('../../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
|
||||
}));
|
||||
vi.mock('../components/SettingsHeader', () => ({ default: () => null }));
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands', () => ({
|
||||
openhumanGetVoiceServerSettings: vi.fn(),
|
||||
openhumanUpdateVoiceServerSettings: vi.fn(),
|
||||
openhumanVoiceServerStatus: vi.fn(),
|
||||
openhumanVoiceStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
const SETTINGS: VoiceServerSettings = {
|
||||
auto_start: false,
|
||||
hotkey: 'Fn',
|
||||
activation_mode: 'push',
|
||||
skip_cleanup: true,
|
||||
min_duration_secs: 0.3,
|
||||
silence_threshold: 0.002,
|
||||
custom_dictionary: [],
|
||||
always_on_enabled: false,
|
||||
};
|
||||
|
||||
const SERVER_STATUS: VoiceServerStatus = {
|
||||
state: 'idle',
|
||||
hotkey: 'Fn',
|
||||
activation_mode: 'push',
|
||||
transcription_count: 0,
|
||||
last_error: null,
|
||||
};
|
||||
|
||||
const VOICE_STATUS: VoiceStatus = {
|
||||
stt_available: true,
|
||||
tts_available: true,
|
||||
stt_model_id: 'ggml-tiny',
|
||||
tts_voice_id: 'en_US',
|
||||
whisper_binary: null,
|
||||
piper_binary: null,
|
||||
stt_model_path: null,
|
||||
tts_voice_path: null,
|
||||
whisper_in_process: true,
|
||||
llm_cleanup_enabled: true,
|
||||
stt_provider: 'cloud',
|
||||
tts_provider: 'cloud',
|
||||
};
|
||||
|
||||
describe('VoiceDebugPanel — always-on toggle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(openhumanGetVoiceServerSettings).mockResolvedValue({
|
||||
result: { ...SETTINGS },
|
||||
logs: [],
|
||||
});
|
||||
vi.mocked(openhumanUpdateVoiceServerSettings).mockResolvedValue({
|
||||
result: {} as unknown as ConfigSnapshot,
|
||||
logs: [],
|
||||
});
|
||||
vi.mocked(openhumanVoiceServerStatus).mockResolvedValue(SERVER_STATUS);
|
||||
vi.mocked(openhumanVoiceStatus).mockResolvedValue(VOICE_STATUS);
|
||||
});
|
||||
|
||||
it('toggles always-on and persists it via the update RPC on save', async () => {
|
||||
render(<VoiceDebugPanel />);
|
||||
|
||||
const toggle = await screen.findByTestId('voice-always-on-toggle');
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
|
||||
// Local optimistic flip (creates an unsaved change → enables Save).
|
||||
fireEvent.click(toggle);
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
fireEvent.click(screen.getByText('common.save'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(vi.mocked(openhumanUpdateVoiceServerSettings)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ always_on_enabled: true })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,15 +19,18 @@ import {
|
||||
import { renderWithProviders } from '../../../../test/test-utils';
|
||||
import {
|
||||
openhumanGetVoiceServerSettings,
|
||||
openhumanUpdateVoiceServerSettings,
|
||||
openhumanVoiceSetProviders,
|
||||
openhumanVoiceStatus,
|
||||
type VoiceServerSettings,
|
||||
type VoiceStatus,
|
||||
} from '../../../../utils/tauriCommands';
|
||||
import type { ConfigSnapshot } from '../../../../utils/tauriCommands/config';
|
||||
import VoicePanel from '../VoicePanel';
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands', () => ({
|
||||
openhumanGetVoiceServerSettings: vi.fn(),
|
||||
openhumanUpdateVoiceServerSettings: vi.fn(),
|
||||
openhumanVoiceSetProviders: vi.fn(),
|
||||
openhumanVoiceStatus: vi.fn(),
|
||||
}));
|
||||
@@ -111,6 +114,7 @@ describe('VoicePanel', () => {
|
||||
min_duration_secs: 0.3,
|
||||
silence_threshold: 0.002,
|
||||
custom_dictionary: [],
|
||||
always_on_enabled: false,
|
||||
},
|
||||
voiceStatus: {
|
||||
stt_available: true,
|
||||
@@ -135,6 +139,12 @@ describe('VoicePanel', () => {
|
||||
result: { ...runtime.settings },
|
||||
logs: [],
|
||||
}));
|
||||
// The panel ignores the snapshot it returns; a minimal cast keeps the
|
||||
// mock typed without constructing a full ConfigSnapshot.
|
||||
vi.mocked(openhumanUpdateVoiceServerSettings).mockImplementation(async () => ({
|
||||
result: {} as unknown as ConfigSnapshot,
|
||||
logs: [],
|
||||
}));
|
||||
vi.mocked(openhumanVoiceStatus).mockImplementation(async () => ({ ...runtime.voiceStatus }));
|
||||
vi.mocked(openhumanVoiceSetProviders).mockImplementation(async update => {
|
||||
if (update.stt_provider) runtime.voiceStatus.stt_provider = update.stt_provider;
|
||||
@@ -496,4 +506,36 @@ describe('VoicePanel', () => {
|
||||
|
||||
await waitFor(() => expect(screen.getByText('core offline')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
// ─── Always-on listening toggle (Phase 2) ───────────────────────────────
|
||||
|
||||
it('persists the always-on toggle and flips aria-checked on click', async () => {
|
||||
renderWithProviders(<VoicePanel />, { initialEntries: ['/settings/voice'] });
|
||||
|
||||
const toggle = await screen.findByTestId('voice-always-on-toggle');
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
|
||||
fireEvent.click(toggle);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(vi.mocked(openhumanUpdateVoiceServerSettings)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ always_on_enabled: true })
|
||||
)
|
||||
);
|
||||
// Optimistic update reflects immediately.
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('reverts the toggle when the update RPC rejects', async () => {
|
||||
vi.mocked(openhumanUpdateVoiceServerSettings).mockRejectedValueOnce(new Error('rpc down'));
|
||||
|
||||
renderWithProviders(<VoicePanel />, { initialEntries: ['/settings/voice'] });
|
||||
|
||||
const toggle = await screen.findByTestId('voice-always-on-toggle');
|
||||
fireEvent.click(toggle);
|
||||
|
||||
// Optimistic on → then reverted back to off after the failure.
|
||||
await waitFor(() => expect(toggle).toHaveAttribute('aria-checked', 'false'));
|
||||
expect(vi.mocked(openhumanUpdateVoiceServerSettings)).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1390,6 +1390,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'عتبة الصمت (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'تُعامَل التسجيلات ذات الطاقة الأدنى من هذا الحد كصمت ويُتخطى فيها. كلما كانت القيمة أصغر، كان النظام أكثر حساسية.',
|
||||
'voice.debug.alwaysOn': 'الاستماع الدائم',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'أبقِ الميكروفون مفتوحًا وأرسل ما تقوله إلى الوكيل تلقائيًا دون مفتاح اختصار. يتوقف مؤقتًا عند قفل الشاشة.',
|
||||
'voice.providers.saved': 'تم حفظ موفري الصوت.',
|
||||
'voice.providers.failedToSave': 'فشل في حفظ موفري الصوت',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4719,6 +4722,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'إضافة كسياق إضافي',
|
||||
'runQueue.status': '{total} في الانتظار',
|
||||
'runQueue.cleared': 'تم مسح قائمة الانتظار',
|
||||
'notch.ready': 'جاهز',
|
||||
'notch.processing': 'جارٍ المعالجة…',
|
||||
'notch.listening': 'أستمع…',
|
||||
'notch.thinking': 'أفكر…',
|
||||
'notch.speaking': 'أتحدث…',
|
||||
'notch.transcribing': 'أفسّر…',
|
||||
'notch.executing': 'أنفّذ…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1418,6 +1418,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'সাইলেন্স থ্রেশহোল্ড (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'এই মানের নিচে শক্তির রেকর্ডিংগুলি নীরবতা হিসেবে গণ্য হয় এবং এড়িয়ে যাওয়া হয়। কম মান = আরও সংবেদনশীল।',
|
||||
'voice.debug.alwaysOn': 'সবসময় শোনা',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'মাইক্রোফোন খোলা রাখুন এবং আপনি যা বলেন তা হটকি ছাড়াই স্বয়ংক্রিয়ভাবে এজেন্টের কাছে পাঠান। স্ক্রিন লক হলে থেমে যায়।',
|
||||
'voice.providers.saved': 'ভয়েস প্রদানকারী সংরক্ষিত।',
|
||||
'voice.providers.failedToSave': 'ভয়েস প্রদানকারী সংরক্ষণ করতে ব্যর্থ',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4809,6 +4812,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'অতিরিক্ত প্রসঙ্গ হিসেবে যোগ করুন',
|
||||
'runQueue.status': '{total}টি সারিবদ্ধ',
|
||||
'runQueue.cleared': 'সারি পরিষ্কার করা হয়েছে',
|
||||
'notch.ready': 'প্রস্তুত',
|
||||
'notch.processing': 'প্রক্রিয়াকরণ চলছে…',
|
||||
'notch.listening': 'শুনছি…',
|
||||
'notch.thinking': 'ভাবছি…',
|
||||
'notch.speaking': 'বলছি…',
|
||||
'notch.transcribing': 'ট্রান্সক্রাইব করছি…',
|
||||
'notch.executing': 'চালাচ্ছি…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1459,6 +1459,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Ruheschwelle (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Aufnahmen mit Energie unterhalb dieses Wertes werden als Stille behandelt und übersprungen. Niedriger = empfindlicher.',
|
||||
'voice.debug.alwaysOn': 'Dauerhaftes Zuhören',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Hält das Mikrofon offen und sendet das Gesagte automatisch an den Agenten, ohne Tastenkürzel. Pausiert, wenn der Bildschirm gesperrt ist.',
|
||||
'voice.providers.saved': 'Sprachanbieter gespeichert.',
|
||||
'voice.providers.failedToSave': 'Sprachanbieter konnten nicht gespeichert werden.',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4944,6 +4947,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Als zusätzlichen Kontext hinzufügen',
|
||||
'runQueue.status': '{total} in der Warteschlange',
|
||||
'runQueue.cleared': 'Warteschlange geleert',
|
||||
'notch.ready': 'Bereit',
|
||||
'notch.processing': 'Wird verarbeitet…',
|
||||
'notch.listening': 'Höre zu…',
|
||||
'notch.thinking': 'Denke nach…',
|
||||
'notch.speaking': 'Spreche…',
|
||||
'notch.transcribing': 'Transkribiere…',
|
||||
'notch.executing': 'Führe aus…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1636,6 +1636,9 @@ const en: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Silence Threshold (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Recordings with energy below this are treated as silence and skipped. Lower = more sensitive.',
|
||||
'voice.debug.alwaysOn': 'Always-on listening',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Keep the microphone open and send what you say to the agent automatically, no hotkey. Pauses when the screen is locked.',
|
||||
'voice.providers.saved': 'Voice providers saved.',
|
||||
'voice.providers.failedToSave': 'Failed to save voice providers',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -5056,6 +5059,13 @@ const en: TranslationMap = {
|
||||
'runQueue.collectHint': 'Add as extra context',
|
||||
'runQueue.status': '{total} queued',
|
||||
'runQueue.cleared': 'Queue cleared',
|
||||
'notch.ready': 'Ready',
|
||||
'notch.processing': 'Processing…',
|
||||
'notch.listening': 'Listening…',
|
||||
'notch.thinking': 'Thinking…',
|
||||
'notch.speaking': 'Speaking…',
|
||||
'notch.transcribing': 'Transcribing…',
|
||||
'notch.executing': 'Executing…',
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -1453,6 +1453,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Umbral de silencio (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Las grabaciones con energía por debajo de este valor se tratan como silencio y se omiten. Menor = más sensible.',
|
||||
'voice.debug.alwaysOn': 'Escucha continua',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Mantén el micrófono abierto y envía lo que dices al agente automáticamente, sin atajo. Se pausa cuando la pantalla está bloqueada.',
|
||||
'voice.providers.saved': 'Proveedores de voz guardados.',
|
||||
'voice.providers.failedToSave': 'No se pudieron guardar los proveedores de voz',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4912,6 +4915,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Añadir como contexto adicional',
|
||||
'runQueue.status': '{total} en cola',
|
||||
'runQueue.cleared': 'Cola vaciada',
|
||||
'notch.ready': 'Listo',
|
||||
'notch.processing': 'Procesando…',
|
||||
'notch.listening': 'Escuchando…',
|
||||
'notch.thinking': 'Pensando…',
|
||||
'notch.speaking': 'Hablando…',
|
||||
'notch.transcribing': 'Transcribiendo…',
|
||||
'notch.executing': 'Ejecutando…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1457,6 +1457,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Seuil de silence (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
"Les enregistrements dont l'énergie est inférieure à ce seuil sont traités comme du silence et ignorés. Plus bas = plus sensible.",
|
||||
'voice.debug.alwaysOn': 'Écoute permanente',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Garde le microphone ouvert et envoie automatiquement ce que vous dites à l’agent, sans raccourci. Se met en pause lorsque l’écran est verrouillé.',
|
||||
'voice.providers.saved': 'Fournisseurs de voix enregistrés.',
|
||||
'voice.providers.failedToSave': 'Échec de la sauvegarde des fournisseurs vocaux',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4927,6 +4930,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Ajouter comme contexte supplémentaire',
|
||||
'runQueue.status': '{total} en attente',
|
||||
'runQueue.cleared': "File d'attente vidée",
|
||||
'notch.ready': 'Prêt',
|
||||
'notch.processing': 'Traitement…',
|
||||
'notch.listening': "J'écoute…",
|
||||
'notch.thinking': 'Je réfléchis…',
|
||||
'notch.speaking': 'Je parle…',
|
||||
'notch.transcribing': 'Transcription…',
|
||||
'notch.executing': "J'exécute…",
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1417,6 +1417,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'साइलेंस थ्रेशोल्ड (आरएमएस)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'इससे कम ऊर्जा वाली रिकॉर्डिंग को साइलेंस माना जाता है और छोड़ दिया जाता है। कम = अधिक संवेदनशील।',
|
||||
'voice.debug.alwaysOn': 'हमेशा-चालू सुनना',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'माइक्रोफ़ोन खुला रखें और आप जो कहते हैं वह बिना हॉटकी के स्वचालित रूप से एजेंट को भेजें। स्क्रीन लॉक होने पर रुक जाता है।',
|
||||
'voice.providers.saved': 'ध्वनि प्रदाता सहेजे गए.',
|
||||
'voice.providers.failedToSave': 'ध्वनि प्रदाताओं को सहेजने में विफल',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4816,6 +4819,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'अतिरिक्त संदर्भ के रूप में जोड़ें',
|
||||
'runQueue.status': '{total} कतार में',
|
||||
'runQueue.cleared': 'कतार साफ़ की गई',
|
||||
'notch.ready': 'तैयार',
|
||||
'notch.processing': 'प्रोसेस हो रहा है…',
|
||||
'notch.listening': 'सुन रहा हूं…',
|
||||
'notch.thinking': 'सोच रहा हूं…',
|
||||
'notch.speaking': 'बोल रहा हूं…',
|
||||
'notch.transcribing': 'ट्रांसक्राइब कर रहा हूं…',
|
||||
'notch.executing': 'चला रहा हूं…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1424,6 +1424,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Ambang Batas Senyap (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Rekaman dengan energi di bawah nilai ini dianggap sebagai keheningan dan dilewati. Lebih rendah = lebih sensitif.',
|
||||
'voice.debug.alwaysOn': 'Mendengarkan terus-menerus',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Biarkan mikrofon tetap aktif dan kirim ucapan Anda ke agen secara otomatis, tanpa pintasan. Berhenti sementara saat layar terkunci.',
|
||||
'voice.providers.saved': 'Penyedia suara disimpan.',
|
||||
'voice.providers.failedToSave': 'Gagal menyimpan penyedia suara',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4828,6 +4831,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Tambahkan sebagai konteks tambahan',
|
||||
'runQueue.status': '{total} dalam antrean',
|
||||
'runQueue.cleared': 'Antrean dikosongkan',
|
||||
'notch.ready': 'Siap',
|
||||
'notch.processing': 'Memproses…',
|
||||
'notch.listening': 'Mendengar…',
|
||||
'notch.thinking': 'Berpikir…',
|
||||
'notch.speaking': 'Berbicara…',
|
||||
'notch.transcribing': 'Mentranskrip…',
|
||||
'notch.executing': 'Mengeksekusi…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1446,6 +1446,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Soglia di silenzio (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Le registrazioni con energia inferiore a questa soglia vengono trattate come silenzio e saltate. Più basso = più sensibile.',
|
||||
'voice.debug.alwaysOn': 'Ascolto sempre attivo',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Mantieni il microfono aperto e invia automaticamente ciò che dici all’agente, senza scorciatoia. Si mette in pausa quando lo schermo è bloccato.',
|
||||
'voice.providers.saved': 'Fornitori di servizi vocali salvati.',
|
||||
'voice.providers.failedToSave': 'Impossibile salvare i provider vocali',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4902,6 +4905,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Aggiungi come contesto extra',
|
||||
'runQueue.status': '{total} in coda',
|
||||
'runQueue.cleared': 'Coda svuotata',
|
||||
'notch.ready': 'Pronto',
|
||||
'notch.processing': 'Elaborazione…',
|
||||
'notch.listening': 'Ascolto…',
|
||||
'notch.thinking': 'Penso…',
|
||||
'notch.speaking': 'Parlo…',
|
||||
'notch.transcribing': 'Trascrizione…',
|
||||
'notch.executing': 'Eseguendo…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1403,6 +1403,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': '무음 임계값(RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'이 값보다 에너지가 낮은 녹음은 무음으로 처리되어 건너뜁니다. 낮을수록 더 민감합니다.',
|
||||
'voice.debug.alwaysOn': '상시 청취',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'단축키 없이 마이크를 계속 열어 두고 말한 내용을 자동으로 에이전트에 보냅니다. 화면이 잠기면 일시 중지됩니다.',
|
||||
'voice.providers.saved': '음성 제공업체가 저장되었습니다.',
|
||||
'voice.providers.failedToSave': '음성 제공자를 저장하지 못했습니다.',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4764,6 +4767,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': '추가 컨텍스트로 추가',
|
||||
'runQueue.status': '{total}개 대기 중',
|
||||
'runQueue.cleared': '대기열이 비워졌습니다',
|
||||
'notch.ready': '준비됨',
|
||||
'notch.processing': '처리 중…',
|
||||
'notch.listening': '듣는 중…',
|
||||
'notch.thinking': '생각 중…',
|
||||
'notch.speaking': '말하는 중…',
|
||||
'notch.transcribing': '변환 중…',
|
||||
'notch.executing': '실행 중…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1438,6 +1438,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Próg ciszy (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Nagrania z energią poniżej tego progu są traktowane jako cisza i pomijane. Niżej = bardziej czułe.',
|
||||
'voice.debug.alwaysOn': 'Ciągłe nasłuchiwanie',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Utrzymuje mikrofon włączony i automatycznie wysyła to, co mówisz, do agenta, bez skrótu. Wstrzymuje się, gdy ekran jest zablokowany.',
|
||||
'voice.providers.saved': 'Zapisano dostawców głosu.',
|
||||
'voice.providers.failedToSave': 'Nie udało się zapisać dostawców głosu',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4893,6 +4896,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Dodaj jako dodatkowy kontekst',
|
||||
'runQueue.status': '{total} w kolejce',
|
||||
'runQueue.cleared': 'Kolejka wyczyszczona',
|
||||
'notch.ready': 'Gotowe',
|
||||
'notch.processing': 'Przetwarzanie…',
|
||||
'notch.listening': 'Słucham…',
|
||||
'notch.thinking': 'Myślę…',
|
||||
'notch.speaking': 'Mówię…',
|
||||
'notch.transcribing': 'Transkrybuję…',
|
||||
'notch.executing': 'Wykonuję…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1453,6 +1453,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Limite de Silêncio (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Gravações com energia abaixo deste valor são tratadas como silêncio e ignoradas. Menor = mais sensível.',
|
||||
'voice.debug.alwaysOn': 'Escuta contínua',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Mantém o microfone aberto e envia o que você diz ao agente automaticamente, sem atalho. Pausa quando a tela está bloqueada.',
|
||||
'voice.providers.saved': 'Provedores de voz salvos.',
|
||||
'voice.providers.failedToSave': 'Falha ao salvar provedores de voz',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4897,6 +4900,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Adicionar como contexto extra',
|
||||
'runQueue.status': '{total} na fila',
|
||||
'runQueue.cleared': 'Fila limpa',
|
||||
'notch.ready': 'Pronto',
|
||||
'notch.processing': 'Processando…',
|
||||
'notch.listening': 'Ouvindo…',
|
||||
'notch.thinking': 'Pensando…',
|
||||
'notch.speaking': 'Falando…',
|
||||
'notch.transcribing': 'Transcrevendo…',
|
||||
'notch.executing': 'Executando…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1431,6 +1431,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.silenceThreshold': 'Порог тишины (RMS)',
|
||||
'voice.debug.silenceThresholdDesc':
|
||||
'Записи с энергией ниже этого значения считаются тишиной и пропускаются. Меньше = чувствительнее.',
|
||||
'voice.debug.alwaysOn': 'Постоянное прослушивание',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'Держит микрофон включённым и автоматически отправляет сказанное агенту без горячей клавиши. Приостанавливается при блокировке экрана.',
|
||||
'voice.providers.saved': 'Поставщики голосовой связи сохранены.',
|
||||
'voice.providers.failedToSave': 'Не удалось сохранить поставщиков голосовой связи.',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4859,6 +4862,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': 'Добавить как дополнительный контекст',
|
||||
'runQueue.status': '{total} в очереди',
|
||||
'runQueue.cleared': 'Очередь очищена',
|
||||
'notch.ready': 'Готово',
|
||||
'notch.processing': 'Обработка…',
|
||||
'notch.listening': 'Слушаю…',
|
||||
'notch.thinking': 'Думаю…',
|
||||
'notch.speaking': 'Говорю…',
|
||||
'notch.transcribing': 'Транскрибирую…',
|
||||
'notch.executing': 'Выполняю…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1340,6 +1340,9 @@ const messages: TranslationMap = {
|
||||
'voice.debug.minimumRecordingSeconds': '最短录音秒数',
|
||||
'voice.debug.silenceThreshold': '静音阈值 (RMS)',
|
||||
'voice.debug.silenceThresholdDesc': '能量低于此值的录音将被视为静音并跳过。值越低,灵敏度越高。',
|
||||
'voice.debug.alwaysOn': '常驻聆听',
|
||||
'voice.debug.alwaysOnDesc':
|
||||
'保持麦克风开启,无需快捷键即可自动将你说的话发送给智能体。屏幕锁定时暂停。',
|
||||
'voice.providers.saved': '语音提供商已保存。',
|
||||
'voice.providers.failedToSave': '无法保存语音提供商',
|
||||
'voice.providers.ellipsis': '…',
|
||||
@@ -4578,6 +4581,13 @@ const messages: TranslationMap = {
|
||||
'runQueue.collectHint': '作为额外上下文添加',
|
||||
'runQueue.status': '已排队 {total} 条',
|
||||
'runQueue.cleared': '队列已清空',
|
||||
'notch.ready': '就绪',
|
||||
'notch.processing': '处理中…',
|
||||
'notch.listening': '聆听中…',
|
||||
'notch.thinking': '思考中…',
|
||||
'notch.speaking': '说话中…',
|
||||
'notch.transcribing': '转录中…',
|
||||
'notch.executing': '执行中…',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface VoiceServerSettings {
|
||||
silence_threshold: number;
|
||||
/** Custom vocabulary words to bias whisper toward (names, technical terms). */
|
||||
custom_dictionary: string[];
|
||||
/** Phase 2: continuous always-on listening (no hotkey). Opt-in. */
|
||||
always_on_enabled: boolean;
|
||||
}
|
||||
|
||||
export async function openhumanVoiceStatus(): Promise<VoiceStatus> {
|
||||
@@ -106,6 +108,7 @@ export async function openhumanUpdateVoiceServerSettings(update: {
|
||||
min_duration_secs?: number;
|
||||
silence_threshold?: number;
|
||||
custom_dictionary?: string[];
|
||||
always_on_enabled?: boolean;
|
||||
}): Promise<CommandResponse<ConfigSnapshot>> {
|
||||
return await callCoreRpc<CommandResponse<ConfigSnapshot>>({
|
||||
method: 'openhuman.config_update_voice_server_settings',
|
||||
|
||||
Reference in New Issue
Block a user