feat(translation): re-translate on model change — artist-scoped + global re-run (#146)
retranslate_posts resets the 5 translation columns to NULL for a scoped set of posts (all, or WHERE artist_id IN ids) then reuses the untranslated sweep to re-run them, chasing the tail until drained (run-until-done). Interpreter cache keys on engine_version so a changed model re-translates, an unchanged one is cache-fast. Reset only happens when the service is configured+healthy so translations are never wiped when they can't be rebuilt. New POST /settings/translation/retranslate (artist_id | all=true). UI: per-artist 'Re-translate posts' on the Artist Management tab + 'Re-translate all' in the Settings Translation card, both with confirm dialogs. No migration (reuses m143 columns). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,47 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<section class="fc-artist-mgmt__sec">
|
||||
<h2 class="fc-h2">Translation</h2>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Re-translate every post by {{ overview.name }} through the current
|
||||
Interpreter model — clears the stored translations and rebuilds them.
|
||||
Use this after switching translation models to refresh just this artist.
|
||||
</p>
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 10px;">
|
||||
<v-btn
|
||||
size="small" variant="tonal" rounded="pill"
|
||||
prepend-icon="mdi-translate"
|
||||
:loading="retranslating" :disabled="!translationEnabled"
|
||||
@click="confirmRetranslate = true"
|
||||
>Re-translate posts</v-btn>
|
||||
<span v-if="!translationEnabled" class="fc-muted text-caption">
|
||||
Translation is off — enable it in Settings → Maintenance.
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- m146: per-artist re-translate — clears + rebuilds this artist's stored
|
||||
translations (used after a translation-model change). -->
|
||||
<v-dialog v-model="confirmRetranslate" max-width="440">
|
||||
<v-card>
|
||||
<v-card-title class="fc-h2">Re-translate {{ overview.name }}?</v-card-title>
|
||||
<v-card-text class="text-body-2">
|
||||
Clears the stored translation for every post by
|
||||
<strong>{{ overview.name }}</strong> and re-runs them through your
|
||||
current Interpreter model. Unchanged text comes back from the cache
|
||||
instantly. Runs in the background until done.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="confirmRetranslate = false">Cancel</v-btn>
|
||||
<v-btn color="accent" :loading="retranslating" @click="onRetranslate">
|
||||
Re-translate
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<section class="fc-artist-mgmt__sec">
|
||||
<h2 class="fc-h2">Danger zone</h2>
|
||||
<ArtistDangerZone
|
||||
@@ -113,9 +154,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
import { useImportStore } from '../../stores/import.js'
|
||||
import { useSourcesStore } from '../../stores/sources.js'
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import ArtistDangerZone from './ArtistDangerZone.vue'
|
||||
@@ -125,8 +168,38 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const api = useApi()
|
||||
const importStore = useImportStore()
|
||||
const sources = useSourcesStore()
|
||||
|
||||
// m146: per-artist re-translate. Gated on translation being enabled globally
|
||||
// (the endpoint 400s otherwise) — read the shared import settings once.
|
||||
const translationEnabled = ref(false)
|
||||
const retranslating = ref(false)
|
||||
const confirmRetranslate = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await importStore.loadSettings()
|
||||
translationEnabled.value = !!importStore.settings?.translation_enabled
|
||||
} catch { /* non-fatal — the button just stays disabled */ }
|
||||
})
|
||||
|
||||
async function onRetranslate () {
|
||||
retranslating.value = true
|
||||
try {
|
||||
await api.post('/api/settings/translation/retranslate', {
|
||||
body: { artist_id: props.overview.id },
|
||||
})
|
||||
confirmRetranslate.value = false
|
||||
toast({ text: `Re-translating ${props.overview.name}…`, type: 'success' })
|
||||
} catch (e) {
|
||||
toast({ text: `Re-translate failed: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
retranslating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// #130: move a source into another artist.
|
||||
const moveOpen = ref(false)
|
||||
const moveSource = ref(null)
|
||||
|
||||
@@ -47,16 +47,49 @@
|
||||
prepend-icon="mdi-translate" :loading="running"
|
||||
:disabled="!enabled || !baseUrl" @click="onRun"
|
||||
>Translate now</v-btn>
|
||||
<v-btn
|
||||
size="small" variant="tonal" rounded="pill"
|
||||
prepend-icon="mdi-refresh" :loading="retranslating"
|
||||
:disabled="!enabled || !baseUrl" @click="confirmAll = true"
|
||||
>Re-translate all</v-btn>
|
||||
<span v-if="status" class="fc-muted text-caption">
|
||||
{{ status.untranslated_count }}
|
||||
post{{ status.untranslated_count === 1 ? '' : 's' }} awaiting translation
|
||||
</span>
|
||||
</div>
|
||||
<p class="fc-muted text-caption mt-2 mb-0">
|
||||
Use <strong>Re-translate all</strong> after switching the Interpreter model —
|
||||
it clears every stored translation and re-runs it through the new model
|
||||
(unchanged text is served from cache, so it's cheap). To refresh just one
|
||||
artist, use the Re-translate button on that artist's page.
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
v-if="err" type="error" variant="tonal" density="compact"
|
||||
class="mt-3" closable @click:close="err = null"
|
||||
>{{ err }}</v-alert>
|
||||
|
||||
<!-- m146: re-translate-everything is a bigger hammer than 'Translate now'
|
||||
(it clears + rebuilds all translations), so it asks first. -->
|
||||
<v-dialog v-model="confirmAll" max-width="440">
|
||||
<v-card>
|
||||
<v-card-title>Re-translate everything?</v-card-title>
|
||||
<v-card-text class="text-body-2">
|
||||
This clears the stored translation for <strong>every</strong> post and
|
||||
re-runs them through your current Interpreter model. Text that hasn't
|
||||
changed comes back from the cache instantly; anything the new model
|
||||
translates differently is refreshed. It runs in the background until
|
||||
done.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="confirmAll = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="accent" :loading="retranslating" @click="onRetranslateAll"
|
||||
>Re-translate all</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
@@ -75,6 +108,8 @@ const baseUrl = ref('')
|
||||
const targetLang = ref('en')
|
||||
const busy = ref(false)
|
||||
const running = ref(false)
|
||||
const retranslating = ref(false)
|
||||
const confirmAll = ref(false)
|
||||
const testing = ref(false)
|
||||
const testResult = ref(null) // null = not tested this session; true/false = last result
|
||||
const status = ref(null)
|
||||
@@ -159,4 +194,18 @@ async function onRun() {
|
||||
setTimeout(loadStatus, 1500) // let the sweep make a dent, then refresh
|
||||
}
|
||||
}
|
||||
async function onRetranslateAll() {
|
||||
retranslating.value = true
|
||||
err.value = null
|
||||
try {
|
||||
await api.post('/api/settings/translation/retranslate', { body: { all: true } })
|
||||
confirmAll.value = false
|
||||
toast({ text: 'Re-translating all posts…', type: 'success' })
|
||||
} catch (e) {
|
||||
err.value = e.message
|
||||
} finally {
|
||||
retranslating.value = false
|
||||
setTimeout(loadStatus, 1500) // reset spikes the untranslated count — refresh it
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user