feat(voice): self-hosted speech recognition via Whisper (STT_*)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m49s

- STT_URL switches voice input to an OpenAI-compatible Whisper server
  (e.g. speaches); without it the browser's recognition stays. Configured
  by environment only, with limits for upload size, parallel jobs, hourly
  budget per user and timeout. The demo always uses the browser.
- POST /voice/transcribe forwards recordings and returns the text; nothing
  is stored. On startup MindSift waits for the server and asks it to
  download the model if missing.
- Admin card shows the active mode and server state, with a test recording.
- docker-compose: optional "whisper" service (speaches 0.8.3 CPU, profile
  "whisper", models in ./whisper).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 22:12:57 +01:00
1 parent 6a5e82431f
commit 7801cd13b9
18 files changed
+816 -6

No files matched your search

+21
View File
@@ -35,6 +35,7 @@ import {
TagSummary,
User,
UserInvite,
VoiceAdminStatus,
WSMessage,
} from './types';
import { extractHashtags } from './hashtags';
@@ -1047,6 +1048,26 @@ export const api = {
await request(`/api/v1/admin/invites/${id}`, { method: 'DELETE' });
},
async adminVoiceStatus(): Promise<VoiceAdminStatus> {
return request<VoiceAdminStatus>('/api/v1/admin/voice');
},
// Sends a recording to the own Whisper server (voice mode "server") and returns the text
async transcribe(audio: Blob): Promise<string> {
const token = getToken();
const ext = audio.type.includes('mp4') ? 'm4a' : audio.type.includes('ogg') ? 'ogg' : 'webm';
const form = new FormData();
form.append('file', audio, `aufnahme.${ext}`);
const res = await fetch('/api/v1/voice/transcribe', {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: form,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Spracherkennung fehlgeschlagen (HTTP ${res.status})`);
return data.text || '';
},
async adminGetSettings(): Promise<SystemSettings> {
return request<SystemSettings>('/api/v1/admin/settings');
},