gts-x / src /lib /feedback /classifier.ts
JAMES HAN
GTS-X: Full HITL editor + QC + file converter for HuggingFace Spaces
c273232
Raw
History Blame Contribute Delete
5.88 kB
/**
* Heuristic diff classifier for feedback analysis.
* Analyzes editDiffs from a completed job and produces feedback classifications.
* Layer 1: rule-based. Layer 2 (Claude skill) will enrich with source code analysis.
*/
type EditDiff = {
id: string;
subtitleIndex: number;
field: string;
oldValue: string | null;
newValue: string | null;
rationale: string;
changeCategory: string;
};
type FeedbackClassification = {
errorType: "input_error" | "cicd_needed" | "style_preference" | "false_positive";
confidenceScore: number;
description: string;
affectedInput: "neon" | "alto_curated" | "lsx_prompt" | "lsx_logic" | "arc_config" | null;
};
// Keyword patterns for rationale analysis
const INPUT_ERROR_KEYWORDS = ["wrong", "incorrect", "mistranslat", "error in source", "bad input", "original is wrong", "source error"];
const STYLE_KEYWORDS = ["style", "preference", "natural", "sounds better", "flow", "readability", "tone", "nuance"];
const NEON_KEYWORDS = ["neon", "source", "template", "original", "input text", "transcription"];
const ALTO_KEYWORDS = ["alto", "curated", "reference", "base translation"];
const LSX_LOGIC_KEYWORDS = ["timing", "cpl", "cps", "overlap", "gap", "duration", "frame", "sync"];
const LSX_PROMPT_KEYWORDS = ["format", "style guide", "capitalization", "punctuation", "tag", "italic"];
function matchesKeywords(text: string, keywords: string[]): boolean {
const lower = text.toLowerCase();
return keywords.some((k) => lower.includes(k));
}
function classifySingleDiff(diff: EditDiff): FeedbackClassification {
const { changeCategory, rationale, field } = diff;
const rat = rationale.toLowerCase();
// timing_fix → usually cicd_needed (LSX should handle timing)
if (changeCategory === "timing_fix") {
return {
errorType: "cicd_needed",
confidenceScore: matchesKeywords(rat, LSX_LOGIC_KEYWORDS) ? 0.85 : 0.65,
description: `Timing correction on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: "lsx_logic",
};
}
// cpl_fix → cicd_needed (LSX should auto-fix CPL violations)
if (changeCategory === "cpl_fix") {
return {
errorType: "cicd_needed",
confidenceScore: 0.8,
description: `CPL violation fix on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: "lsx_logic",
};
}
// split_merge → cicd_needed (LSX cueing should handle this)
if (changeCategory === "split_merge") {
return {
errorType: "cicd_needed",
confidenceScore: 0.75,
description: `Split/merge on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: "lsx_logic",
};
}
// style_correction → style_preference
if (changeCategory === "style_correction") {
return {
errorType: "style_preference",
confidenceScore: 0.7,
description: `Style correction on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: matchesKeywords(rat, LSX_PROMPT_KEYWORDS) ? "lsx_prompt" : null,
};
}
// translation_fix → depends on rationale
if (changeCategory === "translation_fix") {
if (matchesKeywords(rat, INPUT_ERROR_KEYWORDS)) {
return {
errorType: "input_error",
confidenceScore: 0.8,
description: `Translation error on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: matchesKeywords(rat, ALTO_KEYWORDS)
? "alto_curated"
: matchesKeywords(rat, NEON_KEYWORDS)
? "neon"
: "neon",
};
}
if (matchesKeywords(rat, STYLE_KEYWORDS)) {
return {
errorType: "style_preference",
confidenceScore: 0.65,
description: `Style preference on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: "lsx_prompt",
};
}
// Default translation fix → could be input error but low confidence
return {
errorType: "input_error",
confidenceScore: 0.5,
description: `Translation change on subtitle ${diff.subtitleIndex + 1}: ${rationale}`,
affectedInput: "neon",
};
}
// other / fallback → false_positive with low confidence
return {
errorType: "false_positive",
confidenceScore: 0.4,
description: `Edit on subtitle ${diff.subtitleIndex + 1} (${field}): ${rationale}`,
affectedInput: null,
};
}
/**
* Classify all diffs from a completed job.
* Groups similar patterns to avoid 1:1 diff→analysis noise.
* Returns deduplicated feedback classifications.
*/
export function classifyDiffs(diffs: EditDiff[]): FeedbackClassification[] {
if (diffs.length === 0) return [];
// Classify each diff
const classified = diffs.map(classifySingleDiff);
// Group by errorType + affectedInput to reduce noise
const groups = new Map<string, FeedbackClassification[]>();
for (const c of classified) {
const key = `${c.errorType}:${c.affectedInput || "none"}`;
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(c);
}
// Produce one analysis per group with aggregated description
const results: FeedbackClassification[] = [];
for (const [, items] of groups) {
if (items.length === 0) continue;
const first = items[0];
const avgConfidence =
items.reduce((sum, i) => sum + i.confidenceScore, 0) / items.length;
results.push({
errorType: first.errorType,
confidenceScore: Math.round(avgConfidence * 100) / 100,
description:
items.length === 1
? first.description
: `${items.length} ${first.errorType.replace("_", " ")} issues detected. Examples: ${items
.slice(0, 3)
.map((i) => i.description)
.join("; ")}`,
affectedInput: first.affectedInput,
});
}
// Sort by confidence descending
results.sort((a, b) => b.confidenceScore - a.confidenceScore);
return results;
}