ora editor
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ tmp/**
|
||||
.dolt/
|
||||
*.db
|
||||
**/*.pyc
|
||||
tools/ora_editor/node_modules/*
|
||||
node_modules/*
|
||||
|
||||
BIN
asset-work/combo_outputs/003/003_caption_1_2060565090_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/003/003_caption_1_2060565090_generated.ora
LFS
Normal file
Binary file not shown.
BIN
asset-work/combo_outputs/003/003_caption_1_2805438421_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/003/003_caption_1_2805438421_generated.ora
LFS
Normal file
Binary file not shown.
BIN
asset-work/combo_outputs/004/004_caption_1_2880409319_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/004/004_caption_1_2880409319_generated.ora
LFS
Normal file
Binary file not shown.
BIN
asset-work/combo_outputs/017/017_caption_1_3211845159_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/017/017_caption_1_3211845159_generated.ora
LFS
Normal file
Binary file not shown.
BIN
asset-work/combo_outputs/033/033_caption_1_1432568518_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/033/033_caption_1_1432568518_generated.ora
LFS
Normal file
Binary file not shown.
BIN
asset-work/combo_outputs/090/090_caption_1_1547249005_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/090/090_caption_1_1547249005_generated.ora
LFS
Normal file
Binary file not shown.
36
console-logs.txt
Normal file
36
console-logs.txt
Normal 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
mask_view_test.png
LFS
Normal file
BIN
mask_view_test.png
LFS
Normal file
Binary file not shown.
17
opencode.json
Normal file
17
opencode.json
Normal 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
BIN
ora_editor_initial.png
LFS
Normal file
Binary file not shown.
71
package-lock.json
generated
Normal file
71
package-lock.json
generated
Normal 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
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.58.2"
|
||||
}
|
||||
}
|
||||
BIN
page-with-polygon.png
LFS
Normal file
BIN
page-with-polygon.png
LFS
Normal file
Binary file not shown.
8
playwright.config.ts
Normal file
8
playwright.config.ts
Normal 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
BIN
polygon-8points.png
LFS
Normal file
Binary file not shown.
BIN
polygon-drawing-test.png
LFS
Normal file
BIN
polygon-drawing-test.png
LFS
Normal file
Binary file not shown.
BIN
polygon-test.png
LFS
Normal file
BIN
polygon-test.png
LFS
Normal file
Binary file not shown.
BIN
scenes/kq4_001_beach/bg.ora
LFS
BIN
scenes/kq4_001_beach/bg.ora
LFS
Binary file not shown.
BIN
scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora
LFS
Normal file
BIN
scenes/kq4_083_castle_dungeon_cell/pic_083_visual.ora
LFS
Normal file
Binary file not shown.
BIN
scenes/kq4_095_fishermans_pier/pic_095_visual.ora
LFS
Normal file
BIN
scenes/kq4_095_fishermans_pier/pic_095_visual.ora
LFS
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
403
tools/image_mask_extraction_with_start.json
Normal file
403
tools/image_mask_extraction_with_start.json
Normal 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
126
tools/mask_rough_cut.json
Executable 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
71
tools/ora_editor/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
tools/ora_editor/package.json
Normal file
5
tools/ora_editor/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.58.2"
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
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,18 +183,33 @@ def api_mask_extract():
|
||||
prompt_ids = []
|
||||
|
||||
for i in range(count):
|
||||
workflow = comfy_service.prepare_mask_workflow(
|
||||
base_image=base_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
|
||||
)
|
||||
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,
|
||||
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
|
||||
)
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Workflow {i} prepared, sending to ComfyUI at http://{comfy_url}")
|
||||
|
||||
|
||||
107
tools/ora_editor/routes/sam.py
Normal file
107
tools/ora_editor/routes/sam.py
Normal 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}'
|
||||
})
|
||||
52
tools/ora_editor/server.log
Normal file
52
tools/ora_editor/server.log
Normal 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:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on http://127.0.0.1:5001
|
||||
INFO:werkzeug:[33mPress CTRL+C to quit[0m
|
||||
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] "[31m[1mGET /api/image/base?ora_path= HTTP/1.1[0m" 400 -
|
||||
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:02] "[31m[1mGET /api/image/masked?ora_path=&mask_path=null HTTP/1.1[0m" 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] "[35m[1mGET /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[0m" 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] "[31m[1mGET /api/image/base?ora_path= HTTP/1.1[0m" 400 -
|
||||
INFO:werkzeug:127.0.0.1 - - [27/Mar/2026 17:32:37] "[31m[1mGET /api/image/masked?ora_path=&mask_path=null HTTP/1.1[0m" 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] "[35m[1mGET /api/image/masked?ora_path=/home/noti/dev/ai-game-2/scenes/kq4_001_beach/bg.ora&mask_path=null HTTP/1.1[0m" 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 -
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
@@ -46,6 +80,15 @@
|
||||
:height="imageHeight"
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
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({
|
||||
subject: this.maskSubject,
|
||||
use_polygon: this.usePolygonHint && this.polygonPoints.length >= 3,
|
||||
ora_path: this.oraPath,
|
||||
comfy_url: this.comfyUrl,
|
||||
count: this.maskCount
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user