commit caef993e2fbb728f70bb93016f8578889406cfa2
parent 2e76d6eb43027c56c7edc1635d5e45f4df28057f
Author: aditya-samal <samaladitya2004@gmail.com>
Date: Wed, 3 Dec 2025 01:08:54 +0530
Stylesheet prompt
Diffstat:
1 file changed, 282 insertions(+), 22 deletions(-)
diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart
@@ -15,10 +15,10 @@ class FlaskService {
// CONFIGURATION
// ===========================================================================
- static String get _urlGenerate => dotenv.env['URL_GENERATE'] ?? '';
- static String get _urlInpainting => dotenv.env['URL_INPAINTING'] ?? '';
- static String get _urlAsset => dotenv.env['URL_ASSET'] ?? '';
- static String get _urlDescribe => dotenv.env['URL_DESCRIBE'] ?? '';
+ // Ensure SERVER_URL is defined in .env file (New/Refactored Config)
+ static String get _serverUrl {
+ return dotenv.env['SERVER_URL'] ?? 'http://127.0.0.1:5000';
+ }
static const Map<String, String> _headers = {
'Content-Type': 'application/json',
@@ -77,13 +77,271 @@ class FlaskService {
}
// ===========================================================================
+ // 1.5. STYLE PROMPT GENERATION (From Stylesheet - New Feature)
+ // ===========================================================================
+
+ /// Generates a style prompt from a stylesheet JSON following the notebook logic.
+ /// Returns a formatted style instruction string or a default fallback.
+ String generateStylePromptFromStylesheet(String? stylesheetJson) {
+ if (stylesheetJson == null || stylesheetJson.isEmpty) {
+ return "high quality, realistic";
+ }
+
+ try {
+ // Parse the JSON
+ Map<String, dynamic> sheet = jsonDecode(stylesheetJson);
+
+ // Normalize: extract from "results" if present
+ Map<String, dynamic> results =
+ sheet.containsKey('results') ? sheet['results'] as Map<String, dynamic> : sheet;
+
+ // Normalize the sheet structure
+ Map<String, Map<String, dynamic>> normalized = _normalizeStylesheet(results);
+
+ // Extract style phrases
+ List<String> stylePhrases = _pickStylePhrases(normalized);
+
+ // Extract top colors
+ List<String> topColors = _extractTopColors(results);
+
+ // Build style instruction
+ String styleInstruction = "";
+ if (stylePhrases.isNotEmpty) {
+ styleInstruction =
+ "User prefers these style cues (use as inspiration): ${stylePhrases.join('; ')}";
+ }
+ if (topColors.isNotEmpty) {
+ String colorStr = topColors.join(", ");
+ if (styleInstruction.isNotEmpty) {
+ styleInstruction += "; color palette: $colorStr";
+ } else {
+ styleInstruction = "color palette: $colorStr";
+ }
+ }
+
+ // Get tone hint from primary style
+ String primaryStyle = "";
+ if (normalized.containsKey('Style')) {
+ dynamic primary = normalized['Style']!['Primary'];
+ primaryStyle =
+ primary is String ? primary : (primary is List && primary.isNotEmpty ? primary[0] : "");
+ }
+ String toneHint = _styleToTone(primaryStyle);
+
+ // Combine everything
+ List<String> parts = [];
+ if (styleInstruction.isNotEmpty) {
+ parts.add(styleInstruction);
+ }
+ parts.add("Tone: $toneHint");
+
+ return parts.join(". ");
+ } catch (e) {
+ debugPrint("❌ Error generating style prompt: $e");
+ return "high quality, realistic";
+ }
+ }
+
+ /// Normalizes the stylesheet structure into a consistent format
+ Map<String, Map<String, dynamic>> _normalizeStylesheet(Map<String, dynamic> results) {
+ Map<String, Map<String, dynamic>> normalized = {};
+
+ // Categories to extract
+ List<String> categories = [
+ "Style",
+ "Background/Texture",
+ "Lighting",
+ "Composition",
+ "Era/Cultural Reference",
+ "Material Look",
+ "Typography",
+ ];
+
+ // Also check for variations
+ Map<String, List<String>> categoryAliases = {
+ "Style": ["style", "Style"],
+ "Background/Texture": [
+ "Background/Texture",
+ "background/texture",
+ "Texture",
+ "texture",
+ "Background",
+ "background"
+ ],
+ "Lighting": ["Lighting", "lighting"],
+ "Composition": ["Composition", "composition", "Compositions", "compositions"],
+ "Era/Cultural Reference": ["Era/Cultural Reference", "era / reference", "Era", "era"],
+ "Material Look": ["Material Look", "material look", "Material look"],
+ "Typography": ["Typography", "typography", "Fonts", "fonts"],
+ };
+
+ for (String category in categories) {
+ List<String> labels = [];
+ List<String> aliases = categoryAliases[category] ?? [category];
+
+ // Find the category in results
+ dynamic categoryData;
+ for (String alias in aliases) {
+ if (results.containsKey(alias)) {
+ categoryData = results[alias];
+ break;
+ }
+ }
+
+ if (categoryData == null) continue;
+
+ // Extract labels from the category data
+ List<dynamic> items = [];
+ if (categoryData is List) {
+ items = categoryData;
+ } else if (categoryData is Map && categoryData.containsKey('scores')) {
+ items = categoryData['scores'] is List ? categoryData['scores'] : [];
+ }
+
+ // Sort by score if available
+ try {
+ items.sort((a, b) {
+ double scoreA = 0.0, scoreB = 0.0;
+ if (a is Map) scoreA = (a['score'] ?? 0.0).toDouble();
+ if (b is Map) scoreB = (b['score'] ?? 0.0).toDouble();
+ return scoreB.compareTo(scoreA);
+ });
+ } catch (_) {}
+
+ // Extract labels
+ for (var item in items) {
+ String? label;
+ if (item is Map) {
+ label = item['label']?.toString();
+ } else if (item is String) {
+ label = item;
+ }
+ if (label != null && label.isNotEmpty) {
+ labels.add(label.trim());
+ }
+ }
+
+ if (labels.isNotEmpty) {
+ normalized[category] = {
+ 'Primary': labels.first,
+ 'Secondary': labels.length > 1 ? labels.sublist(1) : [],
+ };
+ }
+ }
+
+ return normalized;
+ }
+
+ /// Picks style phrases from normalized stylesheet (following notebook logic)
+ List<String> _pickStylePhrases(Map<String, Map<String, dynamic>> normalized,
+ {int maxSecondaries = 3}) {
+ Map<String, String> keyMap = {
+ "Style": "style",
+ "Background/Texture": "background/texture",
+ "Lighting": "lighting",
+ "Composition": "composition",
+ "Era/Cultural Reference": "era / reference",
+ "Material Look": "material look",
+ "Typography": "typography",
+ };
+
+ List<String> phrases = [];
+
+ for (String category in normalized.keys) {
+ Map<String, dynamic>? entry = normalized[category];
+ if (entry == null) continue;
+
+ dynamic primaryValue = entry['Primary'];
+ List<String> primary =
+ primaryValue is String ? [primaryValue] : (primaryValue is List ? List<String>.from(primaryValue) : []);
+ List<String> secondary = entry['Secondary'] ?? [];
+
+ List<String> chosen = [];
+ if (primary.isNotEmpty) {
+ chosen.addAll(primary);
+ }
+ for (String s in secondary) {
+ if (chosen.length < (1 + maxSecondaries)) {
+ chosen.add(s);
+ }
+ }
+
+ if (chosen.isNotEmpty) {
+ String key = keyMap[category] ?? category.toLowerCase();
+ phrases.add("$key: ${chosen.join(', ')}");
+ }
+ }
+
+ return phrases;
+ }
+
+ /// Extracts top colors from stylesheet
+ List<String> _extractTopColors(Map<String, dynamic> results, {int maxColors = 5}) {
+ dynamic palette = results['Color Palette'] ??
+ results['Colour Palette'] ??
+ results['colors'] ??
+ results['Colors'];
+
+ if (palette == null) return [];
+
+ List<dynamic> paletteList = [];
+ if (palette is List) {
+ paletteList = palette;
+ } else if (palette is Map && palette.containsKey('scores')) {
+ paletteList = palette['scores'] is List ? palette['scores'] : [];
+ }
+
+ // Sort by score
+ try {
+ paletteList.sort((a, b) {
+ double scoreA = 0.0, scoreB = 0.0;
+ if (a is Map) scoreA = (a['score'] ?? 0.0).toDouble();
+ if (b is Map) scoreB = (b['score'] ?? 0.0).toDouble();
+ return scoreB.compareTo(scoreA);
+ });
+ } catch (_) {}
+
+ List<String> colors = [];
+ for (var item in paletteList) {
+ String? label;
+ if (item is Map) {
+ label = item['label']?.toString();
+ } else if (item is String) {
+ label = item;
+ }
+ if (label != null && label.isNotEmpty) {
+ colors.add(label.trim());
+ if (colors.length >= maxColors) break;
+ }
+ }
+
+ return colors;
+ }
+
+ /// Determines tone hint from primary style
+ String _styleToTone(String primary) {
+ String s = primary.toLowerCase();
+ if (s.contains("retro") ||
+ s.contains("collage") ||
+ s.contains("poster") ||
+ s.contains("pop-art") ||
+ s.contains("surreal")) {
+ return "illustrative, textured, poster-like";
+ }
+ if (s.contains("photoreal") || s.contains("realistic") || s.contains("film")) {
+ return "photorealistic, high-detail";
+ }
+ return "high detail, sharp focus";
+ }
+
+ // ===========================================================================
// 2. GENERATION SERVICES (Returns File Path)
// ===========================================================================
/// [Text-to-Image]
Future<String?> generateAndSaveImage(String prompt) async {
return _performImageOperation(
- fullUrl: _urlGenerate,
+ endpoint: '/generate',
logPrefix: '🎨 Text-to-Image',
body: {'prompt': prompt},
filenamePrefix: prompt,
@@ -102,7 +360,7 @@ class FlaskService {
if (base64Image == null || base64Mask == null) return null;
return _performImageOperation(
- fullUrl: _urlInpainting,
+ endpoint: '/inpainting',
logPrefix: '🖌️ Inpainting',
body: {
'prompt': prompt,
@@ -113,8 +371,8 @@ class FlaskService {
filenamePrefix: 'inpaint_$prompt',
);
}
-
- /// [Inpainting]
+
+ /// [Inpainting with Custom API Endpoint] (Restored from Old Code)
Future<String?> inpaintApiImage({
required String imagePath,
required String maskPath,
@@ -126,15 +384,15 @@ class FlaskService {
if (base64Image == null || base64Mask == null) return null;
return _performImageOperation(
- endpoint: '/inpainting-api',
- logPrefix: '🖌️ Inpainting',
+ endpoint: '/inpainting-api', // Keeping the custom endpoint logic
+ logPrefix: '🖌️ Inpainting (API)',
body: {
'prompt': prompt,
'negative_prompt': 'blurry, bad quality, low res, ugly',
'image': base64Image,
'mask_image': base64Mask,
},
- filenamePrefix: 'inpaint_$prompt',
+ filenamePrefix: 'inpaint_api_$prompt',
);
}
@@ -145,7 +403,7 @@ class FlaskService {
if (base64Image == null) return null;
final String? generatedAssetPath = await _performImageOperation(
- fullUrl: _urlAsset,
+ endpoint: '/asset',
logPrefix: '✂️ Asset Gen',
body: {'image': base64Image},
filenamePrefix: 'asset',
@@ -161,7 +419,7 @@ class FlaskService {
projectId = imageModel.projectId;
}
- // --- CHECK 2: Is this a Note Crop? (NEW) ---
+ // --- 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);
@@ -215,11 +473,10 @@ class FlaskService {
if (base64Image == null) return null;
final response = await _postRequest(
- fullUrl: _urlDescribe,
+ endpoint: '/describe',
body: {'image': base64Image, 'prompt': prompt},
);
- // debugPrint(response);
if (response != null && response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['output'] != null) {
@@ -237,14 +494,15 @@ class FlaskService {
// ===========================================================================
Future<String?> _performImageOperation({
- required String fullUrl,
+ required String endpoint,
required String logPrefix,
required Map<String, dynamic> body,
required String filenamePrefix,
}) async {
+ final fullUrl = "$_serverUrl$endpoint";
debugPrint("$logPrefix Sending request to $fullUrl...");
- final response = await _postRequest(fullUrl: fullUrl, body: body);
+ final response = await _postRequest(endpoint: endpoint, body: body);
if (response != null && response.statusCode == 200) {
return _saveImageFromResponse(response, filenamePrefix);
@@ -257,12 +515,13 @@ class FlaskService {
}
Future<http.Response?> _postRequest({
- required String fullUrl,
+ required String endpoint,
required Map<String, dynamic> body,
}) async {
try {
- if (fullUrl.isEmpty) {
- debugPrint("❌ Config Error: URL is missing in .env");
+ final fullUrl = "$_serverUrl$endpoint";
+ if (_serverUrl.isEmpty) {
+ debugPrint("❌ Config Error: SERVER_URL is missing in .env");
return null;
}
return await http.post(
@@ -271,7 +530,7 @@ class FlaskService {
body: jsonEncode(body),
);
} catch (e) {
- debugPrint("❌ Network Error ($fullUrl): $e");
+ debugPrint("❌ Network Error: $e");
return null;
}
}
@@ -324,4 +583,4 @@ class FlaskService {
return null;
}
}
-}
+}
+\ No newline at end of file