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.
This commit is contained in:
Vito Sansevero
2026-10-08 18:01:34 -07:00
parent 0f9569cdd8
commit 4ab0214f09
4 changed files with 78 additions and 22 deletions
+46 -1
View File
@@ -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 = '"><img src=x onerror=1>';
const formatFileSize = (bytes) => `${bytes} B`;
+2 -10
View File
@@ -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',
+2 -10
View File
@@ -3380,16 +3380,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
document.getElementById('reviewTagsContainer').innerHTML = '<div class="text-pm-secondary">Generating tags...</div>';
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',
+28 -1
View File
@@ -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);