commit 971f4b1b518cd0a5c3939868ccf6d13bfc1e266f
parent 95bd819cb97765ce5317c8810f5cc7b18f8786d3
Author: maydayv7 <maydayv7@gmail.com>
Date: Thu, 4 Dec 2025 12:40:07 +0530
Add backend request encryption
Diffstat:
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: