Przejdź do głównej zawartości

Audio & Transcription

Na tej stronie
KomendaOpisTyp
save_audio_fileZapisuje plik audiowrite
process_uploaded_audio_filePrzetwarza uploadowany plikwrite
serve_audio_fileZwraca zawartość pliku audioquery
link_recording_to_visitPowiązuje nagranie z wizytąwrite
cleanup_old_recordingsUsuwa stare nagraniawrite
convert_to_pcm_stereoKonwertuje do PCM stereowrite
KomendaOpisTyp
transcribe_audio_fileTranskrybuje plik audiowrite
transcribe_recording_nowTranskrypcja z recording_idwrite
transcribe_and_save_to_visitTranskrybuje i zapisuje do wizytywrite
queue_transcription_jobKolejkuje job transkrypcjiwrite
get_transcription_job_statusStatus jobu transkrypcjiquery
cleanup_old_transcription_jobsCzyści stare jobywrite
unified_ai_transcribe_fileTranskrypcja przez Unified AIwrite
KomendaOpisTyp
list_pending_audio_capturesLista oczekujących nagrańquery
attach_capture_to_visitDołącza nagranie do wizytywrite
discard_pending_captureOdrzuca oczekujące nagraniewrite

flowchart TB
subgraph Input["Źródła audio"]
LIVE[🎤 Live Recording]
UPLOAD[📁 File Upload]
end
subgraph Processing["Przetwarzanie"]
SAVE[save_audio_file]
PROCESS[process_uploaded_audio_file]
LINK[link_recording_to_visit]
end
subgraph Transcription["Transkrypcja"]
NOW[transcribe_recording_now]
QUEUE[queue_transcription_job]
UNIFIED[unified_ai_transcribe_file]
SAVE_VISIT[transcribe_and_save_to_visit]
end
subgraph Storage["Storage"]
DB[(SQLite)]
FILES[Audio Files]
end
LIVE --> SAVE
UPLOAD --> PROCESS
SAVE --> LINK
PROCESS --> LINK
LINK --> NOW
NOW -->|Fallback| QUEUE
QUEUE -->|Fallback| UNIFIED
NOW --> DB
SAVE_VISIT --> DB

sequenceDiagram
participant UI as Frontend
participant Helper as transcriptionHelpers
participant Tauri as Tauri Backend
participant AI as Unified AI
participant DB as SQLite
UI->>Helper: transcribeRecordingWithFallback(path, recordingId, visitId)
alt Ma recordingId
Helper->>Tauri: transcribe_recording_now(recordingId, visitId)
alt Success
Tauri-->>Helper: TranscriptionResult
else Error
Helper->>Tauri: queue_transcription_job(recordingId, visitId)
Tauri-->>Helper: jobId
Helper->>AI: unified_ai_transcribe_file(path)
AI-->>Helper: transcript
end
else Brak recordingId
Helper->>AI: unified_ai_transcribe_file(path)
AI-->>Helper: transcript
end
Helper-->>UI: TranscriptionResult

Zapisuje dane audio do pliku:

// Frontend (src/types/api/audio.ts)
interface SaveAudioRequest {
audio_data: Uint8Array;
visit_id?: string;
}
const result = await safeInvoke<VistaSavedRecording>(
'save_audio_file',
withSessionPayload({ audio_data, visit_id })
);
interface VistaSavedRecording {
recordingId: string;
path: string;
status: string;
visitId?: string;
sizeBytes: number;
durationMs?: number;
}

Użycie:

  • useAudioRecording - zapis nagrania przed transkrypcją
  • useAudioTranscription.persistBlob - zapis Blob → plik
  • useWorkspaceData - szybkie nagrania do wizyt

Przetwarza uploadowany plik audio:

const result = await safeInvoke<VistaSavedRecording>(
'process_uploaded_audio_file',
withSessionPayload({
source_path: filePath,
visit_id: visitId
})
);
// Użycie:
// - useAudioRecording.uploadController - upload istniejącego pliku
// - useVisitCreation.processAudioData - upload przy tworzeniu wizyty

Zwraca zawartość pliku audio (do odtwarzania):

const audioData = await safeInvoke<Uint8Array>(
'serve_audio_file',
withSessionPayload({ file_path: path })
);
// Użycie:
// - useEmergencyRecordingBridge - odsłuch nagrania awaryjnego
// - useWorkspaceData - odtwarzacz nagrań

Powiązuje nagranie z wizytą:

await safeInvoke(
'link_recording_to_visit',
withSessionPayload({
recording_id: recordingId,
visit_id: visitId
})
);
// Aktualizuje: recordings.visit_id = visitId

Usuwa stare nagrania wg retencji:

await safeInvoke(
'cleanup_old_recordings',
withSessionPayload({ max_age_days: 30 })
);
// Usuwa nagrania starsze niż max_age_days
// Domyślna retencja: user_preferences.audio_retention_days

Konwertuje audio do formatu PCM stereo:

const pcmData = await safeInvoke<Uint8Array>(
'convert_to_pcm_stereo',
{ audio_data: inputData }
);
// Używane wewnętrznie w pipeline'ach audio

Podstawowa transkrypcja pliku:

const transcript = await safeInvoke<string>(
'transcribe_audio_file',
withSessionPayload({ file_path: audioPath })
);
// Zwraca: string z tekstem transkrypcji

Transkrypcja z recording_id (preferowana metoda):

const result = await safeInvoke<RecordingTranscriptionResult>(
'transcribe_recording_now',
withSessionPayload({
recording_id: recordingId,
visit_id: visitId, // Opcjonalne
source_type: 'live' // 'live' | 'upload'
})
);
interface RecordingTranscriptionResult {
recordingId: string;
visitId?: string;
transcript: string;
status: 'success' | 'error';
error?: string;
}

Transkrybuje i od razu zapisuje do wizyty:

await safeInvoke(
'transcribe_and_save_to_visit',
withSessionPayload({
file_path: audioPath,
visit_id: visitId,
source_type: 'live' // lub 'upload'
})
);
// Backend:
// 1. Transkrybuje plik
// 2. UPDATE visits SET transcript = ..., raw_transcript = ...
// 3. Emituje event: transcription_completed

Użycie:

  • EmergencyRecordingButton - nagranie awaryjne
  • VisitSupportPanel - transkrypcja w panelu wizyty
  • useVisitCreation - tworzenie wizyty z nagraniem
  • useWorkspaceData - transkrypcja w workspace

Kolejkuje job transkrypcji (dla długich nagrań):

const jobId = await safeInvoke<string>(
'queue_transcription_job',
withSessionPayload({
recording_id: recordingId,
visit_id: visitId,
source_type: sourceType
})
);
// Job jest przetwarzany w tle
// Status można sprawdzić przez get_transcription_job_status

Sprawdza status jobu:

const status = await safeInvoke<JobStatus>(
'get_transcription_job_status',
withSessionPayload({ job_id: jobId })
);
interface JobStatus {
job_id: string;
status: 'queued' | 'processing' | 'completed' | 'failed';
progress?: number; // 0-100
result?: string; // Transkrypt przy completed
error?: string; // Błąd przy failed
}

Czyści stare joby:

await safeInvoke(
'cleanup_old_transcription_jobs',
withSessionPayload({ max_age_hours: 24 })
);

Transkrypcja przez Unified AI (fallback):

// src/types/api/audio.ts - unifiedAiApi
const transcript = await safeInvoke<string>(
'unified_ai_transcribe_file',
withSessionPayload({ file_path: audioPath })
);
// Używane jako fallback gdy transcribe_recording_now zawiedzie

Frontend używa helpera z automatycznym fallback:

src/helpers/transcriptionHelpers.ts
export async function transcribeRecordingWithFallback(
filePath: string,
recordingId?: string,
visitId?: string,
sourceType: 'live' | 'upload' = 'upload'
): Promise<TranscriptionResult> {
// 1. Jeśli mamy recordingId, próbuj transcribe_recording_now
if (recordingId) {
try {
return await audioApi.transcribeRecording(recordingId, visitId, sourceType);
} catch (error) {
console.warn('transcribe_recording_now failed, trying fallback');
// 2. Kolejkuj job (opcjonalnie)
if (visitId) {
await audioApi.queueTranscription(recordingId, visitId, sourceType);
}
}
}
// 3. Fallback: Unified AI
const transcript = await unifiedAiApi.transcribeFile(filePath);
return { transcript, status: 'success' };
}

Nagrania oczekujące na powiązanie z wizytą:

flowchart LR
RECORD[🎤 Nagranie] --> PENDING[Pending Captures]
PENDING -->|Attach| VISIT[Wizyta]
PENDING -->|Discard| DELETED[Usunięte]
src/hooks/audio/usePendingCaptures.ts
const captures = await safeInvoke<PendingCaptureSummary[]>(
'list_pending_audio_captures',
withSessionPayload({ limit: 10 })
);
interface PendingCaptureSummary {
recording_id: string;
file_path: string;
duration_ms?: number;
size_bytes: number;
created_at: string;
status: 'pending' | 'processing';
}

Dołącza pending capture do wizyty:

await safeInvoke(
'attach_capture_to_visit',
withSessionPayload({
recording_id: recordingId,
visit_id: visitId
})
);
// Po attach:
// 1. recording.visit_id = visitId
// 2. recording.status = 'attached'
// 3. Opcjonalnie: transkrypcja

Odrzuca (usuwa) pending capture:

await safeInvoke(
'discard_pending_capture',
withSessionPayload({ recording_id: recordingId })
);
// Usuwa nagranie i powiązany plik

src/hooks/audio/usePendingCaptures.ts
const {
captures, // PendingCaptureSummary[]
loading,
error,
refresh, // Odśwież listę
attachCapture, // (recordingId, visitId) => Promise
discardCapture, // (recordingId) => Promise
} = usePendingCaptures({
limit: 10,
pollIntervalMs: 5000 // Polling co 5s
});

Główny hook do nagrywania:

src/hooks/audio/useAudioRecording.ts
const {
// Stan
isRecording,
isPaused,
duration,
audioLevel,
// Kontrola
startRecording,
stopRecording,
pauseRecording,
resumeRecording,
// Upload
uploadFile,
// Wynik
recordingResult, // VistaSavedRecording | null
transcriptionResult, // TranscriptionResult | null
// Błędy
error,
} = useAudioRecording({
visitId?: string,
autoTranscribe?: boolean,
onTranscriptionComplete?: (result) => void,
});

Prostszy hook do transkrypcji:

src/hooks/audio/useAudioTranscription.ts
const {
isTranscribing,
progress,
result,
error,
transcribeBlob, // (blob: Blob, visitId?) => Promise
transcribeFile, // (filePath: string, visitId?) => Promise
persistBlob, // (blob: Blob, visitId?) => Promise<path>
cancel,
} = useAudioTranscription();

Vista wspiera następujące formaty:

FormatExtensionUwagi
WebM.webmDomyślny dla live recording
MP3.mp3Uploadowane pliki
WAV.wavUploadowane pliki
FLAC.flacUploadowane pliki
MP4.mp4Audio track z video

Transkrypcja używa providera z automatycznym failover:

flowchart TD
AUDIO[Audio] --> CHECK{Provider available?}
CHECK -->|1| LIBRAXIS[LibraxisAI]
LIBRAXIS -->|Fail| MLX
CHECK -->|2| MLX[MLX Whisper]
MLX -->|Fail| OPENAI
CHECK -->|3| OPENAI[OpenAI Whisper]
LIBRAXIS --> RESULT[Transcript]
MLX --> RESULT
OPENAI --> RESULT
ProviderDiaryzacjaLatencyUwagi
LibraxisAI~2s/minPrimary
MLX Whisper~3s/minApple Silicon only
OpenAI Whisper~4s/minFallback

EventPayloadKiedy
transcription_completed{ visit_id }Po zakończeniu transkrypcji
transcription_progress{ progress, job_id }Postęp transkrypcji
recording_saved{ recording_id, path }Po zapisaniu nagrania
pending_capture_added{ recording_id }Nowe pending capture

KodOpisRozwiązanie
recording_not_foundNagranie nie istniejeSprawdź recording_id
audio_file_not_foundPlik audio nie istniejeSprawdź ścieżkę
audio_too_shortNagranie < 1 sekundaNagraj dłużej
audio_corruptedNie można zdekodowaćNagraj ponownie
transcription_timeoutSTT timeoutSpróbuj krótsze audio
stt_unavailableBrak dostępnego STTSprawdź połączenie
unsupported_formatNieobsługiwany formatUżyj WebM/MP3/WAV