625336b6b4
Live data showed the v1 flat 0.75 cosine over-fired — ~64% of matched images got
3-10 character guesses dominated by the most-referenced characters (a 27-ref
character clears a low bar on many images). A sweep showed 0.85 collapses the
noise (noisy multi-matches 47→3) while keeping the confident single-character
matches.
- ml_settings.ccip_match_threshold (migration 0063, default 0.85); match_image
reads it (override still accepted). DEFAULT_SIM_THRESHOLD fallback 0.75→0.85.
- Exposed in GET/PATCH /api/ml/settings (validated 0.5–0.999).
- Slider in the GPU agent card ("Character-match strictness") — tune live, no
redeploy, same observe-and-tune loop as auto-apply.
Test: a ~0.9-cosine figure matches at 0.85, dropped at 0.95.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
205 lines
7.3 KiB
Vue
205 lines
7.3 KiB
Vue
<template>
|
|
<MaintenanceTile
|
|
icon="mdi-expansion-card"
|
|
title="GPU agent (CCIP + crops)"
|
|
blurb="Connect a desktop-GPU agent to embed characters (CCIP) and crops. It pulls work over HTTP — your database and Redis stay private."
|
|
:open="true"
|
|
>
|
|
<p class="fc-muted text-body-2 mb-3">
|
|
The agent is a container you run on the machine with the GPU. It
|
|
authenticates with the token below, leases jobs from this server, computes
|
|
on the GPU, and posts results back — all over HTTP. Start it when you want
|
|
a burst; stop it to reclaim the card.
|
|
</p>
|
|
|
|
<!-- Token -->
|
|
<div class="fc-section-h mb-1">Agent token</div>
|
|
<div v-if="loading" class="fc-muted text-body-2">Loading…</div>
|
|
<template v-else>
|
|
<div v-if="tokenValue" class="fc-token">
|
|
<code class="fc-token__val">{{ masked ? maskedToken : tokenValue }}</code>
|
|
<v-btn
|
|
size="x-small" variant="text" :icon="masked ? 'mdi-eye' : 'mdi-eye-off'"
|
|
:title="masked ? 'Reveal' : 'Hide'" @click="masked = !masked"
|
|
/>
|
|
<v-btn
|
|
size="x-small" variant="text" icon="mdi-content-copy"
|
|
title="Copy token" @click="onCopy"
|
|
/>
|
|
<v-btn
|
|
size="small" variant="text" color="accent" class="ml-auto"
|
|
prepend-icon="mdi-refresh" :loading="rotating" @click="onRotate"
|
|
>Rotate</v-btn>
|
|
</div>
|
|
<div v-else>
|
|
<v-btn
|
|
color="accent" variant="flat" rounded="pill" size="small"
|
|
prepend-icon="mdi-key-plus" :loading="rotating" @click="onRotate"
|
|
>Generate token</v-btn>
|
|
</div>
|
|
<p class="fc-muted text-caption mt-2 mb-0">
|
|
Point the agent at <code>{{ baseUrl }}</code> with this token. Rotating
|
|
invalidates the old token — update the agent after you rotate.
|
|
</p>
|
|
</template>
|
|
|
|
<!-- Queue -->
|
|
<div class="fc-section-h mt-5 mb-2">Work queue</div>
|
|
<div class="fc-queue">
|
|
<div class="fc-q"><div class="fc-q__n">{{ queue.pending }}</div><div class="fc-q__l">pending</div></div>
|
|
<div class="fc-q"><div class="fc-q__n">{{ queue.leased }}</div><div class="fc-q__l">in flight</div></div>
|
|
<div class="fc-q"><div class="fc-q__n fc-good">{{ queue.done }}</div><div class="fc-q__l">done</div></div>
|
|
<div class="fc-q"><div class="fc-q__n" :class="queue.error ? 'fc-weak' : ''">{{ queue.error }}</div><div class="fc-q__l">errored</div></div>
|
|
</div>
|
|
|
|
<v-btn
|
|
class="mt-4" color="accent" variant="tonal" rounded="pill" size="small"
|
|
prepend-icon="mdi-account-box-multiple" :loading="backfilling" @click="onBackfill"
|
|
>Queue character embedding (CCIP)</v-btn>
|
|
<p class="fc-muted text-caption mt-2 mb-0">
|
|
Enqueues every image that doesn't have a CCIP embedding yet. Nothing
|
|
processes until the agent is running.
|
|
</p>
|
|
|
|
<!-- Match strictness -->
|
|
<div class="fc-section-h mt-5 mb-1">Character-match strictness</div>
|
|
<div v-if="ml.settings" class="d-flex align-center" style="gap:12px">
|
|
<v-slider
|
|
v-model="threshold" :min="0.70" :max="0.95" :step="0.01"
|
|
color="accent" hide-details density="compact" class="flex-grow-1"
|
|
:loading="savingThreshold" @end="onSaveThreshold"
|
|
/>
|
|
<span class="fc-q__n" style="font-size:16px">{{ threshold.toFixed(2) }}</span>
|
|
</div>
|
|
<p class="fc-muted text-caption mt-1 mb-0">
|
|
How close a figure must be (CCIP cosine) to suggest a character. Higher =
|
|
stricter — fewer but more confident matches. 0.85 recommended; below ~0.80
|
|
a heavily-tagged character starts matching everything.
|
|
</p>
|
|
</MaintenanceTile>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { toast } from '../../utils/toast.js'
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
|
|
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
|
import { useGpuStore } from '../../stores/gpu.js'
|
|
import { useMLStore } from '../../stores/ml.js'
|
|
import { copyText } from '../../utils/clipboard.js'
|
|
|
|
const store = useGpuStore()
|
|
const ml = useMLStore()
|
|
const loading = ref(true)
|
|
const tokenValue = ref(null)
|
|
const masked = ref(true)
|
|
const rotating = ref(false)
|
|
const backfilling = ref(false)
|
|
const threshold = ref(0.85)
|
|
const savingThreshold = ref(false)
|
|
const queue = ref({ pending: 0, leased: 0, done: 0, error: 0 })
|
|
let pollTimer = null
|
|
|
|
const baseUrl = computed(() => window.location.origin)
|
|
const maskedToken = computed(() => {
|
|
const t = tokenValue.value || ''
|
|
return t.length > 8 ? `${t.slice(0, 4)}••••••••${t.slice(-4)}` : '••••••••'
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
tokenValue.value = (await store.token()).token
|
|
} catch { /* non-fatal */ } finally {
|
|
loading.value = false
|
|
}
|
|
await refreshQueue()
|
|
pollTimer = setInterval(() => { if (!document.hidden) refreshQueue() }, 5000)
|
|
try {
|
|
await ml.loadSettings()
|
|
if (ml.settings?.ccip_match_threshold != null) {
|
|
threshold.value = ml.settings.ccip_match_threshold
|
|
}
|
|
} catch { /* non-fatal */ }
|
|
})
|
|
onUnmounted(() => { if (pollTimer) clearInterval(pollTimer) })
|
|
|
|
async function onSaveThreshold() {
|
|
savingThreshold.value = true
|
|
try {
|
|
await ml.patchSettings({ ccip_match_threshold: threshold.value })
|
|
toast({ text: `Match strictness set to ${threshold.value.toFixed(2)}`, type: 'success' })
|
|
} catch (e) {
|
|
toast({ text: `Could not save: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
savingThreshold.value = false
|
|
}
|
|
}
|
|
|
|
async function refreshQueue() {
|
|
try { queue.value = await store.status() } catch { /* non-fatal */ }
|
|
}
|
|
|
|
async function onRotate() {
|
|
rotating.value = true
|
|
try {
|
|
tokenValue.value = (await store.rotateToken()).token
|
|
masked.value = false
|
|
toast({ text: 'New agent token generated — update your agent', type: 'success' })
|
|
} catch (e) {
|
|
toast({ text: `Could not rotate token: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
rotating.value = false
|
|
}
|
|
}
|
|
|
|
async function onCopy() {
|
|
try {
|
|
await copyText(tokenValue.value || '') // resolves on success, throws on fail
|
|
toast({ text: 'Token copied', type: 'success' })
|
|
} catch {
|
|
toast({ text: 'Copy failed — select and copy manually', type: 'warning' })
|
|
}
|
|
}
|
|
|
|
async function onBackfill() {
|
|
backfilling.value = true
|
|
try {
|
|
await store.backfill('ccip')
|
|
toast({ text: 'Queued CCIP embedding — run the agent to process it', type: 'success' })
|
|
await refreshQueue()
|
|
} catch (e) {
|
|
toast({ text: `Could not queue backfill: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
backfilling.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
|
.fc-section-h {
|
|
font-size: 13px; font-weight: 700; letter-spacing: 0.03em;
|
|
text-transform: uppercase; color: rgb(var(--v-theme-on-surface));
|
|
}
|
|
.fc-token {
|
|
display: flex; align-items: center; gap: 4px;
|
|
background: rgb(var(--v-theme-surface-light)); border-radius: 6px;
|
|
padding: 4px 6px 4px 10px;
|
|
}
|
|
.fc-token__val {
|
|
font-family: 'JetBrains Mono', monospace; font-size: 13px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.fc-queue { display: flex; gap: 24px; }
|
|
.fc-q__n {
|
|
font-size: 20px; font-weight: 700; line-height: 1.1;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
.fc-q__l {
|
|
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
}
|
|
.fc-good { color: rgb(var(--v-theme-success)); }
|
|
.fc-weak { color: rgb(var(--v-theme-error)); }
|
|
</style>
|