fix(tagger): separate JoyTag confidence threshold

Store confidence thresholds per tagger model so JoyTag no longer inherits WD tuning. Refresh the active threshold when switching models, guard stale threshold saves, and keep UI Lab mocks in sync.

Also tightens the onboarding model selector so the segmented control no longer stretches across the row.
This commit is contained in:
2026-07-03 09:21:39 +01:00
parent f1116c6c26
commit 68932b55c5
7 changed files with 82 additions and 35 deletions
+7 -4
View File
@@ -461,6 +461,9 @@ export function SettingsModal() {
current={taggerModel}
onSelect={(nextModel) => {
if (nextModel === taggerModel) return;
setTaggerThresholdDraft(null);
setTaggerThresholdError(null);
if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current);
setTaggerModelSwitching(true);
setTaggerModelSwitchError(null);
void setTaggerModel(nextModel)
@@ -562,12 +565,12 @@ export function SettingsModal() {
className="w-20 rounded-md border border-white/10 bg-white/5 px-2 py-1.5 text-xs text-white focus:border-white/20 focus:outline-none"
value={thresholdDisplay}
onChange={(event) => setTaggerThresholdDraft(event.target.value)}
onBlur={() => {
const value = parseFloat(thresholdDisplay);
onBlur={(event) => {
const value = parseFloat(event.currentTarget.value);
if (!isNaN(value) && value >= 0.05 && value <= 0.99) {
setTaggerThresholdError(null);
setTaggerThresholdSaving(true);
void setTaggerThreshold(value)
void setTaggerThreshold(value, taggerModel)
.catch((error: unknown) => setTaggerThresholdError(String(error)))
.finally(() => {
setTaggerThresholdDraft(null);
@@ -584,7 +587,7 @@ export function SettingsModal() {
{taggerThresholdError ? (
<p className="text-[11px] text-amber-300">{taggerThresholdError}</p>
) : (
<p className="text-[11px] text-gray-600">{taggerThresholdSaving ? "Saving..." : `Default: ${taggerModel === "joytag" ? "0.4" : "0.35"}`}</p>
<p className="text-[11px] text-gray-600">{taggerThresholdSaving ? "Saving..." : `Default: ${TAGGER_MODELS[taggerModel].defaultThreshold}`}</p>
)}
</div>
</SettingsItem>
+1 -1
View File
@@ -75,7 +75,7 @@ export function StepAiFeatures() {
The AI tagger model labels images so you can search with{" "}
<code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/t</code> — tags look like:
</p>
<div className="mt-3 flex flex-wrap items-center gap-1 rounded-lg border border-white/[0.07] p-0.5 light-theme:border-gray-300/80">
<div className="mt-3 inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg border border-white/[0.07] p-0.5 light-theme:border-gray-300/80">
{(["wd", "joytag"] as const).map((model) => (
<TaggerModelChoice
key={model}
+14 -1
View File
@@ -19,6 +19,10 @@ let nextAlbumId = 100;
let nextTagId = 10_000;
let mockTaggerModel: TaggerModel = db.scenario === "joytag-unready" ? "joytag" : "wd";
let mockTaggerReady = db.scenario !== "unready" && db.scenario !== "joytag-unready";
const mockTaggerThresholds: Record<TaggerModel, number> = {
wd: 0.35,
joytag: 0.4,
};
type AnyPayload = Record<string, any> | undefined;
type Rgb = [number, number, number];
@@ -42,6 +46,10 @@ function isRgb(value: unknown): value is Rgb {
return Array.isArray(value) && value.length === 3 && value.every((entry) => typeof entry === "number");
}
function isTaggerModel(value: unknown): value is TaggerModel {
return value === "wd" || value === "joytag";
}
function colorDistanceSq(left: Rgb, right: Rgb): number {
return (left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2 + (left[2] - right[2]) ** 2;
}
@@ -528,8 +536,13 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise
case "set_tagger_acceleration":
return p.acceleration ?? "auto";
case "get_tagger_threshold":
return mockTaggerThresholds[mockTaggerModel];
case "set_tagger_threshold":
return p.threshold ?? 0.35;
{
const model = isTaggerModel(p.model) ? p.model : mockTaggerModel;
mockTaggerThresholds[model] = p.threshold ?? mockTaggerThresholds[model];
return mockTaggerThresholds[model];
}
case "get_tagger_batch_size":
case "set_tagger_batch_size":
return p.batch_size ?? 8;
+13 -8
View File
@@ -589,7 +589,7 @@ interface GalleryState {
loadTaggerModel: () => Promise<void>;
setTaggerModel: (model: TaggerModel) => Promise<void>;
loadTaggerThreshold: () => Promise<void>;
setTaggerThreshold: (threshold: number) => Promise<void>;
setTaggerThreshold: (threshold: number, model?: TaggerModel) => Promise<void>;
loadTaggerBatchSize: () => Promise<void>;
setTaggerBatchSize: (batchSize: number) => Promise<void>;
probeTaggerRuntime: () => Promise<void>;
@@ -2326,11 +2326,14 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
const taggerModel = await invoke<TaggerModel>("set_tagger_model", {
params: { model },
});
// Switching models changes which files are required on disk, so refresh the
// status too — the download/ready UI must reflect the newly-selected model.
// Switching models changes both readiness and the active threshold setting,
// so refresh them together for the selected model.
try {
const taggerModelStatus = await invoke<TaggerModelStatus>("get_tagger_model_status");
set({ taggerModel, taggerModelStatus, taggerModelError: null, taggerRuntimeProbe: null });
const [taggerModelStatus, taggerThreshold] = await Promise.all([
invoke<TaggerModelStatus>("get_tagger_model_status"),
invoke<number>("get_tagger_threshold"),
]);
set({ taggerModel, taggerModelStatus, taggerThreshold, taggerModelError: null, taggerRuntimeProbe: null });
} catch (error) {
set({ taggerModel, taggerRuntimeProbe: null, taggerModelError: String(error) });
}
@@ -2345,11 +2348,13 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}
},
setTaggerThreshold: async (threshold) => {
setTaggerThreshold: async (threshold, model) => {
const taggerThreshold = await invoke<number>("set_tagger_threshold", {
params: { threshold },
params: { threshold, model },
});
set({ taggerThreshold });
if (!model || get().taggerModel === model) {
set({ taggerThreshold });
}
},
loadTaggerBatchSize: async () => {
+3 -1
View File
@@ -1,15 +1,17 @@
import type { TaggerModel } from "./store";
export const TAGGER_MODELS: Record<TaggerModel, { name: string; tab: string; description: string }> = {
export const TAGGER_MODELS: Record<TaggerModel, { name: string; tab: string; description: string; defaultThreshold: number }> = {
wd: {
name: "WD SwinV2 Tagger v3",
tab: "WD (anime)",
defaultThreshold: 0.35,
description:
"Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds.",
},
joytag: {
name: "JoyTag",
tab: "JoyTag (general)",
defaultThreshold: 0.4,
description:
"Booru-schema tagger that also handles photographic content and is strong on NSFW concepts. The explicitness rating is derived from its tags.",
},