From 082e8b74f147edc2ebd801c1d274a7e5fb3aeb0d Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 13 Aug 2026 04:41:24 +0200 Subject: [PATCH] refactor: share matting model status checks --- js/CanvasView.js | 12 ++-- js/utils/MattingUtils.js | 8 +++ src/CanvasView.ts | 12 ++-- src/utils/MattingUtils.ts | 23 +++++++ .../test_matting_status_characterization.mjs | 60 +++++++++++++++++++ 5 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 js/utils/MattingUtils.js create mode 100644 src/utils/MattingUtils.ts create mode 100644 tests/test_matting_status_characterization.mjs diff --git a/js/CanvasView.js b/js/CanvasView.js index 5367d8a..c81a363 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -13,6 +13,7 @@ import { createModuleLogger } from "./log_system/log_funcs.js"; import { showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification } from "./utils/NotificationUtils.js"; import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { exportCanvasImage } from "./utils/CanvasExportUtils.js"; +import { fetchMattingModelStatus } from "./utils/MattingUtils.js"; import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js"; const log = createModuleLogger('Canvas_view'); const MATTING_SETTINGS_STORAGE_KEY = 'layerforge.matting.settings'; @@ -188,9 +189,8 @@ async function createCanvasWidget(node, widget, app) { let modelOptions = []; let modelStatusMessage = 'Model options are loaded from ComfyUI background-removal storage.'; try { - const response = await fetch('/matting/check-model'); - if (response.ok) { - const status = await response.json(); + const { ok, data: status } = await fetchMattingModelStatus(); + if (ok) { if (Array.isArray(status.models)) { modelOptions = status.models.filter((option) => (option && typeof option.path === 'string' && typeof option.label === 'string')); } @@ -655,11 +655,7 @@ async function createCanvasWidget(node, widget, app) { const mattingSettings = loadMattingSettings(); try { // First check if model is available - const modelCheckUrl = mattingSettings.modelPath - ? `/matting/check-model?model_path=${encodeURIComponent(mattingSettings.modelPath)}` - : "/matting/check-model"; - const modelCheckResponse = await fetch(modelCheckUrl); - const modelStatus = await modelCheckResponse.json(); + const { data: modelStatus } = await fetchMattingModelStatus(mattingSettings.modelPath); if (!modelStatus.available) { switch (modelStatus.reason) { case 'missing_dependency': diff --git a/js/utils/MattingUtils.js b/js/utils/MattingUtils.js new file mode 100644 index 0000000..d2deb0b --- /dev/null +++ b/js/utils/MattingUtils.js @@ -0,0 +1,8 @@ +export async function fetchMattingModelStatus(modelPath) { + const query = modelPath + ? `?model_path=${encodeURIComponent(modelPath)}` + : ''; + const response = await fetch(`/matting/check-model${query}`); + const data = await response.json(); + return { ok: response.ok, data }; +} diff --git a/src/CanvasView.ts b/src/CanvasView.ts index 0cdef2b..91c82f4 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -19,6 +19,7 @@ import {createModuleLogger} from "./log_system/log_funcs.js"; import {showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification} from "./utils/NotificationUtils.js"; import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { exportCanvasImage, type CanvasExportAction, type CanvasExportVariant } from "./utils/CanvasExportUtils.js"; +import { fetchMattingModelStatus } from "./utils/MattingUtils.js"; import { registerImageInClipspace, startSAMDetectorMonitoring, setupSAMDetectorHook } from "./SAMDetectorIntegration.js"; import type { ComfyNode, Layer, AddMode } from './types'; @@ -261,9 +262,8 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): let modelStatusMessage = 'Model options are loaded from ComfyUI background-removal storage.'; try { - const response = await fetch('/matting/check-model'); - if (response.ok) { - const status = await response.json() as { models?: MattingModelOption[] }; + const { ok, data: status } = await fetchMattingModelStatus(); + if (ok) { if (Array.isArray(status.models)) { modelOptions = status.models.filter((option) => ( option && typeof option.path === 'string' && typeof option.label === 'string' @@ -815,11 +815,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): try { // First check if model is available - const modelCheckUrl = mattingSettings.modelPath - ? `/matting/check-model?model_path=${encodeURIComponent(mattingSettings.modelPath)}` - : "/matting/check-model"; - const modelCheckResponse = await fetch(modelCheckUrl); - const modelStatus = await modelCheckResponse.json(); + const { data: modelStatus } = await fetchMattingModelStatus(mattingSettings.modelPath); if (!modelStatus.available) { switch (modelStatus.reason) { diff --git a/src/utils/MattingUtils.ts b/src/utils/MattingUtils.ts new file mode 100644 index 0000000..52c173a --- /dev/null +++ b/src/utils/MattingUtils.ts @@ -0,0 +1,23 @@ +export interface MattingModelStatus { + available: boolean; + reason: string; + message: string; + models?: Model[]; +} + +export interface MattingModelStatusResult { + ok: boolean; + data: MattingModelStatus; +} + +export async function fetchMattingModelStatus( + modelPath?: string, +): Promise> { + const query = modelPath + ? `?model_path=${encodeURIComponent(modelPath)}` + : ''; + const response = await fetch(`/matting/check-model${query}`); + const data = await response.json() as MattingModelStatus; + + return { ok: response.ok, data }; +} diff --git a/tests/test_matting_status_characterization.mjs b/tests/test_matting_status_characterization.mjs new file mode 100644 index 0000000..c385bc6 --- /dev/null +++ b/tests/test_matting_status_characterization.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { fetchMattingModelStatus } from '../js/utils/MattingUtils.js'; + +const canvasViewSource = await readFile( + new URL('../src/CanvasView.ts', import.meta.url), + 'utf8', +); + +test('shared matting status helper preserves endpoint, payload, and errors', async () => { + const originalFetch = globalThis.fetch; + const requests = []; + + globalThis.fetch = async (url) => { + requests.push(url); + const isErrorResponse = requests.length === 3; + return { + ok: !isErrorResponse, + json: async () => ({ + available: !isErrorResponse, + reason: isErrorResponse ? 'error' : 'ready', + message: isErrorResponse ? 'failed' : 'ready', + models: [], + }), + }; + }; + + try { + const automatic = await fetchMattingModelStatus(); + const selected = await fetchMattingModelStatus('models/my model.safetensors'); + const failed = await fetchMattingModelStatus('models/missing.safetensors'); + + assert.deepEqual(requests, [ + '/matting/check-model', + '/matting/check-model?model_path=models%2Fmy%20model.safetensors', + '/matting/check-model?model_path=models%2Fmissing.safetensors', + ]); + assert.equal(automatic.ok, true); + assert.equal(automatic.data.reason, 'ready'); + assert.equal(selected.data.message, 'ready'); + assert.equal(failed.ok, false); + assert.equal(failed.data.reason, 'error'); + + globalThis.fetch = async () => { + throw new Error('offline'); + }; + await assert.rejects(fetchMattingModelStatus(), /offline/); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('CanvasView delegates both model status checks to the shared helper', () => { + assert.match(canvasViewSource, /fetchMattingModelStatus\(\)/); + assert.match(canvasViewSource, /fetchMattingModelStatus\(mattingSettings\.modelPath\)/); + assert.doesNotMatch(canvasViewSource, /const modelCheckUrl =/); + assert.doesNotMatch(canvasViewSource, /fetch\(['"]\/matting\/check-model/); +});