fix(gateway): mediaAnalysis ffprobe path, fallback error-log, generic closer sanitize
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m47s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m11s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m36s

Audit log produksi (sejak deploy13:38) menemukan 3 isu:
1. mediaDownloader.ts spawn /usr/bin/ffprobe + /usr/bin/ffmpeg (path keras) —
   ENOENT di Nix karena binary cuma di ffmpeg-headless closure. Pakai
   PATH-resolved ('ffprobe'/'ffmpeg') seperti voice-recording module
   (ffmpegProcess.ts/transmitter.ts) — 5 media warning hilang.
2. individualFallbackProcessor log error 'Success' di level50 tiap fallback
   BERHASIL (logModerationError dengan new Error('Success')) — ganti
   logger.info dengan verdict yang sama; error log cuma untuk error asli.
3. moderationResponseParser: strip frasa penutup generik ('Tidak ada
   indikasi pelanggaran.') yang masih sering dikeluarkan LLM walau prompt
   melarang (277/1486 analisis mengandung frasa, termasuk hari ini).
   sanitizeGenericCleanCloser hanya mencocok frasa di AKHIR, teks substantif
   tetap utuh. Unit test: 6/6 pass.
This commit is contained in:
asepharyana
2026-08-01 14:00:43 +07:00
parent ada7a768f8
commit dfe689bdec
3 changed files with 29 additions and 10 deletions
@@ -155,13 +155,16 @@ async function processIndividualFallback(
}
const resultSummary = analysisResult.results[0];
logModerationError([messageId], config.AI_LLM_MODEL, new Error("Success"), {
phase: "individual_fallback",
status: resultSummary?.status,
flags: resultSummary?.flags,
severity: resultSummary?.severity,
confidence: resultSummary?.confidence,
});
logger.info(
{
phase: "individual_fallback",
status: resultSummary?.status,
flags: resultSummary?.flags,
severity: resultSummary?.severity,
confidence: resultSummary?.confidence,
},
`Individual fallback moderation complete for ${messageId} (${resultSummary?.status})`,
);
individualConsecutiveErrors = 0;
@@ -198,7 +198,7 @@ async function extractVideoFrames(
try {
await writeFile(inputPath, videoBytes);
const { stdout: durationStr } = await execFileAsync(
"/usr/bin/ffprobe",
"ffprobe",
[
"-v",
"error",
@@ -213,7 +213,7 @@ async function extractVideoFrames(
const duration = parseFloat(durationStr.trim()) || 1;
const fps = (3 / duration).toFixed(6);
await execFileAsync(
"/usr/bin/ffmpeg",
"ffmpeg",
[
"-i",
inputPath,
@@ -157,6 +157,22 @@ export function sanitizeErrorMessage(
return `Analisis gagal dan memerlukan pemeriksaan manual. Error code: MOD_${Date.now().toString(36).slice(0, 6)}`;
}
/**
* Strip generic clean-verdict closing phrases the LLM sometimes appends
* ("Tidak ada indikasi pelanggaran.") even when it already described the
* message content specifically. Keeps the substance, drops the boilerplate.
* Only matches a TRAILING standalone phrase — never mid-analysis text.
*/
export const GENERIC_CLEAN_CLOSER =
/\s*(?:(?:tidak ada (?:indikasi|tanda|temuan)(?: adanya)? pelanggaran(?: kebijakan| aturan)?)|(?:tidak ada (?:indikasi|tanda|temuan)(?: konten| pesan)? (?:yang )?melanggar)|(?:pesan (?:dianggap|tergolong|dinilai) bersih)|(?:tidak ditemukan (?:adanya )?pelanggaran))\.?\s*$/i;
export function sanitizeGenericCleanCloser(analysis: string): string {
if (!analysis) return analysis;
const cleaned = analysis.replace(GENERIC_CLEAN_CLOSER, "").trim();
// Drop a dangling separator the removal may have left behind.
return cleaned.replace(/(\s[.\-–—]\s*)$/, "").trim();
}
export function parseModerationResponse(
content: string,
targetIds: string[],
@@ -250,7 +266,7 @@ export function parseModerationResponse(
status: status as "clean" | "warn" | "flagged",
flags: flags ?? [],
score: normalizedScore,
analysis: coalescedAnalysis,
analysis: sanitizeGenericCleanCloser(coalescedAnalysis),
categories: categories ?? flags ?? [],
severity: normalizedSeverity,
confidence: normalizedConfidence,