commit 15f6cf08c82f3cf9ce761d52ec832e935e3cfdae
parent d601ad746c8825abbe47fedcd6b3a4274bb9f26d
Author: maydayv7 <maydayv7@gmail.com>
Date: Wed, 3 Dec 2025 05:18:25 +0530
Add magic prompt generation
Diffstat:
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) {