creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
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}