Restructure ORA editor into modular blueprints with browse dialog
- Split app.py into route blueprints (files, layers, images, polygon, mask, krita) - Create services layer (polygon_storage, comfyui, file_browser) - Extract config constants to config.py - Split templates into Jinja partials (base, components, modals) - Add browse dialog for visual file navigation - Add /api/browse endpoint for directory listing
This commit is contained in:
17
tools/ora_editor/routes/__init__.py
Normal file
17
tools/ora_editor/routes/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Route blueprints for ORA Editor."""
|
||||
|
||||
from .files import files_bp
|
||||
from .layers import layers_bp
|
||||
from .images import images_bp
|
||||
from .polygon import polygon_bp
|
||||
from .mask import mask_bp
|
||||
from .krita import krita_bp
|
||||
|
||||
__all__ = [
|
||||
'files_bp',
|
||||
'layers_bp',
|
||||
'images_bp',
|
||||
'polygon_bp',
|
||||
'mask_bp',
|
||||
'krita_bp'
|
||||
]
|
||||
100
tools/ora_editor/routes/files.py
Normal file
100
tools/ora_editor/routes/files.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""File operations routes for ORA Editor."""
|
||||
|
||||
import zipfile
|
||||
from flask import Blueprint, request, jsonify
|
||||
from pathlib import Path
|
||||
|
||||
from ora_editor.config import PROJECT_ROOT, TEMP_DIR
|
||||
from ora_editor.ora_ops import (
|
||||
load_ora, create_ora_from_png, parse_stack_xml, save_ora
|
||||
)
|
||||
from ora_editor.services.file_browser import FileBrowserService
|
||||
from ora_editor.services import polygon_storage
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
files_bp = Blueprint('files', __name__)
|
||||
browser_service = FileBrowserService(PROJECT_ROOT)
|
||||
|
||||
|
||||
def ensure_ora_exists(input_path: str) -> str | None:
|
||||
"""Ensure an ORA file exists, creating from PNG if necessary."""
|
||||
full_path = PROJECT_ROOT / input_path
|
||||
|
||||
if not full_path.exists():
|
||||
return None
|
||||
|
||||
if full_path.suffix.lower() == '.png':
|
||||
ora_path = str(full_path.with_suffix('.ora'))
|
||||
result = create_ora_from_png(str(full_path), ora_path)
|
||||
if not result.get('success'):
|
||||
return None
|
||||
return ora_path
|
||||
|
||||
try:
|
||||
parse_stack_xml(str(full_path))
|
||||
return str(full_path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@files_bp.route('/api/browse')
|
||||
def api_browse():
|
||||
"""Browse project directory structure."""
|
||||
path = request.args.get('path', '')
|
||||
return jsonify(browser_service.list_directory(path))
|
||||
|
||||
|
||||
@files_bp.route('/api/open', methods=['POST'])
|
||||
def api_open():
|
||||
"""Open a file (PNG or ORA)."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'path' not in data:
|
||||
return jsonify({'success': False, 'error': 'Missing path parameter'}), 400
|
||||
|
||||
input_path = data['path']
|
||||
ora_path = ensure_ora_exists(input_path)
|
||||
|
||||
if not ora_path:
|
||||
return jsonify({'success': False, 'error': f'File not found or invalid: {input_path}'}), 404
|
||||
|
||||
try:
|
||||
loaded = load_ora(ora_path)
|
||||
|
||||
layers = []
|
||||
for layer in loaded['layers']:
|
||||
layers.append({
|
||||
'name': layer['name'],
|
||||
'src': layer['src'],
|
||||
'group': layer.get('group'),
|
||||
'visible': layer.get('visible', True)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'ora_path': ora_path,
|
||||
'width': loaded['width'],
|
||||
'height': loaded['height'],
|
||||
'layers': layers
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Error loading ORA: {str(e)}'}), 500
|
||||
|
||||
|
||||
@files_bp.route('/api/save', methods=['POST'])
|
||||
def api_save():
|
||||
"""Save the current ORA state."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'ora_path' not in data:
|
||||
return jsonify({'success': False, 'error': 'Missing ora_path parameter'}), 400
|
||||
|
||||
ora_path = data['ora_path']
|
||||
result = save_ora(ora_path)
|
||||
|
||||
if result:
|
||||
return jsonify({'success': True, 'ora_path': ora_path})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Failed to save ORA'}), 500
|
||||
165
tools/ora_editor/routes/images.py
Normal file
165
tools/ora_editor/routes/images.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Image serving routes for ORA Editor."""
|
||||
|
||||
import io
|
||||
import zipfile
|
||||
from flask import Blueprint, request, jsonify, send_file, Response
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from ora_editor.ora_ops import parse_stack_xml
|
||||
from ora_editor.config import PROJECT_ROOT
|
||||
|
||||
images_bp = Blueprint('images', __name__)
|
||||
|
||||
|
||||
@images_bp.route('/api/image/<layer_name>')
|
||||
def api_image(layer_name):
|
||||
"""Serve a specific layer image."""
|
||||
ora_path = request.args.get('ora_path')
|
||||
|
||||
if not ora_path:
|
||||
return jsonify({'error': 'Missing ora_path'}), 400
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
img_data = zf.read('mergedimage.png')
|
||||
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
||||
except Exception as e:
|
||||
return Response(f"Error loading image: {e}", status=500)
|
||||
|
||||
|
||||
@images_bp.route('/api/image/base')
|
||||
def api_image_base():
|
||||
"""Serve the base/merged image."""
|
||||
ora_path = request.args.get('ora_path')
|
||||
|
||||
if not ora_path:
|
||||
return jsonify({'error': 'Missing ora_path'}), 400
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
img_data = zf.read('mergedimage.png')
|
||||
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
||||
except Exception as e:
|
||||
return Response(f"Error loading image: {e}", status=500)
|
||||
|
||||
|
||||
@images_bp.route('/api/image/layer/<layer_name>')
|
||||
def api_image_layer(layer_name):
|
||||
"""Serve a specific layer as image."""
|
||||
ora_path = request.args.get('ora_path')
|
||||
|
||||
if not ora_path:
|
||||
return jsonify({'error': 'Missing ora_path'}), 400
|
||||
|
||||
try:
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
stack = ET.fromstring(zf.read('stack.xml'))
|
||||
|
||||
for child in stack.find('stack'):
|
||||
if child.tag == 'layer' and child.get('name') == layer_name:
|
||||
src = child.get('src')
|
||||
img_data = zf.read(src)
|
||||
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
||||
|
||||
elif child.tag == 'stack':
|
||||
for layer in child:
|
||||
if layer.tag == 'layer' and layer.get('name') == layer_name:
|
||||
src = layer.get('src')
|
||||
img_data = zf.read(src)
|
||||
return send_file(io.BytesIO(img_data), mimetype='image/png')
|
||||
|
||||
return jsonify({'error': 'Layer not found'}), 404
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return Response(f"Error loading layer: {e}", status=500)
|
||||
|
||||
|
||||
@images_bp.route('/api/image/polygon')
|
||||
def api_image_polygon():
|
||||
"""Serve polygon overlay if stored."""
|
||||
from ora_editor.services import polygon_storage
|
||||
|
||||
ora_path = request.args.get('ora_path')
|
||||
|
||||
if not ora_path:
|
||||
return Response("Missing ora_path", status=400)
|
||||
|
||||
poly_data = polygon_storage.get(ora_path)
|
||||
if not poly_data:
|
||||
return Response("No polygon data", status=404)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
base_img = Image.open(zf.open('mergedimage.png')).convert('RGBA')
|
||||
|
||||
points = poly_data.get('points', [])
|
||||
color = poly_data.get('color', '#FF0000')
|
||||
width = poly_data.get('width', 2)
|
||||
|
||||
if len(points) < 3:
|
||||
return Response("Not enough points", status=404)
|
||||
|
||||
w, h = base_img.size
|
||||
pixel_points = [(int(p['x'] * w), int(p['y'] * h)) for p in points]
|
||||
|
||||
draw = ImageDraw.Draw(base_img)
|
||||
hex_color = color if len(color) == 7 else color + 'FF'
|
||||
draw.polygon(pixel_points, outline=hex_color, width=width)
|
||||
|
||||
img_io = io.BytesIO()
|
||||
base_img.save(img_io, format='PNG')
|
||||
img_io.seek(0)
|
||||
|
||||
return send_file(img_io, mimetype='image/png')
|
||||
except Exception as e:
|
||||
return Response(f"Error drawing polygon: {e}", status=500)
|
||||
|
||||
|
||||
@images_bp.route('/api/image/masked')
|
||||
def api_image_masked():
|
||||
"""Serve the base image with mask applied as alpha channel."""
|
||||
ora_path = request.args.get('ora_path')
|
||||
mask_path = request.args.get('mask_path')
|
||||
|
||||
if not ora_path or not mask_path:
|
||||
return jsonify({'error': 'Missing ora_path or mask_path'}), 400
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
base_img = Image.open(zf.open('mergedimage.png')).convert('RGBA')
|
||||
|
||||
mask_img = Image.open(mask_path).convert('L')
|
||||
|
||||
if mask_img.size != base_img.size:
|
||||
mask_img = mask_img.resize(base_img.size, Image.Resampling.BILINEAR)
|
||||
|
||||
r, g, b, _ = base_img.split()
|
||||
result = Image.merge('RGBA', (r, g, b, mask_img))
|
||||
|
||||
img_io = io.BytesIO()
|
||||
result.save(img_io, format='PNG')
|
||||
img_io.seek(0)
|
||||
|
||||
return send_file(img_io, mimetype='image/png')
|
||||
except Exception as e:
|
||||
return Response(f"Error creating masked image: {e}", status=500)
|
||||
|
||||
|
||||
@images_bp.route('/api/file/mask')
|
||||
def api_file_mask():
|
||||
"""Serve a mask file from temp directory."""
|
||||
path = request.args.get('path')
|
||||
|
||||
if not path:
|
||||
return jsonify({'error': 'Missing path'}), 400
|
||||
|
||||
full_path = Path(path)
|
||||
|
||||
if not full_path.exists():
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
|
||||
return send_file(full_path, mimetype='image/png')
|
||||
108
tools/ora_editor/routes/krita.py
Normal file
108
tools/ora_editor/routes/krita.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Krita integration routes for ORA Editor."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, request, jsonify
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from ora_editor.config import TEMP_DIR
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
krita_bp = Blueprint('krita', __name__)
|
||||
|
||||
|
||||
@krita_bp.route('/api/krita/open', methods=['POST'])
|
||||
def api_krita_open():
|
||||
"""Open in Krita - behavior depends on mode.
|
||||
|
||||
Review mode: Open the full ORA file for layer editing.
|
||||
Add mode: Export base image with polygon overlay for manual annotation.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'ora_path' not in data:
|
||||
return jsonify({'success': False, 'error': 'Missing ora_path parameter'}), 400
|
||||
|
||||
ora_path = data['ora_path']
|
||||
|
||||
try:
|
||||
if data.get('open_full_ora'):
|
||||
timestamp = str(int(time.time()))
|
||||
temp_file = TEMP_DIR / f"edit_{timestamp}.ora"
|
||||
shutil.copy2(ora_path, temp_file)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'file_url': f'file://{temp_file}',
|
||||
'temp_path': str(temp_file),
|
||||
'base_mtime': os.path.getmtime(temp_file)
|
||||
})
|
||||
|
||||
elif data.get('open_base_with_polygon'):
|
||||
from ora_editor.services import polygon_storage
|
||||
|
||||
points = data.get('points', [])
|
||||
color = data.get('color', '#FF0000')
|
||||
width = data.get('width', 2)
|
||||
|
||||
with zipfile.ZipFile(ora_path, 'r') as zf:
|
||||
img = Image.open(zf.open('mergedimage.png')).convert('RGBA')
|
||||
|
||||
if points and len(points) >= 3:
|
||||
w, h = img.size
|
||||
pixel_points = [(int(p['x'] * w), int(p['y'] * h)) for p in points]
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
hex_color = color if len(color) == 7 else color + 'FF'
|
||||
draw.polygon(pixel_points, outline=hex_color, width=width)
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
temp_file = TEMP_DIR / f"annotation_{timestamp}.png"
|
||||
img.save(str(temp_file), format='PNG')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'file_url': f'file://{temp_file}',
|
||||
'temp_path': str(temp_file),
|
||||
'base_mtime': os.path.getmtime(temp_file)
|
||||
})
|
||||
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Invalid request - specify open_full_ora or open_base_with_polygon'}), 400
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"[KRITA OPEN] Error: {e}")
|
||||
traceback.print_exc()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@krita_bp.route('/api/krita/status/<layer_name>')
|
||||
def api_krita_status(layer_name):
|
||||
"""Check if a Krita temp file has been modified."""
|
||||
temp_files = list(TEMP_DIR.glob(f"{layer_name}_*.png"))
|
||||
|
||||
if not temp_files:
|
||||
return jsonify({'success': False, 'error': 'No temp file found'}), 404
|
||||
|
||||
temp_file = max(temp_files, key=lambda f: f.stat().st_mtime)
|
||||
|
||||
mtime = temp_file.stat().st_mtime
|
||||
|
||||
stored_mtime = request.args.get('stored_mtime')
|
||||
if stored_mtime:
|
||||
modified = float(stored_mtime) != mtime
|
||||
else:
|
||||
modified = True
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'modified': modified,
|
||||
'mtime': mtime,
|
||||
'file_url': f'file://{temp_file}'
|
||||
})
|
||||
111
tools/ora_editor/routes/layers.py
Normal file
111
tools/ora_editor/routes/layers.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Layer operations routes for ORA Editor."""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from ora_editor.ora_ops import (
|
||||
load_ora, add_masked_layer, rename_layer, delete_layer,
|
||||
reorder_layer, set_layer_visibility
|
||||
)
|
||||
|
||||
layers_bp = Blueprint('layers', __name__)
|
||||
|
||||
|
||||
@layers_bp.route('/api/layers', methods=['GET'])
|
||||
def api_layers():
|
||||
"""Get the current layer structure."""
|
||||
ora_path = request.args.get('ora_path')
|
||||
|
||||
if not ora_path:
|
||||
return jsonify({'success': False, 'error': 'Missing ora_path parameter'}), 400
|
||||
|
||||
try:
|
||||
loaded = load_ora(ora_path)
|
||||
|
||||
layers = []
|
||||
for layer in loaded['layers']:
|
||||
layers.append({
|
||||
'name': layer['name'],
|
||||
'src': layer['src'],
|
||||
'group': layer.get('group'),
|
||||
'visible': layer.get('visible', True)
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'layers': layers})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@layers_bp.route('/api/layer/add', methods=['POST'])
|
||||
def api_layer_add():
|
||||
"""Add a new masked layer."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', 'entity_name', 'mask_path']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
ora_path = data['ora_path']
|
||||
entity_name = data['entity_name']
|
||||
mask_path = data['mask_path']
|
||||
source_layer = data.get('source_layer', 'base')
|
||||
|
||||
result = add_masked_layer(ora_path, entity_name, mask_path, source_layer)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@layers_bp.route('/api/layer/rename', methods=['POST'])
|
||||
def api_layer_rename():
|
||||
"""Rename a layer."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', 'old_name', 'new_name']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
result = rename_layer(data['ora_path'], data['old_name'], data['new_name'])
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@layers_bp.route('/api/layer/delete', methods=['POST'])
|
||||
def api_layer_delete():
|
||||
"""Delete a layer."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', 'layer_name']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
result = delete_layer(data['ora_path'], data['layer_name'])
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@layers_bp.route('/api/layer/reorder', methods=['POST'])
|
||||
def api_layer_reorder():
|
||||
"""Reorder a layer."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', 'layer_name', 'direction']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
result = reorder_layer(data['ora_path'], data['layer_name'], data['direction'])
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@layers_bp.route('/api/layer/visibility', methods=['POST'])
|
||||
def api_layer_visibility():
|
||||
"""Set layer visibility."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', 'layer_name', 'visible']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
result = set_layer_visibility(data['ora_path'], data['layer_name'], data['visible'])
|
||||
return jsonify(result)
|
||||
161
tools/ora_editor/routes/mask.py
Normal file
161
tools/ora_editor/routes/mask.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Mask extraction routes for ORA Editor."""
|
||||
|
||||
import io
|
||||
import json
|
||||
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 import polygon_storage
|
||||
from ora_editor.services.comfyui import ComfyUIService
|
||||
from ora_editor.ora_ops import parse_stack_xml
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mask_bp = Blueprint('mask', __name__)
|
||||
comfy_service = ComfyUIService(COMFYUI_BASE_URL)
|
||||
|
||||
|
||||
@mask_bp.route('/api/webhook/comfyui', methods=['POST'])
|
||||
def api_webhook_comfyui():
|
||||
"""Webhook endpoint for ComfyUI to post completed mask images."""
|
||||
logger.info("[WEBHOOK] Received webhook from ComfyUI")
|
||||
|
||||
result = comfy_service.handle_webhook(
|
||||
request.files,
|
||||
request.form if request.form else {},
|
||||
request.data,
|
||||
TEMP_DIR
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
response = make_response(jsonify({'status': 'ok', 'message': 'Image received'}))
|
||||
response.status_code = 200
|
||||
else:
|
||||
response = make_response(jsonify({'status': 'error', 'message': result.get('error', 'Unknown error')}))
|
||||
response.status_code = 500
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@mask_bp.route('/api/mask/extract', methods=['POST'])
|
||||
def api_mask_extract():
|
||||
"""Extract a mask from the current base image using ComfyUI."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['subject', 'ora_path']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'success': False, 'error': f'Missing {field} parameter'}), 400
|
||||
|
||||
subject = data['subject']
|
||||
use_polygon = data.get('use_polygon', False)
|
||||
ora_path = data['ora_path']
|
||||
comfy_url = data.get('comfy_url', COMFYUI_BASE_URL)
|
||||
|
||||
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}")
|
||||
|
||||
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
|
||||
|
||||
with open(workflow_path) as f:
|
||||
workflow_template = json.load(f)
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Loaded workflow")
|
||||
|
||||
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"[MASK EXTRACT] Loaded base image: {base_img.size}")
|
||||
except Exception as e:
|
||||
logger.error(f"[MASK EXTRACT] Error loading base image: {e}")
|
||||
return jsonify({'success': False, 'error': f'Error loading image: {str(e)}'}), 500
|
||||
|
||||
polygon_points = None
|
||||
polygon_color = '#FF0000'
|
||||
polygon_width = 2
|
||||
|
||||
if use_polygon:
|
||||
poly_data = polygon_storage.get(ora_path)
|
||||
if poly_data:
|
||||
polygon_points = poly_data.get('points', [])
|
||||
polygon_color = poly_data.get('color', '#FF0000')
|
||||
polygon_width = poly_data.get('width', 2)
|
||||
|
||||
if not polygon_points or len(polygon_points) < 3:
|
||||
logger.warning(f"[MASK EXTRACT] Use polygon requested but no valid polygon points stored")
|
||||
return jsonify({'success': False, 'error': 'No valid polygon points. Please draw polygon first.'}), 400
|
||||
|
||||
webhook_url = f"http://localhost:5001/api/webhook/comfyui"
|
||||
|
||||
workflow = comfy_service.prepare_mask_workflow(
|
||||
base_image=base_img,
|
||||
subject=subject,
|
||||
webhook_url=webhook_url,
|
||||
polygon_points=polygon_points,
|
||||
polygon_color=polygon_color,
|
||||
polygon_width=polygon_width,
|
||||
workflow_template=workflow_template
|
||||
)
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Workflow prepared, sending to ComfyUI at http://{comfy_url}")
|
||||
|
||||
try:
|
||||
prompt_id = comfy_service.submit_workflow(workflow, comfy_url)
|
||||
logger.info(f"[MASK EXTRACT] Prompt submitted with ID: {prompt_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"[MASK EXTRACT] Error submitting to ComfyUI: {e}")
|
||||
return jsonify({'success': False, 'error': f'Failed to connect to ComfyUI: {str(e)}'}), 500
|
||||
|
||||
completed = comfy_service.poll_for_completion(prompt_id, comfy_url, timeout=240)
|
||||
|
||||
if not completed:
|
||||
logger.error("[MASK EXTRACT] Timeout waiting for ComfyUI to complete")
|
||||
return jsonify({'success': False, 'error': 'Mask extraction timed out'}), 500
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Checking/waiting for webhook callback from ComfyUI...")
|
||||
|
||||
webhook_result = comfy_service.wait_for_webhook(timeout=60.0)
|
||||
|
||||
if not webhook_result:
|
||||
logger.error(f"[MASK EXTRACT] Timeout waiting for webhook callback")
|
||||
return jsonify({'success': False, 'error': 'Webhook timeout - mask extraction may have failed'}), 500
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Webhook received: {webhook_result}")
|
||||
|
||||
if not webhook_result.get('success'):
|
||||
error_msg = webhook_result.get('error', 'Unknown error')
|
||||
logger.error(f"[MASK EXTRACT] Webhook failed: {error_msg}")
|
||||
return jsonify({'success': False, 'error': f'Webhook error: {error_msg}'}), 500
|
||||
|
||||
final_mask_path = webhook_result.get('path')
|
||||
|
||||
if not final_mask_path:
|
||||
logger.error("[MASK EXTRACT] No mask path in webhook response")
|
||||
return jsonify({'success': False, 'error': 'No mask path returned from webhook'}), 500
|
||||
|
||||
try:
|
||||
if not Path(final_mask_path).exists():
|
||||
logger.error(f"[MASK EXTRACT] Mask file not found at {final_mask_path}")
|
||||
return jsonify({'success': False, 'error': f'Mask file not found: {final_mask_path}'}), 500
|
||||
|
||||
logger.info(f"[MASK EXTRACT] Mask received via webhook at {final_mask_path}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'mask_path': str(final_mask_path),
|
||||
'mask_url': f'/api/file/mask?path={final_mask_path}'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"[MASK EXTRACT] Error processing mask file: {e}")
|
||||
return jsonify({'success': False, 'error': f'Error accessing mask: {str(e)}'}), 500
|
||||
46
tools/ora_editor/routes/polygon.py
Normal file
46
tools/ora_editor/routes/polygon.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Polygon operations routes for ORA Editor."""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
import logging
|
||||
|
||||
from ora_editor.services import polygon_storage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
polygon_bp = Blueprint('polygon', __name__)
|
||||
|
||||
|
||||
@polygon_bp.route('/api/polygon', methods=['POST'])
|
||||
def api_polygon():
|
||||
"""Store polygon points for overlay."""
|
||||
data = request.get_json()
|
||||
|
||||
required = ['ora_path', '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']
|
||||
points = data['points']
|
||||
color = data.get('color', '#FF0000')
|
||||
width = data.get('width', 2)
|
||||
|
||||
logger.info(f"[POLYGON] Storing polygon: {len(points)} points, color: {color}, width: {width}")
|
||||
|
||||
polygon_storage.store(ora_path, points, color, width)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'overlay_url': f'/api/image/polygon?ora_path={ora_path}'
|
||||
})
|
||||
|
||||
|
||||
@polygon_bp.route('/api/polygon/clear', methods=['POST'])
|
||||
def api_polygon_clear():
|
||||
"""Clear stored polygon points."""
|
||||
ora_path = request.json.get('ora_path') if request.data else None
|
||||
|
||||
if ora_path:
|
||||
polygon_storage.clear(ora_path)
|
||||
|
||||
return jsonify({'success': True})
|
||||
Reference in New Issue
Block a user