From a0836c4040fa423415e416cd1e92aa5fda64f165 Mon Sep 17 00:00:00 2001 From: Drewlius Date: Thu, 18 Jun 2026 17:40:53 -0400 Subject: [PATCH 1/4] Upgrading Stashai to ONNX endpoint ; Same HF Host Details Hosted by the original creator at the same HF Spaces URL This was tested and verified by hand with multiple different media formats across both Tags and Markers and is confirmed operational on my machine. Used OpenCode to help understand Gradio and how it handles API interactions as well as how that interacts with stash and base64 data URL. There is a built in retry logic loop as well as built in compatibility for older browsers. if it is not working on your end i would suggest checking the HF Spaces URL above and ensuring that it is fully online and operational before blaming the script. Both the stashai.js and the stashai.yml must be deployed for the change to take effect and be functional. only deploying the stashai.js will result in errors and result in failure of the plugin function completely. Post-Update migrated from a Gradio 3 Space to a Gradio 5 Space, which meant completely rewriting how the plugin talks to the API. After changing stashai.js you must hard refresh your browser (Ctrl + Shift + r). In the Stash App, reload plugins in plugins settings in the main Settings screen. Regular browser refresh won't pick up the changes. Some-Context The ONNX Space has a bigger, better model but runs on HF's free tier (2GB RAM). The 335MB model barely fits and it cold-starts, OOMs occasionally, and then HF auto-restarts it. The plugin needs retry logic to handle this. The 335MB ONNX model takes ~30 seconds to load on HF's free tier. During that time, the Space might OOM and restart. The first request often fails, but the second or third works. This is solved by the retries with delays in the script. --- plugins/stashAI/stashai.js | 191 ++++++++++++++++++++---------------- plugins/stashAI/stashai.yml | 2 +- 2 files changed, 108 insertions(+), 85 deletions(-) diff --git a/plugins/stashAI/stashai.js b/plugins/stashAI/stashai.js index 759af2df..a018784b 100644 --- a/plugins/stashAI/stashai.js +++ b/plugins/stashAI/stashai.js @@ -1,7 +1,8 @@ (function () { "use strict"; - let STASHMARKER_API_URL = "https://cc1234-stashtag.hf.space/api/predict"; + let STASHMARKER_API_URL = "https://cc1234-stashtag-onnx.hf.space/gradio_api/call/predict_tags"; + let STASHMARKER_API_MARKER = "https://cc1234-stashtag-onnx.hf.space/gradio_api/call/predict_markers"; var OPTIONS = [ "Anal", @@ -2543,6 +2544,80 @@ }); } + async function gradioCall(url, image, vtt, threshold, retries = 3) { + for (let attempt = 0; attempt < retries; attempt++) { + try { + return await _gradioCall(url, image, vtt, threshold); + } catch (err) { + if (attempt === retries - 1) throw err; + await new Promise((r) => setTimeout(r, 3000)); + } + } + } + + async function _gradioCall(url, image, vtt, threshold) { + const body = { + data: [ + { url: image, meta: { _type: "gradio.FileData" } }, + vtt, + threshold, + ], + }; + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error("HTTP " + response.status); + } + + const { event_id } = await response.json(); + const sseUrl = url + "/" + event_id; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 120000); + + let text; + try { + const resp = await fetch(sseUrl, { signal: controller.signal }); + if (!resp.ok) { + throw new Error("HTTP " + resp.status); + } + text = await resp.text(); + } finally { + clearTimeout(timeout); + } + + let currentEvent = ""; + let currentData = ""; + + for (const line of text.split("\n")) { + if (line.startsWith("event: ")) { + currentEvent = line.slice(7).trim(); + } else if (line.startsWith("data: ")) { + currentData = line.slice(6); + } else if (line === "") { + if (currentEvent === "complete") { + try { + return JSON.parse(currentData); + } catch (e) { + throw new Error("Failed to parse result"); + } + } + if (currentEvent === "error") { + throw new Error(currentData || "API error"); + } + currentEvent = ""; + currentData = ""; + } + } + + throw new Error("No result received"); + } + function instance$3($$self, $$props, $$invalidate) { let { $$slots: slots = {}, $$scope } = $$props; validate_slots("MarkerButton", slots, []); @@ -2569,51 +2644,23 @@ let vtt = await download(vtt_url); - // query the api with a threshold of 0.4 as we want to do the filtering ourselves - var data = { data: [image, vtt, 0.4] }; - - fetch(STASHMARKER_API_URL + "_1", { - method: "POST", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify(data), - }) - .then((response) => { - if (response.status !== 200) { - $$invalidate(0, (scanner = false)); - alert( - "Something went wrong. It's likely a server issue, Please try again later." - ); - return; - } - - return response.json(); - }) - .then((data) => { - $$invalidate(0, (scanner = false)); - let frames = data.data[0]; - $$invalidate(0, (scanner = false)); - - if (frames.length === 0) { - alert("No tags found"); - return; - } + try { + let result = await gradioCall(STASHMARKER_API_MARKER, image, vtt, 0.4); + let frames = result[0]; - // find a div with class row - let row = document.querySelector(".row"); + $$invalidate(0, (scanner = false)); - new MarkerMatches({ target: row, props: { frames, url } }); - }) - .catch((error) => { - $$invalidate(0, (scanner = false)); + if (!frames || frames.length === 0) { + alert("No tags found"); + return; + } - if (error.message === "") { - alert("Error: Service may be down. please try again later."); - } else { - alert("Error: " + error.message); - } - }); + let row = document.querySelector(".row"); + new MarkerMatches({ target: row, props: { frames, url } }); + } catch (error) { + $$invalidate(0, (scanner = false)); + alert("Error: " + (error.message || "Service may be down. Please try again later.")); + } } const writable_props = []; @@ -4027,52 +4074,28 @@ reader.readAsDataURL(vblob); }); - // query the api with a threshold of 0.2 as we want to do the filtering ourselves - var data = { data: [image, vtt, 0.2] }; - - fetch(STASHMARKER_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify(data), - }) - .then((response) => { - if (response.status !== 200) { - $$invalidate(0, (scanner = false)); - alert( - "Something went wrong. It's likely a server issue, Please try again later." - ); - return; - } + try { + let result = await gradioCall(STASHMARKER_API_URL, image, vtt, 0.2); + let tags = {}; + result.forEach((item) => Object.assign(tags, item)); - return response.json(); - }) - .then((data) => { - $$invalidate(0, (scanner = false)); + $$invalidate(0, (scanner = false)); - if (data.data[0].length === 0) { - alert("No tags found"); - return; - } + if (Object.keys(tags).length === 0) { + alert("No tags found"); + return; + } - // grab stash-tag-threshold from local storage or set to default - let threshold = localStorage.getItem("stash-tag-threshold") || 0.4; + let threshold = localStorage.getItem("stash-tag-threshold") || 0.4; - new TagMatches({ - target: document.body, - props: { matches: data.data[0], url, threshold }, - }); - }) - .catch((error) => { - $$invalidate(0, (scanner = false)); - - if (error.message === "") { - alert("Error: Service may be down. please try again later."); - } else { - alert("Error: " + error.message); - } + new TagMatches({ + target: document.body, + props: { matches: tags, url, threshold }, }); + } catch (error) { + $$invalidate(0, (scanner = false)); + alert("Error: " + (error.message || "Service may be down. Please try again later.")); + } } const writable_props = []; diff --git a/plugins/stashAI/stashai.yml b/plugins/stashAI/stashai.yml index 15975dd6..7111176b 100644 --- a/plugins/stashAI/stashai.yml +++ b/plugins/stashAI/stashai.yml @@ -12,4 +12,4 @@ ui: - stashai.css csp: connect-src: - - "https://cc1234-stashtag.hf.space" + - "https://cc1234-stashtag-onnx.hf.space" From 021ebf0ea486cf0a8b73ce44bcc1a2d1e74d18b0 Mon Sep 17 00:00:00 2001 From: Drewlius Date: Sat, 4 Jul 2026 22:25:34 -0400 Subject: [PATCH 2/4] Bumped Version Changed from version 1.0.2 to version 1.0.3 per maintainer/contributor directions. --- plugins/stashAI/stashai.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stashAI/stashai.yml b/plugins/stashAI/stashai.yml index 7111176b..775628e7 100644 --- a/plugins/stashAI/stashai.yml +++ b/plugins/stashAI/stashai.yml @@ -1,7 +1,7 @@ name: Stash AI # requires: CommunityScriptsUILibrary description: Add Tags or Markers to a video using AI -version: 1.0.2 +version: 1.0.3 url: https://discourse.stashapp.cc/t/stash-ai/1392 ui: requires: From c9be4a966587ccf714e02a919079905e5112a5dd Mon Sep 17 00:00:00 2001 From: Drewlius <240991902+Drewlius@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:36:51 -0400 Subject: [PATCH 3/4] js & .yml now conform to Gradio queue_api Mismatched output format - Old '/gradio_api/call/{event_id}' SSE 'complete' event returned data wrapped as '[function_result]'. Gradio 5's queue API 'process_completed' event returns 'function_result' directly (no outer array). Both callers ('result[0]' for markers, 'result.forEach' for tags) expect the wrapper. Caused 'frames.filter is not a function' and 'result.forEach is not a function' on first test. - Switched '_gradioCall()' to 'POST /gradio_api/queue/join' with 'fn_index' (0 = predict_tags, 1 = predict_markers) + random 'session_hash', SSE stream from '/gradio_api/queue/data' - Parses 'data:' JSON lines for 'msg: "process_completed"' (Gradio 5 SSE v3 format) - Wraps result as '[msg.output.data[0]]' to match old callers' expected shape - Consolidated two URL constants into one 'STASHMARKER_API_BASE' ONNX Space pins 'gradio>=5.0.0' (no minor lock). On container rebuild (cold start, cache eviction), pip resolves to latest Gradio 5.x. Old '/gradio_api/call/' endpoint was never correct for queue-enabled Gradio 5 it worked during testing due to whatever version/cache state the container had at that time. Testing the new endpoint returns tags on inputs that would not appear previously, and the accuracy of the returned tags appears anecdotally to be more accurate than the previous non -ONNX endpoint. Also included an Agnets.md file in the event that a user wants to have an agent work with the API directly. this was sourced from [source](https://huggingface.co/spaces/cc1234/stashtag_onnx/agents.md) --- plugins/stashAI/stashai.js | 96 ++++++++++++++++++++----------------- plugins/stashAI/stashai.yml | 2 +- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/plugins/stashAI/stashai.js b/plugins/stashAI/stashai.js index a018784b..bfda4e87 100644 --- a/plugins/stashAI/stashai.js +++ b/plugins/stashAI/stashai.js @@ -1,8 +1,7 @@ (function () { "use strict"; - let STASHMARKER_API_URL = "https://cc1234-stashtag-onnx.hf.space/gradio_api/call/predict_tags"; - let STASHMARKER_API_MARKER = "https://cc1234-stashtag-onnx.hf.space/gradio_api/call/predict_markers"; + let STASHMARKER_API_BASE = "https://cc1234-stashtag-onnx.hf.space"; var OPTIONS = [ "Anal", @@ -2242,7 +2241,7 @@ const [, scene_id] = getScenarioAndID(); let time; let tagId; - const tagLower = frame.tag.label.toLowerCase(); + const tagLower = frame.tag.label.toLowerCase().replace(/_/g, " "); if (tags[tagLower] === undefined) { const tagID = await createTag(tagLower); @@ -2544,10 +2543,10 @@ }); } - async function gradioCall(url, image, vtt, threshold, retries = 3) { + async function gradioCall(fn_index, image, vtt, threshold, retries = 3) { for (let attempt = 0; attempt < retries; attempt++) { try { - return await _gradioCall(url, image, vtt, threshold); + return await _gradioCall(fn_index, image, vtt, threshold); } catch (err) { if (attempt === retries - 1) throw err; await new Promise((r) => setTimeout(r, 3000)); @@ -2555,27 +2554,33 @@ } } - async function _gradioCall(url, image, vtt, threshold) { - const body = { - data: [ - { url: image, meta: { _type: "gradio.FileData" } }, - vtt, - threshold, - ], - }; + async function _gradioCall(fn_index, image, vtt, threshold) { + const session_hash = crypto.randomUUID + ? crypto.randomUUID() + : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); + }); - const response = await fetch(url, { + const queueResponse = await fetch(STASHMARKER_API_BASE + "/gradio_api/queue/join", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify({ + data: [ + { url: image, meta: { _type: "gradio.FileData" }, orig_name: "sprite.jpg" }, + vtt, + threshold, + ], + fn_index: fn_index, + session_hash: session_hash, + }), }); - if (!response.ok) { - throw new Error("HTTP " + response.status); + if (!queueResponse.ok) { + throw new Error("HTTP " + queueResponse.status); } - const { event_id } = await response.json(); - const sseUrl = url + "/" + event_id; + const sseUrl = STASHMARKER_API_BASE + "/gradio_api/queue/data?session_hash=" + session_hash; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 120000); @@ -2591,31 +2596,33 @@ clearTimeout(timeout); } - let currentEvent = ""; - let currentData = ""; - + let result; for (const line of text.split("\n")) { - if (line.startsWith("event: ")) { - currentEvent = line.slice(7).trim(); - } else if (line.startsWith("data: ")) { - currentData = line.slice(6); - } else if (line === "") { - if (currentEvent === "complete") { - try { - return JSON.parse(currentData); - } catch (e) { - throw new Error("Failed to parse result"); + if (line.startsWith("data: ")) { + try { + const msg = JSON.parse(line.slice(6)); + if (msg.msg === "process_completed") { + if (msg.success === false) { + throw new Error(msg.output?.error || "Model inference failed"); + } + result = [msg.output?.data?.[0]]; + break; } + if (msg.msg === "error") { + throw new Error(msg.output?.error || "API error"); + } + } catch (e) { + if ( + e.message === "Model inference failed" || + e.message === "API error" + ) + throw e; } - if (currentEvent === "error") { - throw new Error(currentData || "API error"); - } - currentEvent = ""; - currentData = ""; } } - throw new Error("No result received"); + if (result === undefined) throw new Error("No result received"); + return result; } function instance$3($$self, $$props, $$invalidate) { @@ -2645,7 +2652,7 @@ let vtt = await download(vtt_url); try { - let result = await gradioCall(STASHMARKER_API_MARKER, image, vtt, 0.4); + let result = await gradioCall(1, image, vtt, 0.4); let frames = result[0]; $$invalidate(0, (scanner = false)); @@ -2677,7 +2684,7 @@ $$self.$capture_state = () => ({ getScenarioAndID, getUrlSprite, - STASHMARKER_API_URL, + STASHMARKER_API_BASE, MarkerMatches, scanner, download, @@ -3772,11 +3779,12 @@ let existingTags = await getTagsForScene(scene_id); for (const [tag] of filteredMatches) { - let tagLower = tag.toLowerCase(); + const tagNormalized = tag.replace(/_/g, " "); + let tagLower = tagNormalized.toLowerCase(); // if tag doesn't exist, create it if (tags[tagLower] === undefined) { - existingTags.push(await createTag(tag)); + existingTags.push(await createTag(tagNormalized)); } else if (!existingTags.includes(tags[tagLower])) { existingTags.push(tags[tagLower]); } @@ -4075,7 +4083,7 @@ }); try { - let result = await gradioCall(STASHMARKER_API_URL, image, vtt, 0.2); + let result = await gradioCall(0, image, vtt, 0.2); let tags = {}; result.forEach((item) => Object.assign(tags, item)); @@ -4112,7 +4120,7 @@ $$self.$capture_state = () => ({ getScenarioAndID, getUrlSprite, - STASHMARKER_API_URL, + STASHMARKER_API_BASE, TagMatches, scanner, getTags, diff --git a/plugins/stashAI/stashai.yml b/plugins/stashAI/stashai.yml index 775628e7..642f3bf8 100644 --- a/plugins/stashAI/stashai.yml +++ b/plugins/stashAI/stashai.yml @@ -1,7 +1,7 @@ name: Stash AI # requires: CommunityScriptsUILibrary description: Add Tags or Markers to a video using AI -version: 1.0.3 +version: 1.1.0 url: https://discourse.stashapp.cc/t/stash-ai/1392 ui: requires: From 496252c204247849390a47dcbe37c2000084e647 Mon Sep 17 00:00:00 2001 From: Drewlius <240991902+Drewlius@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:01:13 -0400 Subject: [PATCH 4/4] added agents.md (not strictly necessary) This config file is meant for a separate agent that a user could choose to integrate with the ONNX model. This was linked directly on the ONNX landing page and can be found [here](https://huggingface.co/spaces/cc1234/stashtag_onnx/agents.md) this can be removed from the code base, or kept for additional integration possibilities in the future. It is not strictly necessary for any functionality of the base stashai plugin. --- plugins/stashAI/agents.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 plugins/stashAI/agents.md diff --git a/plugins/stashAI/agents.md b/plugins/stashAI/agents.md new file mode 100644 index 00000000..62de3d83 --- /dev/null +++ b/plugins/stashAI/agents.md @@ -0,0 +1,7 @@ +To use this application (cc1234/stashtag_onnx: Generate video frame tags and markers): +API schema: GET https://cc1234-stashtag-onnx.hf.space/gradio_api/info +Config (find fn_index): GET https://cc1234-stashtag-onnx.hf.space/config → dependencies[i].id where api_name matches API schema endpoint +Join the queue: POST https://cc1234-stashtag-onnx.hf.space/gradio_api/queue/join (pass {"data": [...], "fn_index": , "session_hash": ""}) +Stream results: GET https://cc1234-stashtag-onnx.hf.space/gradio_api/queue/data?session_hash= +File inputs: POST https://cc1234-stashtag-onnx.hf.space/gradio_api/upload -F "files=@file.ext", use as: {"path": "", "meta": {"_type": "gradio.FileData"}, "orig_name": "file.ext"} +Auth: Bearer $HF_TOKEN (https://huggingface.co/settings/tokens) \ No newline at end of file