From 4ab0214f09b2de226e90151eb55c90a29d1a0b1a Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 7 Oct 2026 19:47:19 -0700 Subject: [PATCH] fix: address gallery auto-tag review images by path, not digest id The gallery review flow builds its list from /prompt_manager/images/output, whose id is a 16-hex path digest, but posted it as image_id to autotag/single, so the server looked up a database row and every request 404ed. autotagSingleBody (pure, tested) now sends path for output-folder entries and keeps image_id for database rows, which the admin review flow (/images/all) still uses. --- tests/js/image-helpers.test.js | 47 +++++++++++++++++++++++++++++++++- web/js/admin.js | 12 ++------- web/js/gallery.js | 12 ++------- web/js/image-helpers.js | 29 ++++++++++++++++++++- 4 files changed, 78 insertions(+), 22 deletions(-) diff --git a/tests/js/image-helpers.test.js b/tests/js/image-helpers.test.js index 65878ed..885f052 100644 --- a/tests/js/image-helpers.test.js +++ b/tests/js/image-helpers.test.js @@ -1,7 +1,52 @@ // Run with: node --test tests/js/ const test = require("node:test"); const assert = require("node:assert/strict"); -const { formatImageCaption, workflowDocumentHtml } = require("../../web/js/image-helpers.js"); +const { + formatImageCaption, + workflowDocumentHtml, + autotagSingleBody, +} = require("../../web/js/image-helpers.js"); + +const WD14 = { modelType: "wd14-vit", generalThreshold: 0.35, characterThreshold: 0.85, prompt: "ignored" }; +const CAPTION = { modelType: "blip", generalThreshold: 0.35, characterThreshold: 0.85, prompt: "describe it" }; + +test("autotagSingleBody sends image_id for a database row", () => { + const body = autotagSingleBody({ id: 42, prompt_id: 7, image_path: "/out/a.png" }, WD14); + assert.equal(body.image_id, 42); + assert.equal("path" in body, false); +}); + +test("autotagSingleBody sends path for an output-folder entry whose id is a digest", () => { + const body = autotagSingleBody({ id: "3f2a9c1b0d4e5f67", path: "/out/sub/a.png", filename: "a.png" }, WD14); + assert.equal(body.path, "/out/sub/a.png"); + assert.equal("image_id" in body, false); +}); + +test("autotagSingleBody prefers path when a numeric-looking string id has no prompt link", () => { + const body = autotagSingleBody({ id: "1234567890abcdef", path: "/out/b.png" }, WD14); + assert.deepEqual(Object.keys(body).filter((k) => k === "path" || k === "image_id"), ["path"]); +}); + +test("autotagSingleBody adds wd14 thresholds and no prompt", () => { + const body = autotagSingleBody({ id: 1 }, WD14); + assert.equal(body.model_type, "wd14-vit"); + assert.equal(body.general_threshold, 0.35); + assert.equal(body.character_threshold, 0.85); + assert.equal("prompt" in body, false); +}); + +test("autotagSingleBody adds the prompt for caption models and no thresholds", () => { + const body = autotagSingleBody({ id: 1 }, CAPTION); + assert.equal(body.model_type, "blip"); + assert.equal(body.prompt, "describe it"); + assert.equal("general_threshold" in body, false); +}); + +test("autotagSingleBody does not mutate its inputs", () => { + const image = Object.freeze({ id: 1 }); + const settings = Object.freeze({ ...WD14 }); + assert.doesNotThrow(() => autotagSingleBody(image, settings)); +}); const XSS = '">'; const formatFileSize = (bytes) => `${bytes} B`; diff --git a/web/js/admin.js b/web/js/admin.js index bec9dd4..bf86496 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -3671,16 +3671,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; this.autoTagState.tagsExpanded = false; // Reset accordion for new image try { - const requestBody = { - image_id: image.id, - model_type: this.autoTagState.modelType, - }; - if (this.autoTagState.modelType.startsWith('wd14')) { - requestBody.general_threshold = this.autoTagState.generalThreshold; - requestBody.character_threshold = this.autoTagState.characterThreshold; - } else { - requestBody.prompt = this.autoTagState.prompt; - } + // Review rows come from /images/all (database ids), so this resolves to image_id + const requestBody = ImageHelpers.autotagSingleBody(image, this.autoTagState); const response = await fetch('/prompt_manager/autotag/single', { method: 'POST', diff --git a/web/js/gallery.js b/web/js/gallery.js index c63b5ba..73e31ab 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -3380,16 +3380,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; document.getElementById('reviewTagsContainer').innerHTML = '
Generating tags...
'; try { - const requestBody = { - image_id: image.id, - model_type: this.autoTagState.modelType, - }; - if (this.autoTagState.modelType.startsWith('wd14')) { - requestBody.general_threshold = this.autoTagState.generalThreshold; - requestBody.character_threshold = this.autoTagState.characterThreshold; - } else { - requestBody.prompt = this.autoTagState.prompt; - } + // Gallery entries come from /images/output: id is a path digest, so address by path + const requestBody = ImageHelpers.autotagSingleBody(image, this.autoTagState); const response = await fetch('/prompt_manager/autotag/single', { method: 'POST', diff --git a/web/js/image-helpers.js b/web/js/image-helpers.js index 9c29786..9e07433 100644 --- a/web/js/image-helpers.js +++ b/web/js/image-helpers.js @@ -62,7 +62,34 @@ ].join("\n"); } - const api = { formatImageCaption, workflowDocumentHtml }; + /** + * Body for POST /prompt_manager/autotag/single. + * + * Database rows (admin review, /images/all) have an integer id the server + * looks up; output-folder entries (gallery review, /images/output) have a + * path digest as id, which would 404, so those are addressed by path. + * + * @param {{id?: unknown, path?: unknown}} image + * @param {{modelType: string, generalThreshold?: number, characterThreshold?: number, prompt?: string}} settings + * @returns {object} JSON-serialisable request body + */ + function autotagSingleBody(image, settings) { + const entry = image || {}; + const target = + typeof entry.path === "string" && entry.path && !Number.isInteger(entry.id) + ? { path: entry.path } + : { image_id: entry.id }; + const modelType = String(settings.modelType || ""); + const params = modelType.startsWith("wd14") + ? { + general_threshold: settings.generalThreshold, + character_threshold: settings.characterThreshold, + } + : { prompt: settings.prompt }; + return { ...target, model_type: modelType, ...params }; + } + + const api = { formatImageCaption, workflowDocumentHtml, autotagSingleBody }; if (typeof module !== "undefined" && module.exports) module.exports = api; else root.ImageHelpers = api; })(typeof window !== "undefined" ? window : globalThis);