ora editor

This commit is contained in:
2026-03-27 23:33:04 -07:00
parent cdc9ca2f92
commit c94988561c
36 changed files with 1564 additions and 125 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ tmp/**
.dolt/
*.db
**/*.pyc
tools/ora_editor/node_modules/*
node_modules/*

36
console-logs.txt Normal file
View File

@@ -0,0 +1,36 @@
Total messages: 34 (Errors: 1, Warnings: 2)
[WARNING] cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation @ https://cdn.tailwindcss.com/:63
[LOG] ORA Editor initialized @ http://localhost:5001/:62
[LOG] ORA Editor initialized @ http://localhost:5001/:62
[ERROR] Failed to load resource: the server responded with a status of 400 (BAD REQUEST) @ http://localhost:5001/api/image/base?ora_path=:0
[LOG] [ORA EDITOR] Opening file: scenes/kq4_001_beach/bg.png @ http://localhost:5001/:76
[LOG] [ORA EDITOR] File opened: {height: 1392, layers: Array(1), ora_path: /home/noti/dev/ai-game-2/scenes/kq4_001_beach/bg.ora, success: true, width: 2496} @ http://localhost:5001/:89
[LOG] [ORA EDITOR] Starting polygon drawing mode @ http://localhost:5001/:193
[LOG] [ORA EDITOR] Canvas setup: 2496 x 1392 @ http://localhost:5001/:224
[LOG] [ORA EDITOR] Adding polygon point: 0.5 0.1997126436781609 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.7115384615384616 0.28735632183908044 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.7996794871794872 0.5 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.7115384615384616 0.7112068965517241 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.5 0.7988505747126436 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.2876602564102564 0.7112068965517241 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.19951923076923078 0.5 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.2876602564102564 0.28735632183908044 @ http://localhost:5001/:233
[LOG] Canvas context: CanvasRenderingContext2D @ :4
[LOG] Canvas size: 2496 1392 @ :5
[LOG] Test rectangle drawn @ :16
[WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ :3
[LOG] [ORA EDITOR] Starting polygon drawing mode @ http://localhost:5001/:193
[LOG] [ORA EDITOR] Canvas setup: 2496 x 1392 @ http://localhost:5001/:224
[LOG] [ORA EDITOR] Adding polygon point: 0.29967948717948717 0.2988505747126437 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.6995192307692307 0.2988505747126437 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.6995192307692307 0.6997126436781609 @ http://localhost:5001/:233
[LOG] [ORA EDITOR] Adding polygon point: 0.29967948717948717 0.6997126436781609 @ http://localhost:5001/:233
[LOG] isDrawing: true @ :4
[LOG] polygonPoints: Proxy(Array) @ :5
[LOG] polygonColor: #FF0000 @ :6
[LOG] polygonWidth: 2 @ :7
[LOG] canvas exists: true @ :10
[LOG] canvas style display: block @ :12
[LOG] canvas width/height: 2496 1392 @ :13
[LOG] Sampled pixels: [Object, Object, Object, Object] @ :31

BIN
icon.ora LFS Normal file

Binary file not shown.

BIN
mask_view_test.png LFS Normal file

Binary file not shown.

17
opencode.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"playwright": {
"type": "local",
"command": [
"npx",
"@playwright/mcp@latest",
"--executable-path",
"/snap/bin/chromium",
"--isolated"
],
"enabled": true
}
}
}

BIN
ora_editor_initial.png LFS Normal file

Binary file not shown.

71
package-lock.json generated Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "ai-game-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@playwright/test": "1.58.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@playwright/test": "1.58.2"
}
}

BIN
page-with-polygon.png LFS Normal file

Binary file not shown.

8
playwright.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig, chromium } from '@playwright/test';
export default defineConfig({
use: {
browserType: chromium,
channel: 'chrome',
},
});

BIN
polygon-8points.png LFS Normal file

Binary file not shown.

BIN
polygon-drawing-test.png LFS Normal file

Binary file not shown.

BIN
polygon-test.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,403 @@
{
"50": {
"inputs": {
"seed": 23278884
},
"class_type": "Seed (rgthree)",
"_meta": {
"title": "Seed (rgthree)"
}
},
"82": {
"inputs": {
"filename_prefix": "masks/mask_359b122e",
"images": [
"95",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"84": {
"inputs": {
"width": [
"86",
0
],
"height": [
"86",
1
],
"upscale_method": "lanczos",
"keep_proportion": "stretch",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 2,
"device": "cpu",
"image": [
"1:8",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"86": {
"inputs": {
"image": [
"87",
0
]
},
"class_type": "GetImageSize",
"_meta": {
"title": "Get Image Size"
}
},
"87": {
"inputs": {
"image": ""
},
"class_type": "ETN_LoadImageBase64",
"_meta": {
"title": "Load Image (Base64)"
}
},
"88": {
"inputs": {
"factor": 1,
"method": "luminance (Rec.709)",
"image": [
"84",
0
]
},
"class_type": "ImageDesaturate+",
"_meta": {
"title": "🔧 Image Desaturate"
}
},
"89": {
"inputs": {
"channel": "RGB",
"black_point": 121,
"white_point": 255,
"gray_point": 1,
"output_black_point": 0,
"output_white_point": 255,
"image": [
"88",
0
]
},
"class_type": "LayerColor: Levels",
"_meta": {
"title": "LayerColor: Levels"
}
},
"91": {
"inputs": {
"channel": "red",
"image": [
"89",
0
]
},
"class_type": "ImageToMask",
"_meta": {
"title": "Convert Image to Mask"
}
},
"95": {
"inputs": {
"mask": [
"91",
0
]
},
"class_type": "MaskToImage",
"_meta": {
"title": "Convert Mask to Image"
}
},
"96": {
"inputs": {
"filename_prefix": "ComfyUI",
"webhook_url": "http://localhost:5001/api/webhook/comfyui",
"metadata": "",
"external_uid": "",
"images": [
"95",
0
]
},
"class_type": "Webhook",
"_meta": {
"title": "Webhook Image Saver"
}
},
"104": {
"inputs": {
"conditioning": [
"1:69",
0
],
"latent": [
"105",
0
]
},
"class_type": "ReferenceLatent",
"_meta": {
"title": "ReferenceLatent"
}
},
"105": {
"inputs": {
"pixels": [
"87",
0
],
"vae": [
"1:93",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"106": {
"inputs": {
"conditioning": [
"1:68",
0
],
"latent": [
"105",
0
]
},
"class_type": "ReferenceLatent",
"_meta": {
"title": "ReferenceLatent"
}
},
"1:92": {
"inputs": {
"clip_name": "qwen25vl.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"1:90": {
"inputs": {
"model_name": "qwen-image-edit-2511-Q8_0.gguf",
"extra_model_name": "none",
"dequant_dtype": "default",
"patch_dtype": "default",
"patch_on_device": false,
"enable_fp16_accumulation": true,
"attention_override": "sageattn"
},
"class_type": "GGUFLoaderKJ",
"_meta": {
"title": "GGUFLoaderKJ"
}
},
"1:93": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"1:91": {
"inputs": {
"lora_01": "qwen/Qwen-Image-Edit-2511-Lightning-8steps-V1.0-fp32.safetensors",
"strength_01": 1,
"lora_02": "qwen/qwen_2511_extract_mask7_000008000.safetensors",
"strength_02": 0.93,
"lora_03": "None",
"strength_03": 1,
"lora_04": "None",
"strength_04": 1,
"model": [
"1:90",
0
],
"clip": [
"1:92",
0
]
},
"class_type": "Lora Loader Stack (rgthree)",
"_meta": {
"title": "Lora Loader Stack (rgthree)"
}
},
"1:67": {
"inputs": {
"shift": 3.1,
"model": [
"1:91",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "ModelSamplingAuraFlow"
}
},
"1:68": {
"inputs": {
"prompt": "Create a black and white alpha mask of The grass and stones in the bottom right, leaving everything else black",
"clip": [
"1:91",
1
]
},
"class_type": "TextEncodeQwenImageEditPlus",
"_meta": {
"title": "TextEncodeQwenImageEditPlus (Positive)"
}
},
"1:69": {
"inputs": {
"prompt": "",
"clip": [
"1:91",
1
]
},
"class_type": "TextEncodeQwenImageEditPlus",
"_meta": {
"title": "TextEncodeQwenImageEditPlus"
}
},
"1:64": {
"inputs": {
"strength": 1,
"model": [
"1:67",
0
]
},
"class_type": "CFGNorm",
"_meta": {
"title": "CFGNorm"
}
},
"1:70": {
"inputs": {
"reference_latents_method": "index_timestep_zero",
"conditioning": [
"106",
0
]
},
"class_type": "FluxKontextMultiReferenceLatentMethod",
"_meta": {
"title": "Edit Model Reference Method"
}
},
"1:71": {
"inputs": {
"reference_latents_method": "index_timestep_zero",
"conditioning": [
"104",
0
]
},
"class_type": "FluxKontextMultiReferenceLatentMethod",
"_meta": {
"title": "Edit Model Reference Method"
}
},
"200": {
"inputs": {
"image": ""
},
"class_type": "ETN_LoadImageBase64",
"_meta": {
"title": "Load Starting Mask (Base64)"
}
},
"201": {
"inputs": {
"pixels": [
"200",
0
],
"vae": [
"1:93",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode Starting Mask"
}
},
"1:65": {
"inputs": {
"seed": [
"50",
0
],
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 0.8,
"model": [
"1:64",
0
],
"positive": [
"1:70",
0
],
"negative": [
"1:71",
0
],
"latent_image": [
"201",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"1:8": {
"inputs": {
"samples": [
"1:65",
0
],
"vae": [
"1:93",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
}
}

126
tools/mask_rough_cut.json Executable file
View File

@@ -0,0 +1,126 @@
{
"1": {
"inputs": {
"refinement_iterations": 0,
"use_multimask": true,
"output_best_mask": true,
"sam3_model": [
"2",
0
],
"image": [
"9",
0
],
"positive_points": [
"8",
0
]
},
"class_type": "SAM3Segmentation",
"_meta": {
"title": "SAM3 Point Segmentation"
}
},
"2": {
"inputs": {
"precision": "auto",
"attention": "auto",
"compile": false
},
"class_type": "LoadSAM3Model",
"_meta": {
"title": "(down)Load SAM3 Model"
}
},
"4": {
"inputs": {
"x": 0.5,
"y": 0.5,
"is_foreground": true
},
"class_type": "SAM3CreatePoint",
"_meta": {
"title": "SAM3 Create Point"
}
},
"8": {
"inputs": {
"point_1": [
"4",
0
],
"point_2": [
"12",
0
],
"point_3": [
"13",
0
]
},
"class_type": "SAM3CombinePoints",
"_meta": {
"title": "SAM3 Combine Points"
}
},
"9": {
"inputs": {
"image": ""
},
"class_type": "ETN_LoadImageBase64",
"_meta": {
"title": "Load Image (Base64)"
}
},
"10": {
"inputs": {
"mask": [
"1",
0
]
},
"class_type": "MaskToImage",
"_meta": {
"title": "Convert Mask to Image"
}
},
"11": {
"inputs": {
"filename_prefix": "ComfyUI",
"webhook_url": "",
"metadata": "",
"external_uid": "",
"images": [
"10",
0
]
},
"class_type": "Webhook",
"_meta": {
"title": "Webhook Image Saver"
}
},
"12": {
"inputs": {
"x": 0.5,
"y": 0.5,
"is_foreground": true
},
"class_type": "SAM3CreatePoint",
"_meta": {
"title": "SAM3 Create Point"
}
},
"13": {
"inputs": {
"x": 0.5,
"y": 0.5,
"is_foreground": true
},
"class_type": "SAM3CreatePoint",
"_meta": {
"title": "SAM3 Create Point"
}
}
}

View File

@@ -12,7 +12,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from ora_editor.config import TEMP_DIR
from ora_editor.routes import (
files_bp, layers_bp, images_bp, polygon_bp, mask_bp, krita_bp
files_bp, layers_bp, images_bp, polygon_bp, mask_bp, krita_bp, sam_bp
)
logging.basicConfig(level=logging.DEBUG)
@@ -34,7 +34,8 @@ app.register_blueprint(images_bp)
app.register_blueprint(polygon_bp)
app.register_blueprint(mask_bp)
app.register_blueprint(krita_bp)
app.register_blueprint(sam_bp)
if __name__ == '__main__':
app.run(debug=False, port=5001, host='127.0.0.1')
app.run(debug=False, port=5001, host='0.0.0.0')

71
tools/ora_editor/package-lock.json generated Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "ora_editor",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@playwright/test": "1.58.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@playwright/test": "1.58.2"
}
}

View File

@@ -6,6 +6,7 @@ from .images import images_bp
from .polygon import polygon_bp
from .mask import mask_bp
from .krita import krita_bp
from .sam import sam_bp
__all__ = [
'files_bp',
@@ -13,5 +14,6 @@ __all__ = [
'images_bp',
'polygon_bp',
'mask_bp',
'krita_bp'
'krita_bp',
'sam_bp'
]

View File

@@ -115,14 +115,20 @@ def api_mask_extract():
ora_path = data['ora_path']
comfy_url = data.get('comfy_url', COMFYUI_BASE_URL)
count = min(max(data.get('count', 1), 1), 10)
start_mask_path = data.get('start_mask_path', None)
logger.info(f"[MASK EXTRACT] Subject: {subject}")
logger.info(f"[MASK EXTRACT] Use polygon: {use_polygon}")
logger.info(f"[MASK EXTRACT] ORA path: {ora_path}")
logger.info(f"[MASK EXTRACT] ComfyUI URL: {comfy_url}")
logger.info(f"[MASK EXTRACT] Count: {count}")
logger.info(f"[MASK EXTRACT] Start mask path: {start_mask_path}")
if start_mask_path:
workflow_path = APP_DIR.parent / "image_mask_extraction_with_start.json"
else:
workflow_path = APP_DIR.parent / "image_mask_extraction.json"
if not workflow_path.exists():
logger.error(f"[MASK EXTRACT] Workflow file not found: {workflow_path}")
return jsonify({'success': False, 'error': f'Workflow file not found: {workflow_path}'}), 500
@@ -142,6 +148,17 @@ def api_mask_extract():
logger.error(f"[MASK EXTRACT] Error loading base image: {e}")
return jsonify({'success': False, 'error': f'Error loading image: {str(e)}'}), 500
start_mask_img = None
if start_mask_path:
try:
start_mask_img = __import__('PIL').Image.open(start_mask_path).convert('RGBA')
if base_img.size != start_mask_img.size:
start_mask_img = start_mask_img.resize(base_img.size, __import__('PIL').Image.LANCZOS)
logger.info(f"[MASK EXTRACT] Loaded start mask: {start_mask_img.size}")
except Exception as e:
logger.error(f"[MASK EXTRACT] Error loading start mask: {e}")
return jsonify({'success': False, 'error': f'Error loading start mask: {str(e)}'}), 500
polygon_points = None
polygon_color = '#FF0000'
polygon_width = 2
@@ -166,6 +183,21 @@ def api_mask_extract():
prompt_ids = []
for i in range(count):
if start_mask_img:
workflow = comfy_service.prepare_mask_workflow_with_start(
base_image=base_img,
start_mask_image=start_mask_img,
subject=subject,
webhook_url=webhook_url,
seed=seeds[i],
batch_id=batch_id,
mask_index=i,
polygon_points=polygon_points,
polygon_color=polygon_color,
polygon_width=polygon_width,
workflow_template=workflow_template
)
else:
workflow = comfy_service.prepare_mask_workflow(
base_image=base_img,
subject=subject,

View File

@@ -0,0 +1,107 @@
"""SAM3 rough mask generation routes for ORA Editor."""
import io
import json
import random
import string
import time
import zipfile
from pathlib import Path
from flask import Blueprint, request, jsonify, make_response
from ora_editor.config import APP_DIR, COMFYUI_BASE_URL, TEMP_DIR
from ora_editor.services.comfyui import ComfyUIService, batch_storage
import logging
logger = logging.getLogger(__name__)
sam_bp = Blueprint('sam', __name__)
comfy_service = ComfyUIService(COMFYUI_BASE_URL)
def generate_batch_id() -> str:
"""Generate a unique batch ID."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
@sam_bp.route('/api/sam/generate', methods=['POST'])
def api_sam_generate():
"""Generate a rough mask using SAM3 with include/exclude points."""
data = request.get_json()
required = ['ora_path', 'include_points']
for field in required:
if field not in data:
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
ora_path = data['ora_path']
include_points = data['include_points']
exclude_points = data.get('exclude_points', [])
comfy_url = data.get('comfy_url', COMFYUI_BASE_URL)
logger.info(f"[SAM] ORA path: {ora_path}")
logger.info(f"[SAM] Include points: {include_points}")
logger.info(f"[SAM] Exclude points: {exclude_points}")
workflow_path = APP_DIR.parent / "mask_rough_cut.json"
if not workflow_path.exists():
logger.error(f"[SAM] Workflow file not found: {workflow_path}")
return jsonify({'success': False, 'error': f'Workflow file not found: {workflow_path}'}), 500
with open(workflow_path) as f:
workflow_template = json.load(f)
base_img = None
try:
with zipfile.ZipFile(ora_path, 'r') as zf:
img_data = zf.read('mergedimage.png')
base_img = __import__('PIL').Image.open(io.BytesIO(img_data)).convert('RGBA')
logger.info(f"[SAM] Loaded base image: {base_img.size}")
except Exception as e:
logger.error(f"[SAM] Error loading base image: {e}")
return jsonify({'success': False, 'error': f'Error loading image: {str(e)}'}), 500
batch_id = generate_batch_id()
webhook_url = f"http://localhost:5001/api/webhook/comfyui"
workflow = comfy_service.prepare_sam_workflow(
base_image=base_img,
include_points=include_points,
exclude_points=exclude_points,
webhook_url=webhook_url,
batch_id=batch_id,
workflow_template=workflow_template
)
logger.info(f"[SAM] Workflow prepared, sending to ComfyUI at http://{comfy_url}")
try:
prompt_id = comfy_service.submit_workflow(workflow, comfy_url)
logger.info(f"[SAM] Prompt submitted with ID: {prompt_id}")
except Exception as e:
logger.error(f"[SAM] Error submitting workflow: {e}")
return jsonify({'success': False, 'error': f'Failed to connect to ComfyUI: {str(e)}'}), 500
batch_storage.create_batch(batch_id, 1)
completed = comfy_service.poll_for_completion(prompt_id, comfy_url, timeout=120)
if not completed:
logger.error("[SAM] Timeout waiting for workflow completion")
return jsonify({'success': False, 'error': 'SAM generation timed out'}), 500
logger.info("[SAM] Workflow completed, waiting for webhook...")
mask_paths = comfy_service.poll_for_batch_completion(batch_id, timeout=60.0)
if not mask_paths:
logger.error("[SAM] No mask received via webhook")
return jsonify({'success': False, 'error': 'No mask received from SAM'}), 500
mask_path = mask_paths[0]
logger.info(f"[SAM] Mask received: {mask_path}")
return jsonify({
'success': True,
'mask_path': str(mask_path),
'mask_url': f'/api/file/mask?path={mask_path}'
})

View File

@@ -0,0 +1,52 @@
INFO:__main__:[CONFIG] COMFYUI_BASE_URL=127.0.0.1:8188
INFO:ora_editor.app:[CONFIG] COMFYUI_BASE_URL=127.0.0.1:8188
* Serving Flask app 'app'
* Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
INFO:werkzeug:Press CTRL+C to quit
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:31:53] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:31:58] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:02] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:02] "GET /api/image/base?ora_path= HTTP/1.1" 400 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:02] "GET /api/image/masked?ora_path=&mask_path=null HTTP/1.1" 400 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:07] "POST /api/open HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:07] "POST /api/polygon/clear HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:07] "GET /api/image/base?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:07] "GET /api/image/layer/base?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora HTTP/1.1" 200 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 7212
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:07] "GET /api/image/masked?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora&mask_path=null HTTP/1.1" 500 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 7212
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:15] "POST /api/save HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:22] "POST /api/krita/open HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:36] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:37] "GET /api/image/base?ora_path= HTTP/1.1" 400 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:37] "GET /api/image/masked?ora_path=&mask_path=null HTTP/1.1" 400 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'tEXt' 41 4534
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 4587 65536
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:42] "POST /api/open HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:42] "POST /api/polygon/clear HTTP/1.1" 200 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 65536
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:42] "GET /api/image/layer/base?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_001_beach/bg.ora HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:42] "GET /api/image/base?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_001_beach/bg.ora HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:42] "GET /api/image/masked?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_001_beach/bg.ora&mask_path=null HTTP/1.1" 500 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:44] "POST /api/krita/open HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:47] "POST /api/polygon/clear HTTP/1.1" 200 -
INFO:__main__:[POLYGON] Storing polygon: 4 points, color: #FF0000, width: 2
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:05] "POST /api/polygon HTTP/1.1" 200 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 7212
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:05] "GET /api/image/polygon?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora&ts=1774657985514 HTTP/1.1" 200 -
INFO:__main__:[POLYGON] Storing polygon: 4 points, color: #FF0000, width: 2
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:22] "POST /api/polygon HTTP/1.1" 200 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 7212
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:22] "GET /api/image/polygon?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora&ts=1774658002210 HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:28] "POST /api/polygon/clear HTTP/1.1" 200 -
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 41 7212
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:33:37] "POST /api/krita/open HTTP/1.1" 200 -

View File

@@ -192,3 +192,135 @@ class ComfyUIService:
workflow["96"]["inputs"]["external_uid"] = metadata
return workflow
def prepare_sam_workflow(
self,
base_image: Image.Image,
include_points: list,
exclude_points: list,
webhook_url: str,
batch_id: str = None,
workflow_template: dict | None = None
) -> dict:
"""Prepare the SAM3 rough mask workflow with user-provided points."""
workflow = json.loads(json.dumps(workflow_template)) if workflow_template else {}
img_io = io.BytesIO()
base_image.save(img_io, format='PNG')
img_io.seek(0)
base64_image = base64.b64encode(img_io.read()).decode('utf-8')
if "9" in workflow:
workflow["9"]["inputs"]["image"] = base64_image
all_points = []
for pt in include_points:
all_points.append({
'x': pt['x'],
'y': pt['y'],
'is_foreground': True
})
for pt in exclude_points:
all_points.append({
'x': pt['x'],
'y': pt['y'],
'is_foreground': False
})
point_nodes = {}
point_node_ids = []
for i, pt in enumerate(all_points):
node_id = str(100 + i)
point_nodes[node_id] = {
"inputs": {
"x": pt['x'],
"y": pt['y'],
"is_foreground": pt['is_foreground']
},
"class_type": "SAM3CreatePoint",
"_meta": {"title": f"SAM3 Point {i+1}"}
}
point_node_ids.append(node_id)
for node_id, node_data in point_nodes.items():
workflow[node_id] = node_data
if point_node_ids:
combine_inputs = {}
for i, node_id in enumerate(point_node_ids):
combine_inputs[f"point_{i+1}"] = [node_id, 0]
workflow["8"] = {
"inputs": combine_inputs,
"class_type": "SAM3CombinePoints",
"_meta": {"title": "SAM3 Combine Points"}
}
if "1" in workflow:
workflow["1"]["inputs"]["positive_points"] = ["8", 0]
if "11" in workflow:
workflow["11"]["inputs"]["webhook_url"] = webhook_url
if batch_id:
workflow["11"]["inputs"]["external_uid"] = f"{batch_id}:0"
return workflow
def prepare_mask_workflow_with_start(
self,
base_image: Image.Image,
start_mask_image: Image.Image,
subject: str,
webhook_url: str,
seed: int,
batch_id: str = None,
mask_index: int = 0,
polygon_points: list | None = None,
polygon_color: str = '#FF0000',
polygon_width: int = 2,
workflow_template: dict | None = None
) -> dict:
"""Prepare the mask extraction workflow with a starting mask (lower denoise)."""
workflow = json.loads(json.dumps(workflow_template)) if workflow_template else {}
img = base_image.copy()
if polygon_points and len(polygon_points) >= 3:
w, h = img.size
pixel_points = [(int(p['x'] * w), int(p['y'] * h)) for p in polygon_points]
draw = ImageDraw.Draw(img)
hex_color = polygon_color if len(polygon_color) == 7 else polygon_color + 'FF'
draw.polygon(pixel_points, outline=hex_color, width=polygon_width)
img_io = io.BytesIO()
img.save(img_io, format='PNG')
img_io.seek(0)
base64_image = base64.b64encode(img_io.read()).decode('utf-8')
if "87" in workflow:
workflow["87"]["inputs"]["image"] = base64_image
start_mask_io = io.BytesIO()
start_mask_image.save(start_mask_io, format='PNG')
start_mask_io.seek(0)
start_mask_base64 = base64.b64encode(start_mask_io.read()).decode('utf-8')
if "200" in workflow:
workflow["200"]["inputs"]["image"] = start_mask_base64
if "1:68" in workflow and 'inputs' in workflow["1:68"]:
workflow["1:68"]["inputs"]["prompt"] = f"Create a black and white alpha mask of {subject}, leaving everything else black"
if "96" in workflow and 'inputs' in workflow["96"]:
workflow["96"]["inputs"]["webhook_url"] = webhook_url
if "50" in workflow and 'inputs' in workflow["50"]:
workflow["50"]["inputs"]["seed"] = seed
if "96" in workflow and 'inputs' in workflow["96"]:
metadata = f"{batch_id}:{mask_index}" if batch_id else str(mask_index)
workflow["96"]["inputs"]["external_uid"] = metadata
return workflow

View File

@@ -17,6 +17,40 @@
</div>
</template>
<!-- SAM Include points (green) -->
<template x-if="mode === 'add' && isSamMode">
<template x-for="(point, idx) in samIncludePoints" :key="'sam-include-' + idx">
<div
class="absolute w-5 h-5 bg-green-500 border-2 border-white rounded-full cursor-move z-20 flex items-center justify-center text-xs font-bold text-white"
style="transform: translate(-50%, -50%);"
:style="`left: ${point.x * 100}%; top: ${point.y * 100}%`"
@contextmenu.prevent="removeSamPoint('include', idx)"
x-text="idx + 1"
></div>
</template>
</template>
<!-- SAM Exclude points (red) -->
<template x-if="mode === 'add' && isSamMode">
<template x-for="(point, idx) in samExcludePoints" :key="'sam-exclude-' + idx">
<div
class="absolute w-5 h-5 bg-red-500 border-2 border-white rounded-full cursor-move z-20 flex items-center justify-center text-xs font-bold text-white"
style="transform: translate(-50%, -50%);"
:style="`left: ${point.x * 100}%; top: ${point.y * 100}%`"
@contextmenu.prevent="removeSamPoint('exclude', idx)"
x-text="'X'"
></div>
</template>
</template>
<!-- SAM mask preview overlay -->
<img
x-show="samMaskUrl"
:src="samMaskUrl"
class="absolute inset-0 w-full h-full object-contain pointer-events-none z-15 opacity-50"
alt="SAM mask preview"
>
<!-- Polygon points markers (draggable) - shown in add mode -->
<template x-if="mode === 'add' && polygonPoints.length > 0">
<template x-for="(point, idx) in polygonPoints" :key="'point-' + idx">
@@ -47,6 +81,15 @@
class="absolute inset-0 cursor-crosshair pointer-events-auto border-2 border-dashed border-blue-500 opacity-90"
></canvas>
<!-- SAM click canvas -->
<div
x-show="isSamMode"
id="samCanvas"
class="absolute inset-0 cursor-crosshair z-10"
@click="handleSamClick($event)"
@contextmenu.prevent="handleSamRightClick($event)"
></div>
</div>
</div>
@@ -60,13 +103,16 @@
<div x-show="error" class="text-red-400 text-center mt-8" x-text="error"></div>
<!-- Mode-specific instructions -->
<div x-show="isSamMode" class="mt-2 text-sm text-gray-400">
Left-click to add include points (green). Right-click to add exclude points (red). Right-click on point to remove.
</div>
<div x-show="isDrawing" class="mt-2 text-sm text-gray-400">
Click to add points. Drag points to adjust. Double-click or Enter to finish, Escape to cancel.
</div>
<div x-show="mode === 'add' && !isDrawing && polygonPoints.length >= 3" class="mt-2 text-sm text-gray-400">
Drag points to adjust polygon, then extract mask or open in Krita.
</div>
<div x-show="mode === 'add' && !isDrawing && !polygonPreviewUrl && polygonPoints.length < 3" class="mt-2 text-sm text-gray-400">
Draw a polygon (optional) then extract mask, or use Open in Krita to annotation manually.
<div x-show="mode === 'add' && !isDrawing && !polygonPreviewUrl && polygonPoints.length < 3 && !isSamMode" class="mt-2 text-sm text-gray-400">
Use SAM rough mask or draw a polygon, then extract mask.
</div>
</main>

View File

@@ -64,6 +64,71 @@
<p class="text-xs text-gray-400 mt-1">Will create layer: <span x-text="entityName ? entityName + '_0' : 'element_0'"></span></p>
</div>
<!-- SAM Rough Mask -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
<h3 class="font-bold mb-3 text-gray-300">Rough Mask (SAM)</h3>
<div class="space-y-3">
<p class="text-xs text-gray-400">Click to mark include points (green). Right-click to mark exclude points (red).</p>
<div x-show="samIncludePoints.length > 0 || samExcludePoints.length > 0" class="text-xs text-gray-300 space-y-1">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-green-500 rounded-full"></span>
<span>Include: <span x-text="samIncludePoints.length"></span></span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-red-500 rounded-full"></span>
<span>Exclude: <span x-text="samExcludePoints.length"></span></span>
</div>
</div>
<div class="flex gap-2">
<button
@click="startSamMode()"
:disabled="isSamMode"
class="flex-1 bg-teal-600 hover:bg-teal-700 disabled:bg-gray-600 px-3 py-1.5 rounded text-sm transition"
>
Start
</button>
<button
@click="clearSamPoints()"
:disabled="samIncludePoints.length === 0 && samExcludePoints.length === 0"
class="bg-gray-600 hover:bg-gray-500 disabled:bg-gray-700 px-3 py-1.5 rounded text-sm transition"
>
Clear
</button>
</div>
<button
@click="generateSamMask()"
:disabled="samIncludePoints.length === 0 || isSamGenerating"
class="w-full bg-teal-600 hover:bg-teal-700 disabled:bg-gray-600 px-4 py-2 rounded transition flex items-center justify-center gap-2"
>
<span x-show="isSamGenerating" class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></span>
<span x-show="!isSamGenerating">Generate Rough Mask</span>
<span x-show="isSamGenerating">Generating...</span>
</button>
<div x-show="samMaskUrl" class="space-y-2">
<p class="text-xs text-green-400">Rough mask ready!</p>
<div class="flex gap-2">
<button
@click="useSamMask()"
class="flex-1 bg-green-600 hover:bg-green-700 px-3 py-1.5 rounded text-sm"
>
Use as Mask
</button>
<button
@click="discardSamMask()"
class="bg-gray-600 hover:bg-gray-500 px-3 py-1.5 rounded text-sm"
>
Discard
</button>
</div>
</div>
</div>
</div>
<!-- Polygon tool -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
<h3 class="font-bold mb-3 text-gray-300">Polygon (Optional)</h3>
@@ -110,6 +175,16 @@
<span class="text-sm">Use polygon hint</span>
</label>
<label x-show="samMaskPath" class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
x-model="useSamAsStart"
:disabled="isExtracting || !samMaskPath"
class="w-4 h-4 rounded"
>
<span class="text-sm">Use SAM mask as starting point (0.8 denoise)</span>
</label>
<!-- Count selector -->
<div class="flex items-center gap-2">
<label class="text-sm text-gray-400">Generate:</label>

View File

@@ -132,6 +132,15 @@ function oraEditor() {
polygonWidth: 2,
polygonPreviewUrl: null,
// SAM rough mask state
isSamMode: false,
samIncludePoints: [],
samExcludePoints: [],
isSamGenerating: false,
samMaskPath: null,
samMaskUrl: null,
useSamAsStart: true,
// Mask extraction
maskSubject: '',
usePolygonHint: true,
@@ -301,6 +310,7 @@ function oraEditor() {
this.entityName = '';
this.maskSubject = '';
this.clearPolygon();
this.clearSamPoints();
this.scale = 100;
},
@@ -310,6 +320,11 @@ function oraEditor() {
this.isDrawing = false;
this.polygonPoints = [];
this.polygonPreviewUrl = null;
this.isSamMode = false;
this.samIncludePoints = [];
this.samExcludePoints = [];
this.samMaskPath = null;
this.samMaskUrl = null;
const canvas = document.getElementById('polygonCanvas');
if (canvas) {
canvas.style.display = 'none';
@@ -319,6 +334,8 @@ function oraEditor() {
async cancelAddMode() {
if (this.isDrawing) {
this.clearPolygon();
} else if (this.isSamMode) {
this.isSamMode = false;
} else {
this.exitAddMode();
}
@@ -585,6 +602,118 @@ function oraEditor() {
}
},
// === SAM Rough Mask ===
startSamMode() {
console.log('[ORA EDITOR] Starting SAM mode');
this.isSamMode = true;
this.samIncludePoints = [];
this.samExcludePoints = [];
this.samMaskPath = null;
this.samMaskUrl = null;
},
clearSamPoints() {
this.samIncludePoints = [];
this.samExcludePoints = [];
this.samMaskPath = null;
this.samMaskUrl = null;
},
handleSamClick(e) {
if (!this.isSamMode) return;
const container = document.getElementById('imageContainer');
if (!container) return;
const rect = container.getBoundingClientRect();
let x = (e.clientX - rect.left) / rect.width;
let y = (e.clientY - rect.top) / rect.height;
x = Math.max(0, Math.min(1, x));
y = Math.max(0, Math.min(1, y));
this.samIncludePoints.push({ x, y });
console.log('[SAM] Added include point:', x, y);
},
handleSamRightClick(e) {
if (!this.isSamMode) return;
e.preventDefault();
const container = document.getElementById('imageContainer');
if (!container) return;
const rect = container.getBoundingClientRect();
let x = (e.clientX - rect.left) / rect.width;
let y = (e.clientY - rect.top) / rect.height;
x = Math.max(0, Math.min(1, x));
y = Math.max(0, Math.min(1, y));
this.samExcludePoints.push({ x, y });
console.log('[SAM] Added exclude point:', x, y);
},
removeSamPoint(type, idx) {
if (type === 'include') {
this.samIncludePoints.splice(idx, 1);
} else {
this.samExcludePoints.splice(idx, 1);
}
},
async generateSamMask() {
if (this.samIncludePoints.length === 0 || !this.oraPath) return;
console.log('[ORA EDITOR] Generating SAM mask with', this.samIncludePoints.length, 'include points');
this.isSamGenerating = true;
try {
const response = await fetch('/api/sam/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ora_path: this.oraPath,
include_points: this.samIncludePoints,
exclude_points: this.samExcludePoints,
comfy_url: this.comfyUrl
})
});
const data = await response.json();
console.log('[ORA EDITOR] SAM response:', data);
if (!data.success) throw new Error(data.error || 'Failed');
this.samMaskPath = data.mask_path;
this.samMaskUrl = data.mask_url;
this.isSamMode = false;
} catch (e) {
console.error('[ORA EDITOR] Error generating SAM mask:', e);
this.lastError = e.message;
} finally {
this.isSamGenerating = false;
}
},
useSamMask() {
if (this.samMaskPath) {
this.tempMaskPath = this.samMaskPath;
this.tempMaskUrl = this.samMaskUrl;
this.tempMaskPaths = [this.samMaskPath];
this.currentMaskIndex = 0;
this.showMaskModal = true;
}
},
discardSamMask() {
this.samMaskPath = null;
this.samMaskUrl = null;
this.samIncludePoints = [];
this.samExcludePoints = [];
},
// === Mask Extraction ===
async extractMask() {
if (!this.maskSubject.trim()) return;
@@ -593,17 +722,24 @@ function oraEditor() {
this.isExtracting = true;
this.lastError = '';
try {
const response = await fetch('/api/mask/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
const requestBody = {
subject: this.maskSubject,
use_polygon: this.usePolygonHint && this.polygonPoints.length >= 3,
ora_path: this.oraPath,
comfy_url: this.comfyUrl,
count: this.maskCount
})
};
if (this.useSamAsStart && this.samMaskPath) {
requestBody.start_mask_path = this.samMaskPath;
console.log('[ORA EDITOR] Using SAM mask as starting point:', this.samMaskPath);
}
try {
const response = await fetch('/api/mask/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();