creek

The AI Image Editor of 2030
commit 971f4b1b518cd0a5c3939868ccf6d13bfc1e266f
parent 95bd819cb97765ce5317c8810f5cc7b18f8786d3
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu,  4 Dec 2025 12:40:07 +0530

Add backend request encryption

Diffstat:
D.env | 15---------------
M.gitignore | 2+-
Mflask/index.py | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mflask/modal_app.py | 565+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mflask/requirements.txt | 1+
Alib/services/encryption_service.dart | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/services/flask_service.dart | 298+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mlib/ui/pages/canvas_toolbar/magic_draw_overlay.dart | 2+-
Mpubspec.yaml | 37+++++++++++++++++++------------------
9 files changed, 694 insertions(+), 443 deletions(-)

diff --git a/.env b/.env @@ -1,15 +0,0 @@ -# Modal Server -URL_ASSET=https://samaladitya2004--adobe-flask-modelbackend-asset.modal.run -URL_DESCRIBE=https://samaladitya2004--adobe-flask-modelbackend-describe.modal.run -URL_GENERATE=https://samaladitya2004--adobe-flask-modelbackend-generate.modal.run -URL_INPAINTING=https://samaladitya2004--adobe-flask-modelbackend-inpainting.modal.run -URL_INPAINTING_API=https://samaladitya2004--adobe-flask-modelbackend-inpainting-api.modal.run -URL_SKETCH_API=https://samaladitya2004--adobe-flask-modelbackend-sketch-api.modal.run - -# Testing -# URL_ASSET=https://locustlike-trieciously-rudolph.ngrok-free.dev/asset -# URL_DESCRIBE=https://locustlike-trieciously-rudolph.ngrok-free.dev/describe -# URL_GENERATE=https://locustlike-trieciously-rudolph.ngrok-free.dev/generate -# URL_INPAINTING=https://locustlike-trieciously-rudolph.ngrok-free.dev/inpainting -# URL_INPAINTING_API=https://locustlike-trieciously-rudolph.ngrok-free.dev/inpainting-api -# URL_SKETCH_API=https://locustlike-trieciously-rudolph.ngrok-free.dev/sketch-api diff --git a/.gitignore b/.gitignore @@ -49,5 +49,5 @@ __pycache__ .vscode/ # .env Files -#.env +.env flask/.env diff --git a/flask/index.py b/flask/index.py @@ -9,22 +9,134 @@ import torch import traceback import requests import scipy.ndimage +import json +from functools import wraps from flask import Flask, jsonify, request, send_file from flask_cors import CORS from PIL import Image, ImageFilter from torchvision import transforms import time import uuid +from Crypto.Cipher import AES from dotenv import load_dotenv load_dotenv() +class CryptoManager: + def __init__(self, key_base64): + # Decode the base64 key to raw bytes (must be 32 bytes for AES-256) + self.key = base64.b64decode(key_base64) + + def encrypt(self, plain_text): + # 1. Generate a random unique Nonce (12 bytes is standard for GCM) + nonce = os.urandom(12) + + # 2. Initialize Cipher + cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) + + # 3. Encrypt and get Tag (MAC) + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode('utf-8')) + + # 4. Pack: Nonce + Ciphertext + Tag + combined = nonce + ciphertext + tag + + # 5. Return as Base64 string + return base64.b64encode(combined).decode('utf-8') + + def decrypt(self, encrypted_b64): + try: + # 1. Decode Base64 + data = base64.b64decode(encrypted_b64) + + # 2. Unpack (Slice the bytes) + nonce = data[:12] + tag = data[-16:] + ciphertext = data[12:-16] + + # 3. Decrypt + cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) + decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) + return decrypted_data.decode('utf-8') + except Exception as e: + print(f"Decryption failed: {e}") + return None + +# Setup Secret Key +SHARED_SECRET_KEY = os.getenv("SHARED_SECRET_KEY") +if not SHARED_SECRET_KEY: + print("❌ Error: SHARED_SECRET_KEY not found in .env") + sys.exit(1) + +crypto = CryptoManager(SHARED_SECRET_KEY) + +# ============================================================================== +# SECURITY DECORATOR (Middleware) +# ============================================================================== +def secure_endpoint(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # --- 1. INCOMING DECRYPTION --- + try: + # Expecting JSON format: { "data": "BASE64_ENCRYPTED_STRING" } + incoming = request.get_json(silent=True) + if not incoming or 'data' not in incoming: + return jsonify({"error": "Invalid format. Expected {'data': 'encrypted_string'}"}), 400 + + encrypted_b64 = incoming['data'] + decrypted_json_str = crypto.decrypt(encrypted_b64) + + if decrypted_json_str is None: + return jsonify({"error": "Decryption failed (Check Key or Nonce)"}), 403 + + # Parse the decrypted string back to a Python dictionary + decrypted_payload = json.loads(decrypted_json_str) + + # OVERRIDE request.get_json() so the inner function sees the decrypted data + request.get_json = lambda **k: decrypted_payload + + except Exception as e: + return jsonify({"error": f"Security Middleware Error: {str(e)}"}), 500 + + # --- 2. EXECUTE ORIGINAL LOGIC --- + response = f(*args, **kwargs) + + # --- 3. OUTGOING ENCRYPTION --- + try: + # Handle Flask response tuples (e.g., jsonify(...), 500) + resp_obj = response + status_code = 200 + if isinstance(response, tuple): + resp_obj = response[0] + if len(response) > 1: status_code = response[1] + + # Extract the plain JSON data from the Response object + if hasattr(resp_obj, 'get_json'): + plain_data = resp_obj.get_json() + else: + # Fallback if it's not a response object yet + plain_data = resp_obj + + # Convert dict -> JSON String -> Encrypt + plain_json_str = json.dumps(plain_data) + encrypted_response = crypto.encrypt(plain_json_str) + + # Return standard encrypted wrapper + return jsonify({"data": encrypted_response}), status_code + + except Exception as e: + return jsonify({"error": f"Response Encryption Error: {str(e)}"}), 500 + + return decorated_function + # --- FAL.AI IMPORTS --- try: import fal_client # SETUP API KEY - if not os.getenv("FAL_KEY"): + if os.getenv("FAL_KEY"): + os.environ["FAL_KEY"] = os.getenv("FAL_KEY") + FAL_AVAILABLE = True + else: print("⚠️ Warning: FAL_KEY not found in environment variables.") - FAL_AVAILABLE = True + FAL_AVAILABLE = False except ImportError: print("⚠️ Fal.ai Client not installed. /inpainting-api will fail.") FAL_AVAILABLE = False @@ -223,11 +335,29 @@ def resize_to_limit(img, max_dim=1024, multiple=8): # ============================================================================== # ROUTES # ============================================================================== + @app.route('/') def index(): return "Image Processing API is running." +@app.route('/test-encrypt', methods=['POST']) +def test_encrypt(): + # Helper route to debug encryption/decryption + try: + data = request.get_json() + plain_text = data.get('text', 'Hello, World!') + encrypted = crypto.encrypt(plain_text) + decrypted = crypto.decrypt(encrypted) + return jsonify({ + "original": plain_text, + "encrypted": encrypted, + "decrypted": decrypted + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + @app.route('/generate', methods=['POST']) +@secure_endpoint def generate_image(): if not sd_pipe: return jsonify({"error": "SD Model not loaded"}), 500 try: @@ -242,6 +372,7 @@ def generate_image(): return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/inpainting', methods=['POST']) +@secure_endpoint def inpaint_image(): if not sd_pipe: return jsonify({"error": "SD Model not loaded"}), 500 try: @@ -330,6 +461,7 @@ def inpaint_image(): return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/asset', methods=['POST']) +@secure_endpoint def remove_background(): if not birefnet_model: return jsonify({"error": "BiRefNet not loaded"}), 500 @@ -359,6 +491,7 @@ def remove_background(): return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/describe', methods=['POST']) +@secure_endpoint def describe_image(): if not florence_model or not florence_processor: return jsonify({"error": "Florence-2 not loaded"}), 500 @@ -434,6 +567,7 @@ def describe_image(): return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/inpainting-api', methods=['POST']) +@secure_endpoint def inpainting_api_fal(): if not FAL_AVAILABLE: return jsonify({"error": "Fal.ai client not installed or API Key missing"}), 500 @@ -537,14 +671,12 @@ def inpainting_api_fal(): traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 -# ============================================================================== -# 5. NEW ROUTE: SKETCH API (Text-to-Image) -# ============================================================================== @app.route('/sketch-api', methods=['POST']) +@secure_endpoint def sketch_api(): if not FAL_AVAILABLE: return jsonify({"error": "Fal.ai client not installed or API Key missing"}), 500 - + try: data = request.get_json() prompt = data.get('prompt') diff --git a/flask/modal_app.py b/flask/modal_app.py @@ -2,6 +2,7 @@ import os import io import sys import base64 +import json import modal # ============================================================================== @@ -36,6 +37,7 @@ image = ( "fastapi[standard]", "fal-client", "requests", + "pycryptodome", ) # --- MOUNT LOCAL MODELS --- .add_local_dir("local_inpainting_model", remote_path="/models/sd-inpainting") @@ -43,16 +45,16 @@ image = ( .add_local_dir("BiRefNet", remote_path="/root/BiRefNet") ) -app = modal.App("creekui-flask", image=image) +app = modal.App("creekui", image=image) # ============================================================================== # 2. THE BACKEND SERVER CLASS # ============================================================================== @app.cls( - gpu="any", + gpu="any", scaledown_window=300, - secrets=[modal.Secret.from_name("fal-secret")] + secrets=[modal.Secret.from_name("creek-secrets")], ) class ModelBackend: @@ -63,19 +65,52 @@ class ModelBackend: import torch import torch.nn as nn import sys + from Crypto.Cipher import AES self.device = "cuda" - # FAL.AI API KEY + # --- 1. SETUP CRYPTO --- + secret_key_b64 = os.environ.get("SHARED_SECRET_KEY") + if not secret_key_b64: + raise ValueError("SHARED_SECRET_KEY not set in Modal Secrets") + + # Define CryptoManager inside container + class CryptoManager: + def __init__(self, key_base64): + self.key = base64.b64decode(key_base64) + + def decrypt(self, encrypted_b64): + try: + data = base64.b64decode(encrypted_b64) + nonce = data[:12] + tag = data[-16:] + ciphertext = data[12:-16] + cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) + return cipher.decrypt_and_verify(ciphertext, tag).decode("utf-8") + except Exception as e: + print(f"Decryption failed: {e}") + return None + + def encrypt(self, plain_text): + nonce = os.urandom(12) + cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode("utf-8")) + combined = nonce + ciphertext + tag + return base64.b64encode(combined).decode("utf-8") + + self.crypto = CryptoManager(secret_key_b64) + print("✅ Crypto Initialized") + + # Check FAL KEY if "FAL_KEY" not in os.environ: print("❌ Error: FAL_KEY secret not found!") else: print("✅ FAL_KEY loaded securely.") - # --- 1. SETUP BiRefNet PATHS --- + # --- 2. SETUP PATHS & MODELS --- sys.path.append("/root/BiRefNet") - # --- 2. LOAD STABLE DIFFUSION --- + # Load Stable Diffusion from diffusers import StableDiffusionInpaintPipeline self.sd_pipe = StableDiffusionInpaintPipeline.from_pretrained( @@ -87,16 +122,12 @@ class ModelBackend: self.sd_pipe.enable_attention_slicing() print("✅ Stable Diffusion Loaded") - # --- 3. LOAD FLORENCE-2 --- + # Load Florence-2 import transformers.dynamic_module_utils - # Patch 1: Fix import check - def check_imports_fixed(filename): - return [] + transformers.dynamic_module_utils.check_imports = lambda f: [] - transformers.dynamic_module_utils.check_imports = check_imports_fixed - - # Patch 2: Fix '_supports_sdpa' error + # Patch for _supports_sdpa _old_getattr = nn.Module.__getattr__ def _fixed_getattr(self, name): @@ -124,17 +155,16 @@ class ModelBackend: ) print("✅ Florence-2 Loaded") - # --- 4. LOAD BIREFNET --- + # Load BiRefNet try: from models.birefnet import BiRefNet self.birefnet = BiRefNet(bb_pretrained=False) - weight_path = "/root/BiRefNet/birefnet_fp16.pt" state_dict = torch.load(weight_path, map_location=self.device) self.birefnet.load_state_dict(state_dict) self.birefnet.to(self.device).half().eval() - print(f"✅ BiRefNet Loaded from {weight_path}") + print(f"✅ BiRefNet Loaded") except Exception as e: print(f"❌ BiRefNet Error: {e}") self.birefnet = None @@ -151,329 +181,288 @@ class ModelBackend: ] ) + # --- SECURITY WRAPPER --- + def _handle_secure_request(self, item: dict, logic_func): + """Decrypts input -> Runs Logic -> Encrypts Output""" + try: + # 1. Decrypt Incoming + if "data" not in item: + return {"error": "Invalid format. Expected {'data': ...}"} + + decrypted_json_str = self.crypto.decrypt(item["data"]) + if decrypted_json_str is None: + return {"error": "Decryption failed (Check Key)"} + + payload = json.loads(decrypted_json_str) + + # 2. Run Actual Logic + result = logic_func(payload) + + # 3. Encrypt Outgoing + encrypted_response = self.crypto.encrypt(json.dumps(result)) + return {"data": encrypted_response} + + except Exception as e: + print(f"Request Error: {e}") + return {"error": str(e)} + # ========================================================================== # 3. ENDPOINTS # ========================================================================== @modal.fastapi_endpoint(method="POST") def generate(self, item: dict): - from PIL import Image - - prompt = item.get("prompt", "A luxury watch") - print(f"🎨 Generating: {prompt}") - - empty_image = Image.new("RGB", (512, 512), (0, 0, 0)) - full_mask = Image.new("L", (512, 512), 255) - - image = self.sd_pipe( - prompt=prompt, - image=empty_image, - mask_image=full_mask, - height=512, - width=512, - num_inference_steps=30, - ).images[0] - - return {"status": "success", "image": self._to_base64(image)} + def logic(data): + from PIL import Image + + prompt = data.get("prompt", "A luxury watch") + print(f"🎨 Generating: {prompt}") + empty = Image.new("RGB", (512, 512)) + mask = Image.new("L", (512, 512), 255) + img = self.sd_pipe( + prompt=prompt, + image=empty, + mask_image=mask, + height=512, + width=512, + num_inference_steps=30, + ).images[0] + return {"status": "success", "image": self._to_base64(img)} + + return self._handle_secure_request(item, logic) @modal.fastapi_endpoint(method="POST") def inpainting(self, item: dict): - """Local Stable Diffusion Inpainting""" - from PIL import Image, ImageFilter - import numpy as np - import scipy.ndimage - import torch - - user_prompt = item.get("prompt", "") - img_b64 = item.get("image") - mask_b64 = item.get("mask_image") - - if not img_b64 or not mask_b64: - return {"status": "error", "message": "Missing image or mask"} + def logic(data): + from PIL import Image, ImageFilter + import numpy as np + import scipy.ndimage + import torch - # 1. Decode Images - raw_clean = self._decode_base64(img_b64).convert("RGB") - raw_drawn = self._decode_base64(mask_b64).convert("RGB") + prompt = data.get("prompt", "") + img_b64 = data.get("image") + mask_b64 = data.get("mask_image") - # 2. Resize maintaining Aspect Ratio (Max 512 for Local SD) - img_clean = self._resize_to_limit(raw_clean, max_dim=512) - # Resize drawn image to match the clean image exactly - img_drawn = raw_drawn.resize(img_clean.size) + clean = self._decode_base64(img_b64).convert("RGB") + drawn = self._decode_base64(mask_b64).convert("RGB") - print(f"🔍 Calculating Robust Difference Mask (Size: {img_clean.size})...") + clean = self._resize_to_limit(clean, 512) + drawn = drawn.resize(clean.size) - # --- ROBUST MASKING --- - clean_blur = np.array( - img_clean.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 - ) - drawn_blur = np.array( - img_drawn.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 - ) - - diff_arr = np.abs(drawn_blur - clean_blur) - mask_arr = np.max(diff_arr, axis=2) - mask_binary = mask_arr > 30 + # Robust Masking + clean_blur = np.array( + clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 + ) + drawn_blur = np.array( + drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 + ) + mask_arr = np.max(np.abs(drawn_blur - clean_blur), axis=2) + mask = Image.fromarray( + (scipy.ndimage.binary_fill_holes(mask_arr > 30) * 255).astype(np.uint8) + ).filter(ImageFilter.MaxFilter(9)) + + # Florence Context + inputs = self.florence_processor( + text="<DETAILED_CAPTION>", images=[drawn], return_tensors="pt" + ) + inputs = { + k: v.to(self.device, torch.float16 if k == "pixel_values" else None) + for k, v in inputs.items() + } + gen_ids = self.florence_model.generate( + **inputs, max_new_tokens=128, num_beams=1, use_cache=False + ) + context = ( + self.florence_processor.batch_decode( + gen_ids, skip_special_tokens=False + )[0] + .replace("</s>", "") + .replace("<s>", "") + .replace("<DETAILED_CAPTION>", "") + .strip() + ) - mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) - mask_image = Image.fromarray((mask_filled * 255).astype(np.uint8)) - mask_image = mask_image.filter(ImageFilter.MaxFilter(9)) - print("✅ Mask calculated.") + full_prompt = f"{context} {prompt}".strip() - # --- FLORENCE-2 CONTEXT GENERATION --- - generated_prompt = "" - if self.florence_model and self.florence_processor: - print("👁️ Generating context with Florence-2...") - try: - task_prompt = "<DETAILED_CAPTION>" - # Use img_drawn (sketch) for context analysis - inputs = self.florence_processor( - text=task_prompt, images=[img_drawn], return_tensors="pt" - ) - inputs["pixel_values"] = inputs["pixel_values"].to( - self.device, torch.float16 - ) - inputs["input_ids"] = inputs["input_ids"].to(self.device) - - generated_ids = self.florence_model.generate( - input_ids=inputs["input_ids"], - pixel_values=inputs["pixel_values"], - max_new_tokens=128, - num_beams=1, - do_sample=False, - use_cache=False, # Fix for transformers crash - ) - - generated_text = self.florence_processor.batch_decode( - generated_ids, skip_special_tokens=False - )[0] - generated_prompt = ( - generated_text.replace(task_prompt, "") - .replace("</s>", "") - .replace("<s>", "") - .strip() - ) - print(f"📝 Florence Generated: {generated_prompt}") - except Exception as e: - print(f"⚠️ Florence captioning failed: {e}") - - final_prompt = f"{generated_prompt} {user_prompt}".strip() - negative_prompt = ( - "blurry, low quality, ugly, text, watermark, bad anatomy, deformed, noisy" - ) + output = self.sd_pipe( + prompt=full_prompt, + negative_prompt="blurry, low quality, ugly, text, watermark, bad anatomy, deformed, noisy", + image=drawn, + mask_image=mask, + num_inference_steps=50, + strength=0.85, + guidance_scale=8.5, + ).images[0] - # --- INFERENCE --- - print(f"🎨 Running Inference: {final_prompt}") - output = self.sd_pipe( - prompt=final_prompt, - negative_prompt=negative_prompt, - image=img_drawn, # Input is the SKETCH - mask_image=mask_image, # Mask is where sketch differs - num_inference_steps=50, - strength=0.85, - guidance_scale=8.5, - ).images[0] + return {"status": "success", "image": self._to_base64(output)} - return {"status": "success", "image": self._to_base64(output)} + return self._handle_secure_request(item, logic) @modal.fastapi_endpoint(method="POST") def inpainting_api(self, item: dict): - """Fal.ai Flux Lora Fill""" - import fal_client - import requests - import uuid - import numpy as np - import scipy.ndimage - from PIL import Image, ImageFilter - - prompt = item.get("prompt", "A high quality image") - img_b64 = item.get("image") - mask_b64 = item.get("mask_image") - - if not img_b64 or not mask_b64: - return {"status": "error", "message": "Missing image or mask"} - - # 1. Decode & Resize (Flux supports higher res) - raw_clean = self._decode_base64(img_b64).convert("RGB") - raw_drawn = self._decode_base64(mask_b64).convert("RGB") - - img_clean = self._resize_to_limit(raw_clean, max_dim=1024) - img_drawn = raw_drawn.resize(img_clean.size) - - # 2. Robust Mask Generation - clean_blur = np.array( - img_clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 - ) - drawn_blur = np.array( - img_drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 - ) + def logic(data): + import fal_client, requests, uuid, scipy.ndimage + from PIL import Image, ImageFilter + import numpy as np - diff_arr = np.abs(drawn_blur - clean_blur) - mask_arr = np.max(diff_arr, axis=2) - mask_binary = mask_arr > 30 + prompt = data.get("prompt", "High quality image") + img_b64 = data.get("image") + mask_b64 = data.get("mask_image") - mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) - mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) - mask = mask.filter(ImageFilter.MaxFilter(9)) + clean = self._decode_base64(img_b64).convert("RGB") + drawn = self._decode_base64(mask_b64).convert("RGB") - # 3. Save to temp files for upload - temp_id = str(uuid.uuid4()) - clean_path = f"/tmp/clean_{temp_id}.png" - mask_path = f"/tmp/mask_{temp_id}.png" + clean = self._resize_to_limit(clean, 1024) + drawn = drawn.resize(clean.size) - img_clean.save(clean_path) - mask.save(mask_path) - - try: - print("🚀 Uploading to Fal.ai...") - image_url = fal_client.upload_file(clean_path) - mask_url = fal_client.upload_file(mask_path) - - print(f"⚡ Running Flux Dev Fill for: {prompt}") - handler = fal_client.submit( - "fal-ai/flux-lora-fill", - arguments={ - "prompt": prompt, - "image_url": image_url, - "mask_url": mask_url, - "guidance_scale": 30, - "num_inference_steps": 28, - "enable_safety_checker": False, - }, + # Masking + clean_blur = np.array( + clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 + ) + drawn_blur = np.array( + drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 ) - result = handler.get() + mask = Image.fromarray( + ( + scipy.ndimage.binary_fill_holes( + np.max(np.abs(drawn_blur - clean_blur), axis=2) > 30 + ) + * 255 + ).astype(np.uint8) + ).filter(ImageFilter.MaxFilter(9)) + + clean_p, mask_p = ( + f"/tmp/c_{uuid.uuid4()}.png", + f"/tmp/m_{uuid.uuid4()}.png", + ) + clean.save(clean_p) + mask.save(mask_p) - if "images" in result: - output_url = result["images"][0]["url"] - response = requests.get(output_url) - result_img = Image.open(io.BytesIO(response.content)) - return {"status": "success", "image": self._to_base64(result_img)} - else: - return {"status": "error", "message": "Fal.ai returned no images"} + try: + res = fal_client.submit( + "fal-ai/flux-lora-fill", + arguments={ + "prompt": prompt, + "image_url": fal_client.upload_file(clean_p), + "mask_url": fal_client.upload_file(mask_p), + "guidance_scale": 30, + "num_inference_steps": 28, + "enable_safety_checker": False, + }, + ).get() + + if "images" in res: + img_resp = requests.get(res["images"][0]["url"]) + img = Image.open(io.BytesIO(img_resp.content)) + return {"status": "success", "image": self._to_base64(img)} + return {"status": "error", "message": "No images from Fal"} + finally: + if os.path.exists(clean_p): + os.remove(clean_p) + if os.path.exists(mask_p): + os.remove(mask_p) - except Exception as e: - print(f"❌ Fal.ai Error: {e}") - return {"status": "error", "message": str(e)} - finally: - if os.path.exists(clean_path): - os.remove(clean_path) - if os.path.exists(mask_path): - os.remove(mask_path) + return self._handle_secure_request(item, logic) @modal.fastapi_endpoint(method="POST") def sketch_api(self, item: dict): - """Sketch Text-to-Image (Flux)""" - import fal_client - import requests - from PIL import Image + def logic(data): + import fal_client, requests + from PIL import Image - prompt = item.get("prompt") - option = item.get("option", 1) + prompt = data.get("prompt", "") + option = int(data.get("option", 1)) - if not prompt: - return {"status": "error", "message": "Missing prompt"} - - enhanced_prompt = ( - f"{prompt}, sharp focus, high definition, 4k, vector art, crisp lines" - ) + enhanced_prompt = ( + f"{prompt}, sharp focus, high definition, 4k, vector art, crisp lines" + ) - if int(option) == 1: - # Nano Banana - print(f"🍌 Using Nano Banana for: {prompt}") - model_id = "fal-ai/nano-banana" - arguments = { - "prompt": enhanced_prompt, - "num_images": 1, - "aspect_ratio": "1:1", - "output_format": "png" - } - else: - # Flux Dev - print(f"🚀 Using Flux Dev for: {prompt}") - model_id = "fal-ai/flux/dev" - arguments = { - "image_size": "square_hd", - "num_inference_steps": 28, - "guidance_scale": 3.5, - "safety_tolerance": "2", - "enable_safety_checker": False, - "prompt": enhanced_prompt, - } + if option == 1: + # Nano Banana + print(f"🍌 Using Nano Banana for: {prompt}") + model_id = "fal-ai/nano-banana" + arguments = { + "prompt": enhanced_prompt, + "num_images": 1, + "aspect_ratio": "1:1", + "output_format": "png", + } + else: + # Flux Dev + print(f"🚀 Using Flux Dev for: {prompt}") + model_id = "fal-ai/flux/dev" + arguments = { + "image_size": "square_hd", + "num_inference_steps": 28, + "guidance_scale": 3.5, + "safety_tolerance": "2", + "enable_safety_checker": False, + "prompt": enhanced_prompt, + } - try: - print(f"🚀 Running Sketch Gen ({model_id})...") - handler = fal_client.submit(model_id, arguments=arguments) - result = handler.get() - - if "images" in result and len(result["images"]) > 0: - image_url = result["images"][0]["url"] - response = requests.get(image_url) - if response.status_code == 200: - img = Image.open(io.BytesIO(response.content)).convert("RGB") - return {"status": "success", "image": self._to_base64(img)} + res = fal_client.submit(model_id, arguments=arguments).get() + if "images" in res: + img_resp = requests.get(res["images"][0]["url"]) + img = Image.open(io.BytesIO(img_resp.content)).convert("RGB") + return {"status": "success", "image": self._to_base64(img)} + return {"status": "error", "message": "No images returned"} - return {"status": "error", "message": "Fal.ai returned no images"} - except Exception as e: - print(f"❌ Sketch API Error: {e}") - return {"status": "error", "message": str(e)} + return self._handle_secure_request(item, logic) @modal.fastapi_endpoint(method="POST") def asset(self, item: dict): - import torch - import numpy as np - from PIL import Image + def logic(data): + import torch, numpy as np + from PIL import Image - if not self.birefnet: - return {"status": "error", "message": "BiRefNet not loaded"} + img_b64 = data.get("image") + img = self._decode_base64(img_b64) + w, h = img.size - img_b64 = item.get("image") - image = self._decode_base64(img_b64) - orig_w, orig_h = image.size + inp = self.transform_birefnet(img).unsqueeze(0).to(self.device).half() + with torch.no_grad(): + preds = self.birefnet(inp)[-1].sigmoid() - input_tensor = ( - self.transform_birefnet(image).unsqueeze(0).to(self.device).half() - ) - with torch.no_grad(): - preds = self.birefnet(input_tensor)[-1].sigmoid() + import torch.nn.functional as F - res = torch.nn.functional.interpolate( - preds, size=(orig_h, orig_w), mode="bilinear", align_corners=True - ) - mask_np = res.squeeze().cpu().numpy() - mask_img = Image.fromarray((mask_np * 255).astype(np.uint8)) + res = F.interpolate(preds, size=(h, w), mode="bilinear", align_corners=True) + mask = Image.fromarray((res.squeeze().cpu().numpy() * 255).astype(np.uint8)) + img.putalpha(mask) + + return {"status": "success", "image": self._to_base64(img)} - image.putalpha(mask_img) - return {"status": "success", "image": self._to_base64(image)} + return self._handle_secure_request(item, logic) @modal.fastapi_endpoint(method="POST") def describe(self, item: dict): - import torch - - img_b64 = item.get("image") - prompt = item.get("prompt", "<DETAILED_CAPTION>") + def logic(data): + import torch - image = self._decode_base64(img_b64) + img_b64 = data.get("image") + img = self._decode_base64(img_b64) + prompt = data.get("prompt", "<DETAILED_CAPTION>") - inputs = self.florence_processor(text=prompt, images=image, return_tensors="pt") - inputs["pixel_values"] = inputs["pixel_values"].to(self.device, torch.float16) - inputs["input_ids"] = inputs["input_ids"].to(self.device) + inputs = self.florence_processor( + text=prompt, images=img, return_tensors="pt" + ) + inputs = { + k: v.to(self.device, torch.float16 if k == "pixel_values" else None) + for k, v in inputs.items() + } - # --- FIX: Explicitly disable caching to prevent beam search crash --- - generated_ids = self.florence_model.generate( - input_ids=inputs["input_ids"], - pixel_values=inputs["pixel_values"], - max_new_tokens=1024, - num_beams=3, - use_cache=False, # <--- CRITICAL FIX for Florence-2 on newer Transformers - ) + gen_ids = self.florence_model.generate( + **inputs, max_new_tokens=1024, num_beams=3, use_cache=False + ) + txt = self.florence_processor.batch_decode( + gen_ids, skip_special_tokens=False + )[0] + clean_txt = ( + txt.replace("</s>", "").replace("<s>", "").replace(prompt, "").strip() + ) - text = self.florence_processor.batch_decode( - generated_ids, skip_special_tokens=False - )[0] - clean_text = ( - text.replace("</s>", "").replace("<s>", "").replace(prompt, "").strip() - ) + return {"status": "success", "output": clean_txt} - return {"status": "success", "output": clean_text} + return self._handle_secure_request(item, logic) # --- HELPERS --- def _decode_base64(self, b64_str): diff --git a/flask/requirements.txt b/flask/requirements.txt @@ -10,6 +10,7 @@ modal numpy opencv-python pillow +pycryptodome python-dotenv requests safetensors diff --git a/lib/services/encryption_service.dart b/lib/services/encryption_service.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class EncryptionService { + final _algorithm = AesGcm.with256bits(); + + Future<SecretKey> _getSecretKey() async { + // 1. Fetch key from environment variables + final base64Key = dotenv.env['SHARED_SECRET_KEY']; + + if (base64Key == null || base64Key.isEmpty) { + throw Exception("❌ SHARED_SECRET_KEY not found in .env file"); + } + + // 2. Decode the Base64 string to bytes + final keyBytes = base64Decode(base64Key); + return SecretKey(keyBytes); + } + + Future<String> encrypt(String plainText) async { + final secretKey = await _getSecretKey(); + + // 1. Convert text to bytes + final messageBytes = utf8.encode(plainText); + + // 2. Encrypt (Generates a random nonce automatically) + final secretBox = await _algorithm.encrypt( + messageBytes, + secretKey: secretKey, + ); + + // 3. Pack: Nonce + Ciphertext + Tag (MAC) + // Note: secretBox.mac.bytes is the Tag + final combined = + secretBox.nonce + secretBox.cipherText + secretBox.mac.bytes; + + // 4. Return Base64 + return base64Encode(combined); + } + + Future<String?> decrypt(String encryptedBase64) async { + try { + final secretKey = await _getSecretKey(); + + // 1. Decode Base64 + final data = base64Decode(encryptedBase64); + + // 2. Unpack + // GCM Nonce is 12 bytes + final nonce = data.sublist(0, 12); + // Tag (MAC) is last 16 bytes + final tag = data.sublist(data.length - 16); + // Ciphertext + final ciphertext = data.sublist(12, data.length - 16); + + // 3. Reconstruct SecretBox + final secretBox = SecretBox(ciphertext, nonce: nonce, mac: Mac(tag)); + + // 4. Decrypt + final decryptedBytes = await _algorithm.decrypt( + secretBox, + secretKey: secretKey, + ); + + return utf8.decode(decryptedBytes); + } catch (e) { + print("Decryption error: $e"); + return null; + } + } +} diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -1,15 +1,18 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:adobe/data/repos/file_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/data/repos/note_repo.dart'; +import 'package:adobe/data/repos/project_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:creekui/data/repos/project_repo.dart'; -import 'package:creekui/data/repos/image_repo.dart'; -import 'package:creekui/data/repos/note_repo.dart'; -import 'package:creekui/data/repos/file_repo.dart'; -import 'package:creekui/services/python_service.dart'; +import 'package:adobe/services/python_service.dart'; +import './encryption_service.dart'; class FlaskService { // =========================================================================== @@ -27,18 +30,21 @@ class FlaskService { 'Content-Type': 'application/json', }; - // --- OPTIMIZATION: Instantiate Repos once --- + // --- REPOSITORIES --- final _imageRepo = ImageRepo(); final _noteRepo = NoteRepo(); final _projectRepo = ProjectRepo(); final _fileRepo = FileRepo(); final _pythonService = PythonService(); + // --- SECURITY --- + final _encryptionService = EncryptionService(); + // =========================================================================== - // 1. PIPELINES (Complex workflows) + // 1. PIPELINES // =========================================================================== - /// [Sketch-to-Image Pipeline] + // [Sketch-to-Image Pipeline] Future<String?> sketchToImage({ required int projectId, required String sketchPath, @@ -96,11 +102,79 @@ class FlaskService { return generatedImagePath; } + // [Sketch-to-Image-API Pipeline] + Future<String?> sketchToImageAPI({ + required int projectId, + required String sketchPath, + required String userPrompt, + required int option, + String? imageDescription, + }) async { + debugPrint("🔗 [Pipeline] Starting Sketch-to-Image-API..."); + + // 1. Analyze Sketch + final String? sketchDescription = + imageDescription ?? + await describeImage( + imagePath: sketchPath, + prompt: '<MORE_DETAILED_CAPTION>', + ); + + if (sketchDescription == null) { + debugPrint("❌ [Pipeline] Failed: Could not analyze sketch."); + return null; + } + + // 2. Fetch Stylesheet & Construct Prompt + final project = await _projectRepo.getProjectById(projectId); + final String stylesheetJson = project?.globalStylesheet ?? "{}"; + + // --- DEBUG LOGS --- + debugPrint("🐛 [DEBUG] 1. User Prompt: $userPrompt"); + debugPrint("🐛 [DEBUG] 2. Image Caption: $sketchDescription"); + await _logToFile("debug_stylesheet.json", stylesheetJson); + + debugPrint("🔗 [Pipeline] Generating magic prompt from stylesheet..."); + + final String? magicPrompt = await _pythonService.generateMagicPrompt( + stylesheetJson: stylesheetJson, + caption: sketchDescription, + userPrompt: userPrompt, + ); + + if (magicPrompt != null) { + await _logToFile("debug_magic_prompt.txt", magicPrompt); + debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt"); + } else { + debugPrint("🐛 [DEBUG] 4. Magic Prompt: null"); + } + + final String globalPrompt = + magicPrompt ?? "$userPrompt. The image features: $sketchDescription"; + + debugPrint("🔗 [Pipeline] Generating base image via API..."); + + final String? generatedImagePath = await _performImageOperation( + fullUrl: _urlSketchApi, + logPrefix: '🖌️ API Sketch', + // The body map is the PLAINTEXT payload + body: {'prompt': globalPrompt, 'option': option}, + filenamePrefix: 'sketch-to-image-api_$globalPrompt', + ); + + if (generatedImagePath == null) { + debugPrint("❌ [Pipeline] Failed: Image generation returned null."); + return null; + } + + return generatedImagePath; + } + // =========================================================================== // 2. GENERATION SERVICES (Returns File Path) // =========================================================================== - /// [Text-to-Image] + // [Text-to-Image] Future<String?> generateAndSaveImage(String prompt) async { return _performImageOperation( fullUrl: _urlGenerate, @@ -110,7 +184,7 @@ class FlaskService { ); } - /// [Inpainting] + // [Inpainting] Future<String?> inpaintImage({ required String imagePath, required String maskPath, @@ -134,7 +208,7 @@ class FlaskService { ); } - /// [Inpainting-API] + // [Inpainting-API] Future<String?> inpaintApiImage({ required String imagePath, required String maskPath, @@ -147,7 +221,7 @@ class FlaskService { return _performImageOperation( fullUrl: _urlInpaintingApi, - logPrefix: '🖌️ Inpainting', + logPrefix: '🖌️ Inpainting API', body: { 'prompt': prompt, 'negative_prompt': 'blurry, bad quality, low res, ugly', @@ -158,70 +232,7 @@ class FlaskService { ); } - /// [Sketch-to-Image-API] - Future<String?> sketchToImageAPI({ - required int projectId, - required String sketchPath, - required String userPrompt, - required int option, - String? imageDescription, - }) async { - debugPrint("🔗 [Pipeline] Starting Sketch-to-Image-API..."); - - // 1. Analyze Sketch (Use cached description if available) - final String? sketchDescription = imageDescription ?? await describeImage( - imagePath: sketchPath, - prompt: '<MORE_DETAILED_CAPTION>', - ); - - if (sketchDescription == null) { - debugPrint("❌ [Pipeline] Failed: Could not analyze sketch."); - return null; - } - - // 2. Fetch Stylesheet & Construct Prompt - final project = await _projectRepo.getProjectById(projectId); - final String stylesheetJson = project?.globalStylesheet ?? "{}"; - - // --- DEBUG LOGS --- - debugPrint("🐛 [DEBUG] 1. User Prompt: $userPrompt"); - debugPrint("🐛 [DEBUG] 2. Image Caption: $sketchDescription"); - await _logToFile("debug_stylesheet.json", stylesheetJson); - - debugPrint("🔗 [Pipeline] Generating magic prompt from stylesheet..."); - - final String? magicPrompt = await _pythonService.generateMagicPrompt( - stylesheetJson: stylesheetJson, - caption: sketchDescription, - userPrompt: userPrompt, - ); - - debugPrint("🐛 [DEBUG] 4. Magic Prompt: ${magicPrompt != null ? '(See debug_magic_prompt.txt)' : 'null'}"); - if(magicPrompt != null) await _logToFile("debug_magic_prompt.txt", magicPrompt); - - final String globalPrompt = magicPrompt ?? "$userPrompt. The image features: $sketchDescription"; - - debugPrint("🔗 [Pipeline] Generating base image..."); - - final String? generatedImagePath = await _performImageOperation( - fullUrl: _urlSketchApi, - logPrefix: '🖌️ Inpainting', - body: { - 'prompt': globalPrompt, - 'option': option, - }, - filenamePrefix: 'sketch-to-image-api_$globalPrompt', - ); - - if (generatedImagePath == null) { - debugPrint("❌ [Pipeline] Failed: Image generation returned null."); - return null; - } - - return generatedImagePath; - } - - /// [Background Removal] + // [Background Removal] Future<String?> generateAsset({required String imagePath}) async { // 1. Prepare and Upload final String? base64Image = await _encodeFile(imagePath); @@ -246,11 +257,8 @@ class FlaskService { // --- CHECK 2: Is this a Note Crop? --- if (projectId == null) { - // You need a method in NoteRepo to find a note by its crop path final noteModel = await _noteRepo.getByCropPath(imagePath); - if (noteModel != null) { - // Traverse up: Note -> Parent Image -> Project final parentImage = await _imageRepo.getById(noteModel.imageId); if (parentImage != null) { projectId = parentImage.projectId; @@ -287,7 +295,7 @@ class FlaskService { // 3. ANALYSIS SERVICES // =========================================================================== - /// [Image Captioning] + // [Image Captioning] Future<String?> describeImage({ required String imagePath, String prompt = '<MORE_DETAILED_CAPTION>', @@ -297,16 +305,42 @@ class FlaskService { final String? base64Image = await _encodeFile(imagePath); if (base64Image == null) return null; + // Send encrypted request final response = await _postRequest( fullUrl: _urlDescribe, body: {'image': base64Image, 'prompt': prompt}, ); if (response != null && response.statusCode == 200) { - final data = jsonDecode(response.body); - if (data['output'] != null) { - debugPrint("✅ [Describe] Success: ${data['output']}"); - return data['output']; + try { + // 1. Decode JSON Wrapper to get 'data' key + final jsonWrapper = jsonDecode(response.body); + + if (!jsonWrapper.containsKey('data')) { + debugPrint("❌ [Describe] Response missing 'data' key"); + return null; + } + + final encryptedData = jsonWrapper['data']; + + // 2. Decrypt the inner data + final String? decryptedBody = await _encryptionService.decrypt( + encryptedData, + ); + + if (decryptedBody == null) { + debugPrint("❌ [Describe] Decryption failed."); + return null; + } + + // 3. Parse the actual result + final data = jsonDecode(decryptedBody); + if (data['output'] != null) { + debugPrint("✅ [Describe] Success: ${data['output']}"); + return data['output']; + } + } catch (e) { + debugPrint("❌ [Describe] Error Parsing Response: $e"); } } @@ -315,23 +349,9 @@ class FlaskService { } // =========================================================================== - // PRIVATE HELPERS + // PRIVATE HELPERS (ENCRYPTION AWARE) // =========================================================================== - // LOG TO FILE HELPER - // Use following command to see logs: - // adb -d shell "run-as com.creek.ui cat /data/user/0/com.creek.ui/app_flutter/debug_magic_prompt.txt" - Future<void> _logToFile(String filename, String content) async { - try { - final dir = await getApplicationDocumentsDirectory(); - final file = File('${dir.path}/$filename'); - await file.writeAsString(content); - debugPrint("📄 [LOG] Saved full content to: ${file.path}"); - } catch (e) { - debugPrint("❌ Failed to log to file: $e"); - } - } - Future<String?> _performImageOperation({ required String fullUrl, required String logPrefix, @@ -352,6 +372,7 @@ class FlaskService { return null; } + // Encrypts the body, wraps it in {"data": ...}, and sends POST Future<http.Response?> _postRequest({ required String fullUrl, required Map<String, dynamic> body, @@ -361,33 +382,62 @@ class FlaskService { debugPrint("❌ Config Error: URL is missing in .env"); return null; } + + // 1. Encrypt the PLAINTEXT JSON body + final String plaintextJson = jsonEncode(body); + final String encryptedString = await _encryptionService.encrypt( + plaintextJson, + ); + + // 2. Package the encrypted string into the Flask wrapper format + final Map<String, String> encryptedBody = {'data': encryptedString}; + return await http.post( Uri.parse(fullUrl), headers: _headers, - body: jsonEncode(body), + body: jsonEncode(encryptedBody), ); } catch (e) { - debugPrint("❌ Network Error ($fullUrl): $e"); + debugPrint("❌ Network/Encryption Error ($fullUrl): $e"); return null; } } - Future<String?> _encodeFile(String path) async { - final file = File(path); - if (!file.existsSync()) { - debugPrint("❌ File not found: $path"); - return null; - } - return base64Encode(await file.readAsBytes()); - } - Future<String?> _saveImageFromResponse( http.Response response, String prefix, ) async { try { - final data = jsonDecode(response.body); - if (data['image'] == null) return null; + // 1. Decode the outer JSON wrapper (Flask returns { "data": "..." }) + final jsonWrapper = jsonDecode(response.body); + + if (!jsonWrapper.containsKey('data')) { + debugPrint("❌ [SaveImage] Response missing 'data' key"); + // Fallback: If server failed encryption, it might send raw error + if (jsonWrapper.containsKey('error')) + debugPrint("Server Error: ${jsonWrapper['error']}"); + return null; + } + + final encryptedData = jsonWrapper['data']; + + // 2. Decrypt the inner content + final String? decryptedBody = await _encryptionService.decrypt( + encryptedData, + ); + + if (decryptedBody == null) { + debugPrint("❌ [SaveImage] Decryption failed."); + return null; + } + + // 3. Parse the decrypted JSON (Should contain { "image": "BASE64..." }) + final data = jsonDecode(decryptedBody); + + if (data['image'] == null) { + debugPrint("❌ [SaveImage] Decrypted data missing 'image' field."); + return null; + } final Uint8List imageBytes = base64Decode(data['image']); @@ -414,8 +464,28 @@ class FlaskService { debugPrint("✅ Image saved: $filePath"); return filePath; } catch (e) { - debugPrint("❌ Error saving image: $e"); + debugPrint("❌ Error saving or decoding image: $e"); + return null; + } + } + + Future<void> _logToFile(String filename, String content) async { + try { + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/$filename'); + await file.writeAsString(content); + debugPrint("📄 [LOG] Saved content to: ${file.path}"); + } catch (e) { + debugPrint("❌ Failed to log to file: $e"); + } + } + + Future<String?> _encodeFile(String path) async { + final file = File(path); + if (!file.existsSync()) { + debugPrint("❌ File not found: $path"); return null; } + return base64Encode(await file.readAsBytes()); } } diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart @@ -86,8 +86,8 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { name: 'Nano Banana', badge: 'Premium', ), - AIModelOption(id: 'sketch_fusion', name: 'Stable Diffusion v1.5', badge: null), AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'), + AIModelOption(id: 'sketch_fusion', name: 'Stable Diffusion v1.5', badge: null), ]; @override diff --git a/pubspec.yaml b/pubspec.yaml @@ -10,29 +10,30 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 - sqflite: ^2.4.2 + cryptography: ^2.9.0 + dotted_border: ^2.0.0 + flutter_box_transform: ^0.4.7 + flutter_colorpicker: ^1.1.0 + flutter_dotenv: ^6.0.0 + flutter_launcher_icons: ^0.14.4 + flutter_svg: ^2.2.3 + google_fonts: ^6.1.0 + google_mlkit_text_recognition: ^0.15.0 + html: ^0.15.6 + http: ^1.6.0 + image_picker: ^1.2.1 + image: ^4.5.4 + intl: ^0.20.2 + onnxruntime: ^1.4.1 path_provider: ^2.1.5 path: ^1.9.1 - image_picker: ^1.2.1 - uuid: ^4.5.2 + provider: ^6.1.5+1 receive_sharing_intent: ^1.8.1 - http: ^1.6.0 - html: ^0.15.6 + share_plus: ^12.0.1 shared_preferences: ^2.5.3 - provider: ^6.1.5+1 - image: ^4.5.4 - onnxruntime: ^1.4.1 - intl: ^0.20.2 - google_mlkit_text_recognition: ^0.15.0 - flutter_svg: ^2.2.3 - google_fonts: ^6.1.0 - flutter_colorpicker: ^1.1.0 + sqflite: ^2.4.2 undo: ^1.0.1 - flutter_box_transform: ^0.4.7 - dotted_border: ^2.0.0 - share_plus: ^12.0.1 - flutter_dotenv: ^6.0.0 - flutter_launcher_icons: ^0.14.4 + uuid: ^4.5.2 dev_dependencies: flutter_test: