creek

The AI Image Editor of 2030
commit ccfa0612d95167c7025dd06d9f38d81f3712ccf5
parent fafebf83417b68babfa39ec6002fb227ddfcf32a
Author: Nilotpal Gupta <nilotpalgupta0701@gmail.com>
Date:   Mon,  1 Dec 2025 07:53:57 +0530

flask service + asset generation + sketch to image + ui integration of assets

Diffstat:
Mlib/data/database.dart | 7+------
Mlib/data/models/project_model.dart | 5+++++
Mlib/data/repos/file_repo.dart | 12++++++++++++
Mlib/data/repos/note_repo.dart | 2--
Mlib/data/repos/project_repo.dart | 16++++++++++++++++
Mlib/services/analyze/image_analyzer.dart | 25+++++++++++++++++++++++--
Mlib/services/flask_service.dart | 50+++++++++++++++++++++++++++++++++++++++++++++-----
Mlib/ui/pages/stylesheet_page.dart | 191++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
8 files changed, 262 insertions(+), 46 deletions(-)

diff --git a/lib/data/database.dart b/lib/data/database.dart @@ -1,12 +1,6 @@ -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_v6.db'; @@ -36,6 +30,7 @@ class AppDatabase { parent_id INTEGER, global_stylesheet TEXT, last_accessed_at TEXT, + assets_path TEXT DEFAULT '[]', created_at TEXT, FOREIGN KEY (parent_id) REFERENCES projects (id) ON DELETE CASCADE ) diff --git a/lib/data/models/project_model.dart b/lib/data/models/project_model.dart @@ -8,6 +8,7 @@ class ProjectModel { final String? globalStylesheet; // Stored as JSON string final DateTime lastAccessedAt; final DateTime createdAt; + final List<String> assetsPath; ProjectModel({ this.id, @@ -17,6 +18,7 @@ class ProjectModel { this.globalStylesheet, required this.lastAccessedAt, required this.createdAt, + this.assetsPath = const [], }); bool get isEvent => parentId != null; @@ -40,6 +42,7 @@ class ProjectModel { 'global_stylesheet': globalStylesheet, 'last_accessed_at': lastAccessedAt.toIso8601String(), 'created_at': createdAt.toIso8601String(), + 'assets_path': jsonEncode(assetsPath), }; } @@ -52,6 +55,8 @@ class ProjectModel { globalStylesheet: map['global_stylesheet'], lastAccessedAt: DateTime.parse(map['last_accessed_at']), createdAt: DateTime.parse(map['created_at']), + assetsPath: + map['assets_path'] != null ? List<String>.from(jsonDecode(map['assets_path'])) : [], ); } } diff --git a/lib/data/repos/file_repo.dart b/lib/data/repos/file_repo.dart @@ -70,6 +70,18 @@ class FileRepo { return res.map((e) => e['file_path'] as String).toList(); } + Future<FileModel?> getByFilePath(String path) async { + final db = await AppDatabase.db; + final res = await db.query( + 'files', + where: 'file_path = ?', + whereArgs: [path], + limit: 1, + ); + if (res.isNotEmpty) return FileModel.fromMap(res.first); + return null; + } + Future<List<FileModel>> getRecentFiles({int limit = 10}) async { final db = await AppDatabase.db; final res = await db.query( diff --git a/lib/data/repos/note_repo.dart b/lib/data/repos/note_repo.dart @@ -2,8 +2,6 @@ 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()); diff --git a/lib/data/repos/project_repo.dart b/lib/data/repos/project_repo.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import '../database.dart'; import '../models/project_model.dart'; @@ -117,4 +119,18 @@ class ProjectRepo { final db = await AppDatabase.db; await db.delete('projects', where: 'id = ?', whereArgs: [id]); } + + Future<void> updateAssets(int projectId, List<String> assets) async { + final db = await AppDatabase.db; + + await db.update( + 'projects', + { + 'assets_path': jsonEncode(assets), + 'last_accessed_at': DateTime.now().toIso8601String(), + }, + where: 'id = ?', + whereArgs: [projectId], + ); + } } diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'dart:developer' as dev; +import 'package:adobe/services/flask_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; @@ -89,6 +90,7 @@ class ImageAnalyzerService { result = await task(imagePath, assetPaths); } + debugPrint("📱: ${result.toString()}"); stopwatch.stop(); timelineTask.finish(); result['execution_time'] = stopwatch.elapsedMilliseconds; @@ -154,8 +156,6 @@ class ImageAnalyzerService { final results = await Future.wait([ // --- GROUP A: PARALLEL --- - // TODO: 'Subject' - // 1. Layout shouldRun(['Compositions']) ? _runProfiledJob( @@ -307,6 +307,25 @@ class ImageAnalyzerService { }, ) : skipTask(), + + // --- GROUP C: SERVER SIDE --- + + // 9. Subject + shouldRun(['Subject', 'Asset']) + ? _runProfiledJob( + name: 'Subject', + imagePath: imagePath, + rootToken: token, + runInIsolate: false, + assetPaths: assetPaths, + task: (path, _) async { + final String? location = await FlaskService().generateAsset(imagePath: path); + final res = {"success": true, "scores": {'image': location}, "error": null}; + return res; + }, + ) + : skipTask(), + ]); totalSw.stop(); @@ -325,6 +344,7 @@ class ImageAnalyzerService { 'Lighting': results[5]['execution_time'], 'Era': results[6]['execution_time'], 'Font': results[7]['execution_time'], + 'Subject': results[8]['execution_time'], }, }; _logSummary(logResult); @@ -342,6 +362,7 @@ class ImageAnalyzerService { 'Era': {"scores": results[6]['scores']}, 'Layout': {"scores": results[0]['scores']}, 'Font': {"scores": results[7]['scores']}, + 'Subject': {"scores": results[8]['scores']}, }, }, 'error': null, diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -1,7 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:adobe/data/repos/file_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/data/repos/project_repo.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; class FlaskService { @@ -9,7 +13,11 @@ class FlaskService { // CONFIGURATION // =========================================================================== - static const String _serverUrl = 'https://locustlike-trieciously-rudolph.ngrok-free.dev'; + // NOTE: REPLACE WITH YOUR WIFI IP ADDRESS + // BOTH PC AND MOBILE SHOULD BE ON SAME WIFI + // NO NEED FOR NGORK OR SMEE + // PORT: 5000, http + static const String _serverUrl = 'http://172.16.114.193:5000'; // --> READ NOTE (REPLACE WITH IITG_CONNECT WIFI IP) static const Map<String, String> _headers = {'Content-Type': 'application/json'}; // =========================================================================== @@ -93,12 +101,39 @@ class FlaskService { final String? base64Image = await _encodeFile(imagePath); if (base64Image == null) return null; - return _performImageOperation( - endpoint: '/remove-background', + final String? path = await _performImageOperation( + endpoint: '/asset', logPrefix: '✂️ Asset Gen', body: {'image': base64Image}, filenamePrefix: 'asset', ); + + if (path != null) { + // 1. Find the project ID + int? projectId; + + final imagemodel = await ImageRepo().getByFilePath(imagePath); + if (imagemodel != null) { + projectId = imagemodel.projectId; + } else { + final filemodel = await FileRepo().getByFilePath(imagePath); + if (filemodel != null) { + projectId = filemodel.projectId; + } + } + + // 2. Update the Project Repo AND SAVE TO DATABASE + if (projectId != null) { + final project = await ProjectRepo().getProjectById(projectId); + if (project != null) { + project.assetsPath.add(path); // Update memory + await ProjectRepo().updateAssets(projectId, project.assetsPath); + debugPrint("✅ Asset path saved to Project DB: $path"); + } + } + } + + return path; } // =========================================================================== @@ -190,13 +225,18 @@ class FlaskService { final Uint8List imageBytes = base64Decode(data['image']); final directory = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${directory.path}/generated_images'); + // Use join for safe path construction + final imagesDirPath = p.join(directory.path, 'generated_images'); + final imagesDir = Directory(imagesDirPath); + if (!await imagesDir.exists()) await imagesDir.create(recursive: true); final timestamp = DateTime.now().millisecondsSinceEpoch; final safePrefix = prefix.replaceAll(RegExp(r'[^\w\s]'), '').trim().replaceAll(' ', '_'); final shortPrefix = safePrefix.length > 20 ? safePrefix.substring(0, 20) : safePrefix; - final String filePath = '${imagesDir.path}/${shortPrefix}_$timestamp.png'; + + // Use join here too + final String filePath = p.join(imagesDir.path, '${shortPrefix}_$timestamp.png'); await File(filePath).writeAsBytes(imageBytes); debugPrint("✅ Image saved: $filePath"); diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:adobe/ui/styles/variables.dart'; @@ -8,6 +9,8 @@ import 'package:adobe/data/repos/image_repo.dart'; import 'package:adobe/data/repos/project_repo.dart'; import 'package:adobe/services/python_service.dart'; import 'package:adobe/data/repos/note_repo.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; class StylesheetPage extends StatefulWidget { final int projectId; @@ -27,6 +30,9 @@ class _StylesheetPageState extends State<StylesheetPage> { Map<String, dynamic>? _stylesheetMap; String? _rawJsonString; + + // New state variable to hold assets from ProjectRepo + List<String> _projectAssets = []; // Cache for font name lookups final Map<String, String> _fontNameCache = {}; @@ -79,17 +85,51 @@ class _StylesheetPageState extends State<StylesheetPage> { return dirtyName; } + Future<File?> _resolveFile(String path) async { + // 1. Try the path exactly as saved + final file = File(path); + if (await file.exists()) return file; + + // 2. If that fails (iOS UUID change?), try to find it in the current docs dir + try { + final filename = p.basename(path); // Get "image_123.png" from the long path + final dir = await getApplicationDocumentsDirectory(); + + // Reconstruct path: CurrentDir + generated_images + filename + final fixedPath = p.join(dir.path, 'generated_images', filename); + + final fixedFile = File(fixedPath); + if (await fixedFile.exists()) { + return fixedFile; + } + } catch (e) { + debugPrint("Error resolving file path: $e"); + } + + return null; + } + Future<void> _loadSavedStylesheet() async { final project = await ProjectRepo().getProjectById(_currentProjectId); - if (project?.globalStylesheet != null && project!.globalStylesheet!.isNotEmpty) { - String raw = project.globalStylesheet!; + + if (project == null) return; + + // 1. Load Assets directly from Project Model + List<String> currentAssets = project.assetsPath; + + // 2. Load Stylesheet JSON + Map<String, dynamic>? parsedMap; + String? rawJson; + + if (project.globalStylesheet != null && project.globalStylesheet!.isNotEmpty) { + rawJson = project.globalStylesheet!; dynamic parsed; try { - parsed = jsonDecode(raw); + parsed = jsonDecode(rawJson); } catch (e) { try { - parsed = jsonDecode(_cleanJsonString(raw)); + parsed = jsonDecode(_cleanJsonString(rawJson)); } catch (_) {} } @@ -97,21 +137,22 @@ class _StylesheetPageState extends State<StylesheetPage> { try { parsed = jsonDecode(parsed); } catch (_) {} } - if (mounted) { - setState(() { - _rawJsonString = raw; - if (parsed is Map<String, dynamic>) { - if (parsed.containsKey('results') && parsed['results'] is Map) { - _stylesheetMap = parsed['results']; - } else { - _stylesheetMap = parsed; - } - } else { - _stylesheetMap = null; - } - }); + if (parsed is Map<String, dynamic>) { + if (parsed.containsKey('results') && parsed['results'] is Map) { + parsedMap = parsed['results']; + } else { + parsedMap = parsed; + } } } + + if (mounted) { + setState(() { + _projectAssets = currentAssets; + _rawJsonString = rawJson; + _stylesheetMap = parsedMap; + }); + } } Future<void> _generateStylesheet() async { @@ -156,14 +197,8 @@ class _StylesheetPageState extends State<StylesheetPage> { final jsonString = jsonEncode(result); await ProjectRepo().updateStylesheet(_currentProjectId, jsonString); - setState(() { - if (result.containsKey('results') && result['results'] is Map) { - _stylesheetMap = result['results']; - } else { - _stylesheetMap = result; - } - _rawJsonString = jsonString; - }); + // Reload everything (assets + stylesheet) to keep sync + await _loadSavedStylesheet(); } } catch (e) { debugPrint("Gen Error: $e"); @@ -193,12 +228,13 @@ class _StylesheetPageState extends State<StylesheetPage> { onProjectChanged: (p) => setState(() { _currentProjectId = p.id!; _stylesheetMap = null; + _projectAssets = []; _loadSavedStylesheet(); }), ), body: _isLoading ? const Center(child: CircularProgressIndicator(color: Variables.textPrimary)) - : (_stylesheetMap == null && _rawJsonString == null) + : (_stylesheetMap == null && _rawJsonString == null && _projectAssets.isEmpty) ? _buildEmptyState() : _buildContent(), bottomNavigationBar: BottomBar( @@ -228,8 +264,11 @@ class _StylesheetPageState extends State<StylesheetPage> { final emotions = _getData(['Emotions', 'emotions']); final era = _getData(['Era/Cultural Reference', 'era']); final typography = _getData(['Typography', 'fonts']); - - final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null); + + // We check _projectAssets.isNotEmpty to determine if we show the section + final hasAssets = _projectAssets.isNotEmpty; + + final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null || hasAssets); return RefreshIndicator( onRefresh: _generateStylesheet, @@ -257,11 +296,15 @@ class _StylesheetPageState extends State<StylesheetPage> { const SizedBox(height: 32), if (foundAny) ...[ - // 1. Typography (Now a Slider) + // 1. Assets / Subjects Section (From Project Repo) + if (hasAssets) + _buildAssetsSection(_projectAssets), + + // 2. Typography (Now a Slider) if (typography != null) _buildTypographySection(typography), - // 2. Color Palette + // 3. Color Palette if (colors != null) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), @@ -276,7 +319,7 @@ class _StylesheetPageState extends State<StylesheetPage> { const SizedBox(height: 32), ], - // 3. Slider Sections + // 4. Slider Sections if (style != null) _buildSliderSection("Style & Aesthetic", style), if (emotions != null) _buildSliderSection("Mood & Emotions", emotions), if (lighting != null) _buildSliderSection("Lighting", lighting), @@ -333,6 +376,92 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } + // --- ASSETS SECTION --- + Widget _buildAssetsSection(List<String> imagePaths) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildSectionHeader("Subjects & Assets"), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: imagePaths.length, + itemBuilder: (context, index) => _buildAssetCard(imagePaths[index]), + ), + ), + const SizedBox(height: 32), + ], + ); + } + + Widget _buildAssetCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; + final bool exists = file != null; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), // Matching color card radius + border: Border.all(color: Variables.borderSubtle), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: exists + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Colors.grey.shade100, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), + ), + ), + // Container( + // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + // color: Colors.white, + // child: const Text( + // "Asset", + // textAlign: TextAlign.center, + // style: TextStyle( + // fontFamily: 'GeneralSans', + // fontSize: 10, + // fontWeight: FontWeight.w600, + // color: Variables.textPrimary, + // ), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // ) + ], + ), + ); + }, + ); + } + // --- TYPOGRAPHY SECTION (UPDATED TO SLIDER) --- Widget _buildTypographySection(dynamic data) { List<String> fontNames = [];