creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/services/analysis_queue_manager.dart (7424B)
1import 'dart:convert';
2import 'dart:io';
3import 'package:flutter/foundation.dart';
4import 'package:path_provider/path_provider.dart';
5import 'package:image/image.dart' as img;
6import 'package:creekui/data/models/image_model.dart';
7import 'package:creekui/data/models/note_model.dart';
8import 'package:creekui/data/repos/image_repo.dart';
9import 'package:creekui/data/repos/note_repo.dart';
10import 'analyze/image_analyzer.dart';
11import 'package:path/path.dart' as p;
12
13class AnalysisQueueManager {
14 static final AnalysisQueueManager _instance =
15 AnalysisQueueManager._internal();
16 factory AnalysisQueueManager() => _instance;
17 AnalysisQueueManager._internal();
18
19 bool _isProcessing = false;
20 final _imageRepo = ImageRepo();
21 final _noteRepo = NoteRepo();
22
23 Future<void> processQueue() async {
24 if (_isProcessing) return;
25 _isProcessing = true;
26
27 try {
28 // Loop until no items are left
29 while (true) {
30 bool processedAny = false;
31
32 // 1. Fetch pending images from DB
33 List<ImageModel> pendingImages = await _imageRepo.getPendingImages();
34 if (pendingImages.isNotEmpty) {
35 processedAny = true;
36 debugPrint("[Queue]: Found ${pendingImages.length} pending images");
37 for (final image in pendingImages) {
38 await _processSingleImage(image);
39 }
40 }
41
42 // 2. Fetch pending notes from DB
43 List<NoteModel> pendingNotes = await _noteRepo.getPendingNotes();
44 if (pendingNotes.isNotEmpty) {
45 processedAny = true;
46 debugPrint("[Queue]: Found ${pendingNotes.length} pending notes");
47
48 // Group notes to avoid decoding parent image multiple times
49 final Map<String, List<NoteModel>> notesByImage = {};
50 for (var note in pendingNotes) {
51 if (!notesByImage.containsKey(note.imageId)) {
52 notesByImage[note.imageId] = [];
53 }
54 notesByImage[note.imageId]!.add(note);
55 }
56
57 for (final entry in notesByImage.entries) {
58 await _processNoteGroup(entry.key, entry.value);
59 }
60 }
61
62 // If no items were processed in this iteration, the queue is drained
63 if (!processedAny) break;
64 }
65 } catch (e) {
66 debugPrint("[Queue]: Critical Error: $e");
67 } finally {
68 _isProcessing = false;
69 }
70 }
71
72 Future<void> _processSingleImage(ImageModel image) async {
73 await _imageRepo.updateStatus(image.id, 'analyzing');
74 try {
75 Map<String, dynamic> result;
76
77 // Run Analysis
78 if (image.tags.isEmpty) {
79 debugPrint("[Queue]: Analyzing Full Suite: ${image.name}");
80 result = await ImageAnalyzerService.analyzeFullSuite(image.filePath);
81 } else {
82 debugPrint(
83 "[Queue]: Analyzing Selected: ${image.name} with tags: ${image.tags}",
84 );
85 result = await ImageAnalyzerService.analyzeSelected(
86 image.filePath,
87 image.tags,
88 );
89 }
90
91 // Handle Result
92 if (result['success'] == true && result['data'] != null) {
93 final resultsMap = result['data']['results'] ?? {};
94 final jsonString = jsonEncode(resultsMap);
95 await _imageRepo.updateAnalysis(image.id, jsonString);
96 await _imageRepo.updateStatus(image.id, 'completed');
97 debugPrint("[Queue]: Completed: ${image.name}");
98 } else {
99 await _imageRepo.updateStatus(image.id, 'failed');
100 debugPrint("[Queue]: Failed: ${result['error']}");
101 }
102 } catch (e) {
103 debugPrint("[Queue]: Analysis Exception: $e");
104 await _imageRepo.updateStatus(image.id, 'failed');
105 }
106 }
107
108 Future<void> _processNoteGroup(String imageId, List<NoteModel> notes) async {
109 img.Image? parentImageCache;
110 try {
111 // 1. Fetch Parent Info
112 final parentImageModel = await _imageRepo.getById(imageId);
113 if (parentImageModel == null) {
114 throw Exception("Parent image not found: $imageId");
115 }
116 final parentFile = File(parentImageModel.filePath);
117 if (!parentFile.existsSync()) throw Exception("Parent file missing");
118
119 // 2. Decode once for all notes in this group
120 final bytes = await parentFile.readAsBytes();
121 parentImageCache = img.decodeImage(bytes);
122 if (parentImageCache == null) {
123 throw Exception("Failed to decode parent image");
124 }
125
126 final appDir = await getApplicationDocumentsDirectory();
127
128 final cropDirPath = p.join(appDir.path, 'crops');
129 final cropDir = Directory(cropDirPath);
130
131 if (!await cropDir.exists()) {
132 await cropDir.create(recursive: true);
133 }
134
135 for (final note in notes) {
136 await _processSingleNoteWithCache(note, parentImageCache, cropDir);
137 }
138 } catch (e) {
139 debugPrint("[Queue]: Error processing group for image $imageId: $e");
140 for (var note in notes) {
141 await _noteRepo.updateNote(note.id!, status: 'failed');
142 }
143 }
144 }
145
146 Future<void> _processSingleNoteWithCache(
147 NoteModel note,
148 img.Image parentImage,
149 Directory cropDir,
150 ) async {
151 try {
152 // 1. Define Permanent Path
153 final String cropPath = p.join(cropDir.path, 'note_crop_${note.id}.jpg');
154 final File cropFile = File(cropPath);
155
156 // 2. Generate Crop (if it doesn't already exist)
157 if (!await cropFile.exists()) {
158 int x = (note.normX * parentImage.width).round();
159 int y = (note.normY * parentImage.height).round();
160 int w = (note.normWidth * parentImage.width).round();
161 int h = (note.normHeight * parentImage.height).round();
162
163 // Center -> Top-Left conversion
164 int left = x - (w ~/ 2);
165 int top = y - (h ~/ 2);
166
167 // Clamping logic (Safety check)
168 if (left < 0) left = 0;
169 if (top < 0) top = 0;
170 if (left + w > parentImage.width) w = parentImage.width - left;
171 if (top + h > parentImage.height) h = parentImage.height - top;
172
173 if (w <= 0 || h <= 0) {
174 debugPrint("[Queue]: Note ${note.id} has invalid dimensions");
175 await _noteRepo.updateNote(note.id!, status: 'failed');
176 return;
177 }
178
179 final croppedImg = img.copyCrop(
180 parentImage,
181 x: left,
182 y: top,
183 width: w,
184 height: h,
185 );
186 await cropFile.writeAsBytes(img.encodeJpg(croppedImg));
187 }
188
189 await _noteRepo.updateNote(
190 note.id!,
191 status: 'analyzing',
192 cropFilePath: cropPath,
193 );
194
195 // 3. Analysis
196 debugPrint("[Queue]: Analyzing Note ${note.id} tag: ${note.category}");
197
198 // Now when this calls generateAsset internally, the DB lookup will succeed
199 final result = await ImageAnalyzerService.analyzeSelected(cropPath, [
200 note.category,
201 ]);
202
203 // 4. Update DB with Results
204 if (result['success'] == true && result['data'] != null) {
205 final resultsMap = result['data']['results'] ?? {};
206 final jsonString = jsonEncode(resultsMap);
207
208 await _noteRepo.updateNote(
209 note.id!,
210 analysisData: jsonString,
211 status: 'completed',
212 // cropFilePath is already saved, no need to save again
213 );
214 } else {
215 await _noteRepo.updateNote(note.id!, status: 'failed');
216 }
217 } catch (e) {
218 debugPrint("[Queue]: Note processing error: $e");
219 await _noteRepo.updateNote(note.id!, status: 'failed');
220 }
221 }
222}