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