import type { TranscriptionProvider } from "@/server/ai/providers"; import { getTranscriptionProvider } from "@/server/ai/transcription/openai-compatible"; import { writeAuditLog } from "@/server/audit"; import { ServiceError, type ServiceCtx } from "@/server/services/context"; import { sniffMime } from "@/server/services/field/mime"; import { assertLotseChatUsable } from "./access"; /** * Microphone key of the Lotse chat (lane L16): audio → text via the existing transcription provider * (L9, Whisper-compatible). The audio is NOT stored — the transcript goes back into the input field * where the technician can edit it before sending. Recorded as AiGeneration (kind `transcription`, * entityType `lotse_chat`) without audio or transcript content, like the L9 voice note transcription. */ export const CHAT_AUDIO_MAX_BYTES = 10 * 1024 * 1024; export type ChatTranscriptionDeps = { provider: TranscriptionProvider | null }; export const defaultChatTranscriptionDeps = (): ChatTranscriptionDeps => ({ provider: getTranscriptionProvider() }); export async function transcribeChatAudio(ctx: ServiceCtx, bytes: Buffer, deps: ChatTranscriptionDeps = defaultChatTranscriptionDeps()): Promise<{ text: string }> { await assertLotseChatUsable(ctx); if (!deps.provider) throw new ServiceError("invalid", "transcription not configured", { reason: "not_configured" }); if (bytes.byteLength === 0 || bytes.byteLength > CHAT_AUDIO_MAX_BYTES) throw new ServiceError("invalid", "audio size", { reason: "invalid" }); const sniffed = sniffMime(bytes); if (!sniffed || sniffed.kind !== "audio") throw new ServiceError("invalid", "not an audio file", { reason: "invalid" }); let text: string; let meta; try { const res = await deps.provider.transcribe({ bytes, mimeType: sniffed.mime, language: "de" }); text = res.text.trim(); meta = res.meta; } catch (err) { console.error("[lotse-chat] transcription failed:", (err as Error).message); throw new ServiceError("conflict", "transcription failed", { reason: "provider_failed" }); } const generation = await ctx.db.aiGeneration.create({ data: { tenantId: ctx.tenantId, kind: "transcription", provider: meta.provider, model: meta.model, entityType: "lotse_chat", entityId: null, input: undefined, output: undefined, createdById: ctx.userId }, }); await writeAuditLog({ tenantId: ctx.tenantId, actorId: ctx.userId, action: "create", entity: "ai_generation", entityId: generation.id, after: { kind: "transcription", source: "lotse_chat", bytes: bytes.byteLength } }); return { text }; }