import { IsBase64, IsIn, IsInt, IsString, MaxLength, Min, } from 'class-validator'; import { ErrorCode } from '../../../common/errors/error-codes'; /** Containers OpenRouter's transcription endpoint accepts, and MediaRecorder can produce. */ export const VOICE_AUDIO_FORMATS = [ 'webm', 'mp4', 'm4a', 'aac', 'ogg', 'wav', 'mp3', 'flac', ] as const; export type VoiceAudioFormat = (typeof VOICE_AUDIO_FORMATS)[number]; /** Locales the app ships; a profile still has to be configured for one to be usable. */ export const VOICE_LOCALES = ['en', 'fa', 'nl'] as const; export class ExtractVoiceDto { /** * Base64 audio, no data: prefix. Capped well above a 2-minute opus clip (~400 KB) but * far below OpenRouter's 25 MB ceiling, so an oversized upload is rejected before it * costs a vendor call. */ @IsString() @IsBase64() // Both constraints name their own code. Left to the default mapping, `maxLength` falls // through to VALIDATION_FIELD_REQUIRED and `isIn` resolves to // VALIDATION_LANGUAGE_INVALID — so an oversized recording told the clinician a field // was missing, and an unsupported container told them their language was invalid. @MaxLength(8_000_000, { message: ErrorCode.VOICE_CLIP_TOO_LONG }) audio: string; @IsIn(VOICE_AUDIO_FORMATS, { message: ErrorCode.VOICE_UNSUPPORTED_FORMAT }) format: VoiceAudioFormat; /** * The clinician's IANA zone. The server derives "today" from it rather than trusting a * client-supplied date, which is what relative deadlines resolve against. */ @IsString() @MaxLength(64) timeZone: string; /** * Recording length as measured by the client. * * Required, not optional: an optional value means omitting it bypasses * VOICE_MAX_RECORDING_MS entirely, which would make the cap advisory. */ @IsInt() @Min(0) durationMs: number; /** * The locale the clinician is actually speaking, as the UI offered the microphone. * * Sent explicitly rather than read from `user.language`: the two can diverge (a * bookmarked /fa/ URL, a language toggle whose save failed), and a mismatch would * transcribe Persian with an English hint and anchor "next Thursday" to the wrong * week start. Gating the button and resolving the request must agree by construction. */ @IsIn(VOICE_LOCALES) locale: string; }