creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit 8f5b90b67fcd6cc2d51f8f52d9fbccd3b514755c parent 2cecc5ea83303efc4d86fb0294f37a49f352a2ea Author: Sanjeebani Parida <sanjeebani14@gmail.com> Date: Thu, 27 Nov 2025 11:02:26 +0530 Fixed emotion and lighting model
Diffstat:
| M | lib/services/emotional_embeddings_service.dart | | | 139 | ++++++++++++++++++++++++++++++++++++++++--------------------------------------- |
| M | lib/services/image_analyzer_service.dart | | | 8 | ++++++-- |
| M | lib/services/lighting_service.dart | | | 135 | +++++++++++++++++++++++++++++++++++++++---------------------------------------- |
| M | lib/ui/widgets/analysis_dialog.dart | | | 81 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | linux/flutter/generated_plugins.cmake | | | 1 | - |
| M | pubspec.lock | | | 42 | +++++++++++++----------------------------- |
| M | pubspec.yaml | | | 16 | ++++------------ |
| M | windows/flutter/generated_plugins.cmake | | | 1 | - |
8 files changed, 242 insertions(+), 181 deletions(-)
diff --git a/lib/services/emotional_embeddings_service.dart b/lib/services/emotional_embeddings_service.dart @@ -1,11 +1,9 @@ -// lib/services/style_analyzer_service.dart - import 'dart:convert'; import 'dart:math'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:onnxruntime/onnxruntime.dart'; +import 'package:flutter_onnxruntime/flutter_onnxruntime.dart'; import 'package:path_provider/path_provider.dart'; import '../utils/clip_image_processor.dart'; @@ -17,84 +15,89 @@ class EmotionalEmbeddingsService { static Future<void> initialize() async { if (_session != null) return; - debugPrint("Initializing AI..."); - - // 1. Setup ONNX Environment - OrtEnv.instance.init(); - - // 2. Load Model from Assets - final sessionOptions = OrtSessionOptions(); - final rawAsset = await rootBundle.load('assets/clip_vitb32_image.onnx'); - final bytes = rawAsset.buffer.asUint8List(); + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); - // Store temp file to load into ONNX - final dir = await getTemporaryDirectory(); - final modelFile = File('${dir.path}/clip_model.onnx'); - await modelFile.writeAsBytes(bytes); + final rawAsset = await rootBundle.load('assets/clip_image_encoder.onnx'); + final dir = await getTemporaryDirectory(); + final modelFile = File('${dir.path}/clip_model.onnx'); - _session = OrtSession.fromFile(modelFile, sessionOptions); + if (!await modelFile.exists()) { + await modelFile.writeAsBytes(rawAsset.buffer.asUint8List()); + } - // 3. Load Embeddings JSON - final jsonString = await rootBundle.loadString( - 'assets/emotion_centroids.json', - ); - final jsonData = json.decode(jsonString); + // Initialize runtime + final ort = OnnxRuntime(); + _session = await ort.createSession(modelFile.path); - _classes = List<String>.from(jsonData['classes']); - _centroids = - (jsonData['centroids'] as List) - .map((e) => List<double>.from(e)) - .toList(); + final jsonString = await rootBundle.loadString( + 'assets/emotion_centroids.json', + ); + final jsonData = json.decode(jsonString); - debugPrint("Loaded ${_classes!.length} emotions."); + _classes = List<String>.from(jsonData['classes']); + _centroids = + (jsonData['centroids'] as List) + .map((e) => List<double>.from(e)) + .toList(); + } catch (e) { + debugPrint("Embeddings Init Error: $e"); + } } - static Future<Map<String, dynamic>?> analyzeImage(String imagePath) async { + static Future<Map<String, dynamic>?> analyze(String imagePath) async { if (_session == null) await initialize(); + OrtValue? inputOrt; + OrtValue? outputOrt; + try { - // 1. Preprocess Image final float32Input = await ClipImageProcessor.preprocess(imagePath); + //debugPrint("[EMOTION] Preprocess length: ${float32Input?.length}"); if (float32Input == null) return null; - // 2. Create Tensor [1, 3, 224, 224] - final inputTensor = OrtValueTensor.createTensorWithDataList( - float32Input, - [1, 3, 224, 224], - ); + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = await OrtValue.fromList(float32Input, [1, 3, 224, 224]); + + // Run Inference + // 'image' is the input name for CLIP. Check your model if it differs (e.g. 'input') + final outputs = await _session!.run({"image": inputOrt}); + //debugPrint("[EMOTION] Output keys: ${outputs.keys}"); + + // Get Output + outputOrt = + outputs['output']; // or outputs['image_features'] depending on model + // If outputOrt is null, try iterating outputs.values.first + final resultOrt = outputOrt ?? outputs.values.first; - // 3. Run Inference - final inputs = { - 'image': inputTensor, - }; // Check input name in Netron.app if 'image' fails (might be 'input') - final runOptions = OrtRunOptions(); - final outputs = _session!.run(runOptions, inputs); + if (resultOrt == null) throw Exception("No output from model"); - // 4. Get Embedding [1, 512] - // ONNX Runtime usually returns a List<List<double>> for 2D output - final rawOutput = outputs[0]?.value as List; - // Flatten if necessary, or access [0] - List<double> imgFeat = - (rawOutput[0] as List).map((e) => e as double).toList(); + // Flatten Output + final List<dynamic> rawList = await resultOrt.asList(); + final List<double> imgFeat = []; + + void flatten(dynamic data) { + if (data is num) + imgFeat.add(data.toDouble()); + else if (data is List) + for (var item in data) flatten(item); + } - // Cleanup - inputTensor.release(); - runOptions.release(); - outputs.forEach((element) => element?.release()); + flatten(rawList); - // 5. Math: Normalize & Dot Product - imgFeat = _l2Normalize(imgFeat); + // debugPrint("[EMOTION] Embedding length: ${imgFeat.length}"); + // debugPrint("[EMOTION] Centroid length: ${_centroids?[0].length}"); + // Normalize & Compare + final normFeat = _l2Normalize(imgFeat); List<Map<String, dynamic>> scores = []; + for (int i = 0; i < _classes!.length; i++) { - double score = _dotProduct(imgFeat, _centroids![i]); - scores.add({ - "name": _classes![i], - "score": score * 100.0, // Scale for display - }); + double score = _dotProduct(normFeat, _centroids![i]); + scores.add({"name": _classes![i], "score": score * 100.0}); } - // 6. Sort & Return scores.sort( (a, b) => (b['score'] as double).compareTo(a['score'] as double), ); @@ -103,28 +106,28 @@ class EmotionalEmbeddingsService { "success": true, "label": scores.first['name'], "top5": scores.take(5).toList(), - "scores": {for (var e in scores) e['name']: e['score']}, }; } catch (e) { - debugPrint("Analysis Error: $e"); + debugPrint("Embeddings Analysis Error: $e"); return null; + } finally { + inputOrt?.dispose(); + outputOrt?.dispose(); } } - // --- MATH HELPERS --- - static List<double> _l2Normalize(List<double> vec) { - double sum = 0; - for (var v in vec) { - sum += v * v; - } + double sum = vec.fold(0, (p, c) => p + c * c); double norm = sqrt(sum); + if (norm == 0) return vec; return vec.map((v) => v / norm).toList(); } static double _dotProduct(List<double> a, List<double> b) { double sum = 0; - for (int i = 0; i < a.length; i++) { + // Ensure lengths match to avoid range errors + int len = min(a.length, b.length); + for (int i = 0; i < len; i++) { sum += a[i] * b[i]; } return sum; diff --git a/lib/services/image_analyzer_service.dart b/lib/services/image_analyzer_service.dart @@ -15,7 +15,7 @@ class ImageAnalyzerService { // 1. Instantiate services final layoutService = LayoutAnalyzerService(); final colorService = ColorAnalyzerService(); - final textureService = TextureAnalyzerService(); + final textureService = TextureAnalyzerService(); // EmbeddingService is static try { @@ -24,7 +24,9 @@ class ImageAnalyzerService { layoutService.analyze(imagePath), // Index 0: Layout colorService.analyze(imagePath), // Index 1: Color textureService.analyze(imagePath), // Index 2: Texture - EmbeddingAnalyzerService.analyze(imagePath) // Index 3: Embeddings + EmbeddingAnalyzerService.analyze(imagePath), // Index 3: Embeddings + EmotionalEmbeddingsService.analyze(imagePath), // Index 4: Emotional Embeddings + LightingEmbeddingsService.analyze(imagePath), // Index 5: Lighting Embeddings ]); // 3. Construct Unified Result @@ -35,6 +37,8 @@ class ImageAnalyzerService { 'color': results[1], 'texture': results[2], // This is a List 'embedding': results[3], + 'emotions': results[4], + 'lighting': results[5], }; return combinedResult; diff --git a/lib/services/lighting_service.dart b/lib/services/lighting_service.dart @@ -1,11 +1,9 @@ -// lib/services/style_analyzer_service.dart - import 'dart:convert'; import 'dart:math'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:onnxruntime/onnxruntime.dart'; +import 'package:flutter_onnxruntime/flutter_onnxruntime.dart'; import 'package:path_provider/path_provider.dart'; import '../utils/clip_image_processor.dart'; @@ -17,84 +15,85 @@ class LightingEmbeddingsService { static Future<void> initialize() async { if (_session != null) return; - debugPrint("Initializing AI..."); - - // 1. Setup ONNX Environment - OrtEnv.instance.init(); - - // 2. Load Model from Assets - final sessionOptions = OrtSessionOptions(); - final rawAsset = await rootBundle.load('assets/clip_vitb32_image.onnx'); - final bytes = rawAsset.buffer.asUint8List(); + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); - // Store temp file to load into ONNX - final dir = await getTemporaryDirectory(); - final modelFile = File('${dir.path}/clip_model.onnx'); - await modelFile.writeAsBytes(bytes); + final rawAsset = await rootBundle.load('assets/clip_image_encoder.onnx'); + final dir = await getTemporaryDirectory(); + final modelFile = File('${dir.path}/clip_model.onnx'); - _session = OrtSession.fromFile(modelFile, sessionOptions); + if (!await modelFile.exists()) { + await modelFile.writeAsBytes(rawAsset.buffer.asUint8List()); + } - // 3. Load Embeddings JSON - final jsonString = await rootBundle.loadString( - 'assets/lighting_centroids.json', - ); - final jsonData = json.decode(jsonString); + // Initialize runtime + final ort = OnnxRuntime(); + _session = await ort.createSession(modelFile.path); - _classes = List<String>.from(jsonData['classes']); - _centroids = - (jsonData['centroids'] as List) - .map((e) => List<double>.from(e)) - .toList(); + final jsonString = await rootBundle.loadString( + 'assets/lighting_centroids.json', + ); + final jsonData = json.decode(jsonString); - debugPrint("Loaded ${_classes!.length} lighting styles."); + _classes = List<String>.from(jsonData['classes']); + _centroids = + (jsonData['centroids'] as List) + .map((e) => List<double>.from(e)) + .toList(); + } catch (e) { + debugPrint("Embeddings Init Error: $e"); + } } - static Future<Map<String, dynamic>?> analyzeImage(String imagePath) async { + static Future<Map<String, dynamic>?> analyze(String imagePath) async { if (_session == null) await initialize(); + OrtValue? inputOrt; + OrtValue? outputOrt; + try { - // 1. Preprocess Image final float32Input = await ClipImageProcessor.preprocess(imagePath); if (float32Input == null) return null; - // 2. Create Tensor [1, 3, 224, 224] - final inputTensor = OrtValueTensor.createTensorWithDataList( - float32Input, - [1, 3, 224, 224], - ); + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = await OrtValue.fromList(float32Input, [1, 3, 224, 224]); + + // Run Inference + // 'image' is the input name for CLIP. Check your model if it differs (e.g. 'input') + final outputs = await _session!.run({"image": inputOrt}); + //debugPrint("[LIGHTING] Output keys: ${outputs.keys}"); - // 3. Run Inference - final inputs = { - 'image': inputTensor, - }; // Check input name in Netron.app if 'image' fails (might be 'input') - final runOptions = OrtRunOptions(); - final outputs = _session!.run(runOptions, inputs); + // Get Output + outputOrt = + outputs['output']; // or outputs['image_features'] depending on model + // If outputOrt is null, try iterating outputs.values.first + final resultOrt = outputOrt ?? outputs.values.first; - // 4. Get Embedding [1, 512] - // ONNX Runtime usually returns a List<List<double>> for 2D output - final rawOutput = outputs[0]?.value as List; - // Flatten if necessary, or access [0] - List<double> imgFeat = - (rawOutput[0] as List).map((e) => e as double).toList(); + if (resultOrt == null) throw Exception("No output from model"); - // Cleanup - inputTensor.release(); - runOptions.release(); - outputs.forEach((element) => element?.release()); + // Flatten Output + final List<dynamic> rawList = await resultOrt.asList(); + final List<double> imgFeat = []; + + void flatten(dynamic data) { + if (data is num) + imgFeat.add(data.toDouble()); + else if (data is List) + for (var item in data) flatten(item); + } - // 5. Math: Normalize & Dot Product - imgFeat = _l2Normalize(imgFeat); + flatten(rawList); + // Normalize & Compare + final normFeat = _l2Normalize(imgFeat); List<Map<String, dynamic>> scores = []; + for (int i = 0; i < _classes!.length; i++) { - double score = _dotProduct(imgFeat, _centroids![i]); - scores.add({ - "name": _classes![i], - "score": score * 100.0, // Scale for display - }); + double score = _dotProduct(normFeat, _centroids![i]); + scores.add({"name": _classes![i], "score": score * 100.0}); } - // 6. Sort & Return scores.sort( (a, b) => (b['score'] as double).compareTo(a['score'] as double), ); @@ -103,28 +102,28 @@ class LightingEmbeddingsService { "success": true, "label": scores.first['name'], "top5": scores.take(5).toList(), - "scores": {for (var e in scores) e['name']: e['score']}, }; } catch (e) { - debugPrint("Analysis Error: $e"); + debugPrint("Embeddings Analysis Error: $e"); return null; + } finally { + inputOrt?.dispose(); + outputOrt?.dispose(); } } - // --- MATH HELPERS --- - static List<double> _l2Normalize(List<double> vec) { - double sum = 0; - for (var v in vec) { - sum += v * v; - } + double sum = vec.fold(0, (p, c) => p + c * c); double norm = sqrt(sum); + if (norm == 0) return vec; return vec.map((v) => v / norm).toList(); } static double _dotProduct(List<double> a, List<double> b) { double sum = 0; - for (int i = 0; i < a.length; i++) { + // Ensure lengths match to avoid range errors + int len = min(a.length, b.length); + for (int i = 0; i < len; i++) { sum += a[i] * b[i]; } return sum; diff --git a/lib/ui/widgets/analysis_dialog.dart b/lib/ui/widgets/analysis_dialog.dart @@ -99,6 +99,87 @@ class AnalysisDialog extends StatelessWidget { )).toList(), ), ), + + // 5. Emotional Embeddings + // 5. Emotional Embeddings + if (data['emotions'] != null) + _buildSection( + "Emotional Tone", + Icons.mood, + Colors.redAccent, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text( + data['emotions']['label'] ?? "Unknown", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Colors.redAccent, + ), + ), + const SizedBox(height: 6), + + Wrap( + spacing: 6, + children: + (data['emotions']['top5'] as List? ?? []) + .map<Widget>( + (e) => Chip( + label: Text( + e['name'], + style: const TextStyle(fontSize: 10), + ), + visualDensity: VisualDensity.compact, + ), + ) + .toList(), + ), + ], + ), + ), + + // 6. Lighting Embeddings + // 6. Lighting Embeddings + if (data['lighting'] != null) + _buildSection( + "Lighting Style", + Icons.wb_sunny, + Colors.amber, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text( + data['lighting']['label'] ?? "Unknown", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Colors.amber, + ), + ), + const SizedBox(height: 6), + + Wrap( + spacing: 6, + children: + (data['lighting']['top5'] as List? ?? []) + .map<Widget>( + (e) => Chip( + label: Text( + e['name'], + style: const TextStyle(fontSize: 10), + ), + visualDensity: VisualDensity.compact, + ), + ) + .toList(), + ), + ], + ), + ), + ], ), ), diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - onnxruntime ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.7" - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" async: dependency: transitive description: @@ -292,26 +284,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -340,10 +332,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -360,14 +352,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - onnxruntime: - dependency: "direct main" - description: - name: onnxruntime - sha256: e77ec05acafc135cc5fe7bcdf11b101b39f06513c9d5e9fa02cb1929f6bac72a - url: "https://pub.dev" - source: hosted - version: "1.4.1" path: dependency: "direct main" description: @@ -625,10 +609,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -649,10 +633,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -686,5 +670,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.7.2 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml @@ -23,12 +23,6 @@ dependencies: image: ^4.5.4 flutter_onnxruntime: ^1.6.0 - onnxruntime: ^1.2.0 - image: ^4.5.4 - - onnxruntime: ^1.2.0 - image: ^4.5.4 - dev_dependencies: flutter_test: sdk: flutter @@ -39,17 +33,14 @@ flutter: uses-material-design: true assets: - - assets/clip_vitb32_image.onnx - - assets/emotion_centroids.json - - assets/lighting_centroids.json - - assets: - assets/fonts/ - assets/dinov2.onnx - assets/dinov2.onnx.data - assets/texture_centroids.json - assets/clip_image_encoder.onnx - assets/style_embeddings.json + - assets/emotion_centroids.json + - assets/lighting_centroids.json fonts: - family: GeneralSans @@ -63,4 +54,4 @@ flutter: - asset: assets/fonts/GeneralSans-Medium.otf weight: 500 - asset: assets/fonts/GeneralSans-Semibold.otf - weight: 600 + weight: 600 +\ No newline at end of file diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - onnxruntime ) set(PLUGIN_BUNDLED_LIBRARIES)