creek

The AI Image Editor of 2030

lib/services/analyze/image_analyzer.dart (13681B)


  1import 'dart:async';
  2import 'dart:io';
  3import 'dart:isolate';
  4import 'dart:ui';
  5import 'dart:developer' as dev;
  6import 'package:creekui/services/flask_service.dart';
  7import 'package:flutter/foundation.dart';
  8import 'package:flutter/services.dart';
  9import 'package:path_provider/path_provider.dart';
 10import 'package:path/path.dart' as p;
 11
 12// Import analysis services
 13import 'color.dart';
 14import 'texture.dart';
 15import 'embedding.dart';
 16import 'layout.dart';
 17import 'font.dart';
 18
 19class ImageAnalyzerService {
 20  /// Prepare ALL assets (Models + JSONs) on Main Thread.
 21  /// Returns a map of absolute paths.
 22  static Future<Map<String, String>> _prepareAssets() async {
 23    final dir = await getApplicationDocumentsDirectory();
 24
 25    Future<String> copy(String assetName, {String? filename}) async {
 26      final name = filename ?? assetName;
 27      final file = File('${dir.path}/$name');
 28      if (!await file.exists() || await file.length() == 0) {
 29        final data = await rootBundle.load('assets/$assetName');
 30        await file.writeAsBytes(data.buffer.asUint8List(), flush: true);
 31      }
 32      return file.path;
 33    }
 34
 35    // Copy everything needed by the services
 36    final paths = await Future.wait([
 37      // DINO
 38      copy('dinov2.onnx'),
 39      copy('dinov2.onnx.data'),
 40      copy('texture_centroids.json'),
 41
 42      // CLIP
 43      copy('clip_image_encoder.onnx'),
 44      copy('style_embeddings.json'),
 45      copy('emotion_centroids.json'),
 46      copy('lighting_centroids.json'),
 47      copy('era_centroids.json'),
 48      copy('fannet.onnx'),
 49      copy('font_database.json'),
 50    ]);
 51
 52    return {
 53      'texture_model': paths[0],
 54      // paths[1] is .data, implicit
 55      'texture_json': paths[2],
 56      'clip_model': paths[3],
 57      'embedding_json': paths[4],
 58      'emotion_json': paths[5],
 59      'lighting_json': paths[6],
 60      'era_json': paths[7],
 61      'fannet_model': paths[8],
 62      'font_json': paths[9],
 63    };
 64  }
 65
 66  static Future<Map<String, dynamic>> _runProfiledJob({
 67    required String name,
 68    required String imagePath,
 69    required RootIsolateToken rootToken,
 70    required bool runInIsolate,
 71    required Map<String, String> assetPaths,
 72    required Future<dynamic> Function(String, Map<String, String>) task,
 73  }) async {
 74    final stopwatch = Stopwatch()..start();
 75    final dev.TimelineTask timelineTask = dev.TimelineTask();
 76    timelineTask.start('Analyze: $name');
 77
 78    try {
 79      dynamic result;
 80
 81      if (runInIsolate) {
 82        // PARALLEL (Isolate)
 83        result = await Isolate.run(() async {
 84          BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken);
 85          DartPluginRegistrant.ensureInitialized();
 86          return await task(imagePath, assetPaths);
 87        });
 88      } else {
 89        // CONCURRENT (Main Thread)
 90        result = await task(imagePath, assetPaths);
 91      }
 92
 93      debugPrint("📱: ${result.toString()}");
 94      stopwatch.stop();
 95      timelineTask.finish();
 96      result['execution_time'] = stopwatch.elapsedMilliseconds;
 97      return result;
 98    } catch (e) {
 99      stopwatch.stop();
100      timelineTask.finish();
101      debugPrint("[$name] Failed: $e");
102
103      return {
104        'success': false,
105        'scores': {},
106        'error': e.toString(),
107        'execution_time': stopwatch.elapsedMilliseconds,
108      };
109    }
110  }
111
112  // ---------------------------------------------------------------------------
113  // PUBLIC METHODS
114  // ---------------------------------------------------------------------------
115
116  // Runs ALL analysis models on the image
117  static Future<Map<String, dynamic>> analyzeFullSuite(String imagePath) async {
118    return _runAnalysisInternal(imagePath, (_) => true);
119  }
120
121  // Runs ONLY the analysis models corresponding to the provided [tags]
122  static Future<Map<String, dynamic>> analyzeSelected(
123    String imagePath,
124    List<String> tags,
125  ) async {
126    return _runAnalysisInternal(imagePath, (validTags) {
127      return tags.any((tag) => validTags.contains(tag));
128    });
129  }
130
131  // ---------------------------------------------------------------------------
132  // INTERNAL LOGIC
133  // ---------------------------------------------------------------------------
134
135  static Future<Map<String, dynamic>> _runAnalysisInternal(
136    String imagePath,
137    bool Function(List<String> validTags) shouldRun,
138  ) async {
139    final totalSw = Stopwatch()..start();
140
141    final RootIsolateToken? token = RootIsolateToken.instance;
142    if (token == null) {
143      throw Exception("RootIsolateToken is null.");
144    }
145
146    try {
147      // 1. Prepare Assets on Main Thread (Safe)
148      final assetPaths = await _prepareAssets();
149
150      // Helper for skipped tasks to maintain list structure
151      Future<Map<String, dynamic>> skipTask() async {
152        return {'success': true, 'scores': {}, 'execution_time': 0};
153      }
154
155      // 2. Run Analysis
156      final results = await Future.wait([
157        // --- GROUP A: PARALLEL ---
158
159        // 1. Layout
160        shouldRun(['Compositions'])
161            ? _runProfiledJob(
162              name: 'Layout',
163              imagePath: imagePath,
164              rootToken: token,
165              runInIsolate: true,
166              assetPaths: assetPaths,
167              task: (path, _) => LayoutAnalyzerService().analyze(path),
168            )
169            : skipTask(),
170
171        // 2. Color
172        shouldRun(['Colours'])
173            ? _runProfiledJob(
174              name: 'Color',
175              imagePath: imagePath,
176              rootToken: token,
177              runInIsolate: true,
178              assetPaths: assetPaths,
179              task: (path, _) => ColorAnalyzerService().analyze(path),
180            )
181            : skipTask(),
182
183        // 3. Texture
184        shouldRun(['Texture', 'Material Look'])
185            ? _runProfiledJob(
186              name: 'Texture',
187              imagePath: imagePath,
188              rootToken: token,
189              runInIsolate: true,
190              assetPaths: assetPaths,
191              task: (path, assets) async {
192                final service = TextureAnalyzerService();
193                final res = await service.analyze(
194                  path,
195                  modelPath: assets['texture_model'],
196                  jsonPath: assets['texture_json'],
197                );
198                service.dispose();
199                return res;
200              },
201            )
202            : skipTask(),
203
204        // 4. Embeddings
205        shouldRun(['Style'])
206            ? _runProfiledJob(
207              name: 'Embedding',
208              imagePath: imagePath,
209              rootToken: token,
210              runInIsolate: true,
211              assetPaths: assetPaths,
212              task: (path, assets) async {
213                final service = EmbeddingAnalyzerService();
214                final res = await service.analyze(
215                  path,
216                  modelPath: assets['clip_model'],
217                  jsonPath: assets['embedding_json'],
218                );
219                service.dispose();
220                return res;
221              },
222            )
223            : skipTask(),
224
225        // 5. Emotion
226        shouldRun(['Emotion'])
227            ? _runProfiledJob(
228              name: 'Emotional',
229              imagePath: imagePath,
230              rootToken: token,
231              runInIsolate: true,
232              assetPaths: assetPaths,
233              task: (path, assets) async {
234                final service = EmbeddingAnalyzerService();
235                final res = await service.analyze(
236                  path,
237                  modelPath: assets['clip_model'],
238                  jsonPath: assets['emotion_json'],
239                );
240                service.dispose();
241                return res;
242              },
243            )
244            : skipTask(),
245
246        // 6. Lighting
247        shouldRun(['Lighting'])
248            ? _runProfiledJob(
249              name: 'Lighting',
250              imagePath: imagePath,
251              rootToken: token,
252              runInIsolate: true,
253              assetPaths: assetPaths,
254              task: (path, assets) async {
255                final service = EmbeddingAnalyzerService();
256                final res = await service.analyze(
257                  path,
258                  modelPath: assets['clip_model'],
259                  jsonPath: assets['lighting_json'],
260                );
261                service.dispose();
262                return res;
263              },
264            )
265            : skipTask(),
266
267        // 7. Era
268        shouldRun(['Era'])
269            ? _runProfiledJob(
270              name: 'Era',
271              imagePath: imagePath,
272              rootToken: token,
273              runInIsolate: true,
274              assetPaths: assetPaths,
275              task: (path, assets) async {
276                final service = EmbeddingAnalyzerService();
277                final res = await service.analyze(
278                  path,
279                  modelPath: assets['clip_model'],
280                  jsonPath: assets['era_json'],
281                );
282                service.dispose();
283                return res;
284              },
285            )
286            : skipTask(),
287
288        // --- GROUP B: MAIN THREAD ---
289
290        // 8. Font
291        shouldRun(['Fonts'])
292            ? _runProfiledJob(
293              name: 'Font',
294              imagePath: imagePath,
295              rootToken: token,
296              runInIsolate: false,
297              assetPaths: assetPaths,
298              task: (path, assets) async {
299                final service = FontIdentifierService();
300                final res = await service.analyze(
301                  path,
302                  modelPath: assets['fannet_model'],
303                  jsonPath: assets['font_json'],
304                );
305                service.dispose();
306                return res;
307              },
308            )
309            : skipTask(),
310
311        // --- GROUP C: SERVER SIDE ---
312
313        // 9. Subject
314        shouldRun(['Subject', 'Asset'])
315            ? _runProfiledJob(
316              name: 'Subject',
317              imagePath: imagePath,
318              rootToken: token,
319              runInIsolate: false,
320              assetPaths: assetPaths,
321              task: (path, _) async {
322                final String? location = await FlaskService().generateAsset(
323                  imagePath: path,
324                );
325                final res = {
326                  "success": true,
327                  "scores": {'image': location},
328                  "error": null,
329                };
330                return res;
331              },
332            )
333            : skipTask(),
334      ]);
335
336      totalSw.stop();
337
338      // This is only for testing
339      final logResult = {
340        'success': true,
341        'total_time': totalSw.elapsedMilliseconds,
342        'filename': p.basename(imagePath),
343        'results': {
344          'Layout': results[0]['execution_time'],
345          'Color': results[1]['execution_time'],
346          'Texture': results[2]['execution_time'],
347          'Style': results[3]['execution_time'],
348          'Emotions': results[4]['execution_time'],
349          'Lighting': results[5]['execution_time'],
350          'Era': results[6]['execution_time'],
351          'Font': results[7]['execution_time'],
352          'Subject': results[8]['execution_time'],
353        },
354      };
355      _logSummary(logResult);
356
357      final finalResult = {
358        'success': true,
359        'data': {
360          'filename': p.basename(imagePath),
361          'results': {
362            'Style': {"scores": results[3]['scores']},
363            'Texture': {"scores": results[2]['scores']},
364            'Lighting': {"scores": results[5]['scores']},
365            'Colour Palette': {
366              "scores": results[1]['scores'],
367              "palette": results[1]['palette'],
368            },
369            'Emotions': {"scores": results[4]['scores']},
370            'Era': {"scores": results[6]['scores']},
371            'Layout': {"scores": results[0]['scores']},
372            'Font': {"scores": results[7]['scores']},
373            'Subject': {"scores": results[8]['scores']},
374          },
375        },
376        'error': null,
377      };
378      debugPrint(finalResult.toString());
379      return finalResult;
380    } catch (e) {
381      debugPrint("Master Analysis Failed: $e");
382      return {
383        'success': false,
384        'data': {
385          'filename': '',
386          'results': {
387            'Style': {"scores": {}},
388            'Texture': {"scores": {}},
389            'Lighting': {"scores": {}},
390            'Colour Palette': {"scores": {}},
391            'Emotions': {"scores": {}},
392            'Era': {"scores": {}},
393            'Font': {"scores": {}},
394          },
395        },
396        'error': e.toString(),
397      };
398    }
399  }
400
401  static void _logSummary(Map<String, dynamic> result) {
402    final int total = result['total_time'];
403    final Map<String, dynamic> subTasks = result['results'];
404    StringBuffer output = StringBuffer();
405
406    output.writeln('\n══════════ ⚡ HYBRID PARALLEL REPORT ⚡ ══════════');
407    output.writeln(' 🕒 Total Wall Time: ${total}ms');
408    output.writeln('──────────────────────────────────────────────────────');
409
410    subTasks.forEach((key, value) {
411      final int val = value as int;
412      final int barLength = (val / 50).ceil().clamp(0, 20);
413      final String bar = '█' * barLength;
414      output.writeln(
415        ' ${key.padRight(12)} : ${val.toString().padLeft(4)}ms $bar',
416      );
417    });
418
419    output.writeln('══════════════════════════════════════════════════════\n');
420    dev.log(output.toString(), name: 'ImageAnalyzer');
421  }
422}