/** * Audio file utilities: voice IDs, filename conventions, and file listing. */ import { join } from '@std/path' import { Locale } from '$/enums.ts' /** Azure Neural TTS voice ID used per locale. */ export const VOICE_IDS: Record = { [Locale.zh_CN]: 'zh-CN-XiaoxiaoNeural', [Locale.zh_HK]: 'zh-HK-WanLungNeural', [Locale.zh_TW]: 'zh-TW-YunJheNeural', [Locale.ja]: 'ja-JP-NanamiNeural', } /** * Returns the audio filename for a given subject id and locale. * When col and reading are both provided, appends `_{col}_{reading}` to disambiguate * subjects with multiple readings (e.g. polyphonic characters). */ export function getFilename(id: string, locale: Locale, col?: string, reading?: string): string { const base = `${id}_${locale.replace('_', '-')}_${VOICE_IDS[locale]}` if (col && reading) return `${base}_${col}_${reading}.mp3` return `${base}.mp3` } /** * Parses an audio filename into its components. * Handles both formats: * {id}_{locale}_{voiceId}.mp3 * {id}_{locale}_{voiceId}_{col}_{reading}.mp3 * Returns null if the filename doesn't match. */ export function parseAudioFilename( filename: string, ): { id: string; locale: string; voiceId: string; col?: string; reading?: string } | null { const parts = filename.replace('.mp3', '').split('_') if (parts.length < 3) return null const [id, localeHyphen, voiceId, col, reading] = parts const locale = localeHyphen.replace('-', '_') return { id, locale, voiceId, col, reading } } /** * Returns all existing audio filenames (not full paths) for the given locales. * Skips locales whose audio directories don't exist yet (e.g. before first audio run). */ export function listAudioFiles(locales: string[] = ['zh_CN', 'zh_HK', 'zh_TW', 'ja']): string[] { return locales .filter((locale) => locale !== 'tmp') .flatMap((locale) => { try { return Array.from(Deno.readDirSync(join('www/static/gen/audio', locale))) } catch { return [] } }) .filter(({ name }) => /.*\.mp3$/.test(name)) .map((file) => file.name) }