creek

The AI Image Editor of 2030
commit caef993e2fbb728f70bb93016f8578889406cfa2
parent 2e76d6eb43027c56c7edc1635d5e45f4df28057f
Author: aditya-samal <samaladitya2004@gmail.com>
Date:   Wed,  3 Dec 2025 01:08:54 +0530

Stylesheet prompt

Diffstat:
Mlib/services/flask_service.dart | 304+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
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