Improve canvas save logic and add detailed debug logging

Enhanced the canvas save mechanism to ensure unique file names per node, prevent concurrent saves and executions, and handle missing files more robustly. Switched all logger levels to DEBUG for detailed tracing. Added fallback logic for file naming, improved error handling, and ensured that empty canvases are not saved. These changes improve reliability and traceability of canvas operations, especially in multi-node scenarios.
This commit is contained in:
Dariusz L
2025-06-25 23:21:50 +02:00
parent 29ec41b0a1
commit 38ad476719
10 changed files with 168 additions and 39 deletions

View File

@@ -307,9 +307,25 @@ class CanvasNode:
try:
# Wczytaj obraz bez maski
path_image_without_mask = folder_paths.get_annotated_filepath(
canvas_image.replace('.png', '_without_mask.png'))
log_debug(f"Loading image without mask from: {path_image_without_mask}")
image_without_mask_name = canvas_image.replace('.png', '_without_mask.png')
path_image_without_mask = folder_paths.get_annotated_filepath(image_without_mask_name)
log_debug(f"Canvas image name: {canvas_image}")
log_debug(f"Looking for image without mask: {image_without_mask_name}")
log_debug(f"Full path: {path_image_without_mask}")
# Sprawdź czy plik istnieje
if not os.path.exists(path_image_without_mask):
log_warn(f"Image without mask not found at: {path_image_without_mask}")
# Spróbuj znaleźć plik w katalogu input
input_dir = folder_paths.get_input_directory()
alternative_path = os.path.join(input_dir, image_without_mask_name)
log_debug(f"Trying alternative path: {alternative_path}")
if os.path.exists(alternative_path):
path_image_without_mask = alternative_path
log_info(f"Found image at alternative path: {alternative_path}")
else:
raise FileNotFoundError(f"Image file not found: {image_without_mask_name}")
i = Image.open(path_image_without_mask)
i = ImageOps.exif_transpose(i)
if i.mode not in ['RGB', 'RGBA']:
@@ -330,7 +346,21 @@ class CanvasNode:
# Wczytaj maskę
path_image = folder_paths.get_annotated_filepath(canvas_image)
path_mask = path_image.replace('.png', '_mask.png')
log_debug(f"Canvas image path: {path_image}")
log_debug(f"Looking for mask at: {path_mask}")
# Sprawdź czy plik maski istnieje
if not os.path.exists(path_mask):
log_warn(f"Mask not found at: {path_mask}")
# Spróbuj znaleźć plik w katalogu input
input_dir = folder_paths.get_input_directory()
mask_name = canvas_image.replace('.png', '_mask.png')
alternative_mask_path = os.path.join(input_dir, mask_name)
log_debug(f"Trying alternative mask path: {alternative_mask_path}")
if os.path.exists(alternative_mask_path):
path_mask = alternative_mask_path
log_info(f"Found mask at alternative path: {alternative_mask_path}")
if os.path.exists(path_mask):
log_debug(f"Mask file exists, loading...")
mask = Image.open(path_mask).convert('L')

View File

@@ -604,32 +604,56 @@ export class Canvas {
async saveToServer(fileName) {
// Sprawdź czy już trwa zapis
if (this._saveInProgress) {
log.warn(`Save already in progress, waiting...`);
return this._saveInProgress;
// Globalna mapa do śledzenia zapisów dla wszystkich node-ów
if (!window.canvasSaveStates) {
window.canvasSaveStates = new Map();
}
const nodeId = this.node.id;
const saveKey = `${nodeId}_${fileName}`;
// Sprawdź czy już trwa zapis dla tego node-a i pliku
if (this._saveInProgress || window.canvasSaveStates.get(saveKey)) {
log.warn(`Save already in progress for node ${nodeId}, waiting...`);
return this._saveInProgress || window.canvasSaveStates.get(saveKey);
}
log.info(`Starting saveToServer with fileName: ${fileName}`);
log.info(`Starting saveToServer with fileName: ${fileName} for node: ${nodeId}`);
log.debug(`Canvas dimensions: ${this.width}x${this.height}`);
log.debug(`Number of layers: ${this.layers.length}`);
// Utwórz Promise dla aktualnego zapisu
this._saveInProgress = this._performSave(fileName);
window.canvasSaveStates.set(saveKey, this._saveInProgress);
try {
const result = await this._saveInProgress;
return result;
} finally {
this._saveInProgress = null;
log.debug(`Save completed, lock released`);
window.canvasSaveStates.delete(saveKey);
log.debug(`Save completed for node ${nodeId}, lock released`);
}
}
async _performSave(fileName) {
// Sprawdź czy są warstwy do zapisania
if (this.layers.length === 0) {
log.warn(`Node ${this.node.id} has no layers, creating empty canvas`);
// Zwróć sukces ale nie zapisuj pustego canvas-a na serwer
return Promise.resolve(true);
}
// Zapisz stan do IndexedDB przed zapisem na serwer
await this.saveStateToDB(true);
// Dodaj krótkie opóźnienie dla różnych node-ów, aby uniknąć konfliktów
const nodeId = this.node.id;
const delay = (nodeId % 10) * 50; // 0-450ms opóźnienia w zależności od ID node-a
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
return new Promise((resolve) => {
const tempCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas');
@@ -803,8 +827,10 @@ export class Canvas {
if (maskResp.status === 200) {
const data = await resp.json();
this.widget.value = data.name;
log.info(`All files saved successfully, widget value set to: ${data.name}`);
// Ustaw widget.value na rzeczywistą nazwę zapisanego pliku (unikalną)
// aby node zwracał właściwy plik
this.widget.value = fileName;
log.info(`All files saved successfully, widget value set to: ${fileName}`);
resolve(true);
} else {
log.error(`Error saving mask: ${maskResp.status}`);

View File

@@ -9,7 +9,7 @@ const log = {
};
// Konfiguracja loggera dla modułu CanvasInteractions
logger.setModuleLevel('CanvasInteractions', LogLevel.INFO);
logger.setModuleLevel('CanvasInteractions', LogLevel.DEBUG);
export class CanvasInteractions {
constructor(canvas) {

View File

@@ -10,7 +10,7 @@ const log = {
};
// Konfiguracja loggera dla modułu CanvasLayers
logger.setModuleLevel('CanvasLayers', LogLevel.INFO); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
logger.setModuleLevel('CanvasLayers', LogLevel.DEBUG); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
export class CanvasLayers {
constructor(canvas) {
@@ -628,7 +628,32 @@ export class CanvasLayers {
this.canvas.selectedLayer.opacity = slider.value / 100;
this.canvas.render();
await this.canvas.saveToServer(this.canvas.widget.value);
// Funkcja fallback do zapisu
const saveWithFallback = async (fileName) => {
try {
const getUniqueFileName = (baseName) => {
// Sprawdź czy nazwa już zawiera identyfikator node-a (zapobiega nieskończonej pętli)
const nodePattern = new RegExp(`_node_${this.canvas.node.id}(?:_node_\\d+)*`);
if (nodePattern.test(baseName)) {
// Usuń wszystkie poprzednie identyfikatory node-ów i dodaj tylko jeden
const cleanName = baseName.replace(/_node_\d+/g, '');
const extension = cleanName.split('.').pop();
const nameWithoutExt = cleanName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${this.canvas.node.id}.${extension}`;
}
const extension = baseName.split('.').pop();
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${this.canvas.node.id}.${extension}`;
};
const uniqueFileName = getUniqueFileName(fileName);
return await this.canvas.saveToServer(uniqueFileName);
} catch (error) {
console.warn(`Failed to save with unique name, falling back to original: ${fileName}`, error);
return await this.canvas.saveToServer(fileName);
}
};
await saveWithFallback(this.canvas.widget.value);
if (this.canvas.node) {
app.graph.runStep();
}

View File

@@ -10,7 +10,7 @@ const log = {
};
// Konfiguracja loggera dla modułu CanvasState
logger.setModuleLevel('CanvasState', LogLevel.INFO);
logger.setModuleLevel('CanvasState', LogLevel.DEBUG);
// Prosta funkcja generująca UUID
function generateUUID() {

View File

@@ -17,7 +17,7 @@ const log = {
};
// Konfiguracja loggera dla modułu Canvas_view
logger.setModuleLevel('Canvas_view', LogLevel.INFO); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
logger.setModuleLevel('Canvas_view', LogLevel.DEBUG); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów
async function createCanvasWidget(node, widget, app) {
const canvas = new Canvas(node, widget);
@@ -387,7 +387,7 @@ async function createCanvasWidget(node, widget, app) {
const img = new Image();
img.onload = async () => {
canvas.addLayer(img);
await canvas.saveToServer(widget.value);
await saveWithFallback(widget.value);
app.graph.runStep();
};
img.src = event.target.result;
@@ -402,7 +402,7 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Import Input",
onclick: async () => {
if (await canvas.importLatestImage()) {
await canvas.saveToServer(widget.value);
await saveWithFallback(widget.value);
app.graph.runStep();
}
}
@@ -589,7 +589,7 @@ async function createCanvasWidget(node, widget, app) {
canvas.updateSelection([newLayer]);
canvas.render();
canvas.saveState();
await canvas.saveToServer(widget.value);
await saveWithFallback(widget.value);
app.graph.runStep();
} catch (error) {
log.error("Matting error:", error);
@@ -743,7 +743,8 @@ async function createCanvasWidget(node, widget, app) {
const triggerWidget = node.widgets.find(w => w.name === "trigger");
const updateOutput = async () => {
await canvas.saveToServer(widget.value);
// Użyj funkcji fallback do zapisu
await saveWithFallback(widget.value);
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
app.graph.runStep();
};
@@ -918,26 +919,73 @@ async function createCanvasWidget(node, widget, app) {
}
};
// Zmienna do śledzenia czy wykonanie jest w trakcie
let executionInProgress = false;
// Globalna mapa do śledzenia wykonania dla każdego node-a
if (!window.canvasExecutionStates) {
window.canvasExecutionStates = new Map();
}
api.addEventListener("execution_start", async () => {
log.info(`Execution start event for node ${node.id}`);
// Unikalna nazwa pliku dla każdego node-a
const getUniqueFileName = (baseName) => {
// Sprawdź czy nazwa już zawiera identyfikator node-a (zapobiega nieskończonej pętli)
const nodePattern = new RegExp(`_node_${node.id}(?:_node_\\d+)*`);
if (nodePattern.test(baseName)) {
// Usuń wszystkie poprzednie identyfikatory node-ów i dodaj tylko jeden
const cleanName = baseName.replace(/_node_\d+/g, '');
const extension = cleanName.split('.').pop();
const nameWithoutExt = cleanName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${node.id}.${extension}`;
}
const extension = baseName.split('.').pop();
const nameWithoutExt = baseName.replace(`.${extension}`, '');
return `${nameWithoutExt}_node_${node.id}.${extension}`;
};
// Funkcja do czyszczenia nazwy pliku z identyfikatorów node-ów
const getCleanFileName = (fileName) => {
return fileName.replace(/_node_\d+/g, '');
};
// Funkcja fallback w przypadku problemów z unikalną nazwą
const saveWithFallback = async (fileName) => {
try {
const uniqueFileName = getUniqueFileName(fileName);
log.debug(`Attempting to save with unique name: ${uniqueFileName}`);
return await canvas.saveToServer(uniqueFileName);
} catch (error) {
log.warn(`Failed to save with unique name, falling back to original: ${fileName}`, error);
return await canvas.saveToServer(fileName);
}
};
api.addEventListener("execution_start", async (event) => {
// Sprawdź czy event dotyczy tego konkretnego node-a
const executionData = event.detail || {};
const currentPromptId = executionData.prompt_id;
log.info(`Execution start event for node ${node.id}, prompt_id: ${currentPromptId}`);
log.debug(`Widget value: ${widget.value}`);
log.debug(`Node inputs: ${node.inputs?.length || 0}`);
log.debug(`Canvas layers count: ${canvas.layers.length}`);
// Sprawdź czy już trwa wykonanie
if (executionInProgress) {
log.warn(`Execution already in progress, skipping...`);
// Sprawdź czy już trwa wykonanie dla tego node-a
if (window.canvasExecutionStates.get(node.id)) {
log.warn(`Execution already in progress for node ${node.id}, skipping...`);
return;
}
// Ustaw flagę wykonania
executionInProgress = true;
// Ustaw flagę wykonania dla tego node-a
window.canvasExecutionStates.set(node.id, true);
try {
await canvas.saveToServer(widget.value);
log.info(`Canvas saved to server`);
// Sprawdź czy canvas ma jakiekolwiek warstwy przed zapisem
if (canvas.layers.length === 0) {
log.warn(`Node ${node.id} has no layers, skipping save to server`);
// Nie zapisuj pustego canvas-a, ale nadal przetwórz dane wejściowe
} else {
// Użyj funkcji fallback do zapisu tylko jeśli są warstwy
await saveWithFallback(widget.value);
log.info(`Canvas saved to server for node ${node.id}`);
}
if (node.inputs[0]?.link) {
const linkId = node.inputs[0].link;
@@ -951,11 +999,11 @@ async function createCanvasWidget(node, widget, app) {
log.debug(`No input link found`);
}
} catch (error) {
log.error(`Error during execution:`, error);
log.error(`Error during execution for node ${node.id}:`, error);
} finally {
// Zwolnij flagę wykonania
executionInProgress = false;
log.debug(`Execution completed, flag released`);
// Zwolnij flagę wykonania dla tego node-a
window.canvasExecutionStates.set(node.id, false);
log.debug(`Execution completed for node ${node.id}, flag released`);
}
});

View File

@@ -9,7 +9,7 @@ const log = {
};
// Konfiguracja loggera dla modułu ImageCache
logger.setModuleLevel('ImageCache', LogLevel.INFO);
logger.setModuleLevel('ImageCache', LogLevel.DEBUG);
export class ImageCache {
constructor() {

View File

@@ -9,7 +9,7 @@ const log = {
};
// Konfiguracja loggera dla modułu ImageUtils
logger.setModuleLevel('ImageUtils', LogLevel.INFO);
logger.setModuleLevel('ImageUtils', LogLevel.DEBUG);
export function validateImageData(data) {
log.debug("Validating data structure:", {

View File

@@ -9,7 +9,7 @@ const log = {
};
// Konfiguracja loggera dla modułu Mask_tool
logger.setModuleLevel('Mask_tool', LogLevel.INFO);
logger.setModuleLevel('Mask_tool', LogLevel.DEBUG);
export class MaskTool {
constructor(canvasInstance) {

View File

@@ -9,7 +9,7 @@ const log = {
};
// Konfiguracja loggera dla modułu db
logger.setModuleLevel('db', LogLevel.INFO);
logger.setModuleLevel('db', LogLevel.DEBUG);
const DB_NAME = 'CanvasNodeDB';
const STATE_STORE_NAME = 'CanvasState';