Spaces:
Sleeping
Sleeping
| /** | |
| * Rationale Purity Validator β lightweight Haiku check for garbage rationale input. | |
| * Catches: empty strings, random characters, "none", "n/a", "asdf", non-English, lazy input. | |
| */ | |
| import { getClaudeClient } from "../ai/claude-client"; | |
| export interface RationaleEntry { | |
| index: number; | |
| rationale: string; | |
| } | |
| export interface RationaleValidationResult { | |
| valid: boolean; | |
| rejected: Array<{ index: number; reason: string }>; | |
| } | |
| const SYSTEM_PROMPT = `You validate whether subtitle editing rationales are meaningful. Each rationale should briefly explain WHY a change was made to a subtitle. | |
| REJECT if the rationale is: | |
| - Empty, whitespace-only, or very short (1-2 characters) | |
| - Random characters or keyboard smashing (e.g., "asdf", "jkl;", "xxx", "aaa") | |
| - Placeholder text (e.g., "none", "n/a", "test", "todo", ".", "-", "...") | |
| - Not in English | |
| - Nonsensical or clearly not related to subtitle editing | |
| ACCEPT if the rationale: | |
| - Briefly explains a real editing decision (even short ones like "typo fix", "timing adjusted", "split for readability") | |
| - References a specific change type (e.g., "fixed CPL", "corrected speaker", "adjusted overlap") | |
| Respond with JSON: | |
| \`\`\`json | |
| { | |
| "results": [ | |
| { "index": <number>, "valid": <boolean>, "reason": "<only if invalid>" } | |
| ] | |
| } | |
| \`\`\``; | |
| // Quick local pre-filter for obviously bad rationales (no API call needed) | |
| const GARBAGE_PATTERNS = [ | |
| /^[\s\W]*$/, // Only whitespace/punctuation | |
| /^(.)\1{2,}$/, // Repeated single char: "aaa", "xxx" | |
| /^[a-z]{1,4}$/i, // 1-4 random letters | |
| /^(none|n\/a|na|test|todo|tbd|idk|ok|yes|no|null|undefined)$/i, | |
| /^\.{1,3}$/, // Just dots | |
| /^-{1,3}$/, // Just dashes | |
| ]; | |
| export function quickRejectRationale(rationale: string): string | null { | |
| const trimmed = rationale.trim(); | |
| if (trimmed.length === 0) return "Empty rationale"; | |
| if (trimmed.length < 3) return "Too short β must explain the change"; | |
| for (const pattern of GARBAGE_PATTERNS) { | |
| if (pattern.test(trimmed)) return "Invalid rationale β must explain why the change was made"; | |
| } | |
| return null; // Passes quick check | |
| } | |
| export async function validateRationales( | |
| entries: RationaleEntry[], | |
| ): Promise<RationaleValidationResult> { | |
| // Quick local rejection first | |
| const rejected: Array<{ index: number; reason: string }> = []; | |
| const needsAiCheck: RationaleEntry[] = []; | |
| for (const entry of entries) { | |
| const quickReason = quickRejectRationale(entry.rationale); | |
| if (quickReason) { | |
| rejected.push({ index: entry.index, reason: quickReason }); | |
| } else { | |
| needsAiCheck.push(entry); | |
| } | |
| } | |
| // If all rejected locally, skip AI | |
| if (needsAiCheck.length === 0) { | |
| return { valid: rejected.length === 0, rejected }; | |
| } | |
| // AI check with Haiku for the rest | |
| try { | |
| const { client } = getClaudeClient(); | |
| const prompt = needsAiCheck | |
| .map((e) => `Index ${e.index}: "${e.rationale}"`) | |
| .join("\n"); | |
| const response = await client.messages.create({ | |
| model: "claude-haiku-4-5-20251001", | |
| max_tokens: 1024, | |
| system: SYSTEM_PROMPT, | |
| messages: [{ role: "user", content: `Validate these ${needsAiCheck.length} rationales:\n\n${prompt}` }], | |
| }); | |
| const text = response.content | |
| .filter((b: any) => b.type === "text") | |
| .map((b: any) => b.text) | |
| .join(""); | |
| const jsonMatch = text.match(/\{[\s\S]*\}/); | |
| if (jsonMatch) { | |
| const parsed = JSON.parse(jsonMatch[0]); | |
| if (parsed.results) { | |
| for (const r of parsed.results) { | |
| if (!r.valid) { | |
| rejected.push({ index: r.index, reason: r.reason || "Rejected by AI review" }); | |
| } | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.error("[RationaleValidator] AI check failed, accepting all:", err); | |
| // Non-fatal β if Haiku is unavailable, accept (only local checks apply) | |
| } | |
| return { valid: rejected.length === 0, rejected }; | |
| } | |