All files / web/src/lib/audio clipHash.ts

100% Statements 36/36
100% Branches 8/8
100% Functions 3/3
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 371x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 228x 228x 228x 1x 1x 1x 1x 1x 1x 1x 1x 199x 199x 199x 199x 199x 1x 1x 1x 1x 1x 1x 1x 13x 13x  
import { fnv1a32 } from '@/lib/fnv1a'
import type { TtsSay } from './TtsAudioManager'
 
/**
 * Resolve the canonical text from a say map.
 * Priority: say['en-US'] > say['en'] > first value > ''
 *
 * This deterministic resolution means adding lower-priority translations
 * (e.g. 'es', 'ja') does NOT change the canonical text.
 */
export function resolveCanonicalText(say: Record<string, string> | undefined | null): string {
  if (!say) return ''
  return say['en-US'] ?? say['en'] ?? Object.values(say)[0] ?? ''
}
 
/**
 * Compute a deterministic clip ID from say text + tone.
 * Returns 'h-' + 8 hex chars, e.g. 'h-a3f2b1c0'
 *
 * Hash input: resolveCanonicalText(say) + '\0' + tone
 */
export function computeClipHash(say: TtsSay, tone: string): string {
  const canonical = resolveCanonicalText(say)
  const hashInput = canonical + '\0' + tone
  const hash = fnv1a32(hashInput)
  return 'h-' + hash.toString(16).padStart(8, '0')
}
 
const HASH_CLIP_ID_RE = /^h-[0-9a-f]{8}$/
 
/**
 * Returns true if clipId matches the hash-based format: 'h-' + 8 hex chars.
 */
export function isHashClipId(clipId: string): boolean {
  return HASH_CLIP_ID_RE.test(clipId)
}