import { stateFile } from "./paths"; import { readJson, writeJsonAtomic, withStateLock } from "./state"; // The word selector's vocabulary. // // This replaces the free-text "other" note, which was the only way to say what a // clip actually contained and was useless downstream: one 284-clip pass produced // "do uh", "to uh", "when uhh", "uhh literally", "duuhh", "uh-huh" and three // separate clean "and"s -- all prose, none of it filterable by the arranger. // // CURATED comes first because it covers most clips in one keystroke. COMPOUNDS // are the two-word forms that turned up repeatedly in those notes. Anything else // gets typed once and joins vocab.json for every later clip. export const CURATED = ["um", "uh", "umm", "uhh", "er", "erm", "mm-hmm", "uh-huh"]; export const COMPOUNDS = [ "and um", "um and", "the um", "and uh", "uh and", "the uh", "to uh", "do uh", "a um", "in the um", "for the uh", ]; export type Vocab = { words: string[] }; export async function readVocab(): Promise { const v = await readJson(stateFile("vocab.json"), { words: [] }); return v.words ?? []; } /** Add a typed word to the persisted list. Idempotent, case-insensitively. */ export async function addVocab(word: string): Promise { const w = word.trim(); if (!w) return readVocab(); return withStateLock(async () => { const v = await readJson(stateFile("vocab.json"), { words: [] }); const words = v.words ?? []; const known = new Set([...CURATED, ...COMPOUNDS, ...words].map((s) => s.toLowerCase())); if (!known.has(w.toLowerCase())) { words.push(w); await writeJsonAtomic(stateFile("vocab.json"), { words }); } return words; }); }