commit fe164aad35ddc7e9aac86e2b8ee29a18fbfd8d84
parent 8b318745d863720f361b2a05331434220395e157
Author: maydayv7 <maydayv7@gmail.com>
Date: Sat, 29 Nov 2025 23:03:00 +0530
Add stylesheet generation model
Diffstat:
5 files changed, 670 insertions(+), 50 deletions(-)
diff --git a/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt b/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt
@@ -65,4 +65,17 @@ object ImageAnalyzer {
null
}
}
+
+ fun generateStylesheet(jsonList: List<String>): String? {
+ waitForPython()
+ return try {
+ val py = Python.getInstance()
+ val module = py.getModule("stylesheet_generator")
+ val result = module.callAttr("generate_stylesheet", jsonList)
+ result.toString()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
}
diff --git a/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt b/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt
@@ -38,6 +38,10 @@ class MainActivity : FlutterActivity() {
val outputDir = call.argument<String>("outputDir")!!
ImageAnalyzer.downloadInstagramImage(url, outputDir)
}
+ "generateStylesheet" -> {
+ val jsonList = call.argument<List<String>>("jsonList")!!
+ ImageAnalyzer.generateStylesheet(jsonList)
+ }
else -> null
}
@@ -49,9 +53,9 @@ class MainActivity : FlutterActivity() {
if (call.method == "downloadInstagramImage") {
// Specific error handling for downloader if needed
result.error("ERROR", "Download returned null", null)
- } else if (response == null && (call.method == "analyzeLayout" || call.method == "analyzeColorStyle")) {
+ } else if (response == null) {
// Python returned null (error inside script)
- result.error("ANALYSIS_ERROR", "Python returned null", null)
+ result.error("PYTHON_ERROR", "Python returned null for ${call.method}", null)
} else {
result.notImplemented()
}
diff --git a/android/app/src/main/python/stylesheet_generator.py b/android/app/src/main/python/stylesheet_generator.py
@@ -0,0 +1,471 @@
+import json
+import math
+import numpy as np
+from collections import defaultdict
+from typing import List, Dict, Tuple, Any
+from sklearn.cluster import KMeans
+
+# ==========================================
+# HYPERPARAMETERS
+# ==========================================
+RANK_DECAY_FACTOR = 0.85 # Priority drops for lower-ranked tags
+MIN_CONSENSUS_THRESHOLD = 0.05 # Filter out noise
+PALETTE_SIZE = 5 # Number of colors in final palette
+
+# ==========================================
+# HELPER: COLOR MATH
+# ==========================================
+def hex_to_rgb(hex_str: str) -> List[int]:
+ """Converts '#FF5733' to [255, 87, 51] for math operations."""
+ try:
+ hex_str = hex_str.lstrip('#')
+ if len(hex_str) != 6: return [0, 0, 0]
+ return [int(hex_str[i:i+2], 16) for i in (0, 2, 4)]
+ except:
+ return [0, 0, 0]
+
+def rgb_to_hex(rgb: List[int]) -> str:
+ """Converts [255, 87, 51] back to '#FF5733'."""
+ return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2]))
+
+# ==========================================
+# KNOWLEDGE BASE: FONTS
+# ==========================================
+class FontClusterEngine:
+ def __init__(self):
+ self.full_font_map = {
+ "abeezee": "Sans Serif (Generic)", "abel": "Condensed Sans", "abhayalibre": "Serif (Generic)",
+ "abrilfatface": "Modern Serif", "abyssinicasil": "Serif (Generic)", "aclonica": "Display/Other",
+ "acme": "Display/Other", "actor": "Sans Serif (Generic)", "adamina": "Serif (Generic)",
+ "adventpro": "Display/Other", "aguafinascript": "Script/Handwritten", "akronim": "Display/Other",
+ "aladin": "Display/Other", "aldrich": "Display/Other", "alef": "Sans Serif (Generic)",
+ "alegreya": "Old Style Serif", "alegreyasans": "Humanist Sans", "alegreyasanssc": "Sans Serif (Generic)",
+ "alegreyasc": "Old Style Serif", "alexbrush": "Script/Handwritten", "alfaslabone": "Slab Serif",
+ "alice": "Serif (Generic)", "alike": "Serif (Generic)", "alikeangular": "Serif (Generic)",
+ "allan": "Display/Other", "allerta": "Sans Serif (Generic)", "allertastencil": "Display/Other",
+ "allura": "Script/Handwritten", "almendra": "Display/Other", "almendradisplay": "Display/Other",
+ "almendrasc": "Display/Other", "amarante": "Display/Other", "amaranth": "Display/Other",
+ "amaticasc": "Script/Handwritten", "amaticsc": "Script/Handwritten", "amethysta": "Serif (Generic)",
+ "amiko": "Sans Serif (Generic)", "amiri": "Serif (Generic)", "amita": "Script/Handwritten",
+ "anaheim": "Sans Serif (Generic)", "andada": "Serif (Generic)", "andadasc": "Serif (Generic)",
+ "andika": "Sans Serif (Generic)", "annieuseyourtelescope": "Script/Handwritten", "anonymouspro": "Monospace",
+ "antic": "Sans Serif (Generic)", "anticdidone": "Serif (Generic)", "anticslab": "Slab Serif",
+ "anton": "Grotesque Sans", "antonio": "Sans Serif (Generic)", "arapey": "Serif (Generic)",
+ "arbutus": "Display/Other", "arbutusslab": "Slab Serif", "architectsdaughter": "Script/Handwritten",
+ "archivo": "Grotesque Sans", "archivoblack": "Grotesque Sans", "archivonarrow": "Condensed Sans",
+ "archivovfbeta": "Grotesque Sans", "arefruqaa": "Serif (Generic)", "arimamadurai": "Display/Other",
+ "arimo": "Neo-Grotesque Sans", "arizonia": "Script/Handwritten", "armata": "Sans Serif (Generic)",
+ "arsenal": "Sans Serif (Generic)", "artifika": "Serif (Generic)", "arvo": "Slab Serif",
+ "arya": "Sans Serif (Generic)", "asap": "Rounded Sans", "asapcondensed": "Condensed Sans",
+ "asapvfbeta": "Rounded Sans", "asar": "Serif (Generic)", "asset": "Display/Other",
+ "assistant": "Sans Serif (Generic)", "astloch": "Display/Other", "asul": "Sans Serif (Generic)",
+ "athiti": "Sans Serif (Generic)", "atma": "Display/Other", "atomicage": "Display/Other",
+ "aubrey": "Display/Other", "audiowide": "Display/Other", "autourone": "Display/Other",
+ "average": "Serif (Generic)", "averagesans": "Sans Serif (Generic)", "averiagruesalibre": "Display/Other",
+ "averialibre": "Display/Other", "averiasanslibre": "Sans Serif (Generic)", "averiaseriflibre": "Serif (Generic)",
+ "badscript": "Script/Handwritten", "bahiana": "Display/Other", "baloo": "Display/Other",
+ "baloobhai": "Display/Other", "baloobhaijaan": "Display/Other", "baloobhaina": "Display/Other",
+ "baloochettan": "Display/Other", "balooda": "Display/Other", "baloopaaji": "Display/Other",
+ "balootamma": "Display/Other", "balootammudu": "Display/Other", "baloothambi": "Display/Other",
+ "balthazar": "Serif (Generic)", "bangers": "Display", "barlow": "Grotesque Sans",
+ "barlowcondensed": "Condensed Sans", "barlowsemicondensed": "Condensed Sans", "barrio": "Display/Other",
+ "basic": "Sans Serif (Generic)", "baumans": "Display/Other", "belgrano": "Serif (Generic)",
+ "bellefair": "Serif (Generic)", "belleza": "Sans Serif (Generic)", "benchnine": "Sans Serif (Generic)",
+ "bentham": "Serif (Generic)", "berkshireswash": "Script/Handwritten", "bevan": "Slab Serif",
+ "bhavuka": "Display/Other", "bigelowrules": "Display/Other", "bigshotone": "Display/Other",
+ "bilbo": "Script/Handwritten", "bilboswashcaps": "Script/Handwritten", "biorhyme": "Slab Serif",
+ "biorhymeexpanded": "Slab Serif", "biryani": "Sans Serif (Generic)", "bitter": "Slab Serif",
+ "blackandwhitepicture": "Display/Other", "blackhansans": "Sans Serif (Generic)", "blackopsone": "Display/Other",
+ "bonbon": "Display/Other", "boogaloo": "Display/Other", "bowlbyone": "Display/Other",
+ "bowlbyonesc": "Display/Other", "brawler": "Serif (Generic)", "breeserif": "Slab Serif",
+ "brunoace": "Display/Other", "brunoacesc": "Display/Other", "bubblegumsans": "Display/Other",
+ "bubblerone": "Display/Other", "buenard": "Serif (Generic)", "bungee": "Display/Other",
+ "bungeehairline": "Display/Other", "bungeeinline": "Display/Other", "bungeeoutline": "Display/Other",
+ "bungeeshade": "Display/Other", "butcherman": "Display/Other", "butchermancaps": "Display/Other",
+ "butterflykids": "Script/Handwritten", "cabin": "Humanist Sans", "cabincondensed": "Condensed Sans",
+ "cabinsketch": "Display/Other", "cabinvfbeta": "Humanist Sans", "caesardressing": "Display/Other",
+ "cagliostro": "Sans Serif (Generic)", "cairo": "Grotesque Sans", "calligraffitti": "Script/Handwritten",
+ "cambay": "Sans Serif (Generic)", "cambo": "Serif (Generic)", "cantarell": "Sans Serif (Generic)",
+ "cantataone": "Serif (Generic)", "cantoraone": "Sans Serif (Generic)", "capriola": "Sans Serif (Generic)",
+ "cardo": "Serif (Generic)", "carme": "Sans Serif (Generic)", "carroisgothic": "Sans Serif (Generic)",
+ "carroisgothicsc": "Sans Serif (Generic)", "catamaran": "Sans Serif", "caudex": "Serif (Generic)",
+ "caveat": "Script/Handwritten", "caveatbrush": "Script/Handwritten", "cevicheone": "Display/Other",
+ "changa": "Sans Serif (Generic)", "changaone": "Display/Other", "chango": "Display/Other",
+ "chathura": "Sans Serif (Generic)", "chauphilomeneone": "Display/Other", "chelaone": "Display/Other",
+ "chelseamarket": "Display/Other", "cherrycreamsoda": "Display/Other", "cherryswash": "Display/Other",
+ "chewy": "Display", "chicle": "Display/Other", "chivo": "Grotesque Sans",
+ "chonburi": "Display/Other", "cinzel": "Serif", "cinzeldecorative": "Display/Other",
+ "clickerscript": "Script/Handwritten", "coda": "Display/Other", "codystar": "Display/Other",
+ "coiny": "Display/Other", "combo": "Display/Other", "comfortaa": "Rounded Sans",
+ "comingsoon": "Script/Handwritten", "concertone": "Display/Other", "condiment": "Script/Handwritten",
+ "contrailone": "Display/Other", "convergence": "Sans Serif (Generic)", "cookie": "Script",
+ "copse": "Slab Serif", "cormorant": "Old Style Serif", "cormorantgaramond": "Old Style Serif",
+ "cormorantinfant": "Old Style Serif", "cormorantsc": "Old Style Serif", "cormorantunicase": "Old Style Serif",
+ "cormorantupright": "Old Style Serif", "courgette": "Script", "cousine": "Monospace",
+ "coustard": "Slab Serif", "craftygirls": "Script/Handwritten", "creepster": "Display/Other",
+ "creepstercaps": "Display/Other", "creteround": "Slab Serif", "crimsontext": "Old Style Serif",
+ "croissantone": "Display/Other", "crushed": "Display/Other", "cuprum": "Sans Serif",
+ "cutefont": "Display/Other", "cutive": "Serif (Generic)", "cutivemono": "Monospace",
+ "damion": "Script/Handwritten", "dancingscript": "Script/Handwritten", "daysone": "Display/Other",
+ "decovaralpha": "Display/Other", "dekko": "Script/Handwritten", "delius": "Script/Handwritten",
+ "deliusswashcaps": "Script/Handwritten", "deliusunicase": "Script/Handwritten", "dellarespira": "Serif (Generic)",
+ "denkone": "Display/Other", "devonshire": "Script/Handwritten", "dhurjati": "Display/Other",
+ "dhyana": "Display/Other", "didactgothic": "Sans Serif (Generic)", "digitalnumbers": "Display/Other",
+ "diplomata": "Display/Other", "diplomatasc": "Display/Other", "dohyeon": "Sans Serif (Generic)",
+ "dokdo": "Script/Handwritten", "domine": "Serif", "donegalone": "Display/Other",
+ "doppioone": "Sans Serif (Generic)", "dorsa": "Display/Other", "dosis": "Rounded Sans",
+ "drsugiyama": "Script/Handwritten", "durusans": "Sans Serif (Generic)", "dynalight": "Script/Handwritten",
+ "eaglelake": "Script/Handwritten", "eastseadokdo": "Script/Handwritten", "eater": "Display/Other",
+ "eatercaps": "Display/Other", "ebgaramond": "Old Style Serif", "economica": "Sans Serif (Generic)",
+ "eczar": "Display/Other", "ekmukta": "Sans Serif (Generic)", "electrolize": "Display/Other",
+ "elmessiri": "Sans Serif (Generic)", "elsie": "Display/Other", "elsieswashcaps": "Display/Other",
+ "emblemaone": "Display/Other", "emilyscandy": "Display/Other", "encodesans": "Sans Serif",
+ "encodesanscondensed": "Condensed Sans", "encodesansexpanded": "Sans Serif (Generic)", "encodesanssemicondensed": "Condensed Sans",
+ "encodesanssemiexpanded": "Sans Serif (Generic)", "engagement": "Script/Handwritten", "englebert": "Display/Other",
+ "enriqueta": "Slab Serif", "ericaone": "Display/Other", "esteban": "Serif (Generic)",
+ "euphoriascript": "Script/Handwritten", "ewert": "Display/Other", "exo": "Geometric Sans",
+ "exo2": "Geometric Sans", "expletussans": "Display/Other", "fanwoodtext": "Serif (Generic)",
+ "farsan": "Display/Other", "fascinate": "Display/Other", "fascinateinline": "Display/Other",
+ "fasterone": "Display/Other", "fasthand": "Script/Handwritten", "faunaone": "Serif (Generic)",
+ "faustina": "Serif (Generic)", "faustinavfbeta": "Serif (Generic)", "federant": "Display/Other",
+ "federo": "Display/Other", "felipa": "Script/Handwritten", "fenix": "Serif (Generic)",
+ "fingerpaint": "Display/Other", "firasans": "Humanist Sans", "firasanscondensed": "Condensed Sans",
+ "firasansextracondensed": "Condensed Sans", "fjallaone": "Grotesque Sans", "fjordone": "Serif (Generic)",
+ "flamenco": "Display/Other", "flavors": "Display/Other", "fondamento": "Script/Handwritten",
+ "fontdinerswanky": "Display/Other", "forum": "Display/Other", "francoisone": "Sans Serif",
+ "frankruhllibre": "Serif (Generic)", "freckleface": "Display/Other", "frederickathegreat": "Display/Other",
+ "fredokaone": "Rounded Display", "fresca": "Sans Serif (Generic)", "frijole": "Display/Other",
+ "fruktur": "Display/Other", "fugazone": "Display/Other", "gabriela": "Serif (Generic)",
+ "gaegu": "Script/Handwritten", "gafata": "Sans Serif (Generic)", "galada": "Display/Other",
+ "galdeano": "Sans Serif (Generic)", "galindo": "Display/Other", "gamjaflower": "Script/Handwritten",
+ "gemunulibre": "Sans Serif (Generic)", "genbasb": "Serif (Generic)", "genbasbi": "Serif (Generic)",
+ "genbkbasb": "Serif (Generic)", "genbkbasbi": "Serif (Generic)", "geo": "Display/Other",
+ "geostar": "Display/Other", "geostarfill": "Display/Other", "germaniaone": "Display/Other",
+ "gfsdidot": "Serif (Generic)", "gfsneohellenic": "Sans Serif (Generic)", "gfsneohellenicbold": "Sans Serif (Generic)",
+ "gfsneohellenicbolditalic": "Sans Serif (Generic)", "gidugu": "Display/Other", "gildadisplay": "Serif (Generic)",
+ "glassantiqua": "Display/Other", "glegoo": "Slab Serif", "gochihand": "Script/Handwritten",
+ "gorditas": "Display/Other", "gothica1": "Sans Serif (Generic)", "graduate": "Display/Other",
+ "grandhotel": "Script/Handwritten", "greatvibes": "Script", "griffy": "Display/Other",
+ "gruppo": "Display/Other", "gudea": "Sans Serif (Generic)", "gugi": "Display/Other",
+ "gurajada": "Display/Other", "habibi": "Serif (Generic)", "halant": "Serif (Generic)",
+ "hammersmithone": "Sans Serif (Generic)", "hanalei": "Display/Other", "hanaleifill": "Display/Other",
+ "handlee": "Script/Handwritten", "hannari": "Display/Other", "hanuman": "Serif (Generic)",
+ "happymonkey": "Display/Other", "harmattan": "Sans Serif (Generic)", "headlandone": "Serif (Generic)",
+ "heebo": "Sans Serif", "hennypenny": "Display/Other", "hermeneusone": "Sans Serif (Generic)",
+ "herrvonmuellerhoff": "Script/Handwritten", "himelody": "Script/Handwritten", "hind": "Humanist Sans",
+ "hindcolombo": "Sans Serif (Generic)", "hindguntur": "Sans Serif (Generic)", "hindjalandhar": "Sans Serif (Generic)",
+ "hindkochi": "Sans Serif (Generic)", "hindmadurai": "Sans Serif (Generic)", "hindmysuru": "Sans Serif (Generic)",
+ "hindsiliguri": "Sans Serif (Generic)", "hindvadodara": "Sans Serif (Generic)", "homemadeapple": "Script/Handwritten",
+ "homenaje": "Sans Serif (Generic)", "ibmplexmono": "Monospace", "ibmplexsans": "Neo-Grotesque Sans",
+ "ibmplexsanscondensed": "Condensed Sans", "ibmplexserif": "Transitional Serif", "iceberg": "Display/Other",
+ "iceland": "Display/Other", "imprima": "Sans Serif (Generic)", "inconsolata": "Monospace",
+ "inder": "Sans Serif (Generic)", "indieflower": "Handwritten", "inika": "Serif (Generic)",
+ "inknutantiqua": "Serif (Generic)", "irishgrover": "Display/Other", "istokweb": "Sans Serif (Generic)",
+ "italiana": "Serif (Generic)", "italianno": "Script/Handwritten", "itim": "Script/Handwritten",
+ "jacquesfrancois": "Display/Other", "jacquesfrancoisshadow": "Display/Other", "jaldi": "Sans Serif (Generic)",
+ "jejugothic": "Sans Serif (Generic)", "jejuhallasan": "Display/Other", "jejumyeongjo": "Serif (Generic)",
+ "jimnightshade": "Display/Other", "jockeyone": "Display/Other", "jollylodger": "Display/Other",
+ "jomhuria": "Display/Other", "josefinsans": "Geometric Sans", "josefinslab": "Slab Serif",
+ "jotione": "Display/Other", "jua": "Sans Serif (Generic)", "judson": "Serif (Generic)",
+ "julee": "Script/Handwritten", "juliussansone": "Sans Serif (Generic)", "junge": "Serif (Generic)",
+ "jura": "Sans Serif (Generic)", "justanotherhand": "Script/Handwritten", "kadwa": "Serif (Generic)",
+ "kalam": "Handwritten", "kameron": "Serif (Generic)", "kanit": "Sans Serif",
+ "kantumruy": "Sans Serif (Generic)", "karla": "Sans Serif (Generic)", "karlatamilinclined": "Sans Serif (Generic)",
+ "karlatamilupright": "Sans Serif (Generic)", "karma": "Serif (Generic)", "katibeh": "Display/Other",
+ "kaushanscript": "Script", "kavivanar": "Script/Handwritten", "kavoon": "Display/Other",
+ "kdamthmor": "Display/Other", "keaniaone": "Display/Other", "kellyslab": "Display/Other",
+ "kenia": "Display/Other", "khand": "Sans Serif", "khula": "Sans Serif (Generic)",
+ "khyay": "Display/Other", "kiranghaerang": "Display/Other", "kiteone": "Sans Serif (Generic)",
+ "knewave": "Display/Other", "kokoro": "Display/Other", "kopubbatang": "Serif (Generic)",
+ "kottaone": "Serif (Generic)", "kranky": "Display/Other", "kreon": "Slab Serif",
+ "kristi": "Script/Handwritten", "kronaone": "Sans Serif (Generic)", "kurale": "Serif (Generic)",
+ "laila": "Serif (Generic)", "lakkireddy": "Display/Other", "lalezar": "Display/Other",
+ "lancelot": "Display/Other", "laomuangdon": "Display/Other", "laomuangkhong": "Display/Other",
+ "laosanspro": "Sans Serif (Generic)", "lato": "Humanist Sans", "leckerlione": "Display/Other",
+ "ledger": "Serif (Generic)", "lekton": "Monospace", "lemon": "Display/Other",
+ "lemonada": "Display/Other", "lemonadavfbeta": "Display/Other", "librebaskerville": "Serif (Generic)",
+ "librecaslontext": "Serif (Generic)", "librefranklin": "Neo-Grotesque Sans", "lifesavers": "Display/Other",
+ "lilitaone": "Display/Other", "lilyscriptone": "Display/Other", "limelight": "Display/Other",
+ "lindenhill": "Serif (Generic)", "lobster": "Script", "lobstertwo": "Script/Handwritten",
+ "londrinaoutline": "Display/Other", "londrinashadow": "Display/Other", "londrinasketch": "Display/Other",
+ "londrinasolid": "Display/Other", "lora": "Old Style Serif", "loversquarrel": "Script/Handwritten",
+ "loveyalikeasister": "Script/Handwritten", "luckiestguy": "Display/Other", "lusitana": "Serif (Generic)",
+ "lustria": "Serif (Generic)", "macondo": "Display/Other", "macondoswashcaps": "Display/Other",
+ "mada": "Sans Serif (Generic)", "magra": "Sans Serif (Generic)", "maidenorange": "Display/Other",
+ "maitree": "Serif (Generic)", "mako": "Sans Serif (Generic)", "mallanna": "Display/Other",
+ "mandali": "Sans Serif (Generic)", "manuale": "Serif (Generic)", "marcellus": "Serif (Generic)",
+ "marcellussc": "Serif (Generic)", "marckscript": "Script/Handwritten", "margarine": "Display/Other",
+ "markazitext": "Serif (Generic)", "markoone": "Serif (Generic)", "marmelad": "Sans Serif (Generic)",
+ "martel": "Serif (Generic)", "martelsans": "Sans Serif (Generic)", "marvel": "Sans Serif (Generic)",
+ "mate": "Serif (Generic)", "matesc": "Serif (Generic)", "mavenpro": "Geometric Sans",
+ "mavenprovfbeta": "Geometric Sans", "mclaren": "Display/Other", "medulaone": "Display/Other",
+ "meerainimai": "Display/Other", "meiescript": "Script/Handwritten", "mergeone": "Sans Serif (Generic)",
+ "merienda": "Display/Other", "meriendaone": "Display/Other", "merriweather": "Transitional Serif",
+ "merriweathersans": "Humanist Sans", "mervalescript": "Script/Handwritten", "metalmania": "Display/Other",
+ "metamorphous": "Display/Other", "metrophobic": "Sans Serif (Generic)", "miama": "Script/Handwritten",
+ "milonga": "Display/Other", "miltonian": "Display/Other", "miltoniantattoo": "Display/Other",
+ "mina": "Sans Serif (Generic)", "miniver": "Display/Other", "miriamlibre": "Sans Serif (Generic)",
+ "mirza": "Display/Other", "missfajardose": "Script/Handwritten", "mitr": "Sans Serif (Generic)",
+ "modak": "Display/Other", "modernantiqua": "Serif (Generic)", "mogra": "Display/Other",
+ "molengo": "Sans Serif (Generic)", "molle": "Display/Other", "monda": "Sans Serif (Generic)",
+ "monoton": "Display/Other", "monsieurladoulaise": "Script/Handwritten", "montaga": "Serif (Generic)",
+ "montez": "Script/Handwritten", "montserrat": "Geometric Sans", "montserratalternates": "Geometric Sans",
+ "montserratsubrayada": "Geometric Sans", "mountainsofchristmas": "Display/Other", "mousememoirs": "Display/Other",
+ "mplus1p": "Rounded Sans", "mrbedfort": "Script/Handwritten", "mrdafoe": "Script/Handwritten",
+ "mrdehaviland": "Script/Handwritten", "mrssaintdelafield": "Script/Handwritten", "mrssheppards": "Script/Handwritten",
+ "mukta": "Sans Serif", "muktamahee": "Sans Serif (Generic)", "muktamalar": "Sans Serif (Generic)",
+ "muktavaani": "Sans Serif (Generic)", "muli": "Humanist Sans", "myanmarsanspro": "Sans Serif (Generic)",
+ "mysteryquest": "Display/Other", "nanumbrushscript": "Script/Handwritten", "nanumgothic": "Sans Serif (Generic)",
+ "nanumgothiccoding": "Monospace", "nanummyeongjo": "Serif (Generic)", "nanumpenscript": "Script/Handwritten",
+ "nats": "Display/Other", "neuton": "Serif (Generic)", "newrocker": "Display/Other",
+ "newscycle": "Sans Serif (Generic)", "nicomoji": "Display/Other", "niconne": "Script/Handwritten",
+ "nikukyu": "Display/Other", "nixieone": "Display/Other", "nobile": "Sans Serif (Generic)",
+ "nokora": "Sans Serif (Generic)", "norican": "Script/Handwritten", "nosifer": "Display/Other",
+ "nosifercaps": "Display/Other", "notable": "Display/Other", "noticiatext": "Serif (Generic)",
+ "notosans": "Humanist Sans", "notosanstamil": "Humanist Sans", "notoserif": "Transitional Serif",
+ "novascript": "Display/Other", "ntr": "Sans Serif (Generic)", "numans": "Sans Serif (Generic)",
+ "nunito": "Rounded Sans", "nunitosans": "Rounded Sans", "offside": "Display/Other",
+ "oldenburg": "Display/Other", "oldstandard": "Serif (Generic)", "oleoscript": "Script/Handwritten",
+ "oleoscriptswashcaps": "Script/Handwritten", "opensans": "Humanist Sans", "opensanscondensed": "Condensed Sans",
+ "opensanshebrew": "Humanist Sans", "opensanshebrewcondensed": "Condensed Sans", "oranienbaum": "Serif (Generic)",
+ "orbitron": "Display/Other", "oregano": "Script/Handwritten", "orienta": "Sans Serif (Generic)",
+ "originalsurfer": "Display/Other", "oswald": "Grotesque Sans", "overlock": "Display/Other",
+ "overlocksc": "Display/Other", "overpass": "Sans Serif (Generic)", "overpassmono": "Monospace",
+ "ovo": "Serif (Generic)", "oxygen": "Geometric Sans", "oxygenmono": "Monospace",
+ "pacifico": "Script", "palanquin": "Sans Serif (Generic)", "palanquindark": "Sans Serif (Generic)",
+ "pangolin": "Script/Handwritten", "paprika": "Display/Other", "parisienne": "Script/Handwritten",
+ "passeroone": "Display/Other", "passionone": "Display", "pathwaygothicone": "Condensed Sans",
+ "patrickhand": "Script/Handwritten", "patrickhandsc": "Script/Handwritten", "pattaya": "Display/Other",
+ "patuaone": "Slab Serif", "pavanam": "Sans Serif (Generic)", "paytoneone": "Sans Serif (Generic)",
+ "peddana": "Serif (Generic)", "peralta": "Display/Other", "permanentmarker": "Handwritten",
+ "petitformalscript": "Script/Handwritten", "petrona": "Serif (Generic)", "philosopher": "Sans Serif (Generic)",
+ "piedra": "Display/Other", "pinyonscript": "Script/Handwritten", "pirataone": "Display/Other",
+ "plaster": "Display/Other", "play": "Sans Serif (Generic)", "playball": "Script/Handwritten",
+ "playfairdisplay": "Modern Serif", "playfairdisplaysc": "Modern Serif", "podkova": "Slab Serif",
+ "podkovavfbeta": "Slab Serif", "poetsenone": "Display/Other", "poiretone": "Display/Other",
+ "poly": "Serif (Generic)", "pompiere": "Display/Other", "ponnala": "Display/Other",
+ "pontanosans": "Sans Serif (Generic)", "poorstory": "Display/Other", "poppins": "Geometric Sans",
+ "portersansblock": "Display/Other", "portlligatsans": "Display/Other", "portlligatslab": "Display/Other",
+ "postnobillscolombo": "Display/Other", "postnobillsjaffna": "Display/Other", "pragatinarrow": "Condensed Sans",
+ "prata": "Serif (Generic)", "pressstart2p": "Display/Other", "pridi": "Serif (Generic)",
+ "princesssofia": "Script/Handwritten", "prociono": "Serif (Generic)", "prompt": "Sans Serif",
+ "prostoone": "Display/Other", "prozalibre": "Sans Serif (Generic)", "pt_sans": "Humanist Sans",
+ "pt_serif": "Transitional Serif", "puritan": "Sans Serif (Generic)", "purplepurse": "Display/Other",
+ "questrial": "Geometric Sans", "qwigley": "Script/Handwritten"
+ }
+
+ def get_cluster(self, raw_name: str) -> str:
+ clean = raw_name.lower().split('-')[0].strip()
+ if clean in self.full_font_map:
+ return self.full_font_map[clean]
+ return "Unknown/Other"
+
+# ==========================================
+# UNIFIED CONSENSUS ENGINE
+# ==========================================
+class UnifiedStyleEngine:
+ def __init__(self):
+ self.feature_registry = defaultdict(lambda: defaultdict(list))
+ self.font_engine = FontClusterEngine()
+ self.font_family_scores = defaultdict(float)
+ self.font_individual_scores = defaultdict(float)
+ self.font_members = defaultdict(list)
+ self.color_pool = []
+ self.doc_counter = 0
+
+ self.aliases = {
+ "Color Pallete": "Color Palette",
+ "Texture": "Background/Texture",
+ "Era": "Era/Cultural Reference",
+ "Font": "Typography",
+ "Fonts": "Typography",
+ "Text": "Typography",
+ "Layout": "Composition"
+ }
+
+ def ingest_data(self, data: Dict):
+ """
+ Parses the JSON structure from the app
+ Handles: { "success": true, "data": { "results": { ... } } }
+ """
+ try:
+ self.doc_counter += 1
+
+ # 1. Unwrapping logic to find "results"
+ source_data = {}
+ if "data" in data and isinstance(data["data"], dict):
+ # Check for nested "results" inside "data"
+ if "results" in data["data"]:
+ source_data = data["data"]["results"]
+ else:
+ source_data = data["data"]
+ elif "results" in data:
+ source_data = data["results"]
+ else:
+ source_data = data
+
+ # 2. Iterate through categories
+ for category, payload in source_data.items():
+ if category in ["filename", "meta"]: continue
+
+ # Map "Font" -> "Typography", etc.
+ std_category = self.aliases.get(category, category)
+
+ # Extract scores dictionary if nested
+ if isinstance(payload, dict) and "scores" in payload:
+ payload = payload["scores"]
+
+ # Normalize to list of tuples
+ vectors = self._normalize(payload)
+
+ # PATH A: TYPOGRAPHY
+ if std_category == "Typography":
+ for rank, (font_name, score) in enumerate(vectors):
+ # Skip "No Text Detected"
+ if "no text" in font_name.lower(): continue
+
+ weight = math.pow(RANK_DECAY_FACTOR, rank) * score
+ cluster = self.font_engine.get_cluster(font_name)
+
+ self.font_family_scores[cluster] += weight
+ self.font_individual_scores[font_name] += weight
+ if font_name not in self.font_members[cluster]:
+ self.font_members[cluster].append(font_name)
+
+ # PATH B: COLOR PALETTE
+ elif std_category == "Color Palette":
+ for rank, (hex_code, score) in enumerate(vectors):
+ # Ensure we only pick valid hex codes
+ if rank < 5 and isinstance(hex_code, str) and hex_code.startswith('#'):
+ rgb = hex_to_rgb(hex_code)
+ self.color_pool.append(rgb)
+
+ # PATH C: OTHER TAGS
+ else:
+ for rank, (label, score) in enumerate(vectors):
+ self.feature_registry[std_category][label].append({
+ "raw_score": score,
+ "rank": rank
+ })
+
+ except Exception as e:
+ print(f"Warning during ingestion: {e}")
+
+ def _normalize(self, payload) -> List[Tuple[str, float]]:
+ """
+ Robustly converts inputs to [(label, score), ...]
+ Handles mixed types and weird nesting to prevent crashes.
+ """
+ if isinstance(payload, dict):
+ clean_items = []
+ for k, v in payload.items():
+ # Only accept numeric values as scores
+ if isinstance(v, (int, float)):
+ clean_items.append((k, float(v)))
+ return sorted(clean_items, key=lambda x: x[1], reverse=True)
+
+ if isinstance(payload, list):
+ res = []
+ for item in payload:
+ if isinstance(item, dict):
+ try:
+ score = float(item.get('score', 0.9))
+ except (ValueError, TypeError):
+ score = 0.0
+
+ # Try 'label' or 'tag'
+ label = item.get('label', item.get('tag', 'unknown'))
+ if not isinstance(label, str): label = str(label)
+
+ res.append((label, score))
+ elif isinstance(item, str):
+ res.append((item, 1.0))
+ return res
+ return []
+
+ def compute_final_stylesheet(self) -> Dict:
+ final_json = {"results": {}}
+
+ # 1. Process Standard Tags
+ for category, tags_map in self.feature_registry.items():
+ candidates = []
+ for tag, occurrences in tags_map.items():
+ # Weighted Sum
+ w_sum = sum(obs['raw_score'] * math.pow(RANK_DECAY_FACTOR, obs['rank']) for obs in occurrences)
+ score = w_sum / max(1, self.doc_counter)
+
+ if score > MIN_CONSENSUS_THRESHOLD:
+ candidates.append({"label": tag, "score": round(score, 2)})
+
+ candidates.sort(key=lambda x: x['score'], reverse=True)
+ if candidates: final_json["results"][category] = candidates
+
+ # 2. Process Typography
+ if self.font_family_scores:
+ best_fam, _ = sorted(self.font_family_scores.items(), key=lambda x: x[1], reverse=True)[0]
+ members = self.font_members[best_fam]
+ members.sort(key=lambda x: self.font_individual_scores[x], reverse=True)
+
+ typo_output = []
+ for font in members[:5]:
+ score = self.font_individual_scores[font] / max(1, self.doc_counter)
+ typo_output.append({"label": font, "score": round(score, 2)})
+
+ final_json["results"]["Typography"] = typo_output
+ final_json["results"]["Typography_Family"] = best_fam
+
+ # 3. Process Color Palette (K-Means)
+ if len(self.color_pool) >= PALETTE_SIZE:
+ try:
+ kmeans = KMeans(n_clusters=PALETTE_SIZE, n_init='auto', random_state=42)
+ kmeans.fit(self.color_pool)
+ centers = sorted(kmeans.cluster_centers_.astype(int).tolist(), key=sum)
+
+ palette_output = []
+ for i, rgb in enumerate(centers):
+ palette_output.append({
+ "label": rgb_to_hex(rgb),
+ "score": round(1.0 - (i * 0.1), 2)
+ })
+ final_json["results"]["Color Palette"] = palette_output
+ except Exception as e:
+ print(f"Color clustering failed: {e}")
+
+ final_json["results"]["meta"] = {"source_files": self.doc_counter}
+ return final_json
+
+# ==========================================
+# ENTRY POINT
+# ==========================================
+def generate_stylesheet(json_strings_list: Any) -> str:
+ """
+ Entry point
+ Accepts a Java ArrayList of JSON strings.
+ """
+ engine = UnifiedStyleEngine()
+
+ try:
+ count = json_strings_list.size()
+ for i in range(count):
+ json_str = json_strings_list.get(i)
+ if not json_str: continue
+ try:
+ data = json.loads(json_str)
+ engine.ingest_data(data)
+ except Exception as e:
+ print(f"Error parsing JSON chunk at index {i}: {e}")
+ continue
+
+ except AttributeError:
+ # Fallback for standard Python list (For testing)
+ for json_str in json_strings_list:
+ if not json_str: continue
+ try:
+ data = json.loads(json_str)
+ engine.ingest_data(data)
+ except Exception as e:
+ print(f"Error parsing JSON chunk: {e}")
+ continue
+
+ final_output = engine.compute_final_stylesheet()
+ return json.dumps(final_output)
diff --git a/lib/services/python_service.dart b/lib/services/python_service.dart
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
class PythonService {
static const MethodChannel _channel = MethodChannel('com.example.adobe/methods');
- /// 1. Layout Analysis (OpenCV)
+ // 1. Layout Analysis (OpenCV)
Future<Map<String, dynamic>> analyzeLayout(String imagePath) async {
try {
final String? result = await _channel.invokeMethod('analyzeLayout', {'imagePath': imagePath});
@@ -17,7 +17,7 @@ class PythonService {
}
}
- /// 2. Color Style Analysis (Scikit-Learn)
+ // 2. Color Style Analysis (Scikit-Learn)
Future<Map<String, dynamic>> analyzeColorStyle(String imagePath) async {
try {
final String? result = await _channel.invokeMethod('analyzeColorStyle', {'imagePath': imagePath});
@@ -29,7 +29,7 @@ class PythonService {
}
}
- /// 3. Instagram Downloader
+ // 3. Instagram Downloader
Future<Map<String, dynamic>?> downloadInstagramImage(String url, String outputDir) async {
try {
final String? result = await _channel.invokeMethod('downloadInstagramImage', {
@@ -42,4 +42,17 @@ class PythonService {
return null;
}
}
+
+ // 4. Stylesheet Generation
+ Future<Map<String, dynamic>?> generateStylesheet(List<String> jsonList) async {
+ try {
+ final String? result = await _channel.invokeMethod('generateStylesheet', {
+ 'jsonList': jsonList,
+ });
+ return result != null ? json.decode(result) : null;
+ } catch (e) {
+ debugPrint("Stylesheet Generation Error: $e");
+ return null;
+ }
+ }
}
diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart
@@ -1,8 +1,12 @@
+import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:adobe/ui/styles/variables.dart';
import 'package:adobe/ui/widgets/bottom_bar.dart';
import 'package:adobe/ui/widgets/top_bar.dart';
+import 'package:adobe/data/repos/image_repo.dart';
+import 'package:adobe/data/repos/project_repo.dart';
+import 'package:adobe/services/python_service.dart';
class StylesheetPage extends StatefulWidget {
final int projectId;
@@ -18,11 +22,89 @@ class StylesheetPage extends StatefulWidget {
class _StylesheetPageState extends State<StylesheetPage> {
late int _currentProjectId;
+ bool _isLoading = false;
+ String? _resultJson;
@override
void initState() {
super.initState();
_currentProjectId = widget.projectId;
+ _loadSavedStylesheet();
+ }
+
+ Future<void> _loadSavedStylesheet() async {
+ final project = await ProjectRepo().getProjectById(_currentProjectId);
+ if (project?.globalStylesheet != null && project!.globalStylesheet!.isNotEmpty) {
+ try {
+ final parsed = jsonDecode(project.globalStylesheet!);
+ const encoder = JsonEncoder.withIndent(' ');
+ setState(() {
+ _resultJson = encoder.convert(parsed);
+ });
+ } catch (_) {
+ }
+ }
+ }
+
+ Future<void> _generateStylesheet() async {
+ setState(() {
+ _isLoading = true;
+ _resultJson = null;
+ });
+
+ try {
+ // 1. Fetch images for this project
+ final images = await ImageRepo().getImages(_currentProjectId);
+
+ // 2. Extract valid analysis data
+ final List<String> analysisDataList = images
+ .map((img) => img.analysisData)
+ .where((data) => data != null && data.isNotEmpty)
+ .cast<String>()
+ .toList();
+
+ if (analysisDataList.isEmpty) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("No analyzed images found for this project.")),
+ );
+ }
+ setState(() { _isLoading = false; });
+ return;
+ }
+
+ // 3. Call Python Service
+ final result = await PythonService().generateStylesheet(analysisDataList);
+ if (mounted) {
+ if (result != null) {
+ const encoder = JsonEncoder.withIndent(' ');
+ final jsonString = encoder.convert(result);
+
+ // 4. Save to Database
+ await ProjectRepo().updateStylesheet(_currentProjectId, jsonString);
+ setState(() {
+ _resultJson = jsonString;
+ });
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("Failed to generate stylesheet.")),
+ );
+ }
+ }
+ } catch (e) {
+ debugPrint("Error generating stylesheet: $e");
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Error: $e")),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
}
@override
@@ -36,63 +118,100 @@ class _StylesheetPageState extends State<StylesheetPage> {
onProjectChanged: (newProject) {
setState(() {
_currentProjectId = newProject.id!;
- // If required, reload any page-specific content
+ _resultJson = null;
+ _loadSavedStylesheet();
});
},
),
- // Body
- body: Center(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- "Are you ready to start\nbuilding the visual identity?",
- textAlign: TextAlign.center,
- style: Variables.headerStyle,
+ body: LayoutBuilder(
+ builder: (context, constraints) {
+ return SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight,
),
- const SizedBox(height: 32),
-
- // Pill Button
- GestureDetector(
- onTap: () {
- // TODO
- },
- child: Container(
- width: 274,
- height: 44,
- decoration: BoxDecoration(
- color: Variables.textPrimary,
- borderRadius: BorderRadius.circular(112),
- ),
- alignment: Alignment.center,
- child: Row(
- mainAxisSize: MainAxisSize.min,
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 20),
+ child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Text(
- "Generate Stylesheet",
- style: Variables.buttonTextStyle,
- ),
- const SizedBox(width: 8),
- SvgPicture.asset(
- "assets/icons/generate_icon.svg",
- width: 18,
- height: 18,
- colorFilter: const ColorFilter.mode(
- Variables.textWhite,
- BlendMode.srcIn,
+ // No saved result
+ if (_resultJson == null && !_isLoading) ...[
+ Text(
+ "Are you ready to start\nbuilding the visual identity?",
+ textAlign: TextAlign.center,
+ style: Variables.headerStyle,
),
- ),
+ const SizedBox(height: 32),
+ ],
+
+ // Loading
+ if (_isLoading)
+ const CircularProgressIndicator(color: Variables.textPrimary),
+
+ // Result JSON
+ if (_resultJson != null)
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ margin: const EdgeInsets.only(bottom: 24),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey.shade300),
+ ),
+ child: Text(
+ _resultJson!,
+ style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
+ ),
+ ),
+
+ // Button
+ if (!_isLoading) ...[
+ if (_resultJson != null) const SizedBox(height: 24),
+ GestureDetector(
+ onTap: _generateStylesheet,
+ child: Container(
+ width: 274,
+ height: 44,
+ decoration: BoxDecoration(
+ color: Variables.textPrimary,
+ borderRadius: BorderRadius.circular(112),
+ ),
+ alignment: Alignment.center,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ _resultJson == null ? "Generate Stylesheet" : "Regenerate Stylesheet",
+ style: Variables.buttonTextStyle,
+ ),
+ const SizedBox(width: 8),
+ SvgPicture.asset(
+ "assets/icons/generate_icon.svg",
+ width: 18,
+ height: 18,
+ colorFilter: const ColorFilter.mode(
+ Variables.textWhite,
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
],
),
),
),
- ],
- ),
- ),
+ ),
+ );
+ },
),
bottomNavigationBar: BottomBar(