feat: Schlüssel und gemeinsamer Kern für Sprachnotizen (T7 L2 Tasks 5+6)
Task 5: Schlüsselbund-Zweck voice (VOICE_ENCRYPTION_KEY), AES-256-GCM- Hilfen für Text und Bytes mit AAD capture:<id>:<art>, voice-config (Modell, Tageslimit, Aufnahme, Whisper-Grösse) und anthropic-config (nie geloggt, nur worker/all), Startprüfung verlangt den Schlüssel in Produktion in jeder Rolle, rekey versiegelt offene Sprachnotizen neu (Audio einzeln gelesen, CAS mit Status), Secret-Vorlagen und ensure-secrets.sh ergänzt. Task 6: Text-Normalisierung und Bereinigung (Steuer- und Formatzeichen, Codepoint-Kürzung), Zitatprüfung mit Dezimalzahlen als ein Wort (Ruling 7), Felderliste passend zu CreateInspectionDto, versiegeltes Transkript mit Grenzen, Fachwörter und Skalen mit Paritätstests. Rollout: Ein Image mit diesem Stand startet in Produktion nur mit VOICE_ENCRYPTION_KEY in apicore-backend-secret (ARCHITECTURE Annahme 22). Gates: Backend tsc, eslint, 922 Unit-Tests, e2e 100 Suites / 1004 Tests. Refs #27 #28 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
2a4dc868d9
commit
9795f98006
27 files changed
+2212
-31
No files matched your search
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Voice settings from the environment. Model ids live here and nowhere else
|
||||
* (spec 7.1): a change of model is a change of this list and of VOICE_MODEL.
|
||||
*/
|
||||
export const VOICE_MODELS = [
|
||||
'claude-sonnet-5-5',
|
||||
'claude-haiku-5-5',
|
||||
'claude-opus-5-5',
|
||||
] as const;
|
||||
export type VoiceModel = (typeof VOICE_MODELS)[number];
|
||||
|
||||
export const DEFAULT_VOICE_MODEL: VoiceModel = 'claude-sonnet-5-5';
|
||||
|
||||
export const WHISPER_MODELS = ['small', 'medium'] as const;
|
||||
export type WhisperModel = (typeof WHISPER_MODELS)[number];
|
||||
|
||||
const DEFAULT_DAILY_EXTRACTIONS = 60;
|
||||
const MIN_DAILY_EXTRACTIONS = 1;
|
||||
const MAX_DAILY_EXTRACTIONS = 10000;
|
||||
|
||||
export interface VoiceConfig {
|
||||
model: VoiceModel;
|
||||
/** Operation-wide limit of Claude calls per UTC day. */
|
||||
dailyExtractions: number;
|
||||
/** Switch that shows or hides audio recording in the app (ConfigMap). */
|
||||
recordingEnabled: boolean;
|
||||
whisperModel: WhisperModel;
|
||||
}
|
||||
|
||||
function trimmed(value: string | undefined): string | null {
|
||||
const text = value?.trim();
|
||||
return text ? text : null;
|
||||
}
|
||||
|
||||
function oneOf<T extends string>(
|
||||
name: string,
|
||||
raw: string | undefined,
|
||||
allowed: readonly T[],
|
||||
fallback: T,
|
||||
): T {
|
||||
const value = trimmed(raw);
|
||||
if (value === null) {
|
||||
return fallback;
|
||||
}
|
||||
const match = allowed.find((each) => each === value);
|
||||
if (match === undefined) {
|
||||
throw new Error(`${name} must be one of ${allowed.join(', ')}`);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
function dailyExtractions(raw: string | undefined): number {
|
||||
const value = trimmed(raw);
|
||||
if (value === null) {
|
||||
return DEFAULT_DAILY_EXTRACTIONS;
|
||||
}
|
||||
const number = /^\d+$/.test(value) ? Number(value) : NaN;
|
||||
if (
|
||||
!Number.isInteger(number) ||
|
||||
number < MIN_DAILY_EXTRACTIONS ||
|
||||
number > MAX_DAILY_EXTRACTIONS
|
||||
) {
|
||||
throw new Error(
|
||||
`VOICE_DAILY_EXTRACTIONS must be a whole number from ${MIN_DAILY_EXTRACTIONS} to ${MAX_DAILY_EXTRACTIONS}`,
|
||||
);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function recordingEnabled(raw: string | undefined): boolean {
|
||||
const value = trimmed(raw)?.toLowerCase() ?? null;
|
||||
if (value === null || value === 'false') {
|
||||
return false;
|
||||
}
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
throw new Error('VOICE_RECORDING_ENABLED must be "true" or "false"');
|
||||
}
|
||||
|
||||
/** Invalid values fail with the variable name (never a secret): startup refuses them. */
|
||||
export function voiceConfig(env: NodeJS.ProcessEnv = process.env): VoiceConfig {
|
||||
return {
|
||||
model: oneOf(
|
||||
'VOICE_MODEL',
|
||||
env.VOICE_MODEL,
|
||||
VOICE_MODELS,
|
||||
DEFAULT_VOICE_MODEL,
|
||||
),
|
||||
dailyExtractions: dailyExtractions(env.VOICE_DAILY_EXTRACTIONS),
|
||||
recordingEnabled: recordingEnabled(env.VOICE_RECORDING_ENABLED),
|
||||
whisperModel: oneOf(
|
||||
'WHISPER_MODEL',
|
||||
env.WHISPER_MODEL,
|
||||
WHISPER_MODELS,
|
||||
'small',
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user