feat(translation): 8h sweep + drain button + detection probe (#1376)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 46s
CI / integration (push) Successful in 3m55s

Throughput: translate_posts now runs every 8h (was daily) as the
steady-state cadence for newly-imported posts, and the Settings
"Translate now" button runs it in drain mode (run-until-done, no reset)
so one press clears the whole untranslated backlog instead of a single
300-post chunk. The interrupt/backoff re-enqueue now preserves the drain
flag so a bulk drain resumes cleanly after an Interpreter restart.

Misdetection groundwork: surface the detector's confidence from the
Interpreter client (it was in the detectedLanguage payload but discarded)
and add a read-only "Test translation" box — POST /settings/translation/
probe + TranslationCard UI — that shows detected language + confidence +
engine + result for pasted text, without saving. Lets the operator see
why a short/abbreviation-heavy English title gets mis-detected so the
detection guard (min-length + confidence floor) can be tuned from real
numbers. The guard itself follows once the mis-detected cases are probed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
This commit is contained in:
2026-07-08 20:13:49 -04:00
parent a017771621
commit ffdbdbaf07
8 changed files with 274 additions and 20 deletions
@@ -78,6 +78,61 @@
artist, use the Re-translate button on that artist's page.
</p>
<v-divider class="my-3" />
<!-- Diagnostic probe (task #1376): paste text → what Interpreter DETECTS
(language + confidence) and returns, without saving. Used to see why a
short/abbreviation-heavy English title gets mis-detected, and to pick a
detection-confidence / min-length guard from real numbers. -->
<div class="mb-1">
<span class="fc-section-h">Test translation</span>
<p class="fc-muted text-caption mt-1 mb-2">
Paste a title or snippet to see what Interpreter detects and returns —
handy for checking why a short string is mis-detected. Nothing is saved.
</p>
</div>
<v-textarea
v-model="probeText" label="Text to test" rows="2" auto-grow
density="compact" hide-details class="mb-2" :disabled="!baseUrl"
/>
<div class="d-flex align-center flex-wrap mb-2" style="gap: 8px;">
<v-btn
size="small" variant="tonal" rounded="pill" prepend-icon="mdi-magnify"
:loading="probing" :disabled="!baseUrl || !probeText.trim()"
@click="onProbe"
>Test translation</v-btn>
</div>
<v-sheet
v-if="probeResult" rounded class="pa-3 mb-1 text-body-2"
color="rgba(var(--v-theme-accent), 0.06)"
>
<div class="d-flex flex-wrap" style="gap: 4px 16px;">
<span>
<span class="fc-muted">Detected:</span>
<strong>{{ probeResult.detected_lang || '' }}</strong>
</span>
<span v-if="probeResult.detected_confidence != null">
<span class="fc-muted">Confidence:</span>
<strong>{{ fmtConf(probeResult.detected_confidence) }}</strong>
</span>
<span>
<span class="fc-muted">Engine:</span>
{{ probeResult.engine || '' }}<template
v-if="probeResult.engine_version"
> ({{ probeResult.engine_version }})</template>
</span>
<span><span class="fc-muted">Target:</span> {{ probeResult.target }}</span>
</div>
<v-divider class="my-2" />
<div class="fc-muted mb-1">
Result<template v-if="probeResult.detected_lang === probeResult.target">
— already {{ probeResult.target }}, so the sweep leaves it as-is</template>:
</div>
<p class="mb-0" style="white-space: pre-wrap;">{{
probeResult.translated || '(no change)'
}}</p>
</v-sheet>
<v-alert
v-if="err" type="error" variant="tonal" density="compact"
class="mt-3" closable @click:close="err = null"
@@ -129,6 +184,17 @@ const testResult = ref(null) // null = not tested this session; true/false = l
const status = ref(null)
const err = ref(null)
// #1376 "Test translation" probe: paste text → detected lang/confidence + result.
const probeText = ref('')
const probing = ref(false)
const probeResult = ref(null)
// Confidence scale is Interpreter-defined (0100 or 01) — show it as-is, just
// trimmed to 2 decimals, so the operator reads the true number.
function fmtConf (c) {
return c == null ? '' : Math.round(c * 100) / 100
}
const statusColor = computed(() => {
if (!enabled.value || !baseUrl.value) return 'grey'
return status.value?.healthy ? 'success' : 'error'
@@ -170,6 +236,21 @@ async function onTest() {
}
}
async function onProbe () {
probing.value = true
err.value = null
probeResult.value = null
try {
probeResult.value = await api.post('/api/settings/translation/probe', {
body: { text: probeText.value.trim() },
})
} catch (e) {
err.value = e.message
} finally {
probing.value = false
}
}
onMounted(async () => {
try {
await store.loadSettings()