creek

The AI Image Editor of 2030

lib/services/flask_service.dart (15888B)


  1import 'dart:convert';
  2import 'dart:io';
  3import 'dart:typed_data';
  4import 'package:flutter/foundation.dart';
  5import 'package:path_provider/path_provider.dart';
  6import 'package:http/http.dart' as http;
  7import 'package:path/path.dart' as p;
  8import 'package:flutter_dotenv/flutter_dotenv.dart';
  9import 'package:creekui/data/repos/file_repo.dart';
 10import 'package:creekui/data/repos/image_repo.dart';
 11import 'package:creekui/data/repos/note_repo.dart';
 12import 'package:creekui/data/repos/project_repo.dart';
 13import './python_service.dart';
 14import './encryption_service.dart';
 15
 16class FlaskService {
 17  // ===========================================================================
 18  // CONFIGURATION
 19  // ===========================================================================
 20
 21  static String get _urlGenerate => dotenv.env['URL_GENERATE'] ?? '';
 22  static String get _urlInpainting => dotenv.env['URL_INPAINTING'] ?? '';
 23  static String get _urlInpaintingApi => dotenv.env['URL_INPAINTING_API'] ?? '';
 24  static String get _urlSketchApi => dotenv.env['URL_SKETCH_API'] ?? '';
 25  static String get _urlAsset => dotenv.env['URL_ASSET'] ?? '';
 26  static String get _urlDescribe => dotenv.env['URL_DESCRIBE'] ?? '';
 27
 28  static const Map<String, String> _headers = {
 29    'Content-Type': 'application/json',
 30  };
 31
 32  // --- REPOSITORIES ---
 33  final _imageRepo = ImageRepo();
 34  final _noteRepo = NoteRepo();
 35  final _projectRepo = ProjectRepo();
 36  final _fileRepo = FileRepo();
 37  final _pythonService = PythonService();
 38
 39  // --- SECURITY ---
 40  final _encryptionService = EncryptionService();
 41
 42  // ===========================================================================
 43  // 1. PIPELINES
 44  // ===========================================================================
 45
 46  // [Sketch-to-Image Pipeline]
 47  Future<String?> sketchToImage({
 48    required int projectId,
 49    required String sketchPath,
 50    required String userPrompt,
 51    String? imageDescription,
 52  }) async {
 53    debugPrint("🔗 [Pipeline] Starting Sketch-to-Image...");
 54
 55    // 1. Analyze Sketch (Use cached description if available)
 56    final String? sketchDescription =
 57        imageDescription ??
 58        await describeImage(
 59          imagePath: sketchPath,
 60          prompt: '<MORE_DETAILED_CAPTION>',
 61        );
 62
 63    if (sketchDescription == null) {
 64      debugPrint("❌ [Pipeline] Failed: Could not analyze sketch.");
 65      return null;
 66    }
 67
 68    // 2. Fetch Stylesheet & Construct Prompt
 69    final project = await _projectRepo.getProjectById(projectId);
 70    final String stylesheetJson = project?.globalStylesheet ?? "{}";
 71
 72    // --- DEBUG LOGS ---
 73    debugPrint("🐛 [DEBUG] 1. User Prompt: $userPrompt");
 74    debugPrint("🐛 [DEBUG] 2. Image Caption: $sketchDescription");
 75    await _logToFile("debug_stylesheet.json", stylesheetJson);
 76
 77    debugPrint("🔗 [Pipeline] Generating magic prompt from stylesheet...");
 78
 79    final String? magicPrompt = await _pythonService.generateMagicPrompt(
 80      stylesheetJson: stylesheetJson,
 81      caption: sketchDescription,
 82      userPrompt: userPrompt,
 83    );
 84
 85    if (magicPrompt != null) {
 86      await _logToFile("debug_magic_prompt.txt", magicPrompt);
 87      debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt");
 88    } else {
 89      debugPrint("🐛 [DEBUG] 4. Magic Prompt: null");
 90    }
 91
 92    final String globalPrompt =
 93        magicPrompt ?? "$userPrompt. The image features: $sketchDescription";
 94
 95    debugPrint("🔗 [Pipeline] Generating base image...");
 96
 97    final String? generatedImagePath = await generateAndSaveImage(globalPrompt);
 98
 99    if (generatedImagePath == null) {
100      debugPrint("❌ [Pipeline] Failed: Image generation returned null.");
101      return null;
102    }
103
104    return generatedImagePath;
105  }
106
107  // [Sketch-to-Image-API Pipeline]
108  Future<String?> sketchToImageAPI({
109    required int projectId,
110    required String sketchPath,
111    required String userPrompt,
112    required int option,
113    String? imageDescription,
114  }) async {
115    debugPrint("🔗 [Pipeline] Starting Sketch-to-Image-API...");
116
117    // 1. Analyze Sketch
118    final String? sketchDescription =
119        imageDescription ??
120        await describeImage(
121          imagePath: sketchPath,
122          prompt: '<MORE_DETAILED_CAPTION>',
123        );
124
125    if (sketchDescription == null) {
126      debugPrint("❌ [Pipeline] Failed: Could not analyze sketch.");
127      return null;
128    }
129
130    // 2. Fetch Stylesheet & Construct Prompt
131    final project = await _projectRepo.getProjectById(projectId);
132    final String stylesheetJson = project?.globalStylesheet ?? "{}";
133
134    // --- DEBUG LOGS ---
135    debugPrint("🐛 [DEBUG] 1. User Prompt: $userPrompt");
136    debugPrint("🐛 [DEBUG] 2. Image Caption: $sketchDescription");
137    await _logToFile("debug_stylesheet.json", stylesheetJson);
138
139    debugPrint("🔗 [Pipeline] Generating magic prompt from stylesheet...");
140
141    final String? magicPrompt = await _pythonService.generateMagicPrompt(
142      stylesheetJson: stylesheetJson,
143      caption: sketchDescription,
144      userPrompt: userPrompt,
145    );
146
147    if (magicPrompt != null) {
148      await _logToFile("debug_magic_prompt.txt", magicPrompt);
149      debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt");
150    } else {
151      debugPrint("🐛 [DEBUG] 4. Magic Prompt: null");
152    }
153
154    final String globalPrompt =
155        magicPrompt ?? "$userPrompt. The image features: $sketchDescription";
156
157    debugPrint("🔗 [Pipeline] Generating base image via API...");
158
159    final String? generatedImagePath = await _performImageOperation(
160      fullUrl: _urlSketchApi,
161      logPrefix: '🖌️ API Sketch',
162      // The body map is the PLAINTEXT payload
163      body: {'prompt': globalPrompt, 'option': option},
164      filenamePrefix: 'sketch-to-image-api_$globalPrompt',
165    );
166
167    if (generatedImagePath == null) {
168      debugPrint("❌ [Pipeline] Failed: Image generation returned null.");
169      return null;
170    }
171
172    return generatedImagePath;
173  }
174
175  // ===========================================================================
176  // 2. GENERATION SERVICES (Returns File Path)
177  // ===========================================================================
178
179  // [Text-to-Image]
180  Future<String?> generateAndSaveImage(String prompt) async {
181    return _performImageOperation(
182      fullUrl: _urlGenerate,
183      logPrefix: '🎨 Text-to-Image',
184      body: {'prompt': prompt},
185      filenamePrefix: 'gen',
186    );
187  }
188
189  // [Inpainting]
190  Future<String?> inpaintImage({
191    required String imagePath,
192    required String maskPath,
193    required String prompt,
194  }) async {
195    final String? base64Image = await _encodeFile(imagePath);
196    final String? base64Mask = await _encodeFile(maskPath);
197
198    if (base64Image == null || base64Mask == null) return null;
199
200    return _performImageOperation(
201      fullUrl: _urlInpainting,
202      logPrefix: '🖌️ Inpainting',
203      body: {
204        'prompt': prompt,
205        'negative_prompt': 'blurry, bad quality, low res, ugly',
206        'image': base64Image,
207        'mask_image': base64Mask,
208      },
209      filenamePrefix: 'inpaint_$prompt',
210    );
211  }
212
213  // [Inpainting-API]
214  Future<String?> inpaintApiImage({
215    required String imagePath,
216    required String maskPath,
217    required String prompt,
218  }) async {
219    final String? base64Image = await _encodeFile(imagePath);
220    final String? base64Mask = await _encodeFile(maskPath);
221
222    if (base64Image == null || base64Mask == null) return null;
223
224    return _performImageOperation(
225      fullUrl: _urlInpaintingApi,
226      logPrefix: '🖌️ Inpainting API',
227      body: {
228        'prompt': prompt,
229        'negative_prompt': 'blurry, bad quality, low res, ugly',
230        'image': base64Image,
231        'mask_image': base64Mask,
232      },
233      filenamePrefix: 'inpaint_api$prompt',
234    );
235  }
236
237  // [Background Removal]
238  Future<String?> generateAsset({required String imagePath}) async {
239    // 1. Prepare and Upload
240    final String? base64Image = await _encodeFile(imagePath);
241    if (base64Image == null) return null;
242
243    final String? generatedAssetPath = await _performImageOperation(
244      fullUrl: _urlAsset,
245      logPrefix: '✂️ Asset Gen',
246      body: {'image': base64Image},
247      filenamePrefix: 'asset',
248    );
249
250    // 2. Resolve Project ID and Save
251    if (generatedAssetPath != null) {
252      int? projectId;
253
254      // --- CHECK 1: Is this a Main Image? ---
255      final imageModel = await _imageRepo.getByFilePath(imagePath);
256      if (imageModel != null) {
257        projectId = imageModel.projectId;
258      }
259
260      // --- CHECK 2: Is this a Note Crop? ---
261      if (projectId == null) {
262        final noteModel = await _noteRepo.getByCropPath(imagePath);
263        if (noteModel != null) {
264          final parentImage = await _imageRepo.getById(noteModel.imageId);
265          if (parentImage != null) {
266            projectId = parentImage.projectId;
267            debugPrint("🔗 Linked Asset to Project via Note: ${noteModel.id}");
268          }
269        }
270      }
271
272      // --- CHECK 3: Is this a generic File? ---
273      if (projectId == null) {
274        final fileModel = await _fileRepo.getByFilePath(imagePath);
275        if (fileModel != null) {
276          projectId = fileModel.projectId;
277        }
278      }
279
280      // 3. Update the Project
281      if (projectId != null) {
282        final project = await _projectRepo.getProjectById(projectId);
283        if (project != null) {
284          project.assetsPath.add(generatedAssetPath);
285          await _projectRepo.updateAssets(projectId, project.assetsPath);
286          debugPrint("✅ Asset path saved to Project DB: $generatedAssetPath");
287        }
288      } else {
289        debugPrint("⚠️ Asset generated but could not link to a Project ID.");
290      }
291    }
292
293    return generatedAssetPath;
294  }
295
296  // ===========================================================================
297  // 3. ANALYSIS SERVICES
298  // ===========================================================================
299
300  // [Image Captioning]
301  Future<String?> describeImage({
302    required String imagePath,
303    String prompt = '<MORE_DETAILED_CAPTION>',
304  }) async {
305    debugPrint("👁️ [Describe] Preparing request...");
306
307    final String? base64Image = await _encodeFile(imagePath);
308    if (base64Image == null) return null;
309
310    // Send encrypted request
311    final response = await _postRequest(
312      fullUrl: _urlDescribe,
313      body: {'image': base64Image, 'prompt': prompt},
314    );
315
316    if (response != null && response.statusCode == 200) {
317      try {
318        // 1. Decode JSON Wrapper to get 'data' key
319        final jsonWrapper = jsonDecode(response.body);
320
321        if (!jsonWrapper.containsKey('data')) {
322          debugPrint("❌ [Describe] Response missing 'data' key");
323          return null;
324        }
325
326        final encryptedData = jsonWrapper['data'];
327
328        // 2. Decrypt the inner data
329        final String? decryptedBody = await _encryptionService.decrypt(
330          encryptedData,
331        );
332
333        if (decryptedBody == null) {
334          debugPrint("❌ [Describe] Decryption failed.");
335          return null;
336        }
337
338        // 3. Parse the actual result
339        final data = jsonDecode(decryptedBody);
340        if (data['output'] != null) {
341          debugPrint("✅ [Describe] Success: ${data['output']}");
342          return data['output'];
343        }
344      } catch (e) {
345        debugPrint("❌ [Describe] Error Parsing Response: $e");
346      }
347    }
348
349    debugPrint("❌ [Describe] Failed.");
350    return null;
351  }
352
353  // ===========================================================================
354  // PRIVATE HELPERS (ENCRYPTION AWARE)
355  // ===========================================================================
356
357  Future<String?> _performImageOperation({
358    required String fullUrl,
359    required String logPrefix,
360    required Map<String, dynamic> body,
361    required String filenamePrefix,
362  }) async {
363    debugPrint("$logPrefix Sending request to $fullUrl...");
364
365    final response = await _postRequest(fullUrl: fullUrl, body: body);
366
367    if (response != null && response.statusCode == 200) {
368      return _saveImageFromResponse(response, filenamePrefix);
369    }
370
371    debugPrint(
372      "❌ $logPrefix Failed: ${response?.statusCode ?? 'No Connection'}",
373    );
374    return null;
375  }
376
377  // Encrypts the body, wraps it in {"data": ...}, and sends POST
378  Future<http.Response?> _postRequest({
379    required String fullUrl,
380    required Map<String, dynamic> body,
381  }) async {
382    try {
383      if (fullUrl.isEmpty) {
384        debugPrint("❌ Config Error: URL is missing in .env");
385        return null;
386      }
387
388      // 1. Encrypt the PLAINTEXT JSON body
389      final String plaintextJson = jsonEncode(body);
390      final String encryptedString = await _encryptionService.encrypt(
391        plaintextJson,
392      );
393
394      // 2. Package the encrypted string into the Flask wrapper format
395      final Map<String, String> encryptedBody = {'data': encryptedString};
396
397      return await http.post(
398        Uri.parse(fullUrl),
399        headers: _headers,
400        body: jsonEncode(encryptedBody),
401      );
402    } catch (e) {
403      debugPrint("❌ Network/Encryption Error ($fullUrl): $e");
404      return null;
405    }
406  }
407
408  Future<String?> _saveImageFromResponse(
409    http.Response response,
410    String prefix,
411  ) async {
412    try {
413      // 1. Decode the outer JSON wrapper (Flask returns { "data": "..." })
414      final jsonWrapper = jsonDecode(response.body);
415
416      if (!jsonWrapper.containsKey('data')) {
417        debugPrint("❌ [SaveImage] Response missing 'data' key");
418        // Fallback: If server failed encryption, it might send raw error
419        if (jsonWrapper.containsKey('error'))
420          debugPrint("Server Error: ${jsonWrapper['error']}");
421        return null;
422      }
423
424      final encryptedData = jsonWrapper['data'];
425
426      // 2. Decrypt the inner content
427      final String? decryptedBody = await _encryptionService.decrypt(
428        encryptedData,
429      );
430
431      if (decryptedBody == null) {
432        debugPrint("❌ [SaveImage] Decryption failed.");
433        return null;
434      }
435
436      // 3. Parse the decrypted JSON (Should contain { "image": "BASE64..." })
437      final data = jsonDecode(decryptedBody);
438
439      if (data['image'] == null) {
440        debugPrint("❌ [SaveImage] Decrypted data missing 'image' field.");
441        return null;
442      }
443
444      final Uint8List imageBytes = base64Decode(data['image']);
445
446      final directory = await getApplicationDocumentsDirectory();
447      final imagesDirPath = p.join(directory.path, 'generated_images');
448      final imagesDir = Directory(imagesDirPath);
449
450      if (!await imagesDir.exists()) await imagesDir.create(recursive: true);
451
452      final timestamp = DateTime.now().millisecondsSinceEpoch;
453      final safePrefix = prefix
454          .replaceAll(RegExp(r'[^\w\s]'), '')
455          .trim()
456          .replaceAll(' ', '_');
457      final shortPrefix =
458          safePrefix.length > 20 ? safePrefix.substring(0, 20) : safePrefix;
459
460      final String filePath = p.join(
461        imagesDir.path,
462        '${shortPrefix}_$timestamp.png',
463      );
464
465      await File(filePath).writeAsBytes(imageBytes);
466      debugPrint("✅ Image saved: $filePath");
467      return filePath;
468    } catch (e) {
469      debugPrint("❌ Error saving or decoding image: $e");
470      return null;
471    }
472  }
473
474  Future<void> _logToFile(String filename, String content) async {
475    try {
476      final dir = await getApplicationDocumentsDirectory();
477      final file = File('${dir.path}/$filename');
478      await file.writeAsString(content);
479      debugPrint("📄 [LOG] Saved content to: ${file.path}");
480    } catch (e) {
481      debugPrint("❌ Failed to log to file: $e");
482    }
483  }
484
485  Future<String?> _encodeFile(String path) async {
486    final file = File(path);
487    if (!file.existsSync()) {
488      debugPrint("❌ File not found: $path");
489      return null;
490    }
491    return base64Encode(await file.readAsBytes());
492  }
493}