creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit 0aab94b287b57e9d4498af1b0e19e810e1cb6abf parent 4a461358a30ce16a62feada9ea65418bf1b1513e Author: maydayv7 <maydayv7@gmail.com> Date: Thu, 27 Nov 2025 22:27:37 +0530 Refactor data models
Diffstat:
| M | lib/data/database.dart | | | 119 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| D | lib/data/models/board_image_board.dart | | | 9 | --------- |
| D | lib/data/models/board_model.dart | | | 32 | -------------------------------- |
| D | lib/data/models/category_model.dart | | | 28 | ---------------------------- |
| D | lib/data/models/comment_model.dart | | | 57 | --------------------------------------------------------- |
| A | lib/data/models/file_model.dart | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | lib/data/models/image_model.dart | | | 79 | +++++++++++++++++++++++++------------------------------------------------------ |
| A | lib/data/models/note_model.dart | | | 35 | +++++++++++++++++++++++++++++++++++ |
| A | lib/data/models/project_model.dart | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | lib/data/repos/board_category_repo.dart | | | 27 | --------------------------- |
| D | lib/data/repos/board_image_repo.dart | | | 49 | ------------------------------------------------- |
| D | lib/data/repos/board_repo.dart | | | 35 | ----------------------------------- |
| D | lib/data/repos/comment_repo.dart | | | 34 | ---------------------------------- |
| A | lib/data/repos/file_repo.dart | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | lib/data/repos/image_repo.dart | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++------------------------------ |
| A | lib/data/repos/note_repo.dart | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
| A | lib/data/repos/project_repo.dart | | | 66 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | lib/main.dart | | | 10 | +++++++--- |
| A | lib/services/analyze/color.dart | | | 16 | ++++++++++++++++ |
| A | lib/services/analyze/embedding.dart | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | lib/services/analyze/emotional_embeddings.dart | | | 124 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | lib/services/analyze/era.dart | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | lib/services/analyze/image_analyzer.dart | | | 270 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | lib/services/analyze/layout.dart | | | 16 | ++++++++++++++++ |
| A | lib/services/analyze/lighting.dart | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| R | lib/services/python_service.dart -> lib/services/analyze/python_service.dart | | | 0 | |
| A | lib/services/analyze/texture.dart | | | 161 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | lib/services/board_services.dart | | | 68 | -------------------------------------------------------------------- |
| D | lib/services/color_analyzer_service.dart | | | 18 | ------------------ |
| D | lib/services/embedding_analyzer_service.dart | | | 122 | ------------------------------------------------------------------------------- |
| D | lib/services/emotional_embeddings_service.dart | | | 126 | ------------------------------------------------------------------------------- |
| D | lib/services/era_service.dart | | | 122 | ------------------------------------------------------------------------------- |
| A | lib/services/file_service.dart | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | lib/services/image_analyzer_service.dart | | | 273 | ------------------------------------------------------------------------------- |
| M | lib/services/image_service.dart | | | 80 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| D | lib/services/layout_analyzer_service.dart | | | 18 | ------------------ |
| D | lib/services/lighting_service.dart | | | 122 | ------------------------------------------------------------------------------- |
| A | lib/services/note_service.dart | | | 24 | ++++++++++++++++++++++++ |
| A | lib/services/project_service.dart | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | lib/services/texture_analyzer_service.dart | | | 163 | ------------------------------------------------------------------------------- |
| M | lib/ui/pages/image_analysis_page.dart | | | 242 | +++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------- |
| D | lib/ui/widgets/analysis_dialog.dart | | | 270 | ------------------------------------------------------------------------------- |
| M | pubspec.lock | | | 16 | ++++++++-------- |
43 files changed, 1711 insertions(+), 1874 deletions(-)
diff --git a/lib/data/database.dart b/lib/data/database.dart @@ -1,8 +1,15 @@ +import 'dart:convert'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; +import 'models/project_model.dart'; +import 'models/image_model.dart'; +import 'models/file_model.dart'; +import 'models/note_model.dart'; + class AppDatabase { static Database? _db; + static const String _dbName = 'database_v2.db'; static Future<Database> get db async { if (_db != null) return _db!; @@ -11,101 +18,70 @@ class AppDatabase { } static Future<Database> _init() async { - final path = join(await getDatabasesPath(), 'pinterest.db'); + final path = join(await getDatabasesPath(), _dbName); return await openDatabase( path, - version: 4, // Incremented to 4 for Comments table + version: 1, + onConfigure: (db) async { + await db.execute('PRAGMA foreign_keys = ON'); + }, onCreate: (db, version) async { - // 1. Images Table + // 1. PROJECTS await db.execute(''' - CREATE TABLE images ( - id TEXT PRIMARY KEY, - filePath TEXT, - createdAt TEXT, - analysis_data TEXT - ) - '''); - - // 2. Categories Table - await db.execute(''' - CREATE TABLE board_categories ( + CREATE TABLE projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - createdAt TEXT + title TEXT NOT NULL, + description TEXT, + parent_id INTEGER, + global_stylesheet TEXT, + last_accessed_at TEXT, + created_at TEXT, + FOREIGN KEY (parent_id) REFERENCES projects (id) ON DELETE CASCADE ) '''); - // 3. Boards Table + // 2. IMAGES (Moodboard) await db.execute(''' - CREATE TABLE boards ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + CREATE TABLE images ( + id TEXT PRIMARY KEY, + project_id INTEGER NOT NULL, + file_path TEXT NOT NULL, name TEXT, - category_id INTEGER, - createdAt TEXT, - FOREIGN KEY (category_id) REFERENCES board_categories (id) + tags TEXT DEFAULT '[]', + analysis_data TEXT, + created_at TEXT, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ) '''); - // 4. Junction Table + // 3. FILES (Project Work) await db.execute(''' - CREATE TABLE board_images ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - board_id INTEGER, - image_id TEXT, - createdAt TEXT + CREATE TABLE files ( + id TEXT PRIMARY KEY, + project_id INTEGER NOT NULL, + file_path TEXT NOT NULL, + name TEXT, + description TEXT, + tags TEXT DEFAULT '[]', + last_updated TEXT, + created_at TEXT, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ) '''); - // 5. Comments Table (New) + // 4. NOTES await db.execute(''' - CREATE TABLE comments ( + CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, - image_id TEXT, + image_id TEXT NOT NULL, content TEXT, - comment_type TEXT, - createdAt TEXT, + category TEXT, + created_at TEXT, FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE ) '''); }, - onUpgrade: (db, oldVersion, newVersion) async { - // ... Previous migrations (v1->v2, v2->v3) ... - if (oldVersion < 2) { - final tableInfo = await db.rawQuery('PRAGMA table_info(images)'); - if (!tableInfo.any((c) => c['name'] == 'analysis_data')) { - await db.execute('ALTER TABLE images ADD COLUMN analysis_data TEXT'); - } - } - - if (oldVersion < 3) { - await db.execute(''' - CREATE TABLE IF NOT EXISTS board_categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - createdAt TEXT - ) - '''); - final tableInfo = await db.rawQuery('PRAGMA table_info(boards)'); - if (!tableInfo.any((c) => c['name'] == 'category_id')) { - await db.execute('ALTER TABLE boards ADD COLUMN category_id INTEGER'); - } - } - - // Migration v3 -> v4 (Comments) - if (oldVersion < 4) { - await db.execute(''' - CREATE TABLE IF NOT EXISTS comments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - image_id TEXT, - content TEXT, - comment_type TEXT, - createdAt TEXT, - FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE - ) - '''); - } - }, ); } -} -\ No newline at end of file +} diff --git a/lib/data/models/board_image_board.dart b/lib/data/models/board_image_board.dart @@ -1,9 +0,0 @@ -// lib/data/models/board_image_board.dart - -class BoardImage { - final int id; - final int boardId; - final String imageId; - - BoardImage({required this.id, required this.boardId, required this.imageId}); -} diff --git a/lib/data/models/board_model.dart b/lib/data/models/board_model.dart @@ -1,31 +0,0 @@ -class Board { - final int id; - final String name; - final String createdAt; - final int? categoryId; // Added categoryId - - Board({ - required this.id, - required this.name, - required this.createdAt, - this.categoryId, // Added categoryId to constructor - }); - - factory Board.fromMap(Map<String, dynamic> map) { - return Board( - id: map['id'], - name: map['name'], - createdAt: map['createdAt'], - categoryId: map['category_id'], // Map category_id from DB - ); - } - - Map<String, dynamic> toMap() { - return { - 'id': id, - 'name': name, - 'createdAt': createdAt, - 'category_id': categoryId, // Include category_id in map - }; - } -} -\ No newline at end of file diff --git a/lib/data/models/category_model.dart b/lib/data/models/category_model.dart @@ -1,27 +0,0 @@ -class BoardCategory { - final int id; - final String name; - final String createdAt; - - BoardCategory({ - required this.id, - required this.name, - required this.createdAt, - }); - - factory BoardCategory.fromMap(Map<String, dynamic> map) { - return BoardCategory( - id: map['id'], - name: map['name'], - createdAt: map['createdAt'], - ); - } - - Map<String, dynamic> toMap() { - return { - 'id': id, - 'name': name, - 'createdAt': createdAt, - }; - } -} -\ No newline at end of file diff --git a/lib/data/models/comment_model.dart b/lib/data/models/comment_model.dart @@ -1,56 +0,0 @@ -enum CommentType { - illustrations, - layout, - motif, - typography, - colour, - font, - vibe, - other -} - -class Comment { - final int? id; // Database ID - final String imageId; // Foreign key to the Image - final String content; // The text content - final CommentType type; // The category of comment - final String createdAt; // Timestamp - - Comment({ - this.id, - required this.imageId, - required this.content, - required this.type, - required this.createdAt, - }); - - // Convert from Map (Database) to Model - factory Comment.fromMap(Map<String, dynamic> map) { - return Comment( - id: map['id'] as int?, - imageId: map['image_id'] as String, - content: map['content'] as String, - type: _parseType(map['comment_type'] as String), - createdAt: map['createdAt'] as String, - ); - } - - // Convert from Model to Map (Database) - Map<String, dynamic> toMap() { - return { - 'id': id, - 'image_id': imageId, - 'content': content, - 'comment_type': type.name, // Stores as string e.g., "illustrations" - 'createdAt': createdAt, - }; - } - - // Helper to parse string to Enum safely - static CommentType _parseType(String typeString) { - return CommentType.values.firstWhere( - (e) => e.name == typeString, - orElse: () => CommentType.other, - ); - } -} -\ No newline at end of file diff --git a/lib/data/models/file_model.dart b/lib/data/models/file_model.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +class FileModel { + final String id; + final int projectId; + final String filePath; + final String name; + final String? description; + final List<String> tags; + final DateTime lastUpdated; + final DateTime createdAt; + + FileModel({ + required this.id, + required this.projectId, + required this.filePath, + required this.name, + this.description, + this.tags = const [], + required this.lastUpdated, + required this.createdAt, + }); + + Map<String, dynamic> toMap() { + return { + 'id': id, + 'project_id': projectId, + 'file_path': filePath, + 'name': name, + 'description': description, + 'tags': jsonEncode(tags), + 'last_updated': lastUpdated.toIso8601String(), + 'created_at': createdAt.toIso8601String(), + }; + } + + factory FileModel.fromMap(Map<String, dynamic> map) { + return FileModel( + id: map['id'], + projectId: map['project_id'], + filePath: map['file_path'], + name: map['name'] ?? 'Untitled', + description: map['description'], + tags: map['tags'] != null ? List<String>.from(jsonDecode(map['tags'])) : [], + lastUpdated: map['last_updated'] != null + ? DateTime.parse(map['last_updated']) + : DateTime.parse(map['created_at']), + createdAt: DateTime.parse(map['created_at']), + ); + } +} diff --git a/lib/data/models/image_model.dart b/lib/data/models/image_model.dart @@ -2,73 +2,44 @@ import 'dart:convert'; class ImageModel { final String id; + final int projectId; final String filePath; + final String name; + final List<String> tags; final String? analysisData; - final String? name; // Optional name - final List<String>? tags; // Optional list of tags + final DateTime createdAt; ImageModel({ required this.id, + required this.projectId, required this.filePath, + required this.name, + this.tags = const [], this.analysisData, - this.name, - this.tags, + required this.createdAt, }); - factory ImageModel.fromMap(Map<String, dynamic> m) { - // Helper to parse tags from JSON string or List - List<String>? parseTags(dynamic tagsData) { - if (tagsData == null) return null; - if (tagsData is List) { - return tagsData.map((e) => e.toString()).toList(); - } - if (tagsData is String) { - try { - // If stored as JSON string ["tag1", "tag2"] - final decoded = json.decode(tagsData); - if (decoded is List) { - return decoded.map((e) => e.toString()).toList(); - } - } catch (e) { - // Fallback: maybe comma separated? - return tagsData.split(',').map((e) => e.trim()).toList(); - } - } - return null; - } - - return ImageModel( - id: m['id'], - filePath: m['filePath'], - analysisData: m['analysis_data'], - name: m['name'], - tags: parseTags(m['tags']), - ); - } - - Map<String, dynamic>? get analysis { - if (analysisData == null) return null; - try { - // analysisData is a JSON string, so we need to decode it - if (analysisData is String) { - return json.decode(analysisData as String) as Map<String, dynamic>; - } - // If it's already a Map, convert it - return Map<String, dynamic>.from(analysisData as Map); - } catch (e) { - return null; - } - } - - // You might want a toMap for saving back to DB if not already present elsewhere Map<String, dynamic> toMap() { return { 'id': id, - 'filePath': filePath, - 'analysis_data': analysisData, + 'project_id': projectId, + 'file_path': filePath, 'name': name, - // Store tags as JSON string in SQLite usually - 'tags': tags != null ? json.encode(tags) : null, + 'tags': jsonEncode(tags), + 'analysis_data': analysisData, + 'created_at': createdAt.toIso8601String(), }; } + + factory ImageModel.fromMap(Map<String, dynamic> map) { + return ImageModel( + id: map['id'], + projectId: map['project_id'], + filePath: map['file_path'], + name: map['name'] ?? 'Untitled', + tags: map['tags'] != null ? List<String>.from(jsonDecode(map['tags'])) : [], + analysisData: map['analysis_data'], + createdAt: DateTime.parse(map['created_at']), + ); + } } diff --git a/lib/data/models/note_model.dart b/lib/data/models/note_model.dart @@ -0,0 +1,35 @@ +class NoteModel { + final int? id; + final String imageId; + final String content; + final String category; + final DateTime createdAt; + + NoteModel({ + this.id, + required this.imageId, + required this.content, + required this.category, + required this.createdAt, + }); + + Map<String, dynamic> toMap() { + return { + 'id': id, + 'image_id': imageId, + 'content': content, + 'category': category, + 'created_at': createdAt.toIso8601String(), + }; + } + + factory NoteModel.fromMap(Map<String, dynamic> map) { + return NoteModel( + id: map['id'], + imageId: map['image_id'], + content: map['content'], + category: map['category'], + createdAt: DateTime.parse(map['created_at']), + ); + } +} diff --git a/lib/data/models/project_model.dart b/lib/data/models/project_model.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +class ProjectModel { + final int? id; + final String title; + final String? description; + final int? parentId; // null -> Project, int - Event + final String? globalStylesheet; // Stored as JSON string + final DateTime lastAccessedAt; + final DateTime createdAt; + + ProjectModel({ + this.id, + required this.title, + this.description, + this.parentId, + this.globalStylesheet, + required this.lastAccessedAt, + required this.createdAt, + }); + + bool get isEvent => parentId != null; + + // Helper to get stylesheet as a JSON Object (Map) + Map<String, dynamic> get styleSheetMap { + if (globalStylesheet == null || globalStylesheet!.isEmpty) return {}; + try { + return jsonDecode(globalStylesheet!) as Map<String, dynamic>; + } catch (e) { + return {}; + } + } + + Map<String, dynamic> toMap() { + return { + 'id': id, + 'title': title, + 'description': description, + 'parent_id': parentId, + 'global_stylesheet': globalStylesheet, + 'last_accessed_at': lastAccessedAt.toIso8601String(), + 'created_at': createdAt.toIso8601String(), + }; + } + + factory ProjectModel.fromMap(Map<String, dynamic> map) { + return ProjectModel( + id: map['id'], + title: map['title'], + description: map['description'], + parentId: map['parent_id'], + globalStylesheet: map['global_stylesheet'], + lastAccessedAt: DateTime.parse(map['last_accessed_at']), + createdAt: DateTime.parse(map['created_at']), + ); + } +} diff --git a/lib/data/repos/board_category_repo.dart b/lib/data/repos/board_category_repo.dart @@ -1,26 +0,0 @@ -import 'package:adobe/data/database.dart'; - -class BoardCategoryRepository { - Future<int> createCategory(String name) async { - final db = await AppDatabase.db; - return await db.insert('board_categories', { - 'name': name, - 'createdAt': DateTime.now().toIso8601String(), - }); - } - - Future<List<Map<String, dynamic>>> getAllCategories() async { - final db = await AppDatabase.db; - return await db.query('board_categories', orderBy: 'createdAt DESC'); - } - - Future<List<Map<String, dynamic>>> getBoardsByCategory(int categoryId) async { - final db = await AppDatabase.db; - return await db.query( - 'boards', - where: 'category_id = ?', - whereArgs: [categoryId], - orderBy: 'createdAt DESC', - ); - } -} -\ No newline at end of file diff --git a/lib/data/repos/board_image_repo.dart b/lib/data/repos/board_image_repo.dart @@ -1,49 +0,0 @@ -// lib/data/repos/board_image_repo.dart - -import 'package:adobe/data/database.dart'; - -class BoardImageRepository { - Future<void> saveToBoard(int boardId, String imageId) async { - final db = await AppDatabase.db; - - await db.insert("board_images", { - "board_id": boardId, - "image_id": imageId, - "createdAt": DateTime.now().toString() - }); - } - - Future<List<Map<String, dynamic>>> getImagesOfBoard(int boardId) async { - final db = await AppDatabase.db; - - return await db.rawQuery(''' - SELECT images.* - FROM images - JOIN board_images ON images.id = board_images.image_id - WHERE board_images.board_id = ? - ''', [boardId]); - } - - Future<void> removeImageFromBoard(int boardId, String imageId) async { - final db = await AppDatabase.db; - await db.delete( - "board_images", - where: "board_id = ? AND image_id = ?", - whereArgs: [boardId, imageId], - ); - } - - Future<List<String>> getBoardPreviewImages(int boardId) async { - final db = await AppDatabase.db; - final result = await db.rawQuery(''' - SELECT images.filePath - FROM images - JOIN board_images ON images.id = board_images.image_id - WHERE board_images.board_id = ? - ORDER BY board_images.createdAt DESC - LIMIT 4 - ''', [boardId]); - - return result.map((e) => e['filePath'] as String).toList(); - } -} diff --git a/lib/data/repos/board_repo.dart b/lib/data/repos/board_repo.dart @@ -1,35 +0,0 @@ -import 'package:adobe/data/database.dart'; - -class BoardRepository { - // Updated to accept optional categoryId - Future<int> createBoard(String name, {int? categoryId}) async { - final db = await AppDatabase.db; - return await db.insert("boards", { - "name": name, - "category_id": categoryId, // Save the category link - "createdAt": DateTime.now().toString() - }); - } - - Future<List<Map<String, dynamic>>> getBoards() async { - final db = await AppDatabase.db; - return await db.query("boards", orderBy: "createdAt DESC"); - } - - Future<void> updateBoardName(int id, String newName) async { - final db = await AppDatabase.db; - await db.update( - "boards", - {"name": newName}, - where: "id = ?", - whereArgs: [id], - ); - } - - Future<void> deleteBoard(int id) async { - final db = await AppDatabase.db; - await db.delete("boards", where: "id = ?", whereArgs: [id]); - // Remove associations (images stay in "All Images", but link to board is gone) - await db.delete("board_images", where: "board_id = ?", whereArgs: [id]); - } -} diff --git a/lib/data/repos/comment_repo.dart b/lib/data/repos/comment_repo.dart @@ -1,33 +0,0 @@ -import 'package:adobe/data/database.dart'; -import 'package:adobe/data/models/comment_model.dart'; - -class CommentRepository { - // Add a new comment to the database - Future<int> addComment(Comment comment) async { - final db = await AppDatabase.db; - return await db.insert('comments', comment.toMap()); - } - - // Retrieve all comments for a specific image - Future<List<Comment>> getCommentsForImage(String imageId) async { - final db = await AppDatabase.db; - final result = await db.query( - 'comments', - where: 'image_id = ?', - whereArgs: [imageId], - orderBy: 'createdAt DESC', // Most recent comments first - ); - - return result.map((e) => Comment.fromMap(e)).toList(); - } - - // Optional: Delete a comment - Future<int> deleteComment(int id) async { - final db = await AppDatabase.db; - return await db.delete( - 'comments', - where: 'id = ?', - whereArgs: [id], - ); - } -} -\ No newline at end of file diff --git a/lib/data/repos/file_repo.dart b/lib/data/repos/file_repo.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import '../database.dart'; +import '../models/file_model.dart'; + +class FileRepo { + Future<void> addFile(FileModel file) async { + final db = await AppDatabase.db; + await db.insert('files', file.toMap()); + } + + Future<List<FileModel>> getFiles(int projectId) async { + final db = await AppDatabase.db; + final res = await db.query( + 'files', + where: 'project_id = ?', + whereArgs: [projectId], + orderBy: 'last_updated DESC', + ); + return res.map((e) => FileModel.fromMap(e)).toList(); + } + + Future<FileModel?> getById(String id) async { + final db = await AppDatabase.db; + final res = await db.query('files', where: 'id = ?', whereArgs: [id]); + if (res.isNotEmpty) return FileModel.fromMap(res.first); + return null; + } + + Future<void> updateDetails(String id, {String? name, String? description, List<String>? tags}) async { + final db = await AppDatabase.db; + final Map<String, dynamic> updates = { + 'last_updated': DateTime.now().toIso8601String() + }; + if (name != null) updates['name'] = name; + if (description != null) updates['description'] = description; + if (tags != null) updates['tags'] = jsonEncode(tags); + + await db.update('files', updates, where: 'id = ?', whereArgs: [id]); + } + + Future<void> touchFile(String id) async { + final db = await AppDatabase.db; + await db.update( + 'files', + {'last_updated': DateTime.now().toIso8601String()}, + where: 'id = ?', + whereArgs: [id] + ); + } + + Future<void> deleteFile(String id) async { + final db = await AppDatabase.db; + await db.delete('files', where: 'id = ?', whereArgs: [id]); + } + + Future<List<String>> getAllFilePathsForProjectIds(List<int> projectIds) async { + if (projectIds.isEmpty) return []; + final db = await AppDatabase.db; + final idList = projectIds.join(','); + final res = await db.rawQuery('SELECT file_path FROM files WHERE project_id IN ($idList)'); + return res.map((e) => e['file_path'] as String).toList(); + } +} diff --git a/lib/data/repos/image_repo.dart b/lib/data/repos/image_repo.dart @@ -1,49 +1,65 @@ -// lib/data/repos/image_repo.dart +import 'dart:convert'; +import '../database.dart'; +import '../models/image_model.dart'; -import 'package:adobe/data/database.dart'; +class ImageRepo { + + Future<void> addImage(ImageModel image) async { + final db = await AppDatabase.db; + await db.insert('images', image.toMap()); + } + + Future<List<ImageModel>> getImages(int projectId) async { + final db = await AppDatabase.db; + final res = await db.query( + 'images', + where: 'project_id = ?', + whereArgs: [projectId], + orderBy: 'created_at DESC', + ); + return res.map((e) => ImageModel.fromMap(e)).toList(); + } -class ImageRepository { - Future<void> insertImage( - String id, - String filePath, { - String? analysisData, - }) async { + Future<ImageModel?> getById(String id) async { final db = await AppDatabase.db; - await db.insert("images", { - "id": id, - "filePath": filePath, - "createdAt": DateTime.now().toString(), - "analysis_data": analysisData, - }); + final res = await db.query('images', where: 'id = ?', whereArgs: [id]); + if (res.isNotEmpty) return ImageModel.fromMap(res.first); + return null; } - Future<void> updateImageAnalysis(String id, String analysisData) async { + Future<void> updateAnalysis(String id, String analysisData) async { final db = await AppDatabase.db; await db.update( - "images", - {"analysis_data": analysisData}, - where: "id = ?", - whereArgs: [id], + 'images', + {'analysis_data': analysisData}, + where: 'id = ?', + whereArgs: [id] ); } - Future<List<Map<String, dynamic>>> getAllImages() async { + Future<void> updateTags(String id, List<String> tags) async { final db = await AppDatabase.db; - return await db.query("images"); + await db.update( + 'images', + {'tags': jsonEncode(tags)}, + where: 'id = ?', + whereArgs: [id] + ); } Future<void> deleteImage(String id) async { final db = await AppDatabase.db; - // Delete from images table - await db.delete("images", where: "id = ?", whereArgs: [id]); - // Also delete any connections to boards - await db.delete("board_images", where: "image_id = ?", whereArgs: [id]); + await db.delete('images', where: 'id = ?', whereArgs: [id]); } - - Future<String?> getImagePath(String id) async { + + /// Helper for Project Deletion Service + Future<List<String>> getAllFilePathsForProjectIds(List<int> projectIds) async { + if (projectIds.isEmpty) return []; final db = await AppDatabase.db; - final res = await db.query("images", columns: ["filePath"], where: "id = ?", whereArgs: [id]); - if (res.isNotEmpty) return res.first["filePath"] as String; - return null; + final idList = projectIds.join(','); + final res = await db.rawQuery( + 'SELECT file_path FROM images WHERE project_id IN ($idList)' + ); + return res.map((e) => e['file_path'] as String).toList(); } } diff --git a/lib/data/repos/note_repo.dart b/lib/data/repos/note_repo.dart @@ -0,0 +1,38 @@ +import '../database.dart'; +import '../models/note_model.dart'; + +class NoteRepo { + final _db = AppDatabase(); + + Future<int> addNote(NoteModel note) async { + final db = await AppDatabase.db; + return await db.insert('notes', note.toMap()); + } + + Future<List<NoteModel>> getNotes(String imageId) async { + final db = await AppDatabase.db; + final res = await db.query( + 'notes', + where: 'image_id = ?', + whereArgs: [imageId], + orderBy: 'created_at DESC', + ); + return res.map((e) => NoteModel.fromMap(e)).toList(); + } + + Future<void> updateNote(int id, {String? content, String? category}) async { + final db = await AppDatabase.db; + final Map<String, dynamic> updates = {}; + if (content != null) updates['content'] = content; + if (category != null) updates['category'] = category; + + if (updates.isNotEmpty) { + await db.update('notes', updates, where: 'id = ?', whereArgs: [id]); + } + } + + Future<void> deleteNote(int id) async { + final db = await AppDatabase.db; + await db.delete('notes', where: 'id = ?', whereArgs: [id]); + } +} diff --git a/lib/data/repos/project_repo.dart b/lib/data/repos/project_repo.dart @@ -0,0 +1,66 @@ +import 'package:sqflite/sqflite.dart'; +import '../database.dart'; +import '../models/project_model.dart'; + +class ProjectRepo { + final _db = AppDatabase(); + + Future<int> createProject(ProjectModel project) async { + final db = await AppDatabase.db; + return await db.insert('projects', project.toMap()); + } + + Future<List<ProjectModel>> getRecentProjects(int lim) async { + final db = await AppDatabase.db; + final res = await db.query( + 'projects', + where: 'parent_id IS NULL', + orderBy: 'last_accessed_at DESC', + limit: lim, + ); + return res.map((e) => ProjectModel.fromMap(e)).toList(); + } + + Future<List<ProjectModel>> getEvents(int projectId) async { + final db = await AppDatabase.db; + final res = await db.query( + 'projects', + where: 'parent_id = ?', + whereArgs: [projectId], + orderBy: 'created_at DESC', + ); + return res.map((e) => ProjectModel.fromMap(e)).toList(); + } + + Future<void> updateProject(int id, {String? title, String? description}) async { + final db = await AppDatabase.db; + final Map<String, dynamic> updates = {}; + if (title != null) updates['title'] = title; + if (description != null) updates['description'] = description; + + if (updates.isNotEmpty) { + await db.update('projects', updates, where: 'id = ?', whereArgs: [id]); + } + } + + Future<void> updateStylesheet(int id, String jsonStylesheet) async { + final db = await AppDatabase.db; + await db.update('projects', {'global_stylesheet': jsonStylesheet}, where: 'id = ?', whereArgs: [id]); + } + + Future<void> touchProject(int id) async { + final db = await AppDatabase.db; + await db.update('projects', {'last_accessed_at': DateTime.now().toIso8601String()}, where: 'id = ?', whereArgs: [id]); + } + + Future<List<int>> getAllSubEventIds(int projectId) async { + final db = await AppDatabase.db; + final res = await db.query('projects', columns: ['id'], where: 'parent_id = ?', whereArgs: [projectId]); + return res.map((e) => e['id'] as int).toList(); + } + + Future<void> deleteProject(int id) async { + final db = await AppDatabase.db; + await db.delete('projects', where: 'id = ?', whereArgs: [id]); + } +} diff --git a/lib/main.dart b/lib/main.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:adobe/services/theme_service.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:adobe/ui/pages/home_page.dart'; -import 'package:adobe/ui/pages/share_handler_page.dart'; +// import 'package:adobe/ui/pages/home_page.dart'; +// import 'package:adobe/ui/pages/share_handler_page.dart'; +import 'package:adobe/ui/pages/image_analysis_page.dart'; // For Testing void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -88,11 +89,13 @@ class _MyAppState extends State<MyApp> { void _navigateToSharePage(String sharedText) { // Wait for the frame to render before pushing the route WidgetsBinding.instance.addPostFrameCallback((_) { + /* _navigatorKey.currentState?.push( MaterialPageRoute( builder: (_) => ShareHandlerPage(sharedText: sharedText), ), ); + */ }); } @@ -148,7 +151,8 @@ class _MyAppState extends State<MyApp> { ), ), - home: const HomePage(), + // home: const HomePage(), + home: const ImageAnalysisPage(), ); } } diff --git a/lib/services/analyze/color.dart b/lib/services/analyze/color.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; +import 'python_service.dart'; + +class ColorAnalyzerService { + final PythonService _pythonService = PythonService(); + + Future<Map<String, dynamic>?> analyze(String imagePath) async { + try { + debugPrint("Starting Color Style Analysis..."); + return await _pythonService.analyzeColorStyle(imagePath); + } catch (e) { + debugPrint("Color Analysis Exception: $e"); + return {'success': false, 'error': e.toString()}; + } + } +} diff --git a/lib/services/analyze/embedding.dart b/lib/services/analyze/embedding.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:onnxruntime/onnxruntime.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; + +class EmbeddingAnalyzerService { + OrtSession? _session; + List<List<double>>? _centroids; + List<String>? _classes; + + Future<void> initialize({required String modelPath, required String jsonPath}) async { + if (_session != null) return; + + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); + + // Initialize FFI Env + OrtEnv.instance.init(); + + // 1. Load Model from passed PATH (No rootBundle!) + final sessionOptions = OrtSessionOptions(); + _session = OrtSession.fromFile(File(modelPath), sessionOptions); + sessionOptions.release(); + + final jsonString = await File(jsonPath).readAsString(); + final jsonData = json.decode(jsonString); + + _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"); + } + } + + Future<Map<String, dynamic>?> analyze(String imagePath, { + String? modelPath, + String? jsonPath + }) async { + // Safety check + if (modelPath == null || jsonPath == null) return null; + await initialize(modelPath: modelPath, jsonPath: jsonPath); + if (_session == null || _centroids == null) return null; + + OrtValueTensor? inputOrt; + OrtRunOptions? runOptions; + List<OrtValue?>? outputs; + + try { + final float32Input = await ClipImageProcessor.preprocess(imagePath); + if (float32Input == null) return null; + + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = OrtValueTensor.createTensorWithDataList( + float32Input, + [1, 3, 224, 224] + ); + + runOptions = OrtRunOptions(); + + // Run Inference + // 'image' is the input name for CLIP. + outputs = _session!.run(runOptions, {"image": inputOrt}); + + if (outputs.isEmpty) throw Exception("No output from model"); + + // Get Output + // FFI returns list of outputs. Usually index 0. + final dynamic outputRaw = outputs[0]?.value; + + // Flatten Output + 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); + } + } + } + flatten(outputRaw); + + // Normalize & Compare + final normFeat = l2Normalize(imgFeat); + Map<String, double> scores = {}; + + for (int i = 0; i < _classes!.length; i++) { + double score = dotProduct(normFeat, _centroids![i]); + scores[_classes![i]] = score * 100.0; + } + + scores = Map.fromEntries( + scores.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)) + ); + + return { + "success": true, + "predictions": scores, + "best": {scores.keys.first: scores.values.first}, + }; + + } catch (e) { + debugPrint("Embeddings Analysis Error: $e"); + return null; + } finally { + inputOrt?.release(); + runOptions?.release(); + outputs?.forEach((element) => element?.release()); + } + } + + void dispose() { + _session?.release(); + } +} diff --git a/lib/services/analyze/emotional_embeddings.dart b/lib/services/analyze/emotional_embeddings.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:onnxruntime/onnxruntime.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; + +class EmotionalEmbeddingsService { + OrtSession? _session; + List<List<double>>? _centroids; + List<String>? _classes; + + Future<void> initialize({required String modelPath, required String jsonPath}) async { + if (_session != null) return; + + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); + + OrtEnv.instance.init(); + + // 1. Load Model from passed PATH (No rootBundle!) + final sessionOptions = OrtSessionOptions(); + _session = OrtSession.fromFile(File(modelPath), sessionOptions); + sessionOptions.release(); + + final jsonString = await File(jsonPath).readAsString(); + final jsonData = json.decode(jsonString); + + _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"); + } + } + + Future<Map<String, dynamic>?> analyze(String imagePath, { + String? modelPath, + String? jsonPath + }) async { + // Safety check + if (modelPath == null || jsonPath == null) return null; + await initialize(modelPath: modelPath, jsonPath: jsonPath); + if (_session == null || _centroids == null) return null; + + OrtValueTensor? inputOrt; + OrtRunOptions? runOptions; + List<OrtValue?>? outputs; + + try { + final float32Input = await ClipImageProcessor.preprocess(imagePath); + //debugPrint("[EMOTION] Preprocess length: ${float32Input?.length}"); + if (float32Input == null) return null; + + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = OrtValueTensor.createTensorWithDataList( + float32Input, [1, 3, 224, 224]); + + runOptions = OrtRunOptions(); + + // Run Inference + // 'image' is the input name for CLIP. + outputs = _session!.run(runOptions, {"image": inputOrt}); + //debugPrint("[EMOTION] Output keys: ${outputs.keys}"); + + if (outputs.isEmpty) throw Exception("No output from model"); + + // Get Output + final dynamic outputRaw = outputs[0]?.value; + + // Flatten Output + 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); + } + } + } + + flatten(outputRaw); + + // debugPrint("[EMOTION] Embedding length: ${imgFeat.length}"); + // debugPrint("[EMOTION] Centroid length: ${_centroids?[0].length}"); + + // Normalize & Compare + final normFeat = l2Normalize(imgFeat); + Map<String, dynamic> scores = {}; + + for (int i = 0; i < _classes!.length; i++) { + double score = dotProduct(normFeat, _centroids![i]); + scores[_classes![i]] = score * 100.0; + } + + scores = Map.fromEntries( + scores.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)) + ); + + return { + "success": true, + "predictions": scores, + "best": {scores.keys.first: scores.values.first}, + }; + } catch (e) { + debugPrint("Embeddings Analysis Error: $e"); + return null; + } finally { + inputOrt?.release(); + runOptions?.release(); + outputs?.forEach((element) => element?.release()); + } + } + + void dispose() { + _session?.release(); + } +} diff --git a/lib/services/analyze/era.dart b/lib/services/analyze/era.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:onnxruntime/onnxruntime.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; + +class EraEmbeddingsService { + OrtSession? _session; + List<List<double>>? _centroids; + List<String>? _classes; + + Future<void> initialize({required String modelPath, required String jsonPath}) async { + if (_session != null) return; + + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); + + OrtEnv.instance.init(); + + // 1. Load Model from passed PATH (No rootBundle!) + final sessionOptions = OrtSessionOptions(); + _session = OrtSession.fromFile(File(modelPath), sessionOptions); + sessionOptions.release(); + + final jsonStr = await File(jsonPath).readAsString(); + final jsonData = json.decode(jsonStr); + + _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"); + } + } + + Future<Map<String, dynamic>?> analyze(String imagePath, { + String? modelPath, + String? jsonPath, + }) async { + // Safety check + if (modelPath == null || jsonPath == null) return null; + await initialize(modelPath: modelPath, jsonPath: jsonPath); + if (_session == null || _centroids == null) return null; + + OrtValueTensor? inputOrt; + OrtRunOptions? runOptions; + List<OrtValue?>? outputs; + + try { + final float32Input = await ClipImageProcessor.preprocess(imagePath); + if (float32Input == null) return null; + + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = OrtValueTensor.createTensorWithDataList( + float32Input, [1, 3, 224, 224]); + + runOptions = OrtRunOptions(); + + // Run Inference + // 'image' is the input name for CLIP. + outputs = _session!.run(runOptions, {"image": inputOrt}); // <--- UPDATED + //debugPrint("[LIGHTING] Output keys: ${outputs.keys}"); + + if (outputs.isEmpty) throw Exception("No output from model"); + + // Get Output + final dynamic outputRaw = outputs[0]?.value; + + // Flatten Output + 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); + } + } + } + + flatten(outputRaw); + + // Normalize & Compare + final normFeat = l2Normalize(imgFeat); + Map<String, dynamic> scores = {}; + + for (int i = 0; i < _classes!.length; i++) { + double score = dotProduct(normFeat, _centroids![i]); + scores[_classes![i]] = score * 100.0; + } + + scores = Map.fromEntries( + scores.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)), + ); + + return { + "success": true, + "predictions": scores, + "best": {scores.keys.first: scores.values.first}, + }; + } catch (e) { + debugPrint("Embeddings Analysis Error: $e"); + return null; + } finally { + inputOrt?.release(); + runOptions?.release(); + outputs?.forEach((element) => element?.release()); + } + } + + void dispose() { + _session?.release(); + } +} diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -0,0 +1,270 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; +import 'dart:developer' as dev; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +// Import analysis services +import 'layout.dart'; +import 'color.dart'; +import 'texture.dart'; +import 'embedding.dart'; +import 'emotional_embeddings.dart'; +import 'lighting.dart'; +import 'era.dart'; + +class ImageAnalyzerService { + + /// Prepare ALL assets (Models + JSONs) on Main Thread. + /// Returns a map of absolute paths. + static Future<Map<String, String>> _prepareAssets() async { + final dir = await getApplicationDocumentsDirectory(); + + Future<String> copy(String assetName, {String? filename}) async { + final name = filename ?? assetName; + final file = File('${dir.path}/$name'); + // Only copy if it doesn't exist or is empty + if (!await file.exists() || await file.length() == 0) { + final data = await rootBundle.load('assets/$assetName'); + await file.writeAsBytes(data.buffer.asUint8List(), flush: true); + } + return file.path; + } + + // Copy everything needed by the services + // Make sure your assets/ folder actually contains these files! + final paths = await Future.wait([ + // Texture + copy('dinov2.onnx'), + copy('dinov2.onnx.data'), + copy('texture_centroids.json'), + + // CLIP (Embeddings/Emotion/Lighting) + copy('clip_image_encoder.onnx'), + copy('style_embeddings.json'), + copy('emotion_centroids.json'), + copy('lighting_centroids.json'), + copy('era_centroids.json'), + ]); + + return { + 'texture_model': paths[0], + // paths[1] is .data, implicit + 'texture_json': paths[2], + 'clip_model': paths[3], + 'embedding_json': paths[4], + 'emotion_json': paths[5], + 'lighting_json': paths[6], + 'era_json': paths[7], + }; + } + + static Future<Map<String, dynamic>> _runProfiledJob({ + required String name, + required String imagePath, + required RootIsolateToken rootToken, + required bool runInIsolate, + // Pass config map to the task + required Map<String, String> assetPaths, + required Future<dynamic> Function(String, Map<String, String>) task, + }) async { + final stopwatch = Stopwatch()..start(); + final dev.TimelineTask timelineTask = dev.TimelineTask(); + timelineTask.start('Analyze: $name'); + + try { + dynamic result; + + if (runInIsolate) { + // PARALLEL (Isolate) + result = await Isolate.run(() async { + BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken); + DartPluginRegistrant.ensureInitialized(); + return await task(imagePath, assetPaths); + }); + } else { + // CONCURRENT (Main Thread) + result = await task(imagePath, assetPaths); + } + + stopwatch.stop(); + timelineTask.finish(); + + return { + 'data': result, + 'stats': {'execution_time_ms': stopwatch.elapsedMilliseconds} + }; + } catch (e) { + stopwatch.stop(); + timelineTask.finish(); + debugPrint("[$name] Failed: $e"); + return {'error': e.toString()}; + } + } + + static Future<Map<String, dynamic>> analyzeFullSuite(String imagePath) async { + final totalSw = Stopwatch()..start(); + + final RootIsolateToken? token = RootIsolateToken.instance; + if (token == null) { + throw Exception("RootIsolateToken is null."); + } + + try { + // 1. Prepare Assets on Main Thread (Safe) + final assetPaths = await _prepareAssets(); + + // 2. Run Analysis + final results = await Future.wait([ + + // --- GROUP A: MAIN THREAD (Python) --- + _runProfiledJob( + name: 'Layout', imagePath: imagePath, rootToken: token, runInIsolate: false, + assetPaths: assetPaths, + task: (path, _) => LayoutAnalyzerService().analyze(path) + ), + + _runProfiledJob( + name: 'Color', imagePath: imagePath, rootToken: token, runInIsolate: false, + assetPaths: assetPaths, + task: (path, _) => ColorAnalyzerService().analyze(path) + ), + + // --- GROUP B: PARALLEL (ONNX FFI) --- + // Notice we pass assetPaths to the tasks + + _runProfiledJob( + name: 'Texture', imagePath: imagePath, rootToken: token, runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = TextureAnalyzerService(); + // Pass specific paths + final res = await service.analyze(path, + modelPath: assets['texture_model'], + jsonPath: assets['texture_json'] + ); + service.dispose(); + return res; + } + ), + + _runProfiledJob( + name: 'Embedding', imagePath: imagePath, rootToken: token, runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmbeddingAnalyzerService(); + final res = await service.analyze(path, + modelPath: assets['clip_model'], + jsonPath: assets['embedding_json'] + ); + service.dispose(); + return res; + } + ), + + _runProfiledJob( + name: 'Emotional', imagePath: imagePath, rootToken: token, runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmotionalEmbeddingsService(); + final res = await service.analyze(path, + modelPath: assets['clip_model'], + jsonPath: assets['emotion_json'] + ); + service.dispose(); + return res; + } + ), + + _runProfiledJob( + name: 'Lighting', imagePath: imagePath, rootToken: token, runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = LightingEmbeddingsService(); + final res = await service.analyze(path, + modelPath: assets['clip_model'], + jsonPath: assets['lighting_json'] + ); + service.dispose(); + return res; + } + ), + + _runProfiledJob( + name: 'Era', imagePath: imagePath, rootToken: token, runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EraEmbeddingsService(); + final res = await service.analyze(path, + modelPath: assets['clip_model'], + jsonPath: assets['era_json'] + ); + service.dispose(); + return res; + } + ), + ]); + + totalSw.stop(); + + final finalResult = { + 'success': true, + 'total_time_ms': totalSw.elapsedMilliseconds, + 'timestamp': DateTime.now().toIso8601String(), + 'results': { + 'layout': results[0], + 'color': results[1], + 'texture': results[2], + 'embedding': results[3], + 'emotions': results[4], + 'lighting': results[5], + 'era': results[6], + } + }; + + _logSummary(finalResult); + print("\n\n\n layout: ${results[0]} \n\n\n"); + print("color: ${results[1]} \n\n\n"); + print("texture: ${results[2]} \n\n\n"); + print("embedding: ${results[3]} \n\n\n"); + print("emotions: ${results[4]} \n\n\n"); + print("lighting: ${results[5]} \n\n\n"); + print("era: ${results[6]} \n\n\n"); + return finalResult; + + } catch (e) { + debugPrint("Master Analysis Failed: $e"); + return {'success': false, 'error': e.toString()}; + } + } + + static void _logSummary(Map<String, dynamic> result) { + final int total = result['total_time_ms']; + final Map<String, dynamic> subTasks = result['results']; + + StringBuffer output = StringBuffer(); + output.writeln('\n══════════ ⚡ HYBRID PARALLEL REPORT ⚡ ══════════'); + output.writeln(' 🕒 Total Wall Time: ${total}ms'); + output.writeln('──────────────────────────────────────────────────────'); + + subTasks.forEach((key, value) { + if (value is Map && value.containsKey('stats')) { + final int time = value['stats']['execution_time_ms']; + final int barLength = (time / 50).ceil().clamp(0, 20); + final String bar = '█' * barLength; + + output.writeln(' ${key.padRight(12)} : ${time.toString().padLeft(4)}ms $bar'); + } else { + String err = value['error'] ?? "Unknown"; + if (err.length > 30) err = "${err.substring(0, 30)}..."; + output.writeln(' ${key.padRight(12)} : FAILED ❌ ($err)'); + } + }); + output.writeln('══════════════════════════════════════════════════════\n'); + + dev.log(output.toString(), name: 'ImageAnalyzer'); + } +} diff --git a/lib/services/analyze/layout.dart b/lib/services/analyze/layout.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; +import 'python_service.dart'; + +class LayoutAnalyzerService { + final PythonService _pythonService = PythonService(); + + Future<Map<String, dynamic>?> analyze(String imagePath) async { + try { + debugPrint("Starting Layout Analysis..."); + return await _pythonService.analyzeLayout(imagePath); + } catch (e) { + debugPrint("Layout Analysis Exception: $e"); + return {'success': false, 'error': e.toString()}; + } + } +} diff --git a/lib/services/analyze/lighting.dart b/lib/services/analyze/lighting.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:onnxruntime/onnxruntime.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; + +class LightingEmbeddingsService { + OrtSession? _session; + List<List<double>>? _centroids; + List<String>? _classes; + + Future<void> initialize({required String modelPath, required String jsonPath}) async { + if (_session != null) return; + + try { + debugPrint("Initializing Embeddings (Dino/CLIP)..."); + + OrtEnv.instance.init(); + + // 1. Load Model from passed PATH (No rootBundle!) + final sessionOptions = OrtSessionOptions(); + _session = OrtSession.fromFile(File(modelPath), sessionOptions); + sessionOptions.release(); + + final jsonStr = await File(jsonPath).readAsString(); + final jsonData = json.decode(jsonStr); + + _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"); + } + } + + Future<Map<String, dynamic>?> analyze(String imagePath, { + String? modelPath, + String? jsonPath, + }) async { + // Safety check + if (modelPath == null || jsonPath == null) return null; + await initialize(modelPath: modelPath, jsonPath: jsonPath); + if (_session == null || _centroids == null) return null; + + OrtValueTensor? inputOrt; + OrtRunOptions? runOptions; + List<OrtValue?>? outputs; + + try { + final float32Input = await ClipImageProcessor.preprocess(imagePath); + if (float32Input == null) return null; + + // Create Tensor + // Note: ensure ClipImageProcessor returns a flat List<double> + inputOrt = OrtValueTensor.createTensorWithDataList( + float32Input, [1, 3, 224, 224]); + + runOptions = OrtRunOptions(); + + // Run Inference + // 'image' is the input name for CLIP. + outputs = _session!.run(runOptions, {"image": inputOrt}); // <--- UPDATED + //debugPrint("[LIGHTING] Output keys: ${outputs.keys}"); + + if (outputs.isEmpty) throw Exception("No output from model"); + + // Get Output + final dynamic outputRaw = outputs[0]?.value; + + // Flatten Output + 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); + } + } + } + + flatten(outputRaw); + + // Normalize & Compare + final normFeat = l2Normalize(imgFeat); + Map<String, dynamic> scores = {}; + + for (int i = 0; i < _classes!.length; i++) { + double score = dotProduct(normFeat, _centroids![i]); + scores[_classes![i]] = score * 100.0; + } + + scores = Map.fromEntries( + scores.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)), + ); + + return { + "success": true, + "predictions": scores, + "best": {scores.keys.first: scores.values.first}, + }; + } catch (e) { + debugPrint("Embeddings Analysis Error: $e"); + return null; + } finally { + inputOrt?.release(); + runOptions?.release(); + outputs?.forEach((element) => element?.release()); + } + } + + void dispose() { + _session?.release(); + } +} diff --git a/lib/services/python_service.dart b/lib/services/analyze/python_service.dart diff --git a/lib/services/analyze/texture.dart b/lib/services/analyze/texture.dart @@ -0,0 +1,160 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:image/image.dart' as img; +import 'package:onnxruntime/onnxruntime.dart'; +import 'package:adobe/utils/image_utils.dart'; + +class TextureAnalyzerService { + OrtSession? _session; + Map<String, List<double>>? _centroids; + + static const int INPUT_SIZE = 224; + static const int EMBED_DIM = 384; + static const int MAX_COUNT = 5; + static const double RELATIVE_THRESH = 0.85; + static const double ABSOLUTE_FLOOR = 0.15; + + Future<void> initialize({required String modelPath, required String jsonPath}) async { + if (_session != null) return; + + try { + debugPrint("Initializing Texture Service..."); + + // Initialize ORT Environment (FFI) + OrtEnv.instance.init(); + + // 1. Load Model from passed PATH + final sessionOptions = OrtSessionOptions(); + _session = OrtSession.fromFile(File(modelPath), sessionOptions); + sessionOptions.release(); + + // 2. Load JSON from passed PATH + final jsonStr = await File(jsonPath).readAsString(); + final Map<String, dynamic> jsonMap = json.decode(jsonStr); + _centroids = {}; + jsonMap.forEach((k, v) => _centroids![k] = List<double>.from(v)); + + } catch (e) { + debugPrint("Texture Service Init Error: $e"); + } + } + + // Changed return type to Map to match other services + Future<Map<String, dynamic>> analyze(String path, { + String? modelPath, + String? jsonPath + }) async { + // Safety check + if (modelPath == null || jsonPath == null) return {'success': false, 'error': 'Paths missing'}; + await initialize(modelPath: modelPath, jsonPath: jsonPath); + if (_session == null || _centroids == null) return {'success': false, 'error': 'Init failed'}; + + OrtValueTensor? inputOrt; + OrtRunOptions? runOptions; + List<OrtValue?>? outputs; + + try { + // 1. Preprocess + final bytes = await File(path).readAsBytes(); + final image = img.decodeImage(bytes); + if (image == null) return {'success': false, 'error': 'Image decode failed'}; + + final resized = img.copyResize(image, width: INPUT_SIZE, height: INPUT_SIZE); + + // 2. Normalize (ImageNet stats) + final List<double> inputFloats = List.filled(1 * 3 * INPUT_SIZE * INPUT_SIZE, 0.0); + int pixelIndex = 0; + for (int y = 0; y < INPUT_SIZE; y++) { + for (int x = 0; x < INPUT_SIZE; x++) { + final pixel = resized.getPixel(x, y); + inputFloats[pixelIndex] = ((pixel.r / 255.0) - 0.485) / 0.229; + inputFloats[pixelIndex + (INPUT_SIZE * INPUT_SIZE)] = ((pixel.g / 255.0) - 0.456) / 0.224; + inputFloats[pixelIndex + (2 * INPUT_SIZE * INPUT_SIZE)] = ((pixel.b / 255.0) - 0.406) / 0.225; + pixelIndex++; + } + } + + final float32List = Float32List.fromList(inputFloats); + + // 3. Inference + // Create Tensor using FFI method with Float32List + inputOrt = OrtValueTensor.createTensorWithDataList( + float32List, [1, 3, INPUT_SIZE, INPUT_SIZE]); + + runOptions = OrtRunOptions(); + + // Run Inference + // 'input' is the standard name, check your model using Netron if it fails + outputs = _session!.run(runOptions, {"input": inputOrt}); + + if (outputs.isEmpty) throw Exception("Output tensor missing"); + + // Get Output + // FFI returns a List<OrtValue>, usually index 0 is what we want + final dynamic outputRaw = outputs[0]?.value; + + // Flatten output + final List<double> rawOutput = []; + void flatten(dynamic data) { + if (data is num) { + rawOutput.add(data.toDouble()); + } else if (data is List) { + for (var item in data) { + flatten(item); + } + } + } + flatten(outputRaw); + + // 4. Scoring (Cosine Similarity) + Map<String, double> rawScores = {}; + _centroids!.forEach((name, centroid) { + double sumSim = 0.0; + int patches = 256; + for (int i = 0; i < patches; i++) { + int start = i * EMBED_DIM; + if (start + EMBED_DIM <= rawOutput.length) { + sumSim += cosineSim(rawOutput.sublist(start, start + EMBED_DIM), centroid); + } + } + rawScores[name] = sumSim / patches; + }); + + // 5. Filter & Sort + var sorted = rawScores.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + Map<String, dynamic> finalResults = {}; + + if (sorted.isNotEmpty) { + double topScore = sorted.first.value; + for (var entry in sorted) { + if (finalResults.length >= MAX_COUNT) break; + if (entry.value < ABSOLUTE_FLOOR) continue; + if (entry.value < (topScore * RELATIVE_THRESH)) break; + + finalResults[entry.key] = entry.value; + } + } + + // Return Map consistent with other services + return { + 'success': true, + 'predictions': finalResults, + 'best': {finalResults.keys.first: finalResults.values.first}, + }; + + } catch (e) { + debugPrint("Texture Analysis Failed: $e"); + return {'success': false, 'error': e.toString()}; + } finally { + // FFI Requires manual release of C++ resources + inputOrt?.release(); + runOptions?.release(); + outputs?.forEach((element) => element?.release()); + } + } + + void dispose() { + _session?.release(); + } +} +\ No newline at end of file diff --git a/lib/services/board_services.dart b/lib/services/board_services.dart @@ -1,67 +0,0 @@ -// lib/services/board_services.dart - -import '../data/repos/board_repo.dart'; -import '../data/repos/board_image_repo.dart'; -import '../data/repos/image_repo.dart'; -import 'dart:io'; - -class BoardService { - final _boardRepo = BoardRepository(); - final _boardImgRepo = BoardImageRepository(); - final _imgRepo = ImageRepository(); - - Future<void> renameBoard(int boardId, String newName) async { - if (newName.trim().isEmpty) return; - await _boardRepo.updateBoardName(boardId, newName.trim()); - } - - // Option A: Just delete the board (Images stay in "All Images") - Future<void> deleteBoardOnly(int boardId) async { - await _boardRepo.deleteBoard(boardId); - } - - // Option B: Delete Board AND all images inside it (Physical delete) - Future<void> deleteBoardAndContent(int boardId) async { - // 1. Get all images in this board - final images = await _boardImgRepo.getImagesOfBoard(boardId); - - // 2. Loop through and delete them physically - for (var img in images) { - final String id = img['id']; - final String path = img['filePath']; - - // Delete file - final file = File(path); - if (await file.exists()) { - await file.delete(); - } - - // Delete from DB (images table) - await _imgRepo.deleteImage(id); - } - - // 3. Finally delete the board itself - await _boardRepo.deleteBoard(boardId); - } - - Future<void> copyAllImages(int sourceBoardId, int targetBoardId) async { - // 1. Get all images from source - final images = await _boardImgRepo.getImagesOfBoard(sourceBoardId); - - // 2. Add them to target (Repo handles duplicates usually, or simple insert) - for (var img in images) { - await _boardImgRepo.saveToBoard(targetBoardId, img['id']); - } - } - - Future<void> moveAllImages(int sourceBoardId, int targetBoardId) async { - // 1. Copy first - await copyAllImages(sourceBoardId, targetBoardId); - - // 2. Remove from source - final images = await _boardImgRepo.getImagesOfBoard(sourceBoardId); - for (var img in images) { - await _boardImgRepo.removeImageFromBoard(sourceBoardId, img['id']); - } - } -} -\ No newline at end of file diff --git a/lib/services/color_analyzer_service.dart b/lib/services/color_analyzer_service.dart @@ -1,18 +0,0 @@ -// lib/services/color_analyzer_service.dart - -import 'package:flutter/foundation.dart'; -import 'python_service.dart'; - -class ColorAnalyzerService { - final PythonService _pythonService = PythonService(); - - Future<Map<String, dynamic>?> analyze(String imagePath) async { - try { - debugPrint("Starting Color Style Analysis..."); - return await _pythonService.analyzeColorStyle(imagePath); - } catch (e) { - debugPrint("Color Analysis Exception: $e"); - return {'success': false, 'error': e.toString()}; - } - } -} diff --git a/lib/services/embedding_analyzer_service.dart b/lib/services/embedding_analyzer_service.dart @@ -1,122 +0,0 @@ -// lib/services/embedding_analyzer_service.dart - -import 'dart:convert'; -import 'dart:io'; -import 'package:adobe/utils/image_utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:onnxruntime/onnxruntime.dart'; -import '../utils/clip_image_processor.dart'; - -class EmbeddingAnalyzerService { - OrtSession? _session; - List<List<double>>? _centroids; - List<String>? _classes; - - Future<void> initialize({required String modelPath, required String jsonPath}) async { - if (_session != null) return; - - try { - debugPrint("Initializing Embeddings (Dino/CLIP)..."); - - // Initialize FFI Env - OrtEnv.instance.init(); - - // 1. Load Model from passed PATH (No rootBundle!) - final sessionOptions = OrtSessionOptions(); - _session = OrtSession.fromFile(File(modelPath), sessionOptions); - sessionOptions.release(); - - final jsonString = await File(jsonPath).readAsString(); - final jsonData = json.decode(jsonString); - - _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"); - } - } - - Future<Map<String, dynamic>?> analyze(String imagePath, { - String? modelPath, - String? jsonPath - }) async { - // Safety check - if (modelPath == null || jsonPath == null) return null; - await initialize(modelPath: modelPath, jsonPath: jsonPath); - if (_session == null || _centroids == null) return null; - - OrtValueTensor? inputOrt; - OrtRunOptions? runOptions; - List<OrtValue?>? outputs; - - try { - final float32Input = await ClipImageProcessor.preprocess(imagePath); - if (float32Input == null) return null; - - // Create Tensor - // Note: ensure ClipImageProcessor returns a flat List<double> - inputOrt = OrtValueTensor.createTensorWithDataList( - float32Input, - [1, 3, 224, 224] - ); - - runOptions = OrtRunOptions(); - - // Run Inference - // 'image' is the input name for CLIP. - outputs = _session!.run(runOptions, {"image": inputOrt}); - - if (outputs.isEmpty) throw Exception("No output from model"); - - // Get Output - // FFI returns list of outputs. Usually index 0. - final dynamic outputRaw = outputs[0]?.value; - - // Flatten Output - 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); - } - } - } - flatten(outputRaw); - - // Normalize & Compare - final normFeat = l2Normalize(imgFeat); - Map<String, double> scores = {}; - - for (int i = 0; i < _classes!.length; i++) { - double score = dotProduct(normFeat, _centroids![i]); - scores[_classes![i]] = score * 100.0; - } - - scores = Map.fromEntries( - scores.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)) - ); - - return { - "success": true, - "predictions": scores, - "best": {scores.keys.first: scores.values.first}, - }; - - } catch (e) { - debugPrint("Embeddings Analysis Error: $e"); - return null; - } finally { - inputOrt?.release(); - runOptions?.release(); - outputs?.forEach((element) => element?.release()); - } - } - - void dispose() { - _session?.release(); - } -} diff --git a/lib/services/emotional_embeddings_service.dart b/lib/services/emotional_embeddings_service.dart @@ -1,126 +0,0 @@ -// lib/services/emotional_embeddings_service.dart - -import 'dart:convert'; -import 'dart:io'; -import 'package:adobe/utils/image_utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:onnxruntime/onnxruntime.dart'; -import '../utils/clip_image_processor.dart'; - -class EmotionalEmbeddingsService { - OrtSession? _session; - List<List<double>>? _centroids; - List<String>? _classes; - - Future<void> initialize({required String modelPath, required String jsonPath}) async { - if (_session != null) return; - - try { - debugPrint("Initializing Embeddings (Dino/CLIP)..."); - - OrtEnv.instance.init(); - - // 1. Load Model from passed PATH (No rootBundle!) - final sessionOptions = OrtSessionOptions(); - _session = OrtSession.fromFile(File(modelPath), sessionOptions); - sessionOptions.release(); - - final jsonString = await File(jsonPath).readAsString(); - final jsonData = json.decode(jsonString); - - _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"); - } - } - - Future<Map<String, dynamic>?> analyze(String imagePath, { - String? modelPath, - String? jsonPath - }) async { - // Safety check - if (modelPath == null || jsonPath == null) return null; - await initialize(modelPath: modelPath, jsonPath: jsonPath); - if (_session == null || _centroids == null) return null; - - OrtValueTensor? inputOrt; - OrtRunOptions? runOptions; - List<OrtValue?>? outputs; - - try { - final float32Input = await ClipImageProcessor.preprocess(imagePath); - //debugPrint("[EMOTION] Preprocess length: ${float32Input?.length}"); - if (float32Input == null) return null; - - // Create Tensor - // Note: ensure ClipImageProcessor returns a flat List<double> - inputOrt = OrtValueTensor.createTensorWithDataList( - float32Input, [1, 3, 224, 224]); - - runOptions = OrtRunOptions(); - - // Run Inference - // 'image' is the input name for CLIP. - outputs = _session!.run(runOptions, {"image": inputOrt}); - //debugPrint("[EMOTION] Output keys: ${outputs.keys}"); - - if (outputs.isEmpty) throw Exception("No output from model"); - - // Get Output - final dynamic outputRaw = outputs[0]?.value; - - // Flatten Output - 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); - } - } - } - - flatten(outputRaw); - - // debugPrint("[EMOTION] Embedding length: ${imgFeat.length}"); - // debugPrint("[EMOTION] Centroid length: ${_centroids?[0].length}"); - - // Normalize & Compare - final normFeat = l2Normalize(imgFeat); - Map<String, dynamic> scores = {}; - - for (int i = 0; i < _classes!.length; i++) { - double score = dotProduct(normFeat, _centroids![i]); - scores[_classes![i]] = score * 100.0; - } - - scores = Map.fromEntries( - scores.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)) - ); - - return { - "success": true, - "predictions": scores, - "best": {scores.keys.first: scores.values.first}, - }; - } catch (e) { - debugPrint("Embeddings Analysis Error: $e"); - return null; - } finally { - inputOrt?.release(); - runOptions?.release(); - outputs?.forEach((element) => element?.release()); - } - } - - void dispose() { - _session?.release(); - } -} diff --git a/lib/services/era_service.dart b/lib/services/era_service.dart @@ -1,122 +0,0 @@ -// lib/services/lighting_service.dart - -import 'dart:convert'; -import 'dart:io'; -import 'package:adobe/utils/image_utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:onnxruntime/onnxruntime.dart'; -import '../utils/clip_image_processor.dart'; - -class EraEmbeddingsService { - OrtSession? _session; - List<List<double>>? _centroids; - List<String>? _classes; - - Future<void> initialize({required String modelPath, required String jsonPath}) async { - if (_session != null) return; - - try { - debugPrint("Initializing Embeddings (Dino/CLIP)..."); - - OrtEnv.instance.init(); - - // 1. Load Model from passed PATH (No rootBundle!) - final sessionOptions = OrtSessionOptions(); - _session = OrtSession.fromFile(File(modelPath), sessionOptions); - sessionOptions.release(); - - final jsonStr = await File(jsonPath).readAsString(); - final jsonData = json.decode(jsonStr); - - _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"); - } - } - - Future<Map<String, dynamic>?> analyze(String imagePath, { - String? modelPath, - String? jsonPath, - }) async { - // Safety check - if (modelPath == null || jsonPath == null) return null; - await initialize(modelPath: modelPath, jsonPath: jsonPath); - if (_session == null || _centroids == null) return null; - - OrtValueTensor? inputOrt; - OrtRunOptions? runOptions; - List<OrtValue?>? outputs; - - try { - final float32Input = await ClipImageProcessor.preprocess(imagePath); - if (float32Input == null) return null; - - // Create Tensor - // Note: ensure ClipImageProcessor returns a flat List<double> - inputOrt = OrtValueTensor.createTensorWithDataList( - float32Input, [1, 3, 224, 224]); - - runOptions = OrtRunOptions(); - - // Run Inference - // 'image' is the input name for CLIP. - outputs = _session!.run(runOptions, {"image": inputOrt}); // <--- UPDATED - //debugPrint("[LIGHTING] Output keys: ${outputs.keys}"); - - if (outputs.isEmpty) throw Exception("No output from model"); - - // Get Output - final dynamic outputRaw = outputs[0]?.value; - - // Flatten Output - 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); - } - } - } - - flatten(outputRaw); - - // Normalize & Compare - final normFeat = l2Normalize(imgFeat); - Map<String, dynamic> scores = {}; - - for (int i = 0; i < _classes!.length; i++) { - double score = dotProduct(normFeat, _centroids![i]); - scores[_classes![i]] = score * 100.0; - } - - scores = Map.fromEntries( - scores.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)), - ); - - return { - "success": true, - "predictions": scores, - "best": {scores.keys.first: scores.values.first}, - }; - } catch (e) { - debugPrint("Embeddings Analysis Error: $e"); - return null; - } finally { - inputOrt?.release(); - runOptions?.release(); - outputs?.forEach((element) => element?.release()); - } - } - - void dispose() { - _session?.release(); - } -} diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart @@ -0,0 +1,51 @@ +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; +import '../data/repos/file_repo.dart'; +import '../data/models/file_model.dart'; + +class FileService { + final _repo = FileRepo(); + + Future<String> saveFile(File file, int projectId, {String? description, String? name}) async { + final dir = await getApplicationDocumentsDirectory(); + final folder = Directory("${dir.path}/files"); + if (!await folder.exists()) await folder.create(recursive: true); + + final id = const Uuid().v4(); + String ext = file.path.split('.').last; + final newPath = "${folder.path}/$id.$ext"; + + await file.copy(newPath); + + final projectFile = FileModel( + id: id, + projectId: projectId, + filePath: newPath, + name: name ?? "Untitled File", + description: description, + lastUpdated: DateTime.now(), + createdAt: DateTime.now(), + ); + + await _repo.addFile(projectFile); + return id; + } + + Future<void> openFile(String id) async { + await _repo.touchFile(id); + } + + Future<void> updateFileDetails(String id, {String? name, String? description, List<String>? tags}) async { + await _repo.updateDetails(id, name: name, description: description, tags: tags); + } + + Future<void> deleteFile(String id) async { + final fileData = await _repo.getById(id); + if (fileData != null) { + final file = File(fileData.filePath); + if (await file.exists()) await file.delete(); + await _repo.deleteFile(id); + } + } +} diff --git a/lib/services/image_analyzer_service.dart b/lib/services/image_analyzer_service.dart @@ -1,273 +0,0 @@ -// lib/services/image_analyzer_service.dart - -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui'; -import 'dart:developer' as dev; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:path_provider/path_provider.dart'; - -// Import analysis services -import 'layout_analyzer_service.dart'; -import 'color_analyzer_service.dart'; -import 'texture_analyzer_service.dart'; -import 'embedding_analyzer_service.dart'; -import 'emotional_embeddings_service.dart'; -import 'lighting_service.dart'; -import 'era_service.dart'; - -class ImageAnalyzerService { - - /// Prepare ALL assets (Models + JSONs) on Main Thread. - /// Returns a map of absolute paths. - static Future<Map<String, String>> _prepareAssets() async { - final dir = await getApplicationDocumentsDirectory(); - - Future<String> copy(String assetName, {String? filename}) async { - final name = filename ?? assetName; - final file = File('${dir.path}/$name'); - // Only copy if it doesn't exist or is empty - if (!await file.exists() || await file.length() == 0) { - final data = await rootBundle.load('assets/$assetName'); - await file.writeAsBytes(data.buffer.asUint8List(), flush: true); - } - return file.path; - } - - // Copy everything needed by the services - // Make sure your assets/ folder actually contains these files! - final paths = await Future.wait([ - // Texture - copy('dinov2.onnx'), - copy('dinov2.onnx.data'), - copy('texture_centroids.json'), - - // CLIP (Embeddings/Emotion/Lighting) - copy('clip_image_encoder.onnx'), - copy('style_embeddings.json'), - copy('emotion_centroids.json'), - copy('lighting_centroids.json'), - copy('era_centroids.json'), - ]); - - return { - 'texture_model': paths[0], - // paths[1] is .data, implicit - 'texture_json': paths[2], - 'clip_model': paths[3], - 'embedding_json': paths[4], - 'emotion_json': paths[5], - 'lighting_json': paths[6], - 'era_json': paths[7], - }; - } - - static Future<Map<String, dynamic>> _runProfiledJob({ - required String name, - required String imagePath, - required RootIsolateToken rootToken, - required bool runInIsolate, - // Pass config map to the task - required Map<String, String> assetPaths, - required Future<dynamic> Function(String, Map<String, String>) task, - }) async { - final stopwatch = Stopwatch()..start(); - final dev.TimelineTask timelineTask = dev.TimelineTask(); - timelineTask.start('Analyze: $name'); - - try { - dynamic result; - - if (runInIsolate) { - // PARALLEL (Isolate) - result = await Isolate.run(() async { - BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken); - DartPluginRegistrant.ensureInitialized(); - return await task(imagePath, assetPaths); - }); - } else { - // CONCURRENT (Main Thread) - result = await task(imagePath, assetPaths); - } - - stopwatch.stop(); - timelineTask.finish(); - - return { - 'data': result, - 'stats': {'execution_time_ms': stopwatch.elapsedMilliseconds} - }; - } catch (e) { - stopwatch.stop(); - timelineTask.finish(); - debugPrint("[$name] Failed: $e"); - return {'error': e.toString()}; - } - } - - static Future<Map<String, dynamic>> analyzeFullSuite(String imagePath) async { - final totalSw = Stopwatch()..start(); - - final RootIsolateToken? token = RootIsolateToken.instance; - if (token == null) { - throw Exception("RootIsolateToken is null."); - } - - try { - // 1. Prepare Assets on Main Thread (Safe) - final assetPaths = await _prepareAssets(); - - // 2. Run Analysis - final results = await Future.wait([ - - // --- GROUP A: MAIN THREAD (Python) --- - _runProfiledJob( - name: 'Layout', imagePath: imagePath, rootToken: token, runInIsolate: false, - assetPaths: assetPaths, - task: (path, _) => LayoutAnalyzerService().analyze(path) - ), - - _runProfiledJob( - name: 'Color', imagePath: imagePath, rootToken: token, runInIsolate: false, - assetPaths: assetPaths, - task: (path, _) => ColorAnalyzerService().analyze(path) - ), - - // --- GROUP B: PARALLEL (ONNX FFI) --- - // Notice we pass assetPaths to the tasks - - _runProfiledJob( - name: 'Texture', imagePath: imagePath, rootToken: token, runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = TextureAnalyzerService(); - // Pass specific paths - final res = await service.analyze(path, - modelPath: assets['texture_model'], - jsonPath: assets['texture_json'] - ); - service.dispose(); - return res; - } - ), - - _runProfiledJob( - name: 'Embedding', imagePath: imagePath, rootToken: token, runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmbeddingAnalyzerService(); - final res = await service.analyze(path, - modelPath: assets['clip_model'], - jsonPath: assets['embedding_json'] - ); - service.dispose(); - return res; - } - ), - - _runProfiledJob( - name: 'Emotional', imagePath: imagePath, rootToken: token, runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmotionalEmbeddingsService(); - final res = await service.analyze(path, - modelPath: assets['clip_model'], - jsonPath: assets['emotion_json'] - ); - service.dispose(); - return res; - } - ), - - _runProfiledJob( - name: 'Lighting', imagePath: imagePath, rootToken: token, runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = LightingEmbeddingsService(); - final res = await service.analyze(path, - modelPath: assets['clip_model'], - jsonPath: assets['lighting_json'] - ); - service.dispose(); - return res; - } - ), - - _runProfiledJob( - name: 'Era', imagePath: imagePath, rootToken: token, runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EraEmbeddingsService(); - final res = await service.analyze(path, - modelPath: assets['clip_model'], - jsonPath: assets['era_json'] - ); - service.dispose(); - return res; - } - ), - ]); - - totalSw.stop(); - - final finalResult = { - 'success': true, - 'total_time_ms': totalSw.elapsedMilliseconds, - 'timestamp': DateTime.now().toIso8601String(), - 'results': { - 'layout': results[0], - 'color': results[1], - 'texture': results[2], - 'embedding': results[3], - 'emotions': results[4], - 'lighting': results[5], - 'era': results[6], - } - }; - - _logSummary(finalResult); - print("\n\n\n layout: ${results[0]} \n\n\n"); - print("color: ${results[1]} \n\n\n"); - print("texture: ${results[2]} \n\n\n"); - print("embedding: ${results[3]} \n\n\n"); - print("emotions: ${results[4]} \n\n\n"); - print("lighting: ${results[5]} \n\n\n"); - print("era: ${results[6]} \n\n\n"); - return finalResult; - - } catch (e) { - debugPrint("Master Analysis Failed: $e"); - return {'success': false, 'error': e.toString()}; - } - } - - static void _logSummary(Map<String, dynamic> result) { - // ... (Keep your existing log summary code) ... - final int total = result['total_time_ms']; - final Map<String, dynamic> subTasks = result['results']; - - StringBuffer output = StringBuffer(); - output.writeln('\n══════════ ⚡ HYBRID PARALLEL REPORT ⚡ ══════════'); - output.writeln(' 🕒 Total Wall Time: ${total}ms'); - output.writeln('──────────────────────────────────────────────────────'); - - subTasks.forEach((key, value) { - if (value is Map && value.containsKey('stats')) { - final int time = value['stats']['execution_time_ms']; - final int barLength = (time / 50).ceil().clamp(0, 20); - final String bar = '█' * barLength; - - output.writeln(' ${key.padRight(12)} : ${time.toString().padLeft(4)}ms $bar'); - } else { - String err = value['error'] ?? "Unknown"; - if (err.length > 30) err = "${err.substring(0, 30)}..."; - output.writeln(' ${key.padRight(12)} : FAILED ❌ ($err)'); - } - }); - output.writeln('══════════════════════════════════════════════════════\n'); - - dev.log(output.toString(), name: 'ImageAnalyzer'); - } -} diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart @@ -1,68 +1,51 @@ -// lib/services/image_service.dart - +import 'dart:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; import '../data/repos/image_repo.dart'; -import '../data/repos/board_image_repo.dart'; +import '../data/models/image_model.dart'; class ImageService { - final _imgRepo = ImageRepository(); - final _boardImgRepo = BoardImageRepository(); + final _repo = ImageRepo(); - Future<String> saveImage(File file) async { + Future<String> saveImage(File file, int projectId) async { final dir = await getApplicationDocumentsDirectory(); final folder = Directory("${dir.path}/images"); - - if (!await folder.exists()) { - await folder.create(recursive: true); - } + if (!await folder.exists()) await folder.create(recursive: true); final id = const Uuid().v4(); - // Handle cases where file might not have an extension - String ext = 'jpg'; - if (file.path.contains('.')) { - ext = file.path.split('.').last; - } - + String ext = file.path.split('.').last; final newPath = "${folder.path}/$id.$ext"; - final savedFile = await file.copy(newPath); - - await _imgRepo.insertImage(id, savedFile.path); - + + await file.copy(newPath); + + final image = ImageModel( + id: id, + projectId: projectId, + filePath: newPath, + name: file.path.split('/').last, + createdAt: DateTime.now(), + ); + + await _repo.addImage(image); return id; } - // 1. DELETE IMAGE (Completely removes file and DB entries) - Future<void> deleteImagePermanently(String imageId) async { - // A. Get path to delete physical file - final path = await _imgRepo.getImagePath(imageId); - if (path != null) { - final file = File(path); - if (await file.exists()) { - await file.delete(); - } - } - // B. Delete from DB (Repo handles cascade to board_images) - await _imgRepo.deleteImage(imageId); + Future<void> updateAnalysis(String id, Map<String, dynamic> analysis) async { + final jsonString = jsonEncode(analysis); + await _repo.updateAnalysis(id, jsonString); } - // 2. COPY TO BOARD (Keep in old board, add to new board) - Future<void> copyImageToBoard(String imageId, int targetBoardId) async { - // Just add a new entry in the mapping table - await _boardImgRepo.saveToBoard(targetBoardId, imageId); + Future<void> updateTags(String id, List<String> tags) async { + await _repo.updateTags(id, tags); } - // 3. MOVE TO BOARD (Remove from old board, add to new board) - Future<void> moveImage(String imageId, int currentBoardId, int targetBoardId) async { - // A. Add to new - await _boardImgRepo.saveToBoard(targetBoardId, imageId); - // B. Remove from old - await _boardImgRepo.removeImageFromBoard(currentBoardId, imageId); - } - - // 4. REMOVE FROM BOARD (But keep file in "All Images") - Future<void> removeImageFromSpecificBoard(String imageId, int boardId) async { - await _boardImgRepo.removeImageFromBoard(boardId, imageId); + Future<void> deleteImage(String id) async { + final img = await _repo.getById(id); + if (img != null) { + final file = File(img.filePath); + if (await file.exists()) await file.delete(); + await _repo.deleteImage(id); + } } -} -\ No newline at end of file +} diff --git a/lib/services/layout_analyzer_service.dart b/lib/services/layout_analyzer_service.dart @@ -1,18 +0,0 @@ -// lib/services/layout_analyzer_service.dart - -import 'package:flutter/foundation.dart'; -import 'python_service.dart'; - -class LayoutAnalyzerService { - final PythonService _pythonService = PythonService(); - - Future<Map<String, dynamic>?> analyze(String imagePath) async { - try { - debugPrint("Starting Layout Analysis..."); - return await _pythonService.analyzeLayout(imagePath); - } catch (e) { - debugPrint("Layout Analysis Exception: $e"); - return {'success': false, 'error': e.toString()}; - } - } -} diff --git a/lib/services/lighting_service.dart b/lib/services/lighting_service.dart @@ -1,122 +0,0 @@ -// lib/services/lighting_service.dart - -import 'dart:convert'; -import 'dart:io'; -import 'package:adobe/utils/image_utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:onnxruntime/onnxruntime.dart'; -import '../utils/clip_image_processor.dart'; - -class LightingEmbeddingsService { - OrtSession? _session; - List<List<double>>? _centroids; - List<String>? _classes; - - Future<void> initialize({required String modelPath, required String jsonPath}) async { - if (_session != null) return; - - try { - debugPrint("Initializing Embeddings (Dino/CLIP)..."); - - OrtEnv.instance.init(); - - // 1. Load Model from passed PATH (No rootBundle!) - final sessionOptions = OrtSessionOptions(); - _session = OrtSession.fromFile(File(modelPath), sessionOptions); - sessionOptions.release(); - - final jsonStr = await File(jsonPath).readAsString(); - final jsonData = json.decode(jsonStr); - - _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"); - } - } - - Future<Map<String, dynamic>?> analyze(String imagePath, { - String? modelPath, - String? jsonPath, - }) async { - // Safety check - if (modelPath == null || jsonPath == null) return null; - await initialize(modelPath: modelPath, jsonPath: jsonPath); - if (_session == null || _centroids == null) return null; - - OrtValueTensor? inputOrt; - OrtRunOptions? runOptions; - List<OrtValue?>? outputs; - - try { - final float32Input = await ClipImageProcessor.preprocess(imagePath); - if (float32Input == null) return null; - - // Create Tensor - // Note: ensure ClipImageProcessor returns a flat List<double> - inputOrt = OrtValueTensor.createTensorWithDataList( - float32Input, [1, 3, 224, 224]); - - runOptions = OrtRunOptions(); - - // Run Inference - // 'image' is the input name for CLIP. - outputs = _session!.run(runOptions, {"image": inputOrt}); // <--- UPDATED - //debugPrint("[LIGHTING] Output keys: ${outputs.keys}"); - - if (outputs.isEmpty) throw Exception("No output from model"); - - // Get Output - final dynamic outputRaw = outputs[0]?.value; - - // Flatten Output - 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); - } - } - } - - flatten(outputRaw); - - // Normalize & Compare - final normFeat = l2Normalize(imgFeat); - Map<String, dynamic> scores = {}; - - for (int i = 0; i < _classes!.length; i++) { - double score = dotProduct(normFeat, _centroids![i]); - scores[_classes![i]] = score * 100.0; - } - - scores = Map.fromEntries( - scores.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)), - ); - - return { - "success": true, - "predictions": scores, - "best": {scores.keys.first: scores.values.first}, - }; - } catch (e) { - debugPrint("Embeddings Analysis Error: $e"); - return null; - } finally { - inputOrt?.release(); - runOptions?.release(); - outputs?.forEach((element) => element?.release()); - } - } - - void dispose() { - _session?.release(); - } -} diff --git a/lib/services/note_service.dart b/lib/services/note_service.dart @@ -0,0 +1,24 @@ +import '../data/repos/note_repo.dart'; +import '../data/models/note_model.dart'; + +class NoteService { + final _repo = NoteRepo(); + + Future<void> addNote(String imageId, String content, String category) async { + final note = NoteModel( + imageId: imageId, + content: content, + category: category, + createdAt: DateTime.now(), + ); + await _repo.addNote(note); + } + + Future<void> updateNote(int noteId, {String? content, String? category}) async { + await _repo.updateNote(noteId, content: content, category: category); + } + + Future<void> deleteNote(int noteId) async { + await _repo.deleteNote(noteId); + } +} diff --git a/lib/services/project_service.dart b/lib/services/project_service.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:io'; +import '../data/repos/project_repo.dart'; +import '../data/repos/image_repo.dart'; +import '../data/repos/file_repo.dart'; +import '../data/models/project_model.dart'; + +class ProjectService { + final _projectRepo = ProjectRepo(); + final _imageRepo = ImageRepo(); + final _fileRepo = FileRepo(); + + Future<int> createProject(String title, {String? description, int? parentId}) async { + final project = ProjectModel( + title: title.trim(), + description: description, + parentId: parentId, + lastAccessedAt: DateTime.now(), + createdAt: DateTime.now(), + ); + return await _projectRepo.createProject(project); + } + + Future<void> updateProjectDetails(int projectId, {String? title, String? description}) async { + if (title != null && title.trim().isEmpty) return; // Prevent empty titles + await _projectRepo.updateProject(projectId, title: title?.trim(), description: description?.trim()); + } + + Future<void> openProject(int id) async { + await _projectRepo.touchProject(id); + } + + Future<void> saveStylesheet(int projectId, Map<String, dynamic> stylesheet) async { + final jsonString = jsonEncode(stylesheet); + await _projectRepo.updateStylesheet(projectId, jsonString); + } + + Future<void> deleteProject(int projectId) async { + List<int> allIdsToDelete = [projectId]; + final subEventIds = await _projectRepo.getAllSubEventIds(projectId); + allIdsToDelete.addAll(subEventIds); + + List<String> pathsToDelete = []; + pathsToDelete.addAll(await _imageRepo.getAllFilePathsForProjectIds(allIdsToDelete)); + pathsToDelete.addAll(await _fileRepo.getAllFilePathsForProjectIds(allIdsToDelete)); + + for (var path in pathsToDelete) { + try { + final file = File(path); + if (await file.exists()) await file.delete(); + } catch (e) { + print("Error deleting file $path: $e"); + } + } + + await _projectRepo.deleteProject(projectId); + } +} diff --git a/lib/services/texture_analyzer_service.dart b/lib/services/texture_analyzer_service.dart @@ -1,162 +0,0 @@ -// lib/services/texture_analyzer_service.dart - -import 'dart:io'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:image/image.dart' as img; -import 'package:onnxruntime/onnxruntime.dart'; -import '../utils/image_utils.dart'; - -class TextureAnalyzerService { - OrtSession? _session; - Map<String, List<double>>? _centroids; - - static const int INPUT_SIZE = 224; - static const int EMBED_DIM = 384; - static const int MAX_COUNT = 5; - static const double RELATIVE_THRESH = 0.85; - static const double ABSOLUTE_FLOOR = 0.15; - - Future<void> initialize({required String modelPath, required String jsonPath}) async { - if (_session != null) return; - - try { - debugPrint("Initializing Texture Service..."); - - // Initialize ORT Environment (FFI) - OrtEnv.instance.init(); - - // 1. Load Model from passed PATH - final sessionOptions = OrtSessionOptions(); - _session = OrtSession.fromFile(File(modelPath), sessionOptions); - sessionOptions.release(); - - // 2. Load JSON from passed PATH - final jsonStr = await File(jsonPath).readAsString(); - final Map<String, dynamic> jsonMap = json.decode(jsonStr); - _centroids = {}; - jsonMap.forEach((k, v) => _centroids![k] = List<double>.from(v)); - - } catch (e) { - debugPrint("Texture Service Init Error: $e"); - } - } - - // Changed return type to Map to match other services - Future<Map<String, dynamic>> analyze(String path, { - String? modelPath, - String? jsonPath - }) async { - // Safety check - if (modelPath == null || jsonPath == null) return {'success': false, 'error': 'Paths missing'}; - await initialize(modelPath: modelPath, jsonPath: jsonPath); - if (_session == null || _centroids == null) return {'success': false, 'error': 'Init failed'}; - - OrtValueTensor? inputOrt; - OrtRunOptions? runOptions; - List<OrtValue?>? outputs; - - try { - // 1. Preprocess - final bytes = await File(path).readAsBytes(); - final image = img.decodeImage(bytes); - if (image == null) return {'success': false, 'error': 'Image decode failed'}; - - final resized = img.copyResize(image, width: INPUT_SIZE, height: INPUT_SIZE); - - // 2. Normalize (ImageNet stats) - final List<double> inputFloats = List.filled(1 * 3 * INPUT_SIZE * INPUT_SIZE, 0.0); - int pixelIndex = 0; - for (int y = 0; y < INPUT_SIZE; y++) { - for (int x = 0; x < INPUT_SIZE; x++) { - final pixel = resized.getPixel(x, y); - inputFloats[pixelIndex] = ((pixel.r / 255.0) - 0.485) / 0.229; - inputFloats[pixelIndex + (INPUT_SIZE * INPUT_SIZE)] = ((pixel.g / 255.0) - 0.456) / 0.224; - inputFloats[pixelIndex + (2 * INPUT_SIZE * INPUT_SIZE)] = ((pixel.b / 255.0) - 0.406) / 0.225; - pixelIndex++; - } - } - - final float32List = Float32List.fromList(inputFloats); - - // 3. Inference - // Create Tensor using FFI method with Float32List - inputOrt = OrtValueTensor.createTensorWithDataList( - float32List, [1, 3, INPUT_SIZE, INPUT_SIZE]); - - runOptions = OrtRunOptions(); - - // Run Inference - // 'input' is the standard name, check your model using Netron if it fails - outputs = _session!.run(runOptions, {"input": inputOrt}); - - if (outputs.isEmpty) throw Exception("Output tensor missing"); - - // Get Output - // FFI returns a List<OrtValue>, usually index 0 is what we want - final dynamic outputRaw = outputs[0]?.value; - - // Flatten output - final List<double> rawOutput = []; - void flatten(dynamic data) { - if (data is num) { - rawOutput.add(data.toDouble()); - } else if (data is List) { - for (var item in data) { - flatten(item); - } - } - } - flatten(outputRaw); - - // 4. Scoring (Cosine Similarity) - Map<String, double> rawScores = {}; - _centroids!.forEach((name, centroid) { - double sumSim = 0.0; - int patches = 256; - for (int i = 0; i < patches; i++) { - int start = i * EMBED_DIM; - if (start + EMBED_DIM <= rawOutput.length) { - sumSim += cosineSim(rawOutput.sublist(start, start + EMBED_DIM), centroid); - } - } - rawScores[name] = sumSim / patches; - }); - - // 5. Filter & Sort - var sorted = rawScores.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); - Map<String, dynamic> finalResults = {}; - - if (sorted.isNotEmpty) { - double topScore = sorted.first.value; - for (var entry in sorted) { - if (finalResults.length >= MAX_COUNT) break; - if (entry.value < ABSOLUTE_FLOOR) continue; - if (entry.value < (topScore * RELATIVE_THRESH)) break; - - finalResults[entry.key] = entry.value; - } - } - - // Return Map consistent with other services - return { - 'success': true, - 'predictions': finalResults, - 'best': {finalResults.keys.first: finalResults.values.first}, - }; - - } catch (e) { - debugPrint("Texture Analysis Failed: $e"); - return {'success': false, 'error': e.toString()}; - } finally { - // FFI Requires manual release of C++ resources - inputOrt?.release(); - runOptions?.release(); - outputs?.forEach((element) => element?.release()); - } - } - - void dispose() { - _session?.release(); - } -} -\ No newline at end of file diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -1,11 +1,10 @@ -import 'dart:io'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:adobe/services/image_analyzer_service.dart'; -import 'package:adobe/data/models/image_model.dart'; -import '../widgets/analysis_dialog.dart'; + +import 'package:adobe/services/analyze/image_analyzer.dart'; class ImageAnalysisPage extends StatefulWidget { const ImageAnalysisPage({super.key}); @@ -16,8 +15,8 @@ class ImageAnalysisPage extends StatefulWidget { class _ImageAnalysisPageState extends State<ImageAnalysisPage> { File? _selectedImage; - Map<String, dynamic>? _fullResult; bool _isAnalyzing = false; + Map<String, dynamic>? _analysisResult; String? _errorMessage; Future<void> _pickImage(ImageSource source) async { @@ -27,120 +26,193 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { if (image != null) { setState(() { _selectedImage = File(image.path); - _fullResult = null; + _analysisResult = null; _errorMessage = null; }); + // Auto-run analysis when a new image is picked + _runAnalysis(image.path); } } catch (e) { - setState(() => _errorMessage = 'Error: $e'); + setState(() => _errorMessage = 'Error picking image: $e'); } } - Future<void> _runAnalysis() async { - if (_selectedImage == null) return; - + Future<void> _runAnalysis(String sourcePath) async { setState(() { _isAnalyzing = true; _errorMessage = null; }); try { + // 1. Copy image to app doc dir (simulating real app behavior & safe access) final appDir = await getApplicationDocumentsDirectory(); - final fileName = _selectedImage!.path.split('/').last; + final fileName = sourcePath.split('/').last; final targetPath = '${appDir.path}/$fileName'; - await _selectedImage!.copy(targetPath); + + // Copying ensures the analyzer service can access it freely + final file = File(sourcePath); + await file.copy(targetPath); - // Use the centralized master runner + // 2. Run the Full Suite Analysis final result = await ImageAnalyzerService.analyzeFullSuite(targetPath); if (mounted) { setState(() { + _analysisResult = result; _isAnalyzing = false; - _fullResult = result; }); } } catch (e) { - setState(() { - _isAnalyzing = false; - _errorMessage = 'Analysis Error: $e'; - }); + if (mounted) { + setState(() { + _errorMessage = e.toString(); + _isAnalyzing = false; + }); + } } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Image Analysis')), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - height: 250, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(12), - image: _selectedImage != null - ? DecorationImage(image: FileImage(_selectedImage!), fit: BoxFit.cover) - : null - ), - child: _selectedImage == null - ? const Center(child: Icon(Icons.image, size: 50, color: Colors.grey)) - : null, + appBar: AppBar(title: const Text("Image Analysis Debug")), + body: Column( + children: [ + // --- Top Section: Image Preview & Controls --- + Container( + height: 220, + width: double.infinity, + color: Colors.grey[200], // Neutral grey background for image area + child: Stack( + fit: StackFit.expand, + children: [ + if (_selectedImage != null) + Image.file(_selectedImage!, fit: BoxFit.contain) + else + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.image_search, size: 48, color: Colors.grey), + const SizedBox(height: 8), + Text("No image selected", style: TextStyle(color: Colors.grey[600])), + ], + ), + + // Loading Overlay + if (_isAnalyzing) + Container( + color: Colors.black54, + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ), + + // Control Buttons (Bottom Right) + Positioned( + bottom: 16, + right: 16, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton.small( + heroTag: "cam", + onPressed: () => _pickImage(ImageSource.camera), + child: const Icon(Icons.camera_alt), + ), + const SizedBox(width: 8), + FloatingActionButton.small( + heroTag: "gal", + onPressed: () => _pickImage(ImageSource.gallery), + child: const Icon(Icons.photo_library), + ), + ], + ), + ), + ], ), - const SizedBox(height: 16), - - Row(children: [ - Expanded(child: OutlinedButton.icon( - onPressed: () => _pickImage(ImageSource.gallery), - icon: const Icon(Icons.photo), label: const Text("Gallery")) - ), - const SizedBox(width: 10), - Expanded(child: OutlinedButton.icon( - onPressed: () => _pickImage(ImageSource.camera), - icon: const Icon(Icons.camera_alt), label: const Text("Camera")) - ), - ]), - const SizedBox(height: 16), - - ElevatedButton( - onPressed: (_selectedImage != null && !_isAnalyzing) ? _runAnalysis : null, - style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16)), - child: _isAnalyzing - ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) - : const Text("Analyze Image"), + ), + + // --- Bottom Section: Results or Error --- + Expanded( + child: Container( + width: double.infinity, + color: Colors.grey[100], // Light background for the scroll area + child: _buildContent(), ), - - if (_errorMessage != null) - Padding( - padding: const EdgeInsets.only(top:16), - child: Text(_errorMessage!, style: const TextStyle(color: Colors.red)), - ), + ), + ], + ), + ); + } - const SizedBox(height: 24), - - // Re-use the AnalysisDialog logic by mocking an ImageModel - if (_fullResult != null) - // We wrap the result in a dummy ImageModel to reuse the visualization widget - LayoutBuilder( - builder: (context, constraints) { - // Mocking the data format stored in DB - final mockImage = ImageModel( - id: 'temp', - filePath: _selectedImage!.path, - analysisData: json.encode(_fullResult) - ); - - // Display the content of AnalysisDialog inline - return SizedBox( - height: 600, - child: AnalysisDialog(image: mockImage), - ); - }, - ) - ], + Widget _buildContent() { + if (_errorMessage != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + "❌ Error:\n$_errorMessage", + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), ), + ); + } + + if (_analysisResult == null) { + return const Center( + child: Text("Upload an image to view raw analysis data."), + ); + } + + // Pretty Print JSON + const encoder = JsonEncoder.withIndent(' '); + final String prettyJson = encoder.convert(_analysisResult); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "RAW JSON OUTPUT", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey, + letterSpacing: 1.0, + ), + ), + const SizedBox(height: 8), + + // --- JSON VIEWER CONTAINER --- + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, // FORCE WHITE BACKGROUND + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: SelectableText( + prettyJson, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Colors.black87, // FORCE DARK TEXT + height: 1.3, + ), + ), + ), + const SizedBox(height: 20), + ], ), ); } diff --git a/lib/ui/widgets/analysis_dialog.dart b/lib/ui/widgets/analysis_dialog.dart @@ -1,270 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/material.dart'; -import '../../data/models/image_model.dart'; - -class AnalysisDialog extends StatelessWidget { - final ImageModel image; - - const AnalysisDialog({super.key, required this.image}); - - @override - Widget build(BuildContext context) { - if (image.analysisData == null) { - return const AlertDialog(content: Text("No analysis data available.")); - } - - Map<String, dynamic> data; - try { - data = json.decode(image.analysisData!); - } catch (e) { - return const AlertDialog(content: Text("Corrupt analysis data.")); - } - - return Dialog( - insetPadding: const EdgeInsets.all(16), - child: Container( - constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700), - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Image Intelligence", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), - IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), - ], - ), - const Divider(), - Expanded( - child: ListView( - children: [ - // 1. Layout - if (data['layout'] != null && data['layout']['top5'] != null) - _buildSection( - "Composition", - Icons.grid_goldenratio, - Colors.blue, - _buildList(data['layout']['top5']), - ), - - // 2. Texture - if (data['texture'] != null && (data['texture'] as List).isNotEmpty) - _buildSection( - "Texture", - Icons.texture, - Colors.orange, - Wrap( - spacing: 8, - children: (data['texture'] as List).map((t) => Chip( - label: Text("${t['name']} ${(t['score']*100).toInt()}%"), - backgroundColor: Colors.orange.shade50, - avatar: const CircleAvatar(radius: 8, backgroundColor: Colors.orange), - )).toList(), - ), - ), - - // 3. Color - if (data['color'] != null) - _buildSection( - "Color Style", - Icons.palette, - Colors.purple, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - (data['color']['top_label'] ?? 'Unknown').toString().toUpperCase(), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.purple), - ), - LinearProgressIndicator( - value: (data['color']['top_score'] as num?)?.toDouble() ?? 0.0, - color: Colors.purple, - backgroundColor: Colors.purple.shade50, - ), - ], - ), - ), - - // 4. Embeddings - if (data['embedding'] != null) - _buildSection( - "AI Style", - Icons.auto_awesome, - Colors.indigo, - Wrap( - spacing: 6, - children: (data['embedding']['top5'] as List? ?? []).map<Widget>((e) => Chip( - label: Text(e['name'], style: const TextStyle(fontSize: 10)), - visualDensity: VisualDensity.compact, - )).toList(), - ), - ), - - - // 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 - 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(), - ), - ], - ), - ), - - // 7. Era Embeddings - if (data['era'] != null) - _buildSection( - "Era Style", - Icons.history_edu, - Colors.blue, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data['era']['label'] ?? "Unknown", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Colors.blue, - ), - ), - const SizedBox(height: 6), - - Wrap( - spacing: 6, - children: - (data['era']['top5'] as List? ?? []) - .map<Widget>( - (e) => Chip( - label: Text( - e['name'], - style: const TextStyle(fontSize: 10), - ), - visualDensity: VisualDensity.compact, - ), - ) - .toList(), - ), - ], - ), - ), - - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildSection(String title, IconData icon, Color color, Widget content) { - return Card( - margin: const EdgeInsets.only(bottom: 16), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: color.withValues(alpha: 0.2)) - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Icon(icon, color: color, size: 20), - const SizedBox(width: 8), - Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: color)), - ]), - const SizedBox(height: 12), - content, - ], - ), - ), - ); - } - - Widget _buildList(List items) { - return Column( - children: items.map((item) => Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(item['name'], style: const TextStyle(fontWeight: FontWeight.w500)), - Text("${(item['score'] * 100).toStringAsFixed(1)}%", style: const TextStyle(color: Colors.grey)), - ], - ), - )).toList(), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -284,10 +284,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -649,10 +649,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: