creek

The AI Image Editor of 2030
commit 15f6cf08c82f3cf9ce761d52ec832e935e3cfdae
parent d601ad746c8825abbe47fedcd6b3a4274bb9f26d
Author: maydayv7 <maydayv7@gmail.com>
Date:   Wed,  3 Dec 2025 05:18:25 +0530

Add magic prompt generation

Diffstat:
Mandroid/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt | 13+++++++++++++
Mandroid/app/src/main/kotlin/com/example/adobe/MainActivity.kt | 6++++++
Mandroid/app/src/main/python/stylesheet_generator.py | 217++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mlib/services/flask_service.dart | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mlib/services/python_service.dart | 19+++++++++++++++++++
Mlib/ui/pages/canvas_board_page.dart | 26++++++++++----------------
6 files changed, 333 insertions(+), 35 deletions(-)

diff --git a/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt b/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt @@ -78,4 +78,17 @@ object ImageAnalyzer { null } } + + fun generateMagicPrompt(stylesheetJson: String, caption: String, userPrompt: String): String? { + waitForPython() + return try { + val py = Python.getInstance() + val module = py.getModule("stylesheet_generator") + val result = module.callAttr("generate_magic_prompt", stylesheetJson, caption, userPrompt) + result.toString() + } catch (e: Exception) { + e.printStackTrace() + null + } + } } diff --git a/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt b/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt @@ -48,6 +48,12 @@ class MainActivity : FlutterActivity() { val jsonList = call.argument<List<String>>("jsonList")!! ImageAnalyzer.generateStylesheet(jsonList) } + "generateMagicPrompt" -> { + val stylesheetJson = call.argument<String>("stylesheetJson") ?: "{}" + val caption = call.argument<String>("caption") ?: "" + val userPrompt = call.argument<String>("userPrompt") ?: "" + ImageAnalyzer.generateMagicPrompt(stylesheetJson, caption, userPrompt) + } "getShareSource" -> { val componentName = intent.component?.className when { diff --git a/android/app/src/main/python/stylesheet_generator.py b/android/app/src/main/python/stylesheet_generator.py @@ -1,8 +1,9 @@ import json import math +import re import numpy as np from collections import defaultdict -from typing import List, Dict, Tuple, Any +from typing import List, Dict, Tuple, Any, Optional from sklearn.cluster import KMeans # ========================================== @@ -521,3 +522,217 @@ def generate_stylesheet(json_strings_list: Any) -> str: final_output = engine.compute_final_stylesheet() return json.dumps(final_output) + +# ========================================== +# PROMPT GENERATION LOGIC +# ========================================== + +DEFAULT_OPTIONS = { + "width": 1024, + "height": 1024, + "steps": 30, + "guidance_scale": 7.5, + "sampler": "k_lms", + "seed": "random", +} + +# ---- normalize sheet ---- +def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: + results = sheet.get("results", sheet) + norm: Dict[str, Dict[str, List[str]]] = {} + + for cat, val in results.items(): + if cat in ("filename", "meta"): + continue + + items = [] + if isinstance(val, list): + items = val + elif isinstance(val, dict) and "scores" in val: + items = val["scores"] + + try: + items_sorted = sorted( + items, key=lambda x: float(x.get("score", 0)), reverse=True + ) + except Exception: + items_sorted = items + + labels = [ + str(it.get("label")).strip() for it in items_sorted if it.get("label") + ] + if labels: + norm[cat] = {"Primary": labels[0], "Secondary": labels[1:]} + + return norm + +# ---- extract top colors ---- +def top_color_list(sheet: Dict[str, Any], max_colors: int = 5) -> List[str]: + palette = sheet.get("results", {}).get("Color Palette", []) + if isinstance(palette, dict) and "scores" in palette: + palette = palette["scores"] + + try: + palette_sorted = sorted( + palette, key=lambda x: float(x.get("score", 0)), reverse=True + ) + except Exception: + palette_sorted = palette + + out: List[str] = [] + for it in palette_sorted: + lab = it.get("label") + if lab: + out.append(lab.strip()) + if len(out) >= max_colors: + break + + return out + +# ---- remove style/color words helpers ---- +STYLE_WORDS = [ + "hand-drawn", "hand drawn", "sketch", "thin lines", "curves", "minimalistic", + "minimal", "flat", "cartoon", "comic", "pop-art", "surreal", "photorealistic", + "realistic", "painting", "oil painting", "pastel", "artistic", "illustration", + "line drawing", "line art", "3d effect", "texture", "pattern", "soft light", + "studio light", "dramatic", "cinematic", "moody", "vintage", "retro", "poster", +] + +style_words_regex = re.compile( + r"\b(" + "|".join(re.escape(w) for w in STYLE_WORDS) + r")\b", + flags=re.IGNORECASE, +) + +def remove_style_words(caption: str) -> str: + if not caption: return caption + clean = style_words_regex.sub(" ", caption) + clean = re.sub(r"\s{2,}", " ", clean).strip() + return clean + +COMMON_COLOR_WORDS = { + "black", "white", "red", "green", "blue", "yellow", "orange", "purple", + "pink", "brown", "gray", "grey", "monochrome", "vibrant", "pastel", + "tinted", "sepia", "muted", "desaturated", +} + +_COLOR_PATTERN = re.compile( + r"\b(" + "|".join(re.escape(w) for w in COMMON_COLOR_WORDS) + r")\b", + flags=re.IGNORECASE, +) + +def remove_color_phrases_from_caption(caption: str) -> str: + if not caption: return caption + c = _COLOR_PATTERN.sub(" ", caption) + c = re.sub(r"\s{2,}", " ", c).strip() + if len(c) < 3: return caption + return c + +def extract_subject(caption_text: str) -> str: + txt = caption_text.strip() + if not txt: return "subject" + first_clause = re.split(r"[.?!]\s*", txt)[0] + return " ".join(first_clause.split()[:12]) + +def pick_style_phrases(norm: Dict[str, Dict[str, List[str]]], max_secondaries=3) -> List[str]: + cats = [ + "Style", "Background/Texture", "Lighting", "Composition", + "Era/Cultural Reference", "Material Look", "Typography", + ] + key_map = { + "Style": "style", "Background/Texture": "background/texture", + "Lighting": "lighting", "Composition": "composition", + "Era/Cultural Reference": "era / reference", + "Material Look": "material look", "Typography": "typography", + } + + out: List[str] = [] + for cat in cats: + entry = norm.get(cat) + if not entry: continue + + prim = entry.get("Primary") + secs = entry.get("Secondary", []) + chosen: List[str] = [] + if prim: chosen.append(prim) + for s in secs: + if s and len(chosen) < (1 + max_secondaries): + chosen.append(s) + if chosen: + out.append(f"{key_map.get(cat, cat)}: {', '.join(chosen)}") + return out + +def style_to_tone(primary: str) -> str: + s = (primary or "").lower() + if any(k in s for k in ["retro", "collage", "poster", "pop-art", "surreal"]): + return "illustrative, textured, poster-like" + if any(k in s for k in ["photoreal", "realistic", "film"]): + return "photorealistic, high-detail" + return "high detail, sharp focus" + +# ========================================== +# ENTRY POINT FOR PROMPT GENERATION +# ========================================== +def generate_magic_prompt(stylesheet_json_str: str, caption: str, user_prompt: str) -> str: + try: + sheet = json.loads(stylesheet_json_str) + except Exception as e: + # Fallback if parsing fails + return f"{user_prompt}. The image features: {caption}" + + # CLEAN caption + caption_no_style = remove_style_words(caption) + cleaned_caption = remove_color_phrases_from_caption(caption_no_style) + + norm = normalize_sheet(sheet) + subject = extract_subject(cleaned_caption) + + # Style cues + style_phrases = pick_style_phrases(norm) + style_primary = (norm.get("Style") or {}).get("Primary", "") + tone_hint = style_to_tone(style_primary) + + # Color palette + top_colors = top_color_list(sheet) + + # Build style instruction + style_instruction = "" + if style_phrases: + style_instruction = ( + "User prefers these style cues (use as inspiration): " + "; ".join(style_phrases) + ) + if top_colors: + color_str = ", ".join(top_colors) + style_instruction += f"; color palette: {color_str}" + + # Construct final prompt parts + combined_parts: List[str] = [] + if cleaned_caption: combined_parts.append(cleaned_caption) + if style_phrases: combined_parts.append("; ".join(style_phrases)) + if user_prompt: combined_parts.append(user_prompt) + + combined_description = " | ".join(combined_parts) + + prompt_lines: List[str] = [ + f"Main subject: {subject}.", + f"Description: {combined_description}.", + "NOTE: Ignore any style words in the caption; style comes only from the stylesheet.", + ] + if style_instruction: + prompt_lines.append(style_instruction + ".") + prompt_lines += [ + f"Tone: {tone_hint}.", + "Avoid: blurry, deformed, text, watermark, extra limbs, artifacts.", + ] + + prompt = "\n".join(prompt_lines) + + payload = { + "prompt": prompt, + "negative_prompt": "blurry, low resolution, deformed, text, watermark, extra limbs, artifacts", + "options": DEFAULT_OPTIONS, + "provenance": {"generated_from": "final_output"}, + "weights": {"caption": 0.6, "style_sheet": 0.3, "user": 0.1}, + } + + # Return concatenated prompt and payload as string + return prompt + " " + json.dumps(payload) diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -9,6 +9,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:adobe/services/python_service.dart'; class FlaskService { // =========================================================================== @@ -31,19 +32,18 @@ class FlaskService { final _noteRepo = NoteRepo(); final _projectRepo = ProjectRepo(); final _fileRepo = FileRepo(); + final _pythonService = PythonService(); // =========================================================================== // 1. PIPELINES (Complex workflows) // =========================================================================== /// [Sketch-to-Image Pipeline] - /// 1. Analyzes the sketch to get a text description. - /// 2. Combines User Prompt + Sketch Description + Style Prompt. - /// 3. Generates a new image based on this global prompt. Future<String?> sketchToImage({ + required int projectId, required String sketchPath, required String userPrompt, - required String stylePrompt, + String? stylePrompt, }) async { debugPrint("🔗 [Pipeline] Starting Sketch-to-Image..."); @@ -58,9 +58,31 @@ class FlaskService { return null; } - // 2. Construct Prompt & Generate - final String globalPrompt = - "$stylePrompt. $userPrompt. The image features: $sketchDescription"; + // 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..."); @@ -71,10 +93,9 @@ class FlaskService { return null; } - // 3. Remove Background (Pipeline Extension) + // 3. Remove Background debugPrint("🔗 [Pipeline] Removing background from generated result..."); - // This returns the path to the no-background version return generateAsset(imagePath: generatedImagePath); } @@ -88,7 +109,7 @@ class FlaskService { fullUrl: _urlGenerate, logPrefix: '🎨 Text-to-Image', body: {'prompt': prompt}, - filenamePrefix: prompt, + filenamePrefix: 'gen', ); } @@ -142,9 +163,10 @@ class FlaskService { /// [Sketch-to-Image-API] Future<String?> sketchToImageAPI({ + required int projectId, required String sketchPath, required String userPrompt, - required String stylePrompt, + String? stylePrompt, required int option, }) async { debugPrint("🔗 [Pipeline] Starting Sketch-to-Image-API..."); @@ -160,9 +182,27 @@ class FlaskService { return null; } - // 2. Construct Prompt & Generate - final String globalPrompt = - " $userPrompt.$stylePrompt. The image features: $sketchDescription"; + // 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..."); @@ -184,7 +224,6 @@ class FlaskService { // 3. Remove Background (Pipeline Extension) debugPrint("🔗 [Pipeline] Removing background from generated result..."); - // This returns the path to the no-background version return generateAsset(imagePath: generatedImagePath); } @@ -251,7 +290,7 @@ class FlaskService { } // =========================================================================== - // 3. ANALYSIS SERVICES (Returns String) + // 3. ANALYSIS SERVICES // =========================================================================== /// [Image Captioning] @@ -285,6 +324,20 @@ class FlaskService { // PRIVATE HELPERS // =========================================================================== + // LOG TO FILE HELPER + // Use following command to see logs: + // adb -d shell "run-as com.example.adobe cat /data/user/0/com.example.adobe/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, @@ -345,7 +398,6 @@ class FlaskService { final Uint8List imageBytes = base64Decode(data['image']); final directory = await getApplicationDocumentsDirectory(); - // Use join for safe path construction final imagesDirPath = p.join(directory.path, 'generated_images'); final imagesDir = Directory(imagesDirPath); @@ -359,7 +411,6 @@ class FlaskService { final shortPrefix = safePrefix.length > 20 ? safePrefix.substring(0, 20) : safePrefix; - // Use join here too final String filePath = p.join( imagesDir.path, '${shortPrefix}_$timestamp.png', diff --git a/lib/services/python_service.dart b/lib/services/python_service.dart @@ -66,4 +66,23 @@ class PythonService { return null; } } + + // 5. Magic Prompt Generation + Future<String?> generateMagicPrompt({ + required String stylesheetJson, + required String caption, + required String userPrompt, + }) async { + try { + final String? result = await _channel.invokeMethod('generateMagicPrompt', { + 'stylesheetJson': stylesheetJson, + 'caption': caption, + 'userPrompt': userPrompt, + }); + return result; + } catch (e) { + debugPrint("Magic Prompt Generation Error: $e"); + return null; + } + } } diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart @@ -13,17 +13,15 @@ import 'package:image_picker/image_picker.dart'; import 'package:undo/undo.dart'; import 'package:share_plus/share_plus.dart'; import 'package:image/image.dart' as img; +import 'package:path/path.dart' as p; import './canvas_toolbar/magic_draw_overlay.dart'; import './canvas_toolbar/text_tools_overlay.dart'; import '../../data/repos/project_repo.dart'; +import '../../data/models/file_model.dart'; import '../../services/stylesheet_service.dart'; -import 'project_file_page.dart'; -import 'package:path/path.dart' as p; - import '../../services/file_service.dart'; -import '../../data/models/file_model.dart'; - import '../../services/flask_service.dart'; +import 'project_file_page.dart'; // --- MODELS WITH JSON SUPPORT --- @@ -1409,7 +1407,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { ); }); - // NEW: Throttle Analysis: Trigger "Describe" every 2.5s while actively drawing + // Throttle Analysis: Trigger "Describe" every 2.5s while actively drawing if (_isMagicDrawActive && !_isAnalyzing && !_isInpainting && @@ -1498,37 +1496,33 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { } // CASE 2: SKETCH TO IMAGE (When hasImageLayers is FALSE) else { - // Currently all sketch IDs map to the main sketch endpoint, - // but you can pass the ID if your backend supports different sketch models. if(modelId == 'sketch_fusion'){ newImageUrl = await FlaskService().sketchToImage( + projectId: widget.projectId, sketchPath: _tempBaseImage!.path, userPrompt: prompt, - stylePrompt: - "high quality, realistic", // You could vary this based on modelId + stylePrompt: "high quality, realistic", ); } else if(modelId == 'sketch_advanced'){ newImageUrl = await FlaskService().sketchToImageAPI( + projectId: widget.projectId, sketchPath: _tempBaseImage!.path, userPrompt: prompt, - stylePrompt: - "high quality, realistic", // You could vary this based on modelId + stylePrompt: "high quality, realistic", option:1 , ); } else if (modelId == 'sketch_creative') { newImageUrl = await FlaskService().sketchToImageAPI( + projectId: widget.projectId, sketchPath: _tempBaseImage!.path, userPrompt: prompt, - stylePrompt: - "high quality, realistic", // You could vary this based on modelId + stylePrompt: "high quality, realistic", option: 2, ); } - - } if (newImageUrl != null) {