creek

The AI Image Editor of 2030
commit 6a6713a59a3875805a1a0ec97204422629aef83a
parent 25427ccf943480bc19bc16c0f6f38056fc4115d7
Author: maydayv7 <maydayv7@gmail.com>
Date:   Fri,  5 Dec 2025 20:37:48 +0530

Cleanup

Diffstat:

ADeliverables/README.md | 47+++++++++++++++++++++++++++++++++++++++++++++++
MREADME.md | 69++++++++++++++++++++++++---------------------------------------------
Mandroid/.gitignore | 3+--
Mandroid/app/build.gradle.kts | 10++--------
Mandroid/app/src/main/python/analyze_layout.py | 431+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mandroid/app/src/main/python/color_style_infer.py | 54++++++++++++++++++++++++++++++++----------------------
Mandroid/app/src/main/python/instagram_downloader.py | 48++++++++++++++++++++++--------------------------
Mandroid/app/src/main/python/stylesheet_generator.py | 1097++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Massets/dinov2_convert.py | 6+++---
Mflask/index.py | 435++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mlib/ui/pages/define_brand_page.dart | 16----------------
Mlib/ui/pages/image_details_page.dart | 3+--
Mlib/ui/pages/image_save_page.dart | 3---
Mlib/ui/pages/project_file_page.dart | 12+++++-------
Mlib/ui/pages/share_to_file_page.dart | 7+------
Mmodels/color.py | 65+++++++++++++++++++++++++++++++++++++++--------------------------
Mmodels/composition.py | 433+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mmodels/emotions.py | 31++++++++++++++-----------------
Amodels/era_cult.py | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmodels/era_cult_ref.py | 140-------------------------------------------------------------------------------
Amodels/inpainting.py | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmodels/inpainting_api&local.py | 275-------------------------------------------------------------------------------
Mmodels/lighting.py | 64+++++++++++++++++++++++++++-------------------------------------
Amodels/sketch2img.py | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmodels/sketch2img_local_server.py | 62--------------------------------------------------------------
Mmodels/style.py | 102++++++++++++++++++++++++++++++++-----------------------------------------------
Amodels/stylesheet.py | 1173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmodels/stylesheet_gen&prompt_gen.py | 672-------------------------------------------------------------------------------
Mmodels/subject.py | 89+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Amodels/testing.py | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmodels/testing_data_gen_template.py | 153-------------------------------------------------------------------------------
Dmodels/text&mat/texture&mat.py | 441-------------------------------------------------------------------------------
Rmodels/text&mat/dinov2_vits14_fp16.pt -> models/texture/dinov2_vits14_fp16.pt | 0
Amodels/texture/texture.py | 486+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rmodels/text&mat/texture_centroids_s14.pt -> models/texture/texture_centroids_s14.pt | 0
Dmodels/typo.py | 78------------------------------------------------------------------------------
Amodels/typography.py | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
37 files changed, 4292 insertions(+), 2926 deletions(-)

diff --git a/Deliverables/README.md b/Deliverables/README.md @@ -0,0 +1,47 @@ +# Deliverables + +``` +┌── (A) Task 1 +│ ├── Screen Mockups +│ └── Design Rationale +│ +├── (B) Task 2 +│ └── Editing Ecosystem Analysis +│ +└── (C) Task 3 + ├── Technical Report (containing documentation and model/dataset details) + └── Demo Video -> https://drive.google.com/file/d/1bvroGCCosPUEP54q06nFWq1LhNB4PiAf/view?usp=sharing +``` + +Optional Creative Artefacts -> https://drive.google.com/file/d/1kSpf-y0LhQCn9-fF2gMLyodV8R3NkQeR/view?usp=sharing + +# Problem Statement + +Design and prototype a lightweight, mobile-first AI image editor for 2030 that demonstrates how creative editing can become faster, more intuitive, and energy-efficient on low-compute devices. +Your solution should combine design thinking, market research, and AI engineering across three integrated tracks: + +### Task 1: Product Design + +Create a medium-fidelity mobile wire-frame (phone layout) showing your envisioned editor’s interface, navigation, and AI assistance flow. +Emphasize speed, clarity, and human-in-the-loop control, how the user sees what the AI did and how they can refine it. +Consider next-generation inputs such as gestures, stylus, voice prompts, or context-aware auto-suggestions. + +### Task 2: Understanding the Editing Ecosystem + +Analyze the core tool set of modern image editors (crop, retouch, background removal, relighting, stylization, color balance etc.).  +Identify which operations are already automated by AI and which remain +manual. +Select two key AI-powered features or editing workflows to implement in the +execution phase. +Example feature pairs: + +- Object removal + background reconstruction (segmentation + in-painting) +- Lighting adjustment + style transfer (Adapter-based stylization) +- Automatic subject enhancement + color correction (retouching pipeline) + +### Task 3: Execution + +Build the above selected, two AI editing workflows, each corresponding to one of your selected features. +Use open-source diffusion backbones (SDXL Inpainting, Flux 1.1, Kandinsky) or lightweight adapters (LoRA, quantized weights). +You can employ region selection modules (e.g., SAM, Mask R-CNN, matting models) to define editable areas. +Ideally, target lightweight inference and demonstrate via Stream-lit / web prototype. diff --git a/README.md b/README.md @@ -1,53 +1,16 @@ -# TEAM 33 | CreekUI | Re-Imagining Photoshop (The AI Editor of 2030) - -## See the [Demo Video](https://drive.google.com/file/d/1bvroGCCosPUEP54q06nFWq1LhNB4PiAf/view?usp=sharing) +# CreekUI | Re-Imagining Photoshop | The AI Editor of 2030 CreekUI is a lightweight, intent-driven mobile image editor designed to shift the creative workflow from manual pixel manipulation to semantic, AI-assisted design. It separates creativity into structured phases that prioritize **semantic understanding**, **energy efficiency**, and **selective use of heavy compute**. This repository contains the full analysis pipeline, style synthesis logic, and generation workflow that together simulate how a future (circa 2030) mobile NPU might function in creative applications. -> [!NOTE] -> For installation and setup instructions, refer to [INSTALL.md](./INSTALL.md). -> For model details, check out the [models](./models) directory. - -# Deliverables - -The following is an overview of the [Deliverables](./Deliverables/) structure: - -``` -┌── (A) Task 1 -│ ├── Screen Mockups -│ └── Design Rationale -│ -├── (B) Task 2 -│ └── Editing Ecosystem Analysis -│ -└── (C) Task 3 - ├── Technical Report (containing documentation and model/dataset details) - ├── README -> This file - ├── Source Repository -> This Repository - └── Demo Video -> https://drive.google.com/file/d/1bvroGCCosPUEP54q06nFWq1LhNB4PiAf/view?usp=sharing -``` - -Optional Creative Artefacts -> https://drive.google.com/file/d/1kSpf-y0LhQCn9-fF2gMLyodV8R3NkQeR/view?usp=sharing - ---- - -<details> -<summary><b>DATASETS</b></summary> - -- DIS5K-TR Dataset -- FANnet Dataset -- EasyOCR Dataset -- LAION-5B Dataset (_Stable Diffusion v1.5_) -- LAION-AESTHETICS V2 4.5 (_Stable Diffusion v1.5 inpainting_) - -</details> - ---- +### See the [Demo Video](https://drive.google.com/file/d/1bvroGCCosPUEP54q06nFWq1LhNB4PiAf/view?usp=sharing) -> [!IMPORTANT] -> This architecture makes use of models provided by [Fal.ai](https://fal.ai/) API. +> [!NOTE] +> +> - Check out the project [Deliverables](./Deliverables/README.md) +> - For installation and setup instructions, refer to [INSTALL.md](./INSTALL.md) +> - For model details, see the [models](./models) directory --- @@ -174,10 +137,13 @@ Depending on context: - **Sketch-to-Image** uses lightweight or high-quality external backends (Nano Banana, Flux). - **Inpainting** performs localized edits using Stable Diffusion with a mask derived from canvas stroke differences, or FLUX LoRa Fill. -## Local generation is prioritized for privacy-sensitive edits; API generation is used for high fidelity or speed when allowed. +> Local generation is prioritized for privacy-sensitive edits; API generation is used for high fidelity or speed when allowed. # 4. Compute Profile +> [!IMPORTANT] +> Models provided by [Fal.ai](https://fal.ai/) API are used + Approximate characteristics of each model in its quantized / optimized configuration: | Algorithm / Model | Inference Time | Number of Parameters | VRAM Usage | RAM Usage | @@ -197,6 +163,19 @@ Approximate characteristics of each model in its quantized / optimized configura --- +<details> +<summary><b>DATASETS</b></summary> + +- DIS5K-TR Dataset +- FANnet Dataset +- EasyOCR Dataset +- LAION-5B Dataset (_Stable Diffusion v1.5_) +- LAION-AESTHETICS V2 4.5 (_Stable Diffusion v1.5 inpainting_) + +</details> + +--- + # 5. Deployment & Runtime Decisions - **Quantization** provided the best balance of fidelity and memory footprint. diff --git a/android/.gitignore b/android/.gitignore @@ -7,8 +7,7 @@ gradle-wrapper.jar GeneratedPluginRegistrant.java .cxx/ -# Remember to never publicly share your keystore -# See https://flutter.dev/to/reference-keystore +# https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts @@ -21,7 +21,7 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } -// 1. Load the key.properties file generated by CI (or your local file) +// Load the key.properties file generated by CI (or local file) val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { @@ -45,15 +45,12 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.creek.ui" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 24 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName - + ndk { // On Apple silicon, you can omit x86_64. abiFilters += listOf("arm64-v8a", "x86_64") @@ -71,10 +68,7 @@ android { buildTypes { release { - // Signing with the release keys, so `flutter run --release` works. signingConfig = signingConfigs.getByName("release") - - // Optional: Enable shrinking for smaller APKs (recommended for release) isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") diff --git a/android/app/src/main/python/analyze_layout.py b/android/app/src/main/python/analyze_layout.py @@ -4,6 +4,7 @@ import os import json from math import hypot + # Custom JSON encoder for numpy types class NumpyEncoder(json.JSONEncoder): def default(self, obj): @@ -17,23 +18,29 @@ class NumpyEncoder(json.JSONEncoder): return bool(obj) return super(NumpyEncoder, self).default(obj) + # ========================================== # 1. HELPER FUNCTIONS # ========================================== + def resize_for_fast_processing(img, max_side=640): h, w = img.shape[:2] scale = max_side / max(h, w) if max(h, w) > max_side else 1.0 if scale != 1.0: - img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) + img = cv2.resize( + img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA + ) return img, scale + def compute_saliency_gray(img): sal = None try: salience = cv2.saliency.StaticSaliencySpectralResidual_create() success, sal = salience.computeSaliency(img) - if not success: sal = None + if not success: + sal = None except Exception: sal = None if sal is None: @@ -44,41 +51,54 @@ def compute_saliency_gray(img): sal = sal.astype(np.float32) / (sal.max() + 1e-9) else: sal = sal.astype(np.float32) - if sal.max() > 0: sal /= sal.max() + if sal.max() > 0: + sal /= sal.max() return sal + def saliency_centroid(sal): h, w = sal.shape M = sal.sum() - if M <= 1e-6: return (w / 2, h / 2) + if M <= 1e-6: + return (w / 2, h / 2) ys, xs = np.indices(sal.shape) cx = (xs * sal).sum() / M cy = (ys * sal).sum() / M return (float(cx), float(cy)) + def detect_lines(gray, canny_thresh1=50, canny_thresh2=150, hough_thresh=50): edges = cv2.Canny(gray, canny_thresh1, canny_thresh2) - lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=hough_thresh, - minLineLength=min(gray.shape) // 10, maxLineGap=10) - if lines is None: return edges, [] + lines = cv2.HoughLinesP( + edges, + rho=1, + theta=np.pi / 180, + threshold=hough_thresh, + minLineLength=min(gray.shape) // 10, + maxLineGap=10, + ) + if lines is None: + return edges, [] lines = [tuple(l[0]) for l in lines] return edges, lines + def project_sal_along_axis(sal, axis=0, smooth_k=15): - proj = sal.sum(axis=1-axis) if axis == 0 else sal.sum(axis=0) + proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0) k = max(3, smooth_k) kernel = np.ones(k) / k - proj_s = np.convolve(proj, kernel, mode='same') + proj_s = np.convolve(proj, kernel, mode="same") if proj_s.max() > 0: proj_s = proj_s / proj_s.max() return proj_s + def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2): h, w = sal.shape - proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h*0.03))) + proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h * 0.03))) peaks = [] - for i in range(1, len(proj)-1): - if proj[i] > proj[i-1] and proj[i] > proj[i+1] and proj[i] > threshold: + for i in range(1, len(proj) - 1): + if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > threshold: peaks.append(i) min_sep = int(min_sep_fraction * h) filtered = [] @@ -87,107 +107,139 @@ def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2): filtered.append(p) return len(filtered), filtered + def band_saliency_fractions(sal, bands=3): h, w = sal.shape sums = [] total = sal.sum() + 1e-9 for i in range(bands): - band = sal[:, i*w//bands:(i+1)*w//bands] + band = sal[:, i * w // bands : (i + 1) * w // bands] sums.append(band.sum() / total) return sums + def sal_bbox_and_margins(sal, thresh=0.3): mask = sal > thresh if mask.sum() == 0: - return (0,0,0,0), None + return (0, 0, 0, 0), None ys, xs = np.where(mask) minx, maxx = int(xs.min()), int(xs.max()) miny, maxy = int(ys.min()), int(ys.max()) return (minx, miny, maxx, maxy), mask + def component_bboxes_from_mask(mask): - mask_u8 = (mask.astype(np.uint8) * 255) + mask_u8 = mask.astype(np.uint8) * 255 contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) bboxes = [] for c in contours: - x,y,w,h = cv2.boundingRect(c) - bboxes.append((x,y,x+w-1,y+h-1)) + x, y, w, h = cv2.boundingRect(c) + bboxes.append((x, y, x + w - 1, y + h - 1)) return bboxes + def bbox_laplacian_variance(gray, bbox): - x1,y1,x2,y2 = bbox - roi = gray[y1:y2+1, x1:x2+1] - if roi.size == 0: return 0.0 + x1, y1, x2, y2 = bbox + roi = gray[y1 : y2 + 1, x1 : x2 + 1] + if roi.size == 0: + return 0.0 lap = cv2.Laplacian(roi, cv2.CV_64F) return float(np.var(lap)) + # ========================================== # 2. ROBUST CONTINUOUS LAYOUT SCORERS # ========================================== + def _proj_and_peaks(sal, axis=0, smooth_k=15): - proj = sal.sum(axis=1-axis) if axis==0 else sal.sum(axis=0) + proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0) k = max(3, smooth_k) kernel = np.ones(k) / k - proj_s = np.convolve(proj, kernel, mode='same') - if proj_s.max() > 0: proj_s = proj_s / proj_s.max() + proj_s = np.convolve(proj, kernel, mode="same") + if proj_s.max() > 0: + proj_s = proj_s / proj_s.max() return proj_s + def vertical_stack_score(sal, lines, min_peaks=2): - h,w = sal.shape - proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h*0.03))) - peaks = [i for i in range(1,len(proj)-1) if proj[i]>proj[i-1] and proj[i]>proj[i+1] and proj[i]>0.18] - if len(peaks)==0: - return 0.0, {'peaks':0} + h, w = sal.shape + proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h * 0.03))) + peaks = [ + i + for i in range(1, len(proj) - 1) + if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > 0.18 + ] + if len(peaks) == 0: + return 0.0, {"peaks": 0} avg_peak = float(np.mean([proj[p] for p in peaks])) peak_count_score = min(1.0, len(peaks) / max(1.0, min_peaks)) peak_prom_score = avg_peak h_lines = 0 - for (x1,y1,x2,y2) in lines: - dy = abs(y2-y1); dx = abs(x2-x1)+1e-9 - slope = dy/dx - if slope < 0.3 and abs(x2-x1) > 0.6*w: + for x1, y1, x2, y2 in lines: + dy = abs(y2 - y1) + dx = abs(x2 - x1) + 1e-9 + slope = dy / dx + if slope < 0.3 and abs(x2 - x1) > 0.6 * w: h_lines += 1 sep_score = min(1.0, h_lines / 2.0) span_ok = 0 for p in peaks: - row = sal[max(0,p-2):min(h,p+3), :] - if row.sum()<=1e-9: continue + row = sal[max(0, p - 2) : min(h, p + 3), :] + if row.sum() <= 1e-9: + continue col_sum = row.sum(axis=0) - frac_nonzero = (col_sum > (col_sum.max()*0.05)).sum() / float(w) + frac_nonzero = (col_sum > (col_sum.max() * 0.05)).sum() / float(w) if frac_nonzero > 0.65: span_ok += 1 span_score = min(1.0, span_ok / max(1.0, len(peaks))) - score = 0.45*peak_count_score + 0.25*peak_prom_score + 0.15*sep_score + 0.15*span_score - return float(np.clip(score,0.0,1.0)), {'peaks': len(peaks), 'avg_peak': avg_peak, 'h_lines': h_lines, 'span_ok': span_ok} + score = ( + 0.45 * peak_count_score + + 0.25 * peak_prom_score + + 0.15 * sep_score + + 0.15 * span_score + ) + return float(np.clip(score, 0.0, 1.0)), { + "peaks": len(peaks), + "avg_peak": avg_peak, + "h_lines": h_lines, + "span_ok": span_ok, + } + def triptych_score(sal, lines): - h,w = sal.shape + h, w = sal.shape total = sal.sum() + 1e-9 - bands = [sal[:, i*w//3:(i+1)*w//3].sum() / total for i in range(3)] + bands = [sal[:, i * w // 3 : (i + 1) * w // 3].sum() / total for i in range(3)] mn, mx = min(bands), max(bands) balance = 1.0 - (mx - mn) balance = np.clip(balance, 0.0, 1.0) v_hits = 0 - for (x1,y1,x2,y2) in lines: - dx = abs(x2-x1)+1e-9; dy = abs(y2-y1) - slope = dy / dx if dx>1 else 1e9 - if slope > 3 and dy > 0.6*h: - cx = (x1+x2)/2 - if abs(cx - w/3) < 0.08*w or abs(cx - 2*w/3) < 0.08*w: + for x1, y1, x2, y2 in lines: + dx = abs(x2 - x1) + 1e-9 + dy = abs(y2 - y1) + slope = dy / dx if dx > 1 else 1e9 + if slope > 3 and dy > 0.6 * h: + cx = (x1 + x2) / 2 + if abs(cx - w / 3) < 0.08 * w or abs(cx - 2 * w / 3) < 0.08 * w: v_hits += 1 - line_score = np.tanh(v_hits/2.0) + line_score = np.tanh(v_hits / 2.0) band_floor = min(1.0, mn / 0.15) - score = 0.55*balance + 0.25*line_score + 0.20*band_floor - return float(np.clip(score,0.0,1.0)), {'band_fracs': bands, 'v_line_hits': v_hits} - -def full_bleed_score(sal, img, edge_margin_frac=0.03, - edge_sal_thresh=0.25, - sal_bbox_thresh=0.2, - white_thresh=245, - white_frac_thresh=0.80, - edge_sal_frac_thresh=0.50, - bbox_touch_required=0.75): + score = 0.55 * balance + 0.25 * line_score + 0.20 * band_floor + return float(np.clip(score, 0.0, 1.0)), {"band_fracs": bands, "v_line_hits": v_hits} + + +def full_bleed_score( + sal, + img, + edge_margin_frac=0.03, + edge_sal_thresh=0.25, + sal_bbox_thresh=0.2, + white_thresh=245, + white_frac_thresh=0.80, + edge_sal_frac_thresh=0.50, + bbox_touch_required=0.75, +): """ Binary full-bleed detector (returns 0 or 1, plus info). - Rejects as full-bleed if a white margin/border is detected. @@ -202,7 +254,9 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, mask_edge[-m:, :] = True mask_edge[:, :m] = True mask_edge[:, -m:] = True - edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / (mask_edge.sum() + 1e-9) + edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / ( + mask_edge.sum() + 1e-9 + ) # 2) compute saliency bbox touch (based on sal > sal_bbox_thresh) mask2 = sal > sal_bbox_thresh @@ -212,7 +266,9 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, ys, xs = np.where(mask2) minx, maxx = int(xs.min()), int(xs.max()) miny, maxy = int(ys.min()), int(ys.max()) - touches = int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2) + touches = ( + int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2) + ) bbox_touch = touches / 4.0 # 3) check for white margin/border @@ -224,10 +280,15 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, band_right = img[:, -m:, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype) # stack all border samples into one array (may contain duplicates at corners but that's fine) - border_pixels = np.concatenate([band_top.reshape(-1, 3), - band_bottom.reshape(-1, 3), - band_left.reshape(-1, 3), - band_right.reshape(-1, 3)], axis=0) + border_pixels = np.concatenate( + [ + band_top.reshape(-1, 3), + band_bottom.reshape(-1, 3), + band_left.reshape(-1, 3), + band_right.reshape(-1, 3), + ], + axis=0, + ) if border_pixels.size == 0: white_border_frac = 0.0 else: @@ -242,155 +303,199 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, # 4) Decide binary score # Conditions favoring full-bleed: cond_edge_sal = edge_sal_frac >= edge_sal_frac_thresh - cond_bbox_touch = bbox_touch >= bbox_touch_required # e.g. >=0.75 means touching 3 or 4 edges + cond_bbox_touch = ( + bbox_touch >= bbox_touch_required + ) # e.g. >=0.75 means touching 3 or 4 edges # Final logic: require (edge saliency OR bbox touch) AND NOT white border - is_full_bleed = (cond_edge_sal or cond_bbox_touch) and (white_border_frac < white_frac_thresh) and (not (border_uniform and white_border_frac > 0.25)) + is_full_bleed = ( + (cond_edge_sal or cond_bbox_touch) + and (white_border_frac < white_frac_thresh) + and (not (border_uniform and white_border_frac > 0.25)) + ) score = 1 if is_full_bleed else 0 info = { - 'edge_margin_px': m, - 'edge_sal_frac': edge_sal_frac, - 'edge_sal_thresh': edge_sal_thresh, - 'cond_edge_sal': cond_edge_sal, - 'bbox_touch': bbox_touch, - 'bbox_touch_required': bbox_touch_required, - 'white_border_frac': white_border_frac, - 'white_thresh': white_thresh, - 'white_frac_thresh': white_frac_thresh, - 'border_std': border_std, - 'border_uniform': border_uniform, - 'decision_full_bleed': bool(is_full_bleed) + "edge_margin_px": m, + "edge_sal_frac": edge_sal_frac, + "edge_sal_thresh": edge_sal_thresh, + "cond_edge_sal": cond_edge_sal, + "bbox_touch": bbox_touch, + "bbox_touch_required": bbox_touch_required, + "white_border_frac": white_border_frac, + "white_thresh": white_thresh, + "white_frac_thresh": white_frac_thresh, + "border_std": border_std, + "border_uniform": border_uniform, + "decision_full_bleed": bool(is_full_bleed), } return score, info def tight_crop_score(sal, gray, sal_thresh=0.1): - h,w = sal.shape + h, w = sal.shape mask = sal > sal_thresh - if mask.sum()==0: - return 0.0, {'area_frac':0.0, 'tightness':1.0} - ys,xs = np.where(mask) - minx,maxx = int(xs.min()), int(xs.max()) - miny,maxy = int(ys.min()), int(ys.max()) - bbox_area = (maxx-minx+1)*(maxy-miny+1) - area_frac = bbox_area / float(w*h) - left = minx; right = w-1-maxx; top = miny; bottom = h-1-maxy - tightness = min(left,right,top,bottom) / float(min(w,h)+1e-9) + if mask.sum() == 0: + return 0.0, {"area_frac": 0.0, "tightness": 1.0} + ys, xs = np.where(mask) + minx, maxx = int(xs.min()), int(xs.max()) + miny, maxy = int(ys.min()), int(ys.max()) + bbox_area = (maxx - minx + 1) * (maxy - miny + 1) + area_frac = bbox_area / float(w * h) + left = minx + right = w - 1 - maxx + top = miny + bottom = h - 1 - maxy + tightness = min(left, right, top, bottom) / float(min(w, h) + 1e-9) area_score = np.clip((area_frac - 0.15) / (0.6 - 0.15), 0.0, 1.0) tight_score = 1.0 - np.clip(tightness / 0.08, 0.0, 1.0) - score = 0.6*area_score + 0.4*tight_score - return float(np.clip(score,0.0,1.0)), {'area_frac': area_frac, 'tightness': tightness, 'margins': (left,right,top,bottom)} + score = 0.6 * area_score + 0.4 * tight_score + return float(np.clip(score, 0.0, 1.0)), { + "area_frac": area_frac, + "tightness": tightness, + "margins": (left, right, top, bottom), + } + def layered_foreground_score(sal, gray): mask = (sal > 0.25).astype(np.uint8) - kernel = np.ones((5,5), np.uint8) + kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) - contours, _ = cv2.findContours((mask*255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - bboxes=[]; sharpness=[] + contours, _ = cv2.findContours( + (mask * 255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + bboxes = [] + sharpness = [] for c in contours: - x,y,w,h = cv2.boundingRect(c) - if w*h < 0.005*sal.size: continue - bboxes.append((x,y,x+w-1,y+h-1)) - roi = gray[y:y+h, x:x+w] + x, y, w, h = cv2.boundingRect(c) + if w * h < 0.005 * sal.size: + continue + bboxes.append((x, y, x + w - 1, y + h - 1)) + roi = gray[y : y + h, x : x + w] lap = cv2.Laplacian(roi, cv2.CV_64F) sharpness.append(float(np.var(lap))) n = len(bboxes) if n < 2: - return 0.0, {'num_components': n} + return 0.0, {"num_components": n} s = np.array(sharpness) + 1e-9 - s_norm = (s - s.mean()) / (s.std()+1e-9) + s_norm = (s - s.mean()) / (s.std() + 1e-9) sharp_std = float(np.std(s_norm)) overlaps = 0 for i in range(len(bboxes)): - for j in range(i+1, len(bboxes)): - a=bboxes[i]; b=bboxes[j] - ix1 = max(a[0], b[0]); iy1 = max(a[1], b[1]) - ix2 = min(a[2], b[2]); iy2 = min(a[3], b[3]) + for j in range(i + 1, len(bboxes)): + a = bboxes[i] + b = bboxes[j] + ix1 = max(a[0], b[0]) + iy1 = max(a[1], b[1]) + ix2 = min(a[2], b[2]) + iy2 = min(a[3], b[3]) if ix2 >= ix1 and iy2 >= iy1: - inter = (ix2-ix1+1)*(iy2-iy1+1) - area_a = (a[2]-a[0]+1)*(a[3]-a[1]+1) - if inter > 0.05*area_a: + inter = (ix2 - ix1 + 1) * (iy2 - iy1 + 1) + area_a = (a[2] - a[0] + 1) * (a[3] - a[1] + 1) + if inter > 0.05 * area_a: overlaps += 1 - comp_score = np.clip((n-1)/4.0, 0, 1) + comp_score = np.clip((n - 1) / 4.0, 0, 1) sharp_score = np.tanh(sharp_std) - overlap_score = np.tanh(overlaps/2.0) - score = 0.45*comp_score + 0.35*sharp_score + 0.20*overlap_score - return float(np.clip(score,0.0,1.0)), {'num_components': n, 'sharp_std': sharp_std, 'overlaps': overlaps} + overlap_score = np.tanh(overlaps / 2.0) + score = 0.45 * comp_score + 0.35 * sharp_score + 0.20 * overlap_score + return float(np.clip(score, 0.0, 1.0)), { + "num_components": n, + "sharp_std": sharp_std, + "overlaps": overlaps, + } def balance_score(img_bgr, sal): img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) - L = img_lab[:,:,0] / 255.0 + L = img_lab[:, :, 0] / 255.0 img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) - S = img_hsv[:,:,1] / 255.0 + S = img_hsv[:, :, 1] / 255.0 weight = sal * (L + 0.5 * S) M = weight.sum() - h,w = sal.shape - if M <= 1e-6: return 0.0 + h, w = sal.shape + if M <= 1e-6: + return 0.0 ys, xs = np.indices(sal.shape) cx = (xs * weight).sum() / M cy = (ys * weight).sum() / M - dx = abs(cx - w/2) / (w/2) - dy = abs(cy - h/2) / (h/2) - dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) - return float(max(0.0, 1.0 - dist*1.4)) + dx = abs(cx - w / 2) / (w / 2) + dy = abs(cy - h / 2) / (h / 2) + dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist * 1.4)) + def symmetry_score(img_gray): - h,w = img_gray.shape - left = img_gray[:, :w//2] - right = img_gray[:, w - w//2:] + h, w = img_gray.shape + left = img_gray[:, : w // 2] + right = img_gray[:, w - w // 2 :] right_flipped = cv2.flip(right, 1) if left.shape != right_flipped.shape: right_flipped = cv2.resize(right_flipped, (left.shape[1], left.shape[0])) - leftf = left.astype(np.float32); rightf = right_flipped.astype(np.float32) - if leftf.std() < 1e-3 or rightf.std() < 1e-3: return 0.0 - corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0,1] - return float(max(0.0, min(1.0, (corr + 1)/2))) + leftf = left.astype(np.float32) + rightf = right_flipped.astype(np.float32) + if leftf.std() < 1e-3 or rightf.std() < 1e-3: + return 0.0 + corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0, 1] + return float(max(0.0, min(1.0, (corr + 1) / 2))) + def thirds_score(sal, centroid): - h,w = sal.shape + h, w = sal.shape cx, cy = centroid - thirds_pts = [(w/3, h/3), (w/3, 2*h/3), (2*w/3, h/3), (2*w/3, 2*h/3)] - dists = [hypot(cx-x, cy-y) for (x,y) in thirds_pts] + thirds_pts = [ + (w / 3, h / 3), + (w / 3, 2 * h / 3), + (2 * w / 3, h / 3), + (2 * w / 3, 2 * h / 3), + ] + dists = [hypot(cx - x, cy - y) for (x, y) in thirds_pts] maxd = hypot(w, h) - score = 1.0 - min(dists)/maxd*1.2 - vline_dist = min(abs(cx - w/3), abs(cx - 2*w/3)) / w - hline_dist = min(abs(cy - h/3), abs(cy - 2*h/3)) / h + score = 1.0 - min(dists) / maxd * 1.2 + vline_dist = min(abs(cx - w / 3), abs(cx - 2 * w / 3)) / w + hline_dist = min(abs(cy - h / 3), abs(cy - 2 * h / 3)) / h line_score = 1.0 - min(vline_dist, hline_dist) - return float(max(0.0, min(1.0, 0.6*score + 0.4*line_score))) + return float(max(0.0, min(1.0, 0.6 * score + 0.4 * line_score))) + def golden_ratio_score(sal, centroid): - h,w = sal.shape - gx = [0.382, 0.618]; gy = [0.382, 0.618] + h, w = sal.shape + gx = [0.382, 0.618] + gy = [0.382, 0.618] cx, cy = centroid - pts = [(g*w, g2*h) for g in gx for g2 in gy] - dists = [hypot(cx-x, cy-y) for x,y in pts] + pts = [(g * w, g2 * h) for g in gx for g2 in gy] + dists = [hypot(cx - x, cy - y) for x, y in pts] maxd = hypot(w, h) - return float(max(0.0, min(1.0, 1.0 - min(dists)/ (maxd*0.9)))) + return float(max(0.0, min(1.0, 1.0 - min(dists) / (maxd * 0.9)))) + def negative_space_score(sal): low = (sal < 0.15).sum() / sal.size return float(min(1.0, low)) + def center_score(sal, centroid): - h,w = sal.shape + h, w = sal.shape cx, cy = centroid - dx = abs(cx - w/2) / (w/2) - dy = abs(cy - h/2) / (h/2) - dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) - return float(max(0.0, 1.0 - dist*1.2)) + dx = abs(cx - w / 2) / (w / 2) + dy = abs(cy - h / 2) / (h / 2) + dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist * 1.2)) def analyze_single_image(img_path): if not os.path.exists(img_path): - return json.dumps({'scores': {}, 'error': f"File not found at {img_path}"}, cls=NumpyEncoder) + return json.dumps( + {"scores": {}, "error": f"File not found at {img_path}"}, cls=NumpyEncoder + ) img_full = cv2.imread(img_path) if img_full is None: - return json.dumps({'scores': {}, 'error': f"Could not load image. Check format."}, cls=NumpyEncoder) + return json.dumps( + {"scores": {}, "error": f"Could not load image. Check format."}, + cls=NumpyEncoder, + ) img, scale = resize_for_fast_processing(img_full, max_side=640) h, w = img.shape[:2] @@ -402,15 +507,15 @@ def analyze_single_image(img_path): centroid = saliency_centroid(sal) edges, lines = detect_lines(gray) - + # ======= compute composition scores (existing + new layout scores) ======= composition_scores = { - 'Rule of Thirds': thirds_score(sal, centroid), - 'Golden Ratio': golden_ratio_score(sal, centroid), - 'Symmetry': symmetry_score(gray), - 'Negative Space': negative_space_score(sal), - 'Center Composition': center_score(sal, centroid), - 'Balance': balance_score(img, sal) + "Rule of Thirds": thirds_score(sal, centroid), + "Golden Ratio": golden_ratio_score(sal, centroid), + "Symmetry": symmetry_score(gray), + "Negative Space": negative_space_score(sal), + "Center Composition": center_score(sal, centroid), + "Balance": balance_score(img, sal), } v_score, v_info = vertical_stack_score(sal, lines) @@ -419,21 +524,18 @@ def analyze_single_image(img_path): tc_score, tc_info = tight_crop_score(sal, gray) lf_score, lf_info = layered_foreground_score(sal, gray) - composition_scores['Vertical Stack'] = v_score - composition_scores['Triptych'] = t_score - composition_scores['Full Bleed'] = fb_score - composition_scores['Tight Crop'] = tc_score - composition_scores['Layered Foreground'] = lf_score + composition_scores["Vertical Stack"] = v_score + composition_scores["Triptych"] = t_score + composition_scores["Full Bleed"] = fb_score + composition_scores["Tight Crop"] = tc_score + composition_scores["Layered Foreground"] = lf_score diagnostics = { - 'Vertical Stack_info': v_info, - 'Triptych_info': t_info, - 'Full Bleed_info': fb_info, - 'Tight Crop_info': tc_info, - 'Layered Foreground_info': lf_info - } - result = { - 'scores': composition_scores, - 'diagnostics': diagnostics + "Vertical Stack_info": v_info, + "Triptych_info": t_info, + "Full Bleed_info": fb_info, + "Tight Crop_info": tc_info, + "Layered Foreground_info": lf_info, } - return json.dumps(result, cls=NumpyEncoder) -\ No newline at end of file + result = {"scores": composition_scores, "diagnostics": diagnostics} + return json.dumps(result, cls=NumpyEncoder) diff --git a/android/app/src/main/python/color_style_infer.py b/android/app/src/main/python/color_style_infer.py @@ -9,6 +9,7 @@ from sklearn.cluster import KMeans # HELPER FUNCTIONS # ========================================== + def get_dominant_colors(img_rgb, k=5): """ Extracts dominant colors using KMeans. @@ -21,16 +22,14 @@ def get_dominant_colors(img_rgb, k=5): pixels = img_small.reshape((-1, 3)) # KMeans Clustering - # FIX: n_init='auto' crashes on older sklearn versions found in Chaquopy. - # We use n_init=10 which is the standard default for older versions. kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) kmeans.fit(pixels) - colors = kmeans.cluster_centers_.astype(int) # Sort by brightness (Sum of RGB channels) return sorted(colors.tolist(), key=lambda x: sum(x)) + def classify_mood(rgb_colors): """ Classifies mood based on HSV values. @@ -41,11 +40,11 @@ def classify_mood(rgb_colors): # Reshape to (1, N, 3) image format for cv2.cvtColor img_reshaped = norm_colors.reshape(1, -1, 3) - + # Convert RGB to HSV # OpenCV with float32 input returns: H[0-360], S[0-1], V[0-1] hsv_img = cv2.cvtColor(img_reshaped, cv2.COLOR_RGB2HSV) - hsv_stats = hsv_img[0] # Shape (N, 3) + hsv_stats = hsv_img[0] # Shape (N, 3) # Normalize Hue to 0-1 range to match original logic (Matplotlib uses 0-1) hsv_stats[:, 0] /= 360.0 @@ -56,50 +55,59 @@ def classify_mood(rgb_colors): avg_val = np.mean(hsv_stats[:, 2]) # Logic Rules - if avg_sat < 0.15 and avg_val > 0.65: return "Minimalist" - if avg_val < 0.35: return "Dark/Moody" - if avg_sat < 0.45 and avg_val > 0.75: return "Pastel" - if avg_sat > 0.65 and avg_val > 0.5: return "Neon" + if avg_sat < 0.15 and avg_val > 0.65: + return "Minimalist" + if avg_val < 0.35: + return "Dark/Moody" + if avg_sat < 0.45 and avg_val > 0.75: + return "Pastel" + if avg_sat > 0.65 and avg_val > 0.5: + return "Neon" # Earthy logic: Hue between 0.02 and 0.42 (approx 7 to 150 deg), low saturation earthy_votes = sum(1 for p in hsv_stats if (0.02 <= p[0] <= 0.42) and p[1] < 0.8) - if earthy_votes >= 3: return "Earthy" + if earthy_votes >= 3: + return "Earthy" # Warm/Cool logic: Warm is usually red/orange/yellow (low Hue or very high Hue) warm_votes = sum(1 for p in hsv_stats if p[0] < 0.17 or p[0] > 0.83) return "Warm" if warm_votes >= 3 else "Cool" + def rgb_to_hex(rgb): - return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2]) + return "#{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]) + # ========================================== # MAIN API # ========================================== + def analyze_color_style(image_path): try: # 1. Load Image # OpenCV reads in BGR by default img_bgr = cv2.imread(image_path) if img_bgr is None: - return json.dumps({"success": False, "scores": {}, "error": "CV2 could not read image"}) - + return json.dumps( + {"success": False, "scores": {}, "error": "CV2 could not read image"} + ) + # Convert to RGB img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 2. Extract Colors colors = get_dominant_colors(img_rgb) - + # 3. Classify Mood mood = classify_mood(colors) - + # 4. Format Results hex_colors = [rgb_to_hex(c) for c in colors] response = { "success": True, - # We return the mood as a score of 1.0 to maintain compatibility - # with existing UI components that expect a map of scores. + # Return the mood as a score of 1.0 for compatibility "scores": {mood: 1.0}, "palette": hex_colors, "error": None, @@ -108,8 +116,10 @@ def analyze_color_style(image_path): return json.dumps(response) except Exception as e: - return json.dumps({ - "success": False, - "scores": {}, - "error": f"Python Exception: {str(e)} | {traceback.format_exc()}" - }) + return json.dumps( + { + "success": False, + "scores": {}, + "error": f"Python Exception: {str(e)} | {traceback.format_exc()}", + } + ) diff --git a/android/app/src/main/python/instagram_downloader.py b/android/app/src/main/python/instagram_downloader.py @@ -4,6 +4,7 @@ import os import json from pathlib import Path + def download_instagram_image(url, output_dir): """ Downloads all images from an Instagram post. @@ -14,9 +15,9 @@ def download_instagram_image(url, output_dir): # Initialize Instaloader L = instaloader.Instaloader( download_pictures=True, - download_videos=False, + download_videos=False, download_video_thumbnails=False, - download_geotags=False, + download_geotags=False, download_comments=False, save_metadata=False, compress_json=False, @@ -24,12 +25,14 @@ def download_instagram_image(url, output_dir): L.filename_pattern = "{shortcode}" # Extract shortcode from URL - shortcode_match = re.search(r'instagram\.com/(?:p|reel|tv)/([^/?#&]+)', url) + shortcode_match = re.search(r"instagram\.com/(?:p|reel|tv)/([^/?#&]+)", url) if not shortcode_match: - return json.dumps({ - "success": False, - "error": "Could not extract shortcode. Use format: https://instagram.com/p/ShortCode/" - }) + return json.dumps( + { + "success": False, + "error": "Could not extract shortcode. Use format: https://instagram.com/p/ShortCode/", + } + ) shortcode = shortcode_match.group(1) # Download post @@ -37,7 +40,7 @@ def download_instagram_image(url, output_dir): L.download_post(post, target=Path(output_dir)) found_files = [] - extensions = ['jpg', 'png', 'webp', 'jpeg'] + extensions = ["jpg", "png", "webp", "jpeg"] patterns = [f"{shortcode}.{{ext}}", f"{shortcode}_*.{{ext}}"] output_path = Path(output_dir) @@ -50,25 +53,19 @@ def download_instagram_image(url, output_dir): found_files = sorted(list(set(found_files))) if found_files: - return json.dumps({ - "success": True, - "file_paths": found_files, - "shortcode": shortcode - }) + return json.dumps( + {"success": True, "file_paths": found_files, "shortcode": shortcode} + ) else: all_files = [f.name for f in output_path.glob("*")] - return json.dumps({ - "success": False, - "error": f"Downloaded but no images found. Files in dir: {all_files}" - }) + return json.dumps( + { + "success": False, + "error": f"Downloaded but no images found. Files in dir: {all_files}", + } + ) except instaloader.exceptions.InstaloaderException as e: - return json.dumps({ - "success": False, - "error": f"Instaloader error: {str(e)}" - }) + return json.dumps({"success": False, "error": f"Instaloader error: {str(e)}"}) except Exception as e: - return json.dumps({ - "success": False, - "error": f"Python error: {str(e)}" - }) -\ No newline at end of file + return json.dumps({"success": False, "error": f"Python error: {str(e)}"}) diff --git a/android/app/src/main/python/stylesheet_generator.py b/android/app/src/main/python/stylesheet_generator.py @@ -9,9 +9,10 @@ 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 +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 @@ -19,15 +20,18 @@ PALETTE_SIZE = 5 # Number of colors in final palette def hex_to_rgb(hex_str: str) -> List[int]: """Converts '#FF5733' to [255, 87, 51] for math operations.""" try: - hex_str = hex_str.strip().lstrip('#') - if len(hex_str) != 6: return [0, 0, 0] - return [int(hex_str[i:i+2], 16) for i in (0, 2, 4)] + hex_str = hex_str.strip().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])) + return "#{:02x}{:02x}{:02x}".format(int(rgb[0]), int(rgb[1]), int(rgb[2])) + # ========================================== # KNOWLEDGE BASE: FONTS @@ -35,231 +39,667 @@ def rgb_to_hex(rgb: List[int]) -> str: 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" + "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() + clean = raw_name.lower().split("-")[0].strip() if clean in self.full_font_map: return self.full_font_map[clean] - return "Display/Other" # Default fallback + return "Display/Other" # Default fallback + # ========================================== # UNIFIED CONSENSUS ENGINE @@ -275,19 +715,27 @@ class UnifiedStyleEngine: self.doc_counter = 0 self.material_filter = { - "glossy", "matte", "metallic", "organic", - "wood", "laminated", "porcelain", "wet-look", - "denim", "fabric", "leather" + "glossy", + "matte", + "metallic", + "organic", + "wood", + "laminated", + "porcelain", + "wet-look", + "denim", + "fabric", + "leather", } self.aliases = { "Colour Palette": "Color Palette", - "Texture": "Background/Texture", + "Texture": "Background/Texture", "Era": "Era/Cultural Reference", "Font": "Typography", "Fonts": "Typography", "Text": "Typography", - "Layout": "Composition" + "Layout": "Composition", } def ingest_data(self, data: Dict): @@ -313,7 +761,8 @@ class UnifiedStyleEngine: # 2. Iterate through categories for category, payload in source_data.items(): - if category in ["filename", "meta"]: continue + if category in ["filename", "meta"]: + continue # Map "Font" -> "Typography", etc. std_category = self.aliases.get(category, category) @@ -322,24 +771,25 @@ class UnifiedStyleEngine: # NOTE: We intentionally SKIP processing 'scores' (moods) here if std_category == "Color Palette": if isinstance(payload, dict) and "palette" in payload: - for hex_code in payload["palette"]: - if isinstance(hex_code, str) and hex_code.startswith('#'): - self.color_pool.append(hex_to_rgb(hex_code)) + for hex_code in payload["palette"]: + if isinstance(hex_code, str) and hex_code.startswith("#"): + self.color_pool.append(hex_to_rgb(hex_code)) # Fallback: if payload is just a list of hex strings elif isinstance(payload, list): for item in payload: - if isinstance(item, str) and item.startswith('#'): + if isinstance(item, str) and item.startswith("#"): self.color_pool.append(hex_to_rgb(item)) # PATH B: TYPOGRAPHY elif std_category == "Typography": - if isinstance(payload, dict) and "scores" in payload: + if isinstance(payload, dict) and "scores" in payload: payload = payload["scores"] vectors = self._normalize(payload) for rank, (font_name, score) in enumerate(vectors): # Skip "No Text Detected" - if "no text" in font_name.lower(): continue + if "no text" in font_name.lower(): + continue weight = math.pow(RANK_DECAY_FACTOR, rank) * score cluster = self.font_engine.get_cluster(font_name) @@ -351,15 +801,14 @@ class UnifiedStyleEngine: # PATH C: OTHER TAGS else: - if isinstance(payload, dict) and "scores" in payload: + if isinstance(payload, dict) and "scores" in payload: payload = payload["scores"] vectors = self._normalize(payload) for rank, (label, score) in enumerate(vectors): - self.feature_registry[std_category][label].append({ - "raw_score": score, - "rank": rank - }) + self.feature_registry[std_category][label].append( + {"raw_score": score, "rank": rank} + ) except Exception as e: print(f"Warning during ingestion: {e}") @@ -382,14 +831,15 @@ class UnifiedStyleEngine: for item in payload: if isinstance(item, dict): try: - score = float(item.get('score', 0.9)) + 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) - + 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)) @@ -404,14 +854,17 @@ class UnifiedStyleEngine: 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) + 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: + candidates.sort(key=lambda x: x["score"], reverse=True) + if candidates: final_json["results"][category] = candidates[:5] # 1.5 Split Texture into 'Background/Texture' and 'Material Look' @@ -426,11 +879,11 @@ class UnifiedStyleEngine: if item["label"] in self.material_filter: is_material = True else: - # Partial matching (e.g. "wood grain" -> wood) - for mat in self.material_filter: - if mat in label: - is_material = True - break + # Partial matching (e.g. "wood grain" -> wood) + for mat in self.material_filter: + if mat in label: + is_material = True + break if is_material: materials.append(item) @@ -447,13 +900,17 @@ class UnifiedStyleEngine: # Merge with existing Material Look if present if "Material Look" in final_json["results"]: final_json["results"]["Material Look"].extend(materials) - final_json["results"]["Material Look"].sort(key=lambda x: x['score'], reverse=True) + final_json["results"]["Material Look"].sort( + key=lambda x: x["score"], reverse=True + ) else: final_json["results"]["Material Look"] = materials # 2. Process Typography if self.font_family_scores: - best_fam, _ = sorted(self.font_family_scores.items(), key=lambda x: x[1], reverse=True)[0] + 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) @@ -478,10 +935,9 @@ class UnifiedStyleEngine: palette_output = [] for i, rgb in enumerate(centers): - palette_output.append({ - "label": rgb_to_hex(rgb), - "score": round(1.0 - (i * 0.1), 2) - }) + 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}") @@ -489,6 +945,7 @@ class UnifiedStyleEngine: final_json["results"]["meta"] = {"source_files": self.doc_counter} return final_json + # ========================================== # ENTRY POINT # ========================================== @@ -503,7 +960,8 @@ def generate_stylesheet(json_strings_list: Any) -> str: count = json_strings_list.size() for i in range(count): json_str = json_strings_list.get(i) - if not json_str: continue + if not json_str: + continue try: data = json.loads(json_str) engine.ingest_data(data) @@ -514,15 +972,18 @@ def generate_stylesheet(json_strings_list: Any) -> str: except AttributeError: # Fallback for standard Python list (For testing) for json_str in json_strings_list: - if not json_str: continue + if not json_str: + continue try: data = json.loads(json_str) engine.ingest_data(data) - except: continue + except: + continue final_output = engine.compute_final_stylesheet() return json.dumps(final_output) + # ========================================== # PROMPT GENERATION LOGIC # ========================================== @@ -536,7 +997,7 @@ DEFAULT_OPTIONS = { "seed": "random", } -# ---- normalize sheet ---- + def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: """ Organizes the flat stylesheet tags into Primary and Secondary categories. @@ -545,7 +1006,11 @@ def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: norm: Dict[str, Dict[str, List[str]]] = {} for cat, val in results.items(): - if cat in ("filename", "meta", "Color Palette"): # Explicitly ignore Palette here if desired, or later + if cat in ( + "filename", + "meta", + "Color Palette", + ): # Explicitly ignore Palette here if desired, or later continue items = [] @@ -569,14 +1034,43 @@ def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: return norm -# ---- remove style words helpers ---- + STYLE_WORDS = [ - "hand-drawn", "hand drawn", "sketch", "thin lines", "minimalistic", - "minimal", "flat", "cartoon", "comic", "pop-art", "surreal", "photorealistic", - "realistic", "painting", "oil painting", "pastel", "artistic", "illustration", - "line drawing", "line art", "3d effect", "texture", "pattern", "soft light", - "studio light", "dramatic", "cinematic", "moody", "vintage", "retro", "poster", - "render", "octane render", "unreal engine","digital illustration", + "hand-drawn", + "hand drawn", + "sketch", + "thin lines", + "minimalistic", + "minimal", + "flat", + "cartoon", + "comic", + "pop-art", + "surreal", + "photorealistic", + "realistic", + "painting", + "oil painting", + "pastel", + "artistic", + "illustration", + "line drawing", + "line art", + "3d effect", + "texture", + "pattern", + "soft light", + "studio light", + "dramatic", + "cinematic", + "moody", + "vintage", + "retro", + "poster", + "render", + "octane render", + "unreal engine", + "digital illustration", ] style_words_regex = re.compile( @@ -584,54 +1078,68 @@ style_words_regex = re.compile( flags=re.IGNORECASE, ) + def remove_style_words(caption: str) -> str: """ - Removes specific artistic style descriptions from the caption so they + Removes specific artistic style descriptions from the caption so they don't conflict with the Stylesheet. """ - if not caption: return caption + if not caption: + return caption clean = style_words_regex.sub(" ", caption) # Collapse multiple spaces into one clean = re.sub(r"\s{2,}", " ", clean).strip() return clean + def extract_subject(caption_text: str) -> str: """ Heuristic to guess the main subject (first few words of the first sentence). """ txt = caption_text.strip() - if not txt: return "subject" + if not txt: + return "subject" first_clause = re.split(r"[.?!]\s*", txt)[0] return " ".join(first_clause.split()[:12]) -def pick_style_phrases(norm: Dict[str, Dict[str, List[str]]], max_secondaries=3) -> List[str]: + +def pick_style_phrases( + norm: Dict[str, Dict[str, List[str]]], max_secondaries=3 +) -> List[str]: """ Extracts non-color features from the Stylesheet. """ cats = [ - "Style", "Background/Texture", "Lighting", "Composition", - "Era/Cultural Reference", "Material Look", "Typography", + "Style", + "Background/Texture", + "Lighting", + "Composition", + "Era/Cultural Reference", + "Material Look", + "Typography", ] # Mappings for cleaner prompt text key_map = { - "Style": "art style", + "Style": "art style", "Background/Texture": "background texture", - "Lighting": "lighting", + "Lighting": "lighting", "Composition": "composition", "Era/Cultural Reference": "era", - "Material Look": "material", + "Material Look": "material", "Typography": "typography", } out: List[str] = [] for cat in cats: entry = norm.get(cat) - if not entry: continue + if not entry: + continue prim = entry.get("Primary") secs = entry.get("Secondary", []) chosen: List[str] = [] - if prim: chosen.append(prim) + if prim: + chosen.append(prim) for s in secs: if s and len(chosen) < (1 + max_secondaries): chosen.append(s) @@ -640,6 +1148,7 @@ def pick_style_phrases(norm: Dict[str, Dict[str, List[str]]], max_secondaries=3) out.append(f"{key_map.get(cat, cat)}: {', '.join(chosen)}") return out + def style_to_tone(primary: str) -> str: """ Infers a general tone based on the primary style. @@ -651,10 +1160,13 @@ def style_to_tone(primary: str) -> str: return "photorealistic, high-detail" return "high detail, sharp focus" + # ========================================== # ENTRY POINT FOR PROMPT GENERATION # ========================================== -def generate_magic_prompt(stylesheet_json_str: str, caption: str, user_prompt: str) -> str: +def generate_magic_prompt( + stylesheet_json_str: str, caption: str, user_prompt: str +) -> str: try: sheet = json.loads(stylesheet_json_str) except Exception as e: @@ -671,14 +1183,12 @@ def generate_magic_prompt(stylesheet_json_str: str, caption: str, user_prompt: s # 3. EXTRACT STYLESHEET FEATURES (Ignoring Color) style_phrases = pick_style_phrases(norm) - + # 4. INFER TONE style_primary = (norm.get("Style") or {}).get("Primary", "") tone_hint = style_to_tone(style_primary) # 5. CONSTRUCT PROMPT - # We do NOT append 'top_color_list' from the sheet. - style_instruction = "" if style_phrases: style_instruction = "Style elements: " + "; ".join(style_phrases) @@ -686,13 +1196,12 @@ def generate_magic_prompt(stylesheet_json_str: str, caption: str, user_prompt: s prompt_lines: List[str] = [ f"Main subject: {subject}.", - # cleaned_caption contains the original colors from the image - f"Description: {cleaned_caption}.", + f"Description: {cleaned_caption}.", ] - + if user_prompt: prompt_lines.append(f"User instruction: {user_prompt}.") - + if style_instruction: prompt_lines.append(f"{style_instruction}.") diff --git a/assets/dinov2_convert.py b/assets/dinov2_convert.py @@ -4,8 +4,8 @@ from onnx.external_data_helper import convert_model_to_external_data # -------------------------------------------------------------- # CONFIG # -------------------------------------------------------------- -in_model = "assets/dinov2.onnx" # your original IR10 model -out_model = "dinov2.onnx" # final IR9 model +in_model = "assets/dinov2.onnx" # your original IR10 model +out_model = "dinov2.onnx" # final IR9 model data_file = "dinov2.onnx.data" tensor_threshold = 1024 # everything >1KB will go into the .data file # -------------------------------------------------------------- @@ -22,7 +22,7 @@ convert_model_to_external_data( all_tensors_to_one_file=True, location=data_file, size_threshold=tensor_threshold, - convert_attribute=False + convert_attribute=False, ) # 4) Save model diff --git a/flask/index.py b/flask/index.py @@ -19,8 +19,10 @@ import time import uuid from Crypto.Cipher import AES from dotenv import load_dotenv + load_dotenv() + class CryptoManager: def __init__(self, key_base64): # Decode the base64 key to raw bytes (must be 32 bytes for AES-256) @@ -34,13 +36,13 @@ class CryptoManager: cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) # 3. Encrypt and get Tag (MAC) - ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode('utf-8')) + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode("utf-8")) # 4. Pack: Nonce + Ciphertext + Tag combined = nonce + ciphertext + tag # 5. Return as Base64 string - return base64.b64encode(combined).decode('utf-8') + return base64.b64encode(combined).decode("utf-8") def decrypt(self, encrypted_b64): try: @@ -55,11 +57,12 @@ class CryptoManager: # 3. Decrypt cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) - return decrypted_data.decode('utf-8') + return decrypted_data.decode("utf-8") except Exception as e: print(f"Decryption failed: {e}") return None + # Setup Secret Key SHARED_SECRET_KEY = os.getenv("SHARED_SECRET_KEY") if not SHARED_SECRET_KEY: @@ -68,6 +71,7 @@ if not SHARED_SECRET_KEY: crypto = CryptoManager(SHARED_SECRET_KEY) + # ============================================================================== # SECURITY DECORATOR (Middleware) # ============================================================================== @@ -78,10 +82,17 @@ def secure_endpoint(f): try: # Expecting JSON format: { "data": "BASE64_ENCRYPTED_STRING" } incoming = request.get_json(silent=True) - if not incoming or 'data' not in incoming: - return jsonify({"error": "Invalid format. Expected {'data': 'encrypted_string'}"}), 400 + if not incoming or "data" not in incoming: + return ( + jsonify( + { + "error": "Invalid format. Expected {'data': 'encrypted_string'}" + } + ), + 400, + ) - encrypted_b64 = incoming['data'] + encrypted_b64 = incoming["data"] decrypted_json_str = crypto.decrypt(encrypted_b64) if decrypted_json_str is None: @@ -106,10 +117,11 @@ def secure_endpoint(f): status_code = 200 if isinstance(response, tuple): resp_obj = response[0] - if len(response) > 1: status_code = response[1] + if len(response) > 1: + status_code = response[1] # Extract the plain JSON data from the Response object - if hasattr(resp_obj, 'get_json'): + if hasattr(resp_obj, "get_json"): plain_data = resp_obj.get_json() else: # Fallback if it's not a response object yet @@ -127,9 +139,11 @@ def secure_endpoint(f): return decorated_function + # --- FAL.AI IMPORTS --- try: import fal_client + # SETUP API KEY if os.getenv("FAL_KEY"): os.environ["FAL_KEY"] = os.getenv("FAL_KEY") @@ -147,9 +161,12 @@ try: from transformers import AutoModelForCausalLM, AutoProcessor, BitsAndBytesConfig import transformers.dynamic_module_utils import torch.nn as nn + FLORENCE_AVAILABLE = True except ImportError as e: - print(f"⚠️ Florence-2 Disabled: {e} (Ensure 'bitsandbytes' and 'transformers' are installed)") + print( + f"⚠️ Florence-2 Disabled: {e} (Ensure 'bitsandbytes' and 'transformers' are installed)" + ) FLORENCE_AVAILABLE = False except Exception as e: print(f"⚠️ Florence-2 Disabled: Unexpected initialization error: {e}") @@ -168,15 +185,19 @@ else: try: from models.birefnet import BiRefNet + print("✅ BiRefNet imported successfully.") except ImportError as e: print(f"⚠️ Import Error: {e}") try: import BiRefNet.models.birefnet as brn + BiRefNet = brn.BiRefNet print("✅ BiRefNet imported via package path.") except ImportError: - print("❌ Failed to import BiRefNet. Ensure 'BiRefNet/models/birefnet.py' exists.") + print( + "❌ Failed to import BiRefNet. Ensure 'BiRefNet/models/birefnet.py' exists." + ) # --- IMPORT STABLE DIFFUSION --- try: @@ -189,7 +210,7 @@ except ImportError: app = Flask(__name__) CORS(app) -DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' +DEVICE = "cuda" if torch.cuda.is_available() else "cpu" print(f"🚀 Running on device: {DEVICE}") # ============================================================================== @@ -199,15 +220,19 @@ print("⏳ Loading Stable Diffusion (Inpainting)...") sd_pipe = None try: if StableDiffusionInpaintPipeline: - SD_MODEL_ID = "./local_inpainting_model" if os.path.exists("./local_inpainting_model") else "runwayml/stable-diffusion-inpainting" - + SD_MODEL_ID = ( + "./local_inpainting_model" + if os.path.exists("./local_inpainting_model") + else "runwayml/stable-diffusion-inpainting" + ) + sd_pipe = StableDiffusionInpaintPipeline.from_pretrained( SD_MODEL_ID, - torch_dtype=torch.float16 if DEVICE == 'cuda' else torch.float32, + torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32, use_safetensors=True, ).to(DEVICE) sd_pipe.enable_attention_slicing() - sd_pipe.enable_model_cpu_offload() + sd_pipe.enable_model_cpu_offload() print("✅ Stable Diffusion Loaded!") except Exception as e: print(f"❌ Failed to load SD: {e}") @@ -221,12 +246,12 @@ BIREFNET_WEIGHTS = "./BiRefNet/birefnet_fp16.pt" BIREFNET_SIZE = (1024, 1024) try: - if 'BiRefNet' in locals() and os.path.exists(BIREFNET_WEIGHTS): + if "BiRefNet" in locals() and os.path.exists(BIREFNET_WEIGHTS): birefnet_model = BiRefNet(bb_pretrained=False) state_dict = torch.load(BIREFNET_WEIGHTS, map_location=DEVICE) birefnet_model.load_state_dict(state_dict) birefnet_model.to(DEVICE) - if DEVICE == 'cuda': + if DEVICE == "cuda": birefnet_model.half() birefnet_model.eval() print("✅ BiRefNet Weights Loaded!") @@ -235,11 +260,13 @@ try: except Exception as e: print(f"❌ Failed to load BiRefNet: {e}") -transform_birefnet = transforms.Compose([ - transforms.Resize(BIREFNET_SIZE), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), -]) +transform_birefnet = transforms.Compose( + [ + transforms.Resize(BIREFNET_SIZE), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] +) # ============================================================================== # 3. LOAD FLORENCE-2 (QUANTIZED) @@ -251,37 +278,45 @@ FLORENCE_PATH = os.path.join(current_dir, "Florence-2-4bit-Quantized") if FLORENCE_AVAILABLE: try: - def check_imports_fixed(filename): return [] + + def check_imports_fixed(filename): + return [] + transformers.dynamic_module_utils.check_imports = check_imports_fixed _old_getattr = nn.Module.__getattr__ + def _fixed_getattr(self, name): if name == "_supports_sdpa": return False return _old_getattr(self, name) + nn.Module.__getattr__ = _fixed_getattr if os.path.exists(FLORENCE_PATH): bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", - bnb_4bit_compute_dtype=torch.float16 + bnb_4bit_compute_dtype=torch.float16, ) - + florence_model = AutoModelForCausalLM.from_pretrained( FLORENCE_PATH, quantization_config=bnb_config, trust_remote_code=True, - device_map="cuda" if DEVICE == 'cuda' else 'cpu', - local_files_only=True + device_map="cuda" if DEVICE == "cuda" else "cpu", + local_files_only=True, + ) + florence_processor = AutoProcessor.from_pretrained( + FLORENCE_PATH, trust_remote_code=True ) - florence_processor = AutoProcessor.from_pretrained(FLORENCE_PATH, trust_remote_code=True) print("✅ Florence-2 Loaded Successfully!") else: print(f"⚠️ Florence-2 folder not found at: {FLORENCE_PATH}") except Exception as e: print(f"❌ Failed to load Florence-2: {e}") + # ============================================================================== # HELPER FUNCTIONS # ============================================================================== @@ -290,19 +325,22 @@ def decode_base64_image(b64_str): b64_str = b64_str.split(",")[1] image_data = base64.b64decode(b64_str) img = Image.open(io.BytesIO(image_data)) - - if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info): - background = Image.new('RGB', img.size, (255, 255, 255)) - if img.mode == 'P': img = img.convert('RGBA') + + if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info): + background = Image.new("RGB", img.size, (255, 255, 255)) + if img.mode == "P": + img = img.convert("RGBA") background.paste(img, mask=img.split()[3]) return background else: return img.convert("RGB") + def encode_image_to_base64(pil_img): buffered = io.BytesIO() pil_img.save(buffered, format="PNG") - return base64.b64encode(buffered.getvalue()).decode('utf-8') + return base64.b64encode(buffered.getvalue()).decode("utf-8") + def process_birefnet_output(preds, original_size): if isinstance(preds, (list, tuple)): @@ -312,15 +350,16 @@ def process_birefnet_output(preds, original_size): pred_tensor = pred_tensor.sigmoid().cpu() mask_np = pred_tensor.squeeze().numpy().astype(np.float32) - + if len(mask_np.shape) > 2: mask_np = mask_np[0] mask_resized = cv2.resize(mask_np, original_size, interpolation=cv2.INTER_LINEAR) mask = (mask_resized > 0.5).astype(np.uint8) * 255 - + return Image.fromarray(mask) + def resize_to_limit(img, max_dim=1024, multiple=8): w, h = img.size ratio = min(max_dim / w, max_dim / h) @@ -328,60 +367,78 @@ def resize_to_limit(img, max_dim=1024, multiple=8): new_h = int(h * ratio) new_w = new_w - (new_w % multiple) new_h = new_h - (new_h % multiple) - if new_w < multiple: new_w = multiple - if new_h < multiple: new_h = multiple + if new_w < multiple: + new_w = multiple + if new_h < multiple: + new_h = multiple return img.resize((new_w, new_h), Image.LANCZOS) + # ============================================================================== # ROUTES # ============================================================================== -@app.route('/') + +@app.route("/") def index(): return "Image Processing API is running." -@app.route('/test-encrypt', methods=['POST']) + +@app.route("/test-encrypt", methods=["POST"]) def test_encrypt(): # Helper route to debug encryption/decryption try: data = request.get_json() - plain_text = data.get('text', 'Hello, World!') + plain_text = data.get("text", "Hello, World!") encrypted = crypto.encrypt(plain_text) decrypted = crypto.decrypt(encrypted) - return jsonify({ - "original": plain_text, - "encrypted": encrypted, - "decrypted": decrypted - }) + return jsonify( + {"original": plain_text, "encrypted": encrypted, "decrypted": decrypted} + ) except Exception as e: return jsonify({"error": str(e)}), 500 -@app.route('/generate', methods=['POST']) + +@app.route("/generate", methods=["POST"]) @secure_endpoint def generate_image(): - if not sd_pipe: return jsonify({"error": "SD Model not loaded"}), 500 + if not sd_pipe: + return jsonify({"error": "SD Model not loaded"}), 500 try: data = request.get_json() - prompt = data.get('prompt', 'The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.') + prompt = data.get( + "prompt", + "The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", + ) empty_image = Image.new("RGB", (512, 512), (0, 0, 0)) full_mask = Image.new("L", (512, 512), 255) print(f"🎨 Generating: {prompt}") - image = sd_pipe(prompt=prompt, image=empty_image, mask_image=full_mask, height=512, width=512, num_inference_steps=30).images[0] + image = sd_pipe( + prompt=prompt, + image=empty_image, + mask_image=full_mask, + height=512, + width=512, + num_inference_steps=30, + ).images[0] return jsonify({"status": "success", "image": encode_image_to_base64(image)}) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 -@app.route('/inpainting', methods=['POST']) + +@app.route("/inpainting", methods=["POST"]) @secure_endpoint def inpaint_image(): - if not sd_pipe: return jsonify({"error": "SD Model not loaded"}), 500 + if not sd_pipe: + return jsonify({"error": "SD Model not loaded"}), 500 try: data = request.get_json() - user_prompt = data.get('prompt', '') - clean_b64 = data.get('image') - drawn_b64 = data.get('mask_image') + user_prompt = data.get("prompt", "") + clean_b64 = data.get("image") + drawn_b64 = data.get("mask_image") - if not clean_b64 or not drawn_b64: return jsonify({"error": "Missing image or mask"}), 400 + if not clean_b64 or not drawn_b64: + return jsonify({"error": "Missing image or mask"}), 400 # 1. Decode Images raw_clean = decode_base64_image(clean_b64).convert("RGB") @@ -393,11 +450,15 @@ def inpaint_image(): img_drawn = raw_drawn.resize(img_clean.size) print(f"🔍 Calculating Robust Difference Mask (Size: {img_clean.size})...") - clean_blur = np.array(img_clean.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16) - drawn_blur = np.array(img_drawn.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16) + clean_blur = np.array( + img_clean.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 + ) + drawn_blur = np.array( + img_drawn.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 + ) diff_arr = np.abs(drawn_blur - clean_blur) mask_arr = np.max(diff_arr, axis=2) - mask_binary = mask_arr > 30 + mask_binary = mask_arr > 30 mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) mask_image = Image.fromarray((mask_filled * 255).astype(np.uint8)) mask_image = mask_image.filter(ImageFilter.MaxFilter(9)) @@ -407,9 +468,13 @@ def inpaint_image(): if florence_model and florence_processor: print("👁️ Generating context with Florence-2...") try: - task_prompt = '<DETAILED_CAPTION>' - inputs = florence_processor(text=task_prompt, images=[img_drawn], return_tensors="pt") - inputs["pixel_values"] = inputs["pixel_values"].to(DEVICE, torch.float16) + task_prompt = "<DETAILED_CAPTION>" + inputs = florence_processor( + text=task_prompt, images=[img_drawn], return_tensors="pt" + ) + inputs["pixel_values"] = inputs["pixel_values"].to( + DEVICE, torch.float16 + ) inputs["input_ids"] = inputs["input_ids"].to(DEVICE) generated_ids = florence_model.generate( @@ -418,17 +483,26 @@ def inpaint_image(): max_new_tokens=128, num_beams=1, do_sample=False, - use_cache=False + use_cache=False, ) - generated_text = florence_processor.batch_decode(generated_ids, skip_special_tokens=False)[0] - generated_prompt = generated_text.replace(task_prompt, "").replace("</s>", "").replace("<s>", "").strip() + generated_text = florence_processor.batch_decode( + generated_ids, skip_special_tokens=False + )[0] + generated_prompt = ( + generated_text.replace(task_prompt, "") + .replace("</s>", "") + .replace("<s>", "") + .strip() + ) print(f"📝 Florence Generated: {generated_prompt}") except Exception as e: print(f"⚠️ Florence captioning failed: {e}") - + final_prompt = f"{generated_prompt} {user_prompt}".strip() - negative_prompt = "blurry, low quality, ugly, text, watermark, bad anatomy, deformed, noisy" + negative_prompt = ( + "blurry, low quality, ugly, text, watermark, bad anatomy, deformed, noisy" + ) print(f"✨ Final Inpaint Prompt: {final_prompt}") save_dir = "input_data" @@ -440,19 +514,19 @@ def inpaint_image(): print(f"🎨 Running Inference with strength=0.85...") image = sd_pipe( - prompt=final_prompt, + prompt=final_prompt, negative_prompt=negative_prompt, image=img_drawn, mask_image=mask_image, - num_inference_steps=50, + num_inference_steps=50, strength=0.85, - guidance_scale=8.5 + guidance_scale=8.5, ).images[0] final_image_path = os.path.join(save_dir, f"result_{timestamp}.png") image.save(final_image_path) print(f"💾 Saved output to {final_image_path}") - + return jsonify({"status": "success", "image": encode_image_to_base64(image)}) except Exception as e: @@ -460,21 +534,23 @@ def inpaint_image(): traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 -@app.route('/asset', methods=['POST']) + +@app.route("/asset", methods=["POST"]) @secure_endpoint def remove_background(): - if not birefnet_model: + if not birefnet_model: return jsonify({"error": "BiRefNet not loaded"}), 500 try: data = request.get_json() - image_b64 = data.get('image') - if not image_b64: return jsonify({"error": "No image provided"}), 400 + image_b64 = data.get("image") + if not image_b64: + return jsonify({"error": "No image provided"}), 400 original_image = decode_base64_image(image_b64) orig_w, orig_h = original_image.size - + input_tensor = transform_birefnet(original_image).unsqueeze(0).to(DEVICE) - if DEVICE == 'cuda': + if DEVICE == "cuda": input_tensor = input_tensor.half() print("✂️ Removing background...") @@ -484,28 +560,34 @@ def remove_background(): mask_pil = process_birefnet_output(preds, (orig_w, orig_h)) original_image.putalpha(mask_pil) - return jsonify({"status": "success", "image": encode_image_to_base64(original_image)}) + return jsonify( + {"status": "success", "image": encode_image_to_base64(original_image)} + ) except Exception as e: print(f"❌ Error: {e}") traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 -@app.route('/describe', methods=['POST']) + +@app.route("/describe", methods=["POST"]) @secure_endpoint def describe_image(): if not florence_model or not florence_processor: return jsonify({"error": "Florence-2 not loaded"}), 500 try: data = request.get_json() - image_b64 = data.get('image') - prompt_type = data.get('prompt', '<DETAILED_CAPTION>') + image_b64 = data.get("image") + prompt_type = data.get("prompt", "<DETAILED_CAPTION>") - if not image_b64: return jsonify({"error": "No image provided"}), 400 + if not image_b64: + return jsonify({"error": "No image provided"}), 400 image = decode_base64_image(image_b64) print(f"👁️ Analyzing image with Florence-2...") - inputs = florence_processor(text=prompt_type, images=[image], return_tensors="pt") + inputs = florence_processor( + text=prompt_type, images=[image], return_tensors="pt" + ) inputs["pixel_values"] = inputs["pixel_values"].to(DEVICE, torch.float16) inputs["input_ids"] = inputs["input_ids"].to(DEVICE) @@ -515,49 +597,60 @@ def describe_image(): max_new_tokens=128, num_beams=1, do_sample=False, - use_cache=False + use_cache=False, ) - generated_text = florence_processor.batch_decode(generated_ids, skip_special_tokens=False)[0] - cleaned_text = generated_text.replace(prompt_type, "").replace("</s>", "").replace("<s>", "").strip() + generated_text = florence_processor.batch_decode( + generated_ids, skip_special_tokens=False + )[0] + cleaned_text = ( + generated_text.replace(prompt_type, "") + .replace("</s>", "") + .replace("<s>", "") + .strip() + ) - if cleaned_text and cleaned_text[-1] not in ['.', '!', '?']: - last_dot = cleaned_text.rfind('.') - last_excl = cleaned_text.rfind('!') - last_ques = cleaned_text.rfind('?') + if cleaned_text and cleaned_text[-1] not in [".", "!", "?"]: + last_dot = cleaned_text.rfind(".") + last_excl = cleaned_text.rfind("!") + last_ques = cleaned_text.rfind("?") cut_off = max(last_dot, last_excl, last_ques) if cut_off != -1: - cleaned_text = cleaned_text[:cut_off+1] - + cleaned_text = cleaned_text[: cut_off + 1] + final_answer = cleaned_text print(final_answer) if "<loc_" in cleaned_text or "<poly_" in cleaned_text: try: parsed = florence_processor.post_process_generation( - generated_text, - task=prompt_type, - image_size=(image.width, image.height) + generated_text, + task=prompt_type, + image_size=(image.width, image.height), ) if isinstance(parsed, dict) and prompt_type in parsed: final_answer = parsed[prompt_type] else: final_answer = parsed except Exception: + def parse_loc_manually(text, w, h): - locs = re.findall(r'<loc_(\d+)>', text) + locs = re.findall(r"<loc_(\d+)>", text) if locs and len(locs) % 4 == 0: bboxes = [] for i in range(0, len(locs), 4): x1 = int(int(locs[i]) / 1000 * w) - y1 = int(int(locs[i+1]) / 1000 * h) - x2 = int(int(locs[i+2]) / 1000 * w) - y2 = int(int(locs[i+3]) / 1000 * h) + y1 = int(int(locs[i + 1]) / 1000 * h) + x2 = int(int(locs[i + 2]) / 1000 * w) + y2 = int(int(locs[i + 3]) / 1000 * h) bboxes.append([x1, y1, x2, y2]) - clean_text = re.sub(r'<loc_\d+>', '', text).strip() + clean_text = re.sub(r"<loc_\d+>", "", text).strip() return {"text": clean_text, "bboxes": bboxes} return text - final_answer = parse_loc_manually(cleaned_text, image.width, image.height) + + final_answer = parse_loc_manually( + cleaned_text, image.width, image.height + ) return jsonify({"status": "success", "output": final_answer}) @@ -566,21 +659,28 @@ def describe_image(): traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 -@app.route('/inpainting-api', methods=['POST']) + +@app.route("/inpainting-api", methods=["POST"]) @secure_endpoint def inpainting_api_fal(): if not FAL_AVAILABLE: return jsonify({"error": "Fal.ai client not installed or API Key missing"}), 500 - + try: data = request.get_json() - - clean_b64 = data.get('image') - drawn_b64 = data.get('mask_image') - prompt = data.get('prompt', "The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.") - + + clean_b64 = data.get("image") + drawn_b64 = data.get("mask_image") + prompt = data.get( + "prompt", + "The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", + ) + if not clean_b64 or not drawn_b64: - return jsonify({"error": "Missing 'image' (clean) or 'mask_image' (drawn)"}), 400 + return ( + jsonify({"error": "Missing 'image' (clean) or 'mask_image' (drawn)"}), + 400, + ) print(f"📥 Received Request: Prompt='{prompt}'") @@ -592,23 +692,27 @@ def inpainting_api_fal(): img_clean = resize_to_limit(raw_clean, max_dim=1024) # Resize drawn to match exactly img_drawn = raw_drawn.resize(img_clean.size) - + # 3. Setup Debug Directory debug_dir = "debug_fal" os.makedirs(debug_dir, exist_ok=True) unique_id = str(int(time.time())) - + clean_path = os.path.join(debug_dir, f"fal_clean_{unique_id}.png") mask_path = os.path.join(debug_dir, f"fal_mask_{unique_id}.png") fal_result_path = os.path.join(debug_dir, f"fal_result_{unique_id}.png") # 4. Mask Generation print(f"🛠️ Generating mask (Size: {img_clean.size})...") - clean_blur = np.array(img_clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) - drawn_blur = np.array(img_drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) + clean_blur = np.array( + img_clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 + ) + drawn_blur = np.array( + img_drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16 + ) diff_arr = np.abs(drawn_blur - clean_blur) - mask_arr = np.max(diff_arr, axis=2) + mask_arr = np.max(diff_arr, axis=2) mask_binary = mask_arr > 30 white_pixels = np.sum(mask_binary) @@ -618,7 +722,7 @@ def inpainting_api_fal(): mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) - mask = mask.filter(ImageFilter.MaxFilter(9)) + mask = mask.filter(ImageFilter.MaxFilter(9)) # 5. Save Inputs for Inspection mask.save(mask_path) @@ -629,49 +733,66 @@ def inpainting_api_fal(): print("🚀 Uploading images to Fal.ai...") image_url = fal_client.upload_file(clean_path) mask_url = fal_client.upload_file(mask_path) - + print("⚡ Running Flux Dev Fill...") handler = fal_client.submit( - "fal-ai/flux-lora-fill", + "fal-ai/flux-lora-fill", arguments={ "prompt": prompt, "image_url": image_url, "mask_url": mask_url, - "guidance_scale": 30, + "guidance_scale": 30, "num_inference_steps": 28, - "enable_safety_checker": False - } + "enable_safety_checker": False, + }, ) - + result = handler.get() print("📡 Fal Response:", result) - if 'images' in result and len(result['images']) > 0: - output_url = result['images'][0]['url'] + if "images" in result and len(result["images"]) > 0: + output_url = result["images"][0]["url"] print(f"✨ Downloading Result: {output_url}") - + response = requests.get(output_url) if response.status_code == 200: result_img = Image.open(io.BytesIO(response.content)).convert("RGB") - + # Save Debug Output result_img.save(fal_result_path) print(f"💾 Saved final output to {fal_result_path}") - return jsonify({"status": "success", "image": encode_image_to_base64(result_img)}) + return jsonify( + {"status": "success", "image": encode_image_to_base64(result_img)} + ) else: print(f"❌ Failed to download image. Status: {response.status_code}") - return jsonify({"status": "error", "message": "Failed to download Fal output"}), 500 + return ( + jsonify( + {"status": "error", "message": "Failed to download Fal output"} + ), + 500, + ) else: print("❌ API returned no images.") - return jsonify({"status": "error", "message": "Fal.ai returned no images", "details": result}), 500 + return ( + jsonify( + { + "status": "error", + "message": "Fal.ai returned no images", + "details": result, + } + ), + 500, + ) except Exception as e: print(f"❌ Error in /inpainting-api: {e}") traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 -@app.route('/sketch-api', methods=['POST']) + +@app.route("/sketch-api", methods=["POST"]) @secure_endpoint def sketch_api(): if not FAL_AVAILABLE: @@ -679,14 +800,16 @@ def sketch_api(): try: data = request.get_json() - prompt = data.get('prompt') - option = data.get('option', 1) + prompt = data.get("prompt") + option = data.get("option", 1) - if not prompt: + if not prompt: return jsonify({"error": "Missing prompt"}), 400 # --- ENFORCE SHARPNESS IN PROMPT --- - enhanced_prompt = f"{prompt}, sharp focus, high definition, 4k, vector art, crisp lines" + enhanced_prompt = ( + f"{prompt}, sharp focus, high definition, 4k, vector art, crisp lines" + ) if int(option) == 1: # Nano Banana @@ -696,7 +819,7 @@ def sketch_api(): "prompt": enhanced_prompt, "num_images": 1, "aspect_ratio": "1:1", - "output_format": "png" + "output_format": "png", } elif int(option) == 2: # Flux Dev @@ -704,40 +827,56 @@ def sketch_api(): model_id = "fal-ai/flux/dev" arguments = { "image_size": "square_hd", - "num_inference_steps": 28, - "guidance_scale": 3.5, - "safety_tolerance": "2", + "num_inference_steps": 28, + "guidance_scale": 3.5, + "safety_tolerance": "2", "enable_safety_checker": False, - "prompt": enhanced_prompt + "prompt": enhanced_prompt, } else: - return jsonify({"error": "Invalid option. Use 1 for Nano Banana, 2 for Flux Dev."}), 400 + return ( + jsonify( + {"error": "Invalid option. Use 1 for Nano Banana, 2 for Flux Dev."} + ), + 400, + ) # Execute request - handler = fal_client.submit( - model_id, - arguments=arguments - ) + handler = fal_client.submit(model_id, arguments=arguments) result = handler.get() print("📡 Fal Response:", result) - if 'images' in result and len(result['images']) > 0: - image_url = result['images'][0]['url'] + if "images" in result and len(result["images"]) > 0: + image_url = result["images"][0]["url"] print(f"✨ Success! Image generated: {image_url}") response = requests.get(image_url) if response.status_code == 200: img = Image.open(io.BytesIO(response.content)).convert("RGB") - return jsonify({"status": "success", "image": encode_image_to_base64(img)}) + return jsonify( + {"status": "success", "image": encode_image_to_base64(img)} + ) else: - return jsonify({"status": "error", "message": "Failed to download image from Fal"}), 500 + return ( + jsonify( + { + "status": "error", + "message": "Failed to download image from Fal", + } + ), + 500, + ) else: - return jsonify({"status": "error", "message": "No images returned from Fal"}), 500 + return ( + jsonify({"status": "error", "message": "No images returned from Fal"}), + 500, + ) except Exception as e: print(f"❌ Error in /sketch-api: {e}") traceback.print_exc() return jsonify({"status": "error", "message": str(e)}), 500 + if __name__ == "__main__": - app.run(host='0.0.0.0', port=5000) + app.run(host="0.0.0.0", port=5000) diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart @@ -215,7 +215,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Project Name (required, first field) _buildFormField( label: 'Project Name', hintText: 'Enter project name', @@ -223,8 +222,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> { required: true, ), const SizedBox(height: 16), - - // What do you want & who is it for _buildFormField( label: 'What do you want & who is it for.', hintText: 'Describe your work and your audience.', @@ -233,8 +230,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> { required: false, ), const SizedBox(height: 16), - - // What problem you solve _buildFormField( label: 'What problem you solve.', hintText: @@ -244,8 +239,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> { required: false, ), const SizedBox(height: 16), - - // Long-term goal _buildFormField( label: 'Long-term goal for the brand.', hintText: 'E.g. - Improving food availability...', @@ -253,18 +246,10 @@ class _DefineBrandPageState extends State<DefineBrandPage> { required: false, ), const SizedBox(height: 32), - - // Keywords Section _buildKeywordsSection(), - const SizedBox(height: 32), - - // Competitor Brands Section _buildCompetitorBrandsSection(), - const SizedBox(height: 32), - - // Where will the brand appear _buildFormField( label: 'Where will the brand appear', hintText: 'Banners, Posters, Instagram..', @@ -273,7 +258,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> { ), ], ), - const SizedBox(height: 100), // Space for bottom button ], ), diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -547,7 +547,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { ), const SizedBox(width: 12), - // Send Button with YOUR Logic + // Send Button Container( margin: const EdgeInsets.only(bottom: 2), child: IconButton( @@ -555,7 +555,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { color: Colors.black87, iconSize: 28, onPressed: () async { - // --- YOUR ORIGINAL SAVE LOGIC --- if (newNoteController.text.isNotEmpty && _finalSelectionRect != null && _imageRenderSize != null) { diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -617,7 +617,6 @@ class _ImageSavePageState extends State<ImageSavePage> { }, ); - // Return your custom overlay wrapper with the stateful content inside return NoteModalOverlay( modalContent: modalContent, screenSize: mediaQuery.size, @@ -1264,8 +1263,6 @@ class NoteModalOverlay extends StatelessWidget { } } -// [NoteModalPainter class removed as requested to remove the dimming shadow] - // --- EXTENSION TO NORMALIZE RECT --- extension on Rect { Rect normalize() { diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -68,7 +68,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { } // --------------------------------------- - // LOAD METADATA (Like HomePage) + // LOAD METADATA // --------------------------------------- Future<void> _loadMetadata(List<FileModel> list) async { for (final file in list) { @@ -293,11 +293,10 @@ class _ProjectFilePageState extends State<ProjectFilePage> { border: Border.all(color: const Color(0xFFE4E4E7)), ), - // ❗ NO padding here — keeps left flush child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ---------- THUMBNAIL (FLUSH) ---------- + // ---------- THUMBNAIL ---------- ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), @@ -318,7 +317,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ), ), - // ---------- SPACING ---------- const SizedBox(width: 12), // ---------- RIGHT SIDE TEXT ---------- @@ -391,7 +389,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { icon: const Icon(Icons.more_vert, size: 20), ), - const SizedBox(width: 4), // slight right breathing space + const SizedBox(width: 4), ], ), ), @@ -696,14 +694,14 @@ class _ProjectFilePageState extends State<ProjectFilePage> { } // --------------------------------------- - // DROPDOWN (ONLY — add event removed) + // DROPDOWN // --------------------------------------- Widget _buildEventDropdown() { return Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), // subtle grey + color: const Color(0xFFE4E4E7), borderRadius: BorderRadius.circular(8), ), child: DropdownButton<ProjectModel>( diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -51,8 +51,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { Future<void> _fetchFiles() async { setState(() => _isLoading = true); try { - // Using service-style API: fetch all files and recent files - // If your FileService has different method names, adapt them here. final List<FileModel> files = await _fileService.getAllFiles(); final List<FileModel> recent = await _fileService.getRecentFiles( limit: 10, @@ -389,7 +387,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Recent Files (UI like screenshot) + // Recent Files if (_recentFiles.isNotEmpty && _searchQuery.isEmpty) ...[ const Padding( @@ -501,7 +499,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { ), child: Row( children: [ - // ---- FLUSH LEFT THUMBNAIL ---- ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), @@ -524,7 +521,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { const SizedBox(width: 12), - // ---- TEXT ---- Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), @@ -586,7 +582,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { ), child: Row( children: [ - // ---- FLUSH LEFT THUMBNAIL ---- ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), diff --git a/models/color.py b/models/color.py @@ -1,10 +1,6 @@ -# -*- coding: utf-8 -*- -"""color_1.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1o7cMSAesuuaxFQsteQL4ViwL_qNeTWbm +""" +Color +Original file: https://colab.research.google.com/drive/1o7cMSAesuuaxFQsteQL4ViwL_qNeTWbm """ import cv2 @@ -29,13 +25,14 @@ def get_dominant_colors(image, k=5): pixels = img_small.reshape((-1, 3)) - kmeans = KMeans(n_clusters=k, n_init='auto', random_state=42) + kmeans = KMeans(n_clusters=k, n_init="auto", random_state=42) kmeans.fit(pixels) colors = kmeans.cluster_centers_.astype(int) return sorted(colors.tolist(), key=lambda x: sum(x)) + def classify_mood(rgb_colors): """ Classifies mood based on HSV values using Matplotlib colors. @@ -47,31 +44,39 @@ def classify_mood(rgb_colors): avg_sat = np.mean(hsv_stats[:, 1]) avg_val = np.mean(hsv_stats[:, 2]) - if avg_sat < 0.15 and avg_val > 0.65: return "Minimalist" - if avg_val < 0.35: return "Dark/Moody" - if avg_sat < 0.45 and avg_val > 0.75: return "Pastel" - if avg_sat > 0.65 and avg_val > 0.5: return "Neon" + if avg_sat < 0.15 and avg_val > 0.65: + return "Minimalist" + if avg_val < 0.35: + return "Dark/Moody" + if avg_sat < 0.45 and avg_val > 0.75: + return "Pastel" + if avg_sat > 0.65 and avg_val > 0.5: + return "Neon" earthy_votes = sum(1 for h, s, v in hsv_stats if (0.02 <= h <= 0.42) and s < 0.8) - if earthy_votes >= 3: return "Earthy" + if earthy_votes >= 3: + return "Earthy" warm_votes = sum(1 for h, s, v in hsv_stats if h < 0.17 or h > 0.83) return "Warm" if warm_votes >= 3 else "Cool" + def rgb_to_hex(rgb): - return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2]) + return "#{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]) + + +# Runner -# MAIN EXECUTION def analyze_uploaded_image(): print("Please upload an image file...") uploaded = files.upload() for fn in uploaded.keys(): - print(f'\nProcessing {fn}...') + print(f"\nProcessing {fn}...") image_data = uploaded[fn] - image = Image.open(io.BytesIO(image_data)).convert('RGB') + image = Image.open(io.BytesIO(image_data)).convert("RGB") colors = get_dominant_colors(image) if colors is None: @@ -91,22 +96,31 @@ def analyze_uploaded_image(): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.imshow(image) - ax1.axis('off') - ax1.set_title(f"Mood: {mood}", fontweight='bold') + ax1.axis("off") + ax1.set_title(f"Mood: {mood}", fontweight="bold") y_pos = np.arange(len(colors)) - ax2.barh(y_pos, [1]*len(colors), color=hex_colors) + ax2.barh(y_pos, [1] * len(colors), color=hex_colors) ax2.invert_yaxis() - ax2.axis('off') + ax2.axis("off") for i, h in enumerate(hex_colors): brightness = sum(colors[i]) / 3 - text_col = 'black' if brightness > 128 else 'white' - ax2.text(0.5, i, f"{h}", ha='center', va='center', - color=text_col, fontweight='bold', fontsize=12) + text_col = "black" if brightness > 128 else "white" + ax2.text( + 0.5, + i, + f"{h}", + ha="center", + va="center", + color=text_col, + fontweight="bold", + fontsize=12, + ) plt.tight_layout() plt.show() + if __name__ == "__main__": - analyze_uploaded_image() -\ No newline at end of file + analyze_uploaded_image() diff --git a/models/composition.py b/models/composition.py @@ -1,32 +1,33 @@ -# -*- coding: utf-8 -*- -"""layout - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1NzX6rAQSJkPTpdWWkOowDFlBpypoKl2U +""" +Layout +Original file: https://colab.research.google.com/drive/1NzX6rAQSJkPTpdWWkOowDFlBpypoKl2U """ -pip install opencv-python numpy +!pip install opencv-python numpy import cv2 import numpy as np import os from math import hypot + def resize_for_fast_processing(img, max_side=640): h, w = img.shape[:2] scale = max_side / max(h, w) if max(h, w) > max_side else 1.0 if scale != 1.0: - img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) + img = cv2.resize( + img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA + ) return img, scale + def compute_saliency_gray(img): sal = None try: salience = cv2.saliency.StaticSaliencySpectralResidual_create() success, sal = salience.computeSaliency(img) - if not success: sal = None + if not success: + sal = None except Exception: sal = None if sal is None: @@ -37,41 +38,54 @@ def compute_saliency_gray(img): sal = sal.astype(np.float32) / (sal.max() + 1e-9) else: sal = sal.astype(np.float32) - if sal.max() > 0: sal /= sal.max() + if sal.max() > 0: + sal /= sal.max() return sal + def saliency_centroid(sal): h, w = sal.shape M = sal.sum() - if M <= 1e-6: return (w / 2, h / 2) + if M <= 1e-6: + return (w / 2, h / 2) ys, xs = np.indices(sal.shape) cx = (xs * sal).sum() / M cy = (ys * sal).sum() / M return (float(cx), float(cy)) + def detect_lines(gray, canny_thresh1=50, canny_thresh2=150, hough_thresh=50): edges = cv2.Canny(gray, canny_thresh1, canny_thresh2) - lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=hough_thresh, - minLineLength=min(gray.shape) // 10, maxLineGap=10) - if lines is None: return edges, [] + lines = cv2.HoughLinesP( + edges, + rho=1, + theta=np.pi / 180, + threshold=hough_thresh, + minLineLength=min(gray.shape) // 10, + maxLineGap=10, + ) + if lines is None: + return edges, [] lines = [tuple(l[0]) for l in lines] return edges, lines + def project_sal_along_axis(sal, axis=0, smooth_k=15): - proj = sal.sum(axis=1-axis) if axis == 0 else sal.sum(axis=0) + proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0) k = max(3, smooth_k) kernel = np.ones(k) / k - proj_s = np.convolve(proj, kernel, mode='same') + proj_s = np.convolve(proj, kernel, mode="same") if proj_s.max() > 0: proj_s = proj_s / proj_s.max() return proj_s + def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2): h, w = sal.shape - proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h*0.03))) + proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h * 0.03))) peaks = [] - for i in range(1, len(proj)-1): - if proj[i] > proj[i-1] and proj[i] > proj[i+1] and proj[i] > threshold: + for i in range(1, len(proj) - 1): + if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > threshold: peaks.append(i) min_sep = int(min_sep_fraction * h) filtered = [] @@ -80,105 +94,134 @@ def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2): filtered.append(p) return len(filtered), filtered + def band_saliency_fractions(sal, bands=3): h, w = sal.shape sums = [] total = sal.sum() + 1e-9 for i in range(bands): - band = sal[:, i*w//bands:(i+1)*w//bands] + band = sal[:, i * w // bands : (i + 1) * w // bands] sums.append(band.sum() / total) return sums + def sal_bbox_and_margins(sal, thresh=0.3): mask = sal > thresh if mask.sum() == 0: - return (0,0,0,0), None + return (0, 0, 0, 0), None ys, xs = np.where(mask) minx, maxx = int(xs.min()), int(xs.max()) miny, maxy = int(ys.min()), int(ys.max()) return (minx, miny, maxx, maxy), mask + def component_bboxes_from_mask(mask): - mask_u8 = (mask.astype(np.uint8) * 255) + mask_u8 = mask.astype(np.uint8) * 255 contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) bboxes = [] for c in contours: - x,y,w,h = cv2.boundingRect(c) - bboxes.append((x,y,x+w-1,y+h-1)) + x, y, w, h = cv2.boundingRect(c) + bboxes.append((x, y, x + w - 1, y + h - 1)) return bboxes + def bbox_laplacian_variance(gray, bbox): - x1,y1,x2,y2 = bbox - roi = gray[y1:y2+1, x1:x2+1] - if roi.size == 0: return 0.0 + x1, y1, x2, y2 = bbox + roi = gray[y1 : y2 + 1, x1 : x2 + 1] + if roi.size == 0: + return 0.0 lap = cv2.Laplacian(roi, cv2.CV_64F) return float(np.var(lap)) - def _proj_and_peaks(sal, axis=0, smooth_k=15): - proj = sal.sum(axis=1-axis) if axis==0 else sal.sum(axis=0) + proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0) k = max(3, smooth_k) kernel = np.ones(k) / k - proj_s = np.convolve(proj, kernel, mode='same') - if proj_s.max() > 0: proj_s = proj_s / proj_s.max() + proj_s = np.convolve(proj, kernel, mode="same") + if proj_s.max() > 0: + proj_s = proj_s / proj_s.max() return proj_s + def vertical_stack_score(sal, lines, min_peaks=2): - h,w = sal.shape - proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h*0.03))) - peaks = [i for i in range(1,len(proj)-1) if proj[i]>proj[i-1] and proj[i]>proj[i+1] and proj[i]>0.18] - if len(peaks)==0: - return 0.0, {'peaks':0} + h, w = sal.shape + proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h * 0.03))) + peaks = [ + i + for i in range(1, len(proj) - 1) + if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > 0.18 + ] + if len(peaks) == 0: + return 0.0, {"peaks": 0} avg_peak = float(np.mean([proj[p] for p in peaks])) peak_count_score = min(1.0, len(peaks) / max(1.0, min_peaks)) peak_prom_score = avg_peak h_lines = 0 - for (x1,y1,x2,y2) in lines: - dy = abs(y2-y1); dx = abs(x2-x1)+1e-9 - slope = dy/dx - if slope < 0.3 and abs(x2-x1) > 0.6*w: + for x1, y1, x2, y2 in lines: + dy = abs(y2 - y1) + dx = abs(x2 - x1) + 1e-9 + slope = dy / dx + if slope < 0.3 and abs(x2 - x1) > 0.6 * w: h_lines += 1 sep_score = min(1.0, h_lines / 2.0) span_ok = 0 for p in peaks: - row = sal[max(0,p-2):min(h,p+3), :] - if row.sum()<=1e-9: continue + row = sal[max(0, p - 2) : min(h, p + 3), :] + if row.sum() <= 1e-9: + continue col_sum = row.sum(axis=0) - frac_nonzero = (col_sum > (col_sum.max()*0.05)).sum() / float(w) + frac_nonzero = (col_sum > (col_sum.max() * 0.05)).sum() / float(w) if frac_nonzero > 0.65: span_ok += 1 span_score = min(1.0, span_ok / max(1.0, len(peaks))) - score = 0.45*peak_count_score + 0.25*peak_prom_score + 0.15*sep_score + 0.15*span_score - return float(np.clip(score,0.0,1.0)), {'peaks': len(peaks), 'avg_peak': avg_peak, 'h_lines': h_lines, 'span_ok': span_ok} + score = ( + 0.45 * peak_count_score + + 0.25 * peak_prom_score + + 0.15 * sep_score + + 0.15 * span_score + ) + return float(np.clip(score, 0.0, 1.0)), { + "peaks": len(peaks), + "avg_peak": avg_peak, + "h_lines": h_lines, + "span_ok": span_ok, + } + def triptych_score(sal, lines): - h,w = sal.shape + h, w = sal.shape total = sal.sum() + 1e-9 - bands = [sal[:, i*w//3:(i+1)*w//3].sum() / total for i in range(3)] + bands = [sal[:, i * w // 3 : (i + 1) * w // 3].sum() / total for i in range(3)] mn, mx = min(bands), max(bands) balance = 1.0 - (mx - mn) balance = np.clip(balance, 0.0, 1.0) v_hits = 0 - for (x1,y1,x2,y2) in lines: - dx = abs(x2-x1)+1e-9; dy = abs(y2-y1) - slope = dy / dx if dx>1 else 1e9 - if slope > 3 and dy > 0.6*h: - cx = (x1+x2)/2 - if abs(cx - w/3) < 0.08*w or abs(cx - 2*w/3) < 0.08*w: + for x1, y1, x2, y2 in lines: + dx = abs(x2 - x1) + 1e-9 + dy = abs(y2 - y1) + slope = dy / dx if dx > 1 else 1e9 + if slope > 3 and dy > 0.6 * h: + cx = (x1 + x2) / 2 + if abs(cx - w / 3) < 0.08 * w or abs(cx - 2 * w / 3) < 0.08 * w: v_hits += 1 - line_score = np.tanh(v_hits/2.0) + line_score = np.tanh(v_hits / 2.0) band_floor = min(1.0, mn / 0.15) - score = 0.55*balance + 0.25*line_score + 0.20*band_floor - return float(np.clip(score,0.0,1.0)), {'band_fracs': bands, 'v_line_hits': v_hits} - -def full_bleed_score(sal, img, edge_margin_frac=0.03, - edge_sal_thresh=0.25, - sal_bbox_thresh=0.2, - white_thresh=245, - white_frac_thresh=0.80, - edge_sal_frac_thresh=0.50, - bbox_touch_required=0.75): + score = 0.55 * balance + 0.25 * line_score + 0.20 * band_floor + return float(np.clip(score, 0.0, 1.0)), {"band_fracs": bands, "v_line_hits": v_hits} + + +def full_bleed_score( + sal, + img, + edge_margin_frac=0.03, + edge_sal_thresh=0.25, + sal_bbox_thresh=0.2, + white_thresh=245, + white_frac_thresh=0.80, + edge_sal_frac_thresh=0.50, + bbox_touch_required=0.75, +): """ Binary full-bleed detector (returns 0 or 1, plus info). - Rejects as full-bleed if a white margin/border is detected. @@ -187,14 +230,14 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, h, w = sal.shape m = max(1, int(min(h, w) * edge_margin_frac)) - mask_edge = np.zeros_like(sal, dtype=bool) mask_edge[:m, :] = True mask_edge[-m:, :] = True mask_edge[:, :m] = True mask_edge[:, -m:] = True - edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / (mask_edge.sum() + 1e-9) - + edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / ( + mask_edge.sum() + 1e-9 + ) mask2 = sal > sal_bbox_thresh if mask2.sum() == 0: @@ -203,20 +246,25 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, ys, xs = np.where(mask2) minx, maxx = int(xs.min()), int(xs.max()) miny, maxy = int(ys.min()), int(ys.max()) - touches = int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2) + touches = ( + int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2) + ) bbox_touch = touches / 4.0 - band_top = img[:m, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype) band_bottom = img[-m:, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype) band_left = img[:, :m, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype) band_right = img[:, -m:, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype) - - border_pixels = np.concatenate([band_top.reshape(-1, 3), - band_bottom.reshape(-1, 3), - band_left.reshape(-1, 3), - band_right.reshape(-1, 3)], axis=0) + border_pixels = np.concatenate( + [ + band_top.reshape(-1, 3), + band_bottom.reshape(-1, 3), + band_left.reshape(-1, 3), + band_right.reshape(-1, 3), + ], + axis=0, + ) if border_pixels.size == 0: white_border_frac = 0.0 else: @@ -229,144 +277,180 @@ def full_bleed_score(sal, img, edge_margin_frac=0.03, cond_edge_sal = edge_sal_frac >= edge_sal_frac_thresh cond_bbox_touch = bbox_touch >= bbox_touch_required - is_full_bleed = (cond_edge_sal or cond_bbox_touch) and (white_border_frac < white_frac_thresh) and (not (border_uniform and white_border_frac > 0.25)) + is_full_bleed = ( + (cond_edge_sal or cond_bbox_touch) + and (white_border_frac < white_frac_thresh) + and (not (border_uniform and white_border_frac > 0.25)) + ) score = 1 if is_full_bleed else 0 info = { - 'edge_margin_px': m, - 'edge_sal_frac': edge_sal_frac, - 'edge_sal_thresh': edge_sal_thresh, - 'cond_edge_sal': cond_edge_sal, - 'bbox_touch': bbox_touch, - 'bbox_touch_required': bbox_touch_required, - 'white_border_frac': white_border_frac, - 'white_thresh': white_thresh, - 'white_frac_thresh': white_frac_thresh, - 'border_std': border_std, - 'border_uniform': border_uniform, - 'decision_full_bleed': bool(is_full_bleed) + "edge_margin_px": m, + "edge_sal_frac": edge_sal_frac, + "edge_sal_thresh": edge_sal_thresh, + "cond_edge_sal": cond_edge_sal, + "bbox_touch": bbox_touch, + "bbox_touch_required": bbox_touch_required, + "white_border_frac": white_border_frac, + "white_thresh": white_thresh, + "white_frac_thresh": white_frac_thresh, + "border_std": border_std, + "border_uniform": border_uniform, + "decision_full_bleed": bool(is_full_bleed), } return score, info def tight_crop_score(sal, gray, sal_thresh=0.1): - h,w = sal.shape + h, w = sal.shape mask = sal > sal_thresh - if mask.sum()==0: - return 0.0, {'area_frac':0.0, 'tightness':1.0} - ys,xs = np.where(mask) - minx,maxx = int(xs.min()), int(xs.max()) - miny,maxy = int(ys.min()), int(ys.max()) - bbox_area = (maxx-minx+1)*(maxy-miny+1) - area_frac = bbox_area / float(w*h) - left = minx; right = w-1-maxx; top = miny; bottom = h-1-maxy - tightness = min(left,right,top,bottom) / float(min(w,h)+1e-9) + if mask.sum() == 0: + return 0.0, {"area_frac": 0.0, "tightness": 1.0} + ys, xs = np.where(mask) + minx, maxx = int(xs.min()), int(xs.max()) + miny, maxy = int(ys.min()), int(ys.max()) + bbox_area = (maxx - minx + 1) * (maxy - miny + 1) + area_frac = bbox_area / float(w * h) + left = minx + right = w - 1 - maxx + top = miny + bottom = h - 1 - maxy + tightness = min(left, right, top, bottom) / float(min(w, h) + 1e-9) area_score = np.clip((area_frac - 0.15) / (0.6 - 0.15), 0.0, 1.0) tight_score = 1.0 - np.clip(tightness / 0.08, 0.0, 1.0) - score = 0.6*area_score + 0.4*tight_score - return float(np.clip(score,0.0,1.0)), {'area_frac': area_frac, 'tightness': tightness, 'margins': (left,right,top,bottom)} + score = 0.6 * area_score + 0.4 * tight_score + return float(np.clip(score, 0.0, 1.0)), { + "area_frac": area_frac, + "tightness": tightness, + "margins": (left, right, top, bottom), + } + def layered_foreground_score(sal, gray): mask = (sal > 0.25).astype(np.uint8) - kernel = np.ones((5,5), np.uint8) + kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) - contours, _ = cv2.findContours((mask*255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - bboxes=[]; sharpness=[] + contours, _ = cv2.findContours( + (mask * 255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + bboxes = [] + sharpness = [] for c in contours: - x,y,w,h = cv2.boundingRect(c) - if w*h < 0.005*sal.size: continue - bboxes.append((x,y,x+w-1,y+h-1)) - roi = gray[y:y+h, x:x+w] + x, y, w, h = cv2.boundingRect(c) + if w * h < 0.005 * sal.size: + continue + bboxes.append((x, y, x + w - 1, y + h - 1)) + roi = gray[y : y + h, x : x + w] lap = cv2.Laplacian(roi, cv2.CV_64F) sharpness.append(float(np.var(lap))) n = len(bboxes) if n < 2: - return 0.0, {'num_components': n} + return 0.0, {"num_components": n} s = np.array(sharpness) + 1e-9 - s_norm = (s - s.mean()) / (s.std()+1e-9) + s_norm = (s - s.mean()) / (s.std() + 1e-9) sharp_std = float(np.std(s_norm)) overlaps = 0 for i in range(len(bboxes)): - for j in range(i+1, len(bboxes)): - a=bboxes[i]; b=bboxes[j] - ix1 = max(a[0], b[0]); iy1 = max(a[1], b[1]) - ix2 = min(a[2], b[2]); iy2 = min(a[3], b[3]) + for j in range(i + 1, len(bboxes)): + a = bboxes[i] + b = bboxes[j] + ix1 = max(a[0], b[0]) + iy1 = max(a[1], b[1]) + ix2 = min(a[2], b[2]) + iy2 = min(a[3], b[3]) if ix2 >= ix1 and iy2 >= iy1: - inter = (ix2-ix1+1)*(iy2-iy1+1) - area_a = (a[2]-a[0]+1)*(a[3]-a[1]+1) - if inter > 0.05*area_a: + inter = (ix2 - ix1 + 1) * (iy2 - iy1 + 1) + area_a = (a[2] - a[0] + 1) * (a[3] - a[1] + 1) + if inter > 0.05 * area_a: overlaps += 1 - comp_score = np.clip((n-1)/4.0, 0, 1) + comp_score = np.clip((n - 1) / 4.0, 0, 1) sharp_score = np.tanh(sharp_std) - overlap_score = np.tanh(overlaps/2.0) - score = 0.45*comp_score + 0.35*sharp_score + 0.20*overlap_score - return float(np.clip(score,0.0,1.0)), {'num_components': n, 'sharp_std': sharp_std, 'overlaps': overlaps} + overlap_score = np.tanh(overlaps / 2.0) + score = 0.45 * comp_score + 0.35 * sharp_score + 0.20 * overlap_score + return float(np.clip(score, 0.0, 1.0)), { + "num_components": n, + "sharp_std": sharp_std, + "overlaps": overlaps, + } def balance_score(img_bgr, sal): img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) - L = img_lab[:,:,0] / 255.0 + L = img_lab[:, :, 0] / 255.0 img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) - S = img_hsv[:,:,1] / 255.0 + S = img_hsv[:, :, 1] / 255.0 weight = sal * (L + 0.5 * S) M = weight.sum() - h,w = sal.shape - if M <= 1e-6: return 0.0 + h, w = sal.shape + if M <= 1e-6: + return 0.0 ys, xs = np.indices(sal.shape) cx = (xs * weight).sum() / M cy = (ys * weight).sum() / M - dx = abs(cx - w/2) / (w/2) - dy = abs(cy - h/2) / (h/2) - dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) - return float(max(0.0, 1.0 - dist*1.4)) + dx = abs(cx - w / 2) / (w / 2) + dy = abs(cy - h / 2) / (h / 2) + dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist * 1.4)) + def symmetry_score(img_gray): - h,w = img_gray.shape - left = img_gray[:, :w//2] - right = img_gray[:, w - w//2:] + h, w = img_gray.shape + left = img_gray[:, : w // 2] + right = img_gray[:, w - w // 2 :] right_flipped = cv2.flip(right, 1) if left.shape != right_flipped.shape: right_flipped = cv2.resize(right_flipped, (left.shape[1], left.shape[0])) - leftf = left.astype(np.float32); rightf = right_flipped.astype(np.float32) - if leftf.std() < 1e-3 or rightf.std() < 1e-3: return 0.0 - corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0,1] - return float(max(0.0, min(1.0, (corr + 1)/2))) + leftf = left.astype(np.float32) + rightf = right_flipped.astype(np.float32) + if leftf.std() < 1e-3 or rightf.std() < 1e-3: + return 0.0 + corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0, 1] + return float(max(0.0, min(1.0, (corr + 1) / 2))) + def thirds_score(sal, centroid): - h,w = sal.shape + h, w = sal.shape cx, cy = centroid - thirds_pts = [(w/3, h/3), (w/3, 2*h/3), (2*w/3, h/3), (2*w/3, 2*h/3)] - dists = [hypot(cx-x, cy-y) for (x,y) in thirds_pts] + thirds_pts = [ + (w / 3, h / 3), + (w / 3, 2 * h / 3), + (2 * w / 3, h / 3), + (2 * w / 3, 2 * h / 3), + ] + dists = [hypot(cx - x, cy - y) for (x, y) in thirds_pts] maxd = hypot(w, h) - score = 1.0 - min(dists)/maxd*1.2 - vline_dist = min(abs(cx - w/3), abs(cx - 2*w/3)) / w - hline_dist = min(abs(cy - h/3), abs(cy - 2*h/3)) / h + score = 1.0 - min(dists) / maxd * 1.2 + vline_dist = min(abs(cx - w / 3), abs(cx - 2 * w / 3)) / w + hline_dist = min(abs(cy - h / 3), abs(cy - 2 * h / 3)) / h line_score = 1.0 - min(vline_dist, hline_dist) - return float(max(0.0, min(1.0, 0.6*score + 0.4*line_score))) + return float(max(0.0, min(1.0, 0.6 * score + 0.4 * line_score))) + def golden_ratio_score(sal, centroid): - h,w = sal.shape - gx = [0.382, 0.618]; gy = [0.382, 0.618] + h, w = sal.shape + gx = [0.382, 0.618] + gy = [0.382, 0.618] cx, cy = centroid - pts = [(g*w, g2*h) for g in gx for g2 in gy] - dists = [hypot(cx-x, cy-y) for x,y in pts] + pts = [(g * w, g2 * h) for g in gx for g2 in gy] + dists = [hypot(cx - x, cy - y) for x, y in pts] maxd = hypot(w, h) - return float(max(0.0, min(1.0, 1.0 - min(dists)/ (maxd*0.9)))) + return float(max(0.0, min(1.0, 1.0 - min(dists) / (maxd * 0.9)))) + def negative_space_score(sal): low = (sal < 0.15).sum() / sal.size return float(min(1.0, low)) + def center_score(sal, centroid): - h,w = sal.shape + h, w = sal.shape cx, cy = centroid - dx = abs(cx - w/2) / (w/2) - dy = abs(cy - h/2) / (h/2) - dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) - return float(max(0.0, 1.0 - dist*1.2)) - + dx = abs(cx - w / 2) / (w / 2) + dy = abs(cy - h / 2) / (h / 2) + dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist * 1.2)) def analyze_single_image(img_path): @@ -391,15 +475,17 @@ def analyze_single_image(img_path): centroid = saliency_centroid(sal) edges, lines = detect_lines(gray) - _, thr = cv2.threshold((gray).astype(np.uint8), 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV) + _, thr = cv2.threshold( + (gray).astype(np.uint8), 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV + ) contours, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) composition_scores = { - 'Rule of Thirds': thirds_score(sal, centroid), - 'Golden Ratio': golden_ratio_score(sal, centroid), - 'Symmetry': symmetry_score(gray), - 'Negative Space': negative_space_score(sal), - 'Center Composition': center_score(sal, centroid) + "Rule of Thirds": thirds_score(sal, centroid), + "Golden Ratio": golden_ratio_score(sal, centroid), + "Symmetry": symmetry_score(gray), + "Negative Space": negative_space_score(sal), + "Center Composition": center_score(sal, centroid), } v_score, v_info = vertical_stack_score(sal, lines) @@ -408,11 +494,11 @@ def analyze_single_image(img_path): tc_score, tc_info = tight_crop_score(sal, gray) lf_score, lf_info = layered_foreground_score(sal, gray) - composition_scores['Vertical Stack'] = v_score - composition_scores['Triptych'] = t_score - composition_scores['Full Bleed'] = fb_score - composition_scores['Tight Crop'] = tc_score - composition_scores['Layered Foreground'] = lf_score + composition_scores["Vertical Stack"] = v_score + composition_scores["Triptych"] = t_score + composition_scores["Full Bleed"] = fb_score + composition_scores["Tight Crop"] = tc_score + composition_scores["Layered Foreground"] = lf_score print("-" * 40) print(f"Top Composition Scores:") @@ -426,16 +512,16 @@ def analyze_single_image(img_path): print("-" * 40) diagnostics = { - 'Vertical Stack_info': v_info, - 'Triptych_info': t_info, - 'Full Bleed_info': fb_info, - 'Tight Crop_info': tc_info, - 'Layered Foreground_info': lf_info + "Vertical Stack_info": v_info, + "Triptych_info": t_info, + "Full Bleed_info": fb_info, + "Tight Crop_info": tc_info, + "Layered Foreground_info": lf_info, } -# MAIN EXECUTION +# Runner -if __name__ == '__main__': - target_image_path = "/content/Screenshot 2025-11-24 200949.png" - analyze_single_image(target_image_path) -\ No newline at end of file +if __name__ == "__main__": + target_image_path = "/content/test.png" + analyze_single_image(target_image_path) diff --git a/models/emotions.py b/models/emotions.py @@ -1,16 +1,10 @@ -# -*- coding: utf-8 -*- -"""Untitled4.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1SgZgWv7uK3kibT3KW1zR5jp5pH46AQE0 +""" +Emotions +Original file: https://colab.research.google.com/drive/1SgZgWv7uK3kibT3KW1zR5jp5pH46AQE0 """ !pip install git+https://github.com/openai/CLIP.git --q -!pip install torch torchvision pillow --q - -!pip install ftfy regex tqdm +!pip install torch torchvision pillow ftfy regex tqdm --q import torch import clip @@ -30,7 +24,7 @@ EMOTIONAL_CLASS = [ "luxurious", "cheeky", "ironic", - "mysterious" + "mysterious", ] EMOTIONAL_PHRASES = { @@ -58,6 +52,7 @@ EMOTION_PROMPT_TEMPLATES = [ "a professional photoshoot showcasing {}", ] + def build_text_bank(output_path="lighting_text_bank"): all_prompt_texts = [] all_prompt_labels = [] @@ -100,6 +95,7 @@ def build_text_bank(output_path="lighting_text_bank"): build_text_bank() + def get_image_embedding(image_path: str) -> torch.Tensor: image = Image.open(image_path).convert("RGB") img_tensor = preprocess(image).unsqueeze(0).to(device) @@ -111,6 +107,7 @@ def get_image_embedding(image_path: str) -> torch.Tensor: return img_feat.squeeze(0) + DEFAULT_THRESHOLD = 0.22 @@ -138,10 +135,7 @@ def classify_lighting(image_path, threshold=DEFAULT_THRESHOLD): best_score = float(best_score.detach().cpu()) sims_list = sims.detach().cpu().tolist() - sims_dict = { - str(cls): float(s) - for cls, s in zip(class_names, sims_list) - } + sims_dict = {str(cls): float(s) for cls, s in zip(class_names, sims_list)} if best_score < threshold: predicted = "no_specific_emotion" @@ -150,7 +144,11 @@ def classify_lighting(image_path, threshold=DEFAULT_THRESHOLD): return predicted, best_score, sims_dict + +# Runner + from google.colab import files + uploaded = files.upload() image_path = list(uploaded.keys())[0] @@ -161,4 +159,4 @@ print("Prediction:", pred) print("Best score:", score) print("\nAll similarities:") for k, v in sims.items(): - print(f"{k:20s}: {v:.3f}") -\ No newline at end of file + print(f"{k:20s}: {v:.3f}") diff --git a/models/era_cult.py b/models/era_cult.py @@ -0,0 +1,140 @@ +""" +Era / Cultural Reference +Original file: https://colab.research.google.com/drive/15k15A0X38sOgwqGR5j7QhDBS8_61R_ca +""" + +!pip install git+https://github.com/openai/CLIP.git --q +!pip install torch torchvision ftfy regex tqdm --q + +import torch +import clip +from PIL import Image +import numpy as np +from google.colab import files +import io + +device = "cuda" if torch.cuda.is_available() else "cpu" +print(f"Using device: {device}") + +model, preprocess = clip.load("ViT-B/32", device=device) + +ERA_CLASSES = [ + "vintage-60s", + "modern", + "90s-magazine", + "art-deco", + "punk", + "avant-garde", +] + +ERA_DESCRIPTIONS = { + "vintage-60s": "1960s aesthetic, psychedelic art, mod fashion, kodachrome film look, flower power era", + "modern": "modern contemporary aesthetic, clean design, digital photography, high resolution, 21st century style", + "90s-magazine": "1990s fashion magazine style, grunge aesthetic, harsh flash photography, teen vogue 90s, glossy editorial", + "art-deco": "art deco style, 1920s geometric patterns, gold and black luxury, great gatsby aesthetic, streamline moderne", + "punk": "punk rock aesthetic, diy zine style, xeroxed textures, safety pins, rebellious and chaotic visual style", + "avant-garde": "avant-garde fashion, experimental art, high concept, unconventional silhouette, futuristic and edgy", +} + +PROMPT_TEMPLATES = [ + "a photo representing the {} aesthetic", + "artistic rendering in {} style", + "an image from the {} era", + "visuals depicting {}", + "a high quality example of {} culture", + "a magazine scan showing {}", + "fashion photography in the style of {}", + "a poster with {} design elements", +] + + +class EraClassifier: + def __init__(self): + self.centroids = None + self.class_names = ERA_CLASSES + self.build_text_features() + + def build_text_features(self): + """ + Pre-computes the embedding centroids for all eras. + """ + print("Building text embeddings for eras...") + class_centroids = [] + + with torch.no_grad(): + for label in self.class_names: + desc = ERA_DESCRIPTIONS[label] + + prompts = [tpl.format(desc) for tpl in PROMPT_TEMPLATES] + + tokens = clip.tokenize(prompts).to(device) + text_features = model.encode_text(tokens) + + text_features = text_features / text_features.norm(dim=-1, keepdim=True) + + centroid = text_features.mean(dim=0) + centroid = centroid / centroid.norm() + + class_centroids.append(centroid) + + self.centroids = torch.stack(class_centroids).to(device) + print("Text embeddings built successfully.") + + def predict(self, image_path, threshold=0.20): + image = Image.open(image_path).convert("RGB") + img_tensor = preprocess(image).unsqueeze(0).to(device) + + with torch.no_grad(): + img_feat = model.encode_image(img_tensor) + img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) + + similarity = 100.0 * img_feat @ self.centroids.T + + probs = similarity.softmax(dim=-1).cpu().numpy()[0] + raw_scores = similarity.cpu().numpy()[0] / 100.0 + + results = [] + for i, label in enumerate(self.class_names): + results.append( + { + "label": label, + "score": float(raw_scores[i]), + "confidence_pct": float(probs[i] * 100), + } + ) + + results.sort(key=lambda x: x["score"], reverse=True) + top_result = results[0] + + if top_result["score"] < threshold: + prediction = "undefined/mixed" + else: + prediction = top_result["label"] + + return prediction, results + + +# Runner + +classifier = EraClassifier() + +print("\nPlease upload an image to analyze...") +uploaded = files.upload() + +if len(uploaded) > 0: + img_path = list(uploaded.keys())[0] + + prediction, details = classifier.predict(img_path) + + print("\n" + "=" * 30) + print(f"PREDICTED ERA: {prediction.upper()}") + print("=" * 30) + + print(f"\nDetailed Breakdown:") + print(f"{'Era/Style':<15} | {'Cosine Sim':<12} | {'Rel Confidence':<15}") + print("-" * 45) + + for item in details: + print( + f"{item['label']:<15} | {item['score']:.4f} | {item['confidence_pct']:.1f}%" + ) diff --git a/models/era_cult_ref.py b/models/era_cult_ref.py @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -"""Era/Cult_Ref.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/15k15A0X38sOgwqGR5j7QhDBS8_61R_ca -""" - -!pip install git+https://github.com/openai/CLIP.git --q -!pip install torch torchvision ftfy regex tqdm --q - -import torch -import clip -from PIL import Image -import numpy as np -from google.colab import files -import io - -device = "cuda" if torch.cuda.is_available() else "cpu" -print(f"Using device: {device}") - -model, preprocess = clip.load("ViT-B/32", device=device) - -ERA_CLASSES = [ - "vintage-60s", - "modern", - "90s-magazine", - "art-deco", - "punk", - "avant-garde" -] - -ERA_DESCRIPTIONS = { - "vintage-60s": "1960s aesthetic, psychedelic art, mod fashion, kodachrome film look, flower power era", - "modern": "modern contemporary aesthetic, clean design, digital photography, high resolution, 21st century style", - "90s-magazine": "1990s fashion magazine style, grunge aesthetic, harsh flash photography, teen vogue 90s, glossy editorial", - "art-deco": "art deco style, 1920s geometric patterns, gold and black luxury, great gatsby aesthetic, streamline moderne", - "punk": "punk rock aesthetic, diy zine style, xeroxed textures, safety pins, rebellious and chaotic visual style", - "avant-garde": "avant-garde fashion, experimental art, high concept, unconventional silhouette, futuristic and edgy" -} - -PROMPT_TEMPLATES = [ - "a photo representing the {} aesthetic", - "artistic rendering in {} style", - "an image from the {} era", - "visuals depicting {}", - "a high quality example of {} culture", - "a magazine scan showing {}", - "fashion photography in the style of {}", - "a poster with {} design elements", -] - - -class EraClassifier: - def __init__(self): - self.centroids = None - self.class_names = ERA_CLASSES - self.build_text_features() - - def build_text_features(self): - """ - Pre-computes the embedding centroids for all eras. - """ - print("Building text embeddings for eras...") - class_centroids = [] - - with torch.no_grad(): - for label in self.class_names: - desc = ERA_DESCRIPTIONS[label] - - prompts = [tpl.format(desc) for tpl in PROMPT_TEMPLATES] - - tokens = clip.tokenize(prompts).to(device) - text_features = model.encode_text(tokens) - - text_features = text_features / text_features.norm(dim=-1, keepdim=True) - - centroid = text_features.mean(dim=0) - centroid = centroid / centroid.norm() - - class_centroids.append(centroid) - - self.centroids = torch.stack(class_centroids).to(device) - print("Text embeddings built successfully.") - - def predict(self, image_path, threshold=0.20): - image = Image.open(image_path).convert("RGB") - img_tensor = preprocess(image).unsqueeze(0).to(device) - - with torch.no_grad(): - img_feat = model.encode_image(img_tensor) - img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) - - similarity = (100.0 * img_feat @ self.centroids.T) - - probs = similarity.softmax(dim=-1).cpu().numpy()[0] - raw_scores = similarity.cpu().numpy()[0] / 100.0 - - results = [] - for i, label in enumerate(self.class_names): - results.append({ - "label": label, - "score": float(raw_scores[i]), - "confidence_pct": float(probs[i] * 100) - }) - - results.sort(key=lambda x: x["score"], reverse=True) - top_result = results[0] - - if top_result["score"] < threshold: - prediction = "undefined/mixed" - else: - prediction = top_result["label"] - - return prediction, results - -# RUNNER - -classifier = EraClassifier() - -print("\nPlease upload an image to analyze...") -uploaded = files.upload() - -if len(uploaded) > 0: - img_path = list(uploaded.keys())[0] - - prediction, details = classifier.predict(img_path) - - print("\n" + "="*30) - print(f"PREDICTED ERA: {prediction.upper()}") - print("="*30) - - print(f"\nDetailed Breakdown:") - print(f"{'Era/Style':<15} | {'Cosine Sim':<12} | {'Rel Confidence':<15}") - print("-" * 45) - - for item in details: - print(f"{item['label']:<15} | {item['score']:.4f} | {item['confidence_pct']:.1f}%") -\ No newline at end of file diff --git a/models/inpainting.py b/models/inpainting.py @@ -0,0 +1,283 @@ +""" +Inpainting +Original file: https://colab.research.google.com/drive/15HZcoDGdzTaDO9d1uIUMI8LQToCP4ryC +""" + +## Fal.ai API + +!pip install fal-client + +import numpy as np +import pandas as pd +import fal_client +import requests +import os +import numpy as np +import scipy.ndimage +from io import BytesIO +from PIL import Image, ImageFilter +from IPython.display import display + +os.environ["FAL_KEY"] = "YOUR_API_KEY" + +CLEAN_IMAGE_PATH = "clean_image_path" +DRAWN_IMAGE_PATH = "drawn_image_path" +MASK_OUTPUT_PATH = "solid_mask.png" + + +def generate_and_save_mask(clean_path, drawn_path, save_path): + print("Generating mask with robust filtering...") + + if not os.path.exists(clean_path) or not os.path.exists(drawn_path): + print(" Error: Input images not found.") + return False + + img_clean = Image.open(clean_path).convert("RGB").resize((1024, 1024)) + img_drawn = Image.open(drawn_path).convert("RGB").resize((1024, 1024)) + + clean_blur = np.array(img_clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) + drawn_blur = np.array(img_drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) + + diff_arr = np.abs(drawn_blur - clean_blur) + mask_arr = np.max(diff_arr, axis=2) + + mask_binary = mask_arr > 30 + + mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) + mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) + mask = mask.filter(ImageFilter.MaxFilter(9)) + + mask.save(save_path) + img_clean.save("resized_clean.png") + + print(" The AI will ONLY edit the WHITE area below:") + display(mask.resize((256, 256))) + + return True + + +def run_fal_inpainting(image_path, mask_path): + print("Uploading images to Fal.ai...") + + image_url = fal_client.upload_file(image_path) + mask_url = fal_client.upload_file(mask_path) + + print("Running Flux Dev Fill...") + + try: + handler = fal_client.submit( + "fal-ai/flux-lora-fill", + arguments={ + "prompt": "The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", + "image_url": image_url, + "mask_url": mask_url, + "guidance_scale": 30, + "num_inference_steps": 28, + "enable_safety_checker": False, + }, + ) + + result = handler.get() + + if "images" in result: + output_url = result["images"][0]["url"] + print(f"Success! Image generated: {output_url}") + + response = requests.get(output_url) + img = Image.open(BytesIO(response.content)) + img.save("final_output.png") + display(img) + else: + print(" API returned no images.") + print(result) + + except Exception as e: + print(f"Error during API call: {e}") + + +# MAIN EXECUTION + +if __name__ == "__main__": + success = generate_and_save_mask( + CLEAN_IMAGE_PATH, DRAWN_IMAGE_PATH, MASK_OUTPUT_PATH + ) + if success: + run_fal_inpainting("resized_clean.png", MASK_OUTPUT_PATH) + +## Stable Diffusion + +!pip install torch diffusers transformers accelerate gradio + +import torch +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image, ImageFilter +from diffusers import AutoPipelineForInpainting +import os +import scipy.ndimage + + +def load_model(): + model_id = "runwayml/stable-diffusion-inpainting" + device = "cuda" if torch.cuda.is_available() else "cpu" + + print(f"Loading model to {device} (this may take a minute)...") + try: + pipe = AutoPipelineForInpainting.from_pretrained( + model_id, torch_dtype=torch.float16, variant="fp16" + ).to(device) + pipe.enable_attention_slicing() + return pipe + except Exception as e: + print(f"Error loading model: {e}") + return None + + +def run_sketch_to_image( + pipe, clean_path, drawn_path, prompt, negative_prompt="", strength=0.85, seed=42 +): + if not os.path.exists(clean_path) or not os.path.exists(drawn_path): + print(" Error: Images not found.") + return + + img_clean = Image.open(clean_path).convert("RGB").resize((512, 512)) + img_drawn = Image.open(drawn_path).convert("RGB").resize((512, 512)) + + clean_blur = np.array( + img_clean.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 + ) + drawn_blur = np.array( + img_drawn.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16 + ) + + diff_arr = np.abs(drawn_blur - clean_blur) + + mask_arr = np.max(diff_arr, axis=2) + + mask_binary = mask_arr > 30 + + mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) + mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) + + mask = mask.filter(ImageFilter.MaxFilter(9)) + + generator = torch.Generator(device="cuda").manual_seed(seed) + + print(f" Generating with Strength {strength}...") + with torch.inference_mode(): + output = pipe( + prompt=prompt, + negative_prompt=negative_prompt, + image=img_drawn, + mask_image=mask, + strength=strength, + guidance_scale=8.0, + num_inference_steps=50, + generator=generator, + ).images[0] + + fig, axs = plt.subplots(1, 4, figsize=(20, 6)) + axs[0].imshow(img_clean) + axs[0].set_title("Initial") + axs[1].imshow(img_drawn) + axs[1].set_title("Sketch") + axs[2].imshow(mask, cmap="gray") + axs[2].set_title("Calculated Mask (Fixed)") + axs[3].imshow(output) + axs[3].set_title("Result") + plt.tight_layout() + plt.show() + return output + + +if __name__ == "__main__": + if "pipeline" not in globals(): + pipeline = load_model() + + if pipeline: + run_sketch_to_image( + pipe=pipeline, + clean_path="/kaggle/input/sample/scenery.jpg.png", + drawn_path="/kaggle/input/sample/scenery_sketch.jpg", + prompt="The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", + negative_prompt="", + strength=0.85, + seed=100, + ) + + +## Captioning + +!pip install -q einops timm + +import torch +from transformers import AutoProcessor, AutoModelForCausalLM +from PIL import Image +import time + +device = "cuda" if torch.cuda.is_available() else "cpu" +model_id = "microsoft/Florence-2-base" + +print(f" Loading Model: {model_id}...") + +model = AutoModelForCausalLM.from_pretrained( + model_id, trust_remote_code=True, torch_dtype=torch.float16 +).to(device) + +processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True) + +print("Model Loaded & Moved to GPU. Ready for Inference.") + + +def generate_concise_caption(image_path): + device = "cuda" if torch.cuda.is_available() else "cpu" + dtype = torch.float16 + + torch.cuda.reset_peak_memory_stats() + torch.cuda.synchronize() + start_time = time.time() + + image = Image.open(image_path).convert("RGB") + + prompt_task = "<DETAILED_CAPTION>" + + inputs = processor(text=prompt_task, images=image, return_tensors="pt").to( + device, dtype + ) + + generated_ids = model.generate( + input_ids=inputs["input_ids"], + pixel_values=inputs["pixel_values"], + max_new_tokens=64, + num_beams=1, + do_sample=False, + ) + + generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0] + caption = processor.post_process_generation( + generated_text, task=prompt_task, image_size=(image.width, image.height) + )[prompt_task] + + caption = caption.strip() + if not caption.endswith("."): + last_period_index = caption.rfind(".") + if last_period_index != -1: + caption = caption[: last_period_index + 1] + + torch.cuda.synchronize() + end_time = time.time() + latency = end_time - start_time + mem_gb = torch.cuda.max_memory_allocated() / (1024**3) + + print("-" * 30) + print(f"Result: {caption}") + print(f"Tokens: ~{len(caption.split())} words") # Approx count + print("-" * 30) + print(f"⚡ Latency: {latency:.4f}s") + print(f"VRAM: {mem_gb:.2f} GB") + print("-" * 30) + + return caption + + +text_output = generate_concise_caption("/kaggle/input/test1/tree_sketch.jpg") diff --git a/models/inpainting_api&local.py b/models/inpainting_api&local.py @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -"""inpainting (1) (1).ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/15HZcoDGdzTaDO9d1uIUMI8LQToCP4ryC -""" - -import numpy as np -import pandas as pd - -!pip install fal-client - -import fal_client -import requests -import os -import numpy as np -import scipy.ndimage -from io import BytesIO -from PIL import Image, ImageFilter -from IPython.display import display - -os.environ["FAL_KEY"] = "YOUR_API_KEY" - -CLEAN_IMAGE_PATH = "clean_image_path" -DRAWN_IMAGE_PATH = "drawn_image_path" -MASK_OUTPUT_PATH = "solid_mask.png" - -def generate_and_save_mask(clean_path, drawn_path, save_path): - print("🛠️ Generating mask with robust filtering...") - - if not os.path.exists(clean_path) or not os.path.exists(drawn_path): - print(" Error: Input images not found.") - return False - - img_clean = Image.open(clean_path).convert("RGB").resize((1024, 1024)) - img_drawn = Image.open(drawn_path).convert("RGB").resize((1024, 1024)) - - clean_blur = np.array(img_clean.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) - drawn_blur = np.array(img_drawn.filter(ImageFilter.GaussianBlur(2)), dtype=np.int16) - - diff_arr = np.abs(drawn_blur - clean_blur) - mask_arr = np.max(diff_arr, axis=2) - - mask_binary = mask_arr > 30 - - mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) - mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) - mask = mask.filter(ImageFilter.MaxFilter(9)) - - mask.save(save_path) - img_clean.save("resized_clean.png") - - print(" The AI will ONLY edit the WHITE area below:") - display(mask.resize((256, 256))) - - return True - - -def run_fal_inpainting(image_path, mask_path): - print(" Uploading images to Fal.ai...") - - image_url = fal_client.upload_file(image_path) - mask_url = fal_client.upload_file(mask_path) - - print(" Running Flux Dev Fill...") - - try: - handler = fal_client.submit( - "fal-ai/flux-lora-fill", - arguments={ - "prompt": "The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", - "image_url": image_url, - "mask_url": mask_url, - "guidance_scale": 30, - "num_inference_steps": 28, - "enable_safety_checker": False - } - ) - - result = handler.get() - - if 'images' in result: - output_url = result['images'][0]['url'] - print(f"✨ Success! Image generated: {output_url}") - - response = requests.get(output_url) - img = Image.open(BytesIO(response.content)) - img.save("final_output.png") - display(img) - else: - print(" API returned no images.") - print(result) - - except Exception as e: - print(f"Error during API call: {e}") - - -# MAIN EXECUTION - -if __name__ == "__main__": - success = generate_and_save_mask(CLEAN_IMAGE_PATH, DRAWN_IMAGE_PATH, MASK_OUTPUT_PATH) - if success: - run_fal_inpainting("resized_clean.png", MASK_OUTPUT_PATH) - -!pip install torch diffusers transformers accelerate gradio - -import torch -import numpy as np -import matplotlib.pyplot as plt -from PIL import Image, ImageFilter -from diffusers import AutoPipelineForInpainting -import os -import scipy.ndimage - -def load_model(): - model_id = "runwayml/stable-diffusion-inpainting" - device = "cuda" if torch.cuda.is_available() else "cpu" - - print(f"⏳ Loading model to {device} (this may take a minute)...") - try: - pipe = AutoPipelineForInpainting.from_pretrained( - model_id, - torch_dtype=torch.float16, - variant="fp16" - ).to(device) - pipe.enable_attention_slicing() - return pipe - except Exception as e: - print(f"Error loading model: {e}") - return None - -def run_sketch_to_image(pipe, clean_path, drawn_path, prompt, negative_prompt="", strength=0.85, seed=42): - if not os.path.exists(clean_path) or not os.path.exists(drawn_path): - print(" Error: Images not found.") - return - - img_clean = Image.open(clean_path).convert("RGB").resize((512, 512)) - img_drawn = Image.open(drawn_path).convert("RGB").resize((512, 512)) - - - clean_blur = np.array(img_clean.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16) - drawn_blur = np.array(img_drawn.filter(ImageFilter.GaussianBlur(radius=2)), dtype=np.int16) - - diff_arr = np.abs(drawn_blur - clean_blur) - - mask_arr = np.max(diff_arr, axis=2) - - mask_binary = mask_arr > 30 - - mask_filled = scipy.ndimage.binary_fill_holes(mask_binary) - mask = Image.fromarray((mask_filled * 255).astype(np.uint8)) - - mask = mask.filter(ImageFilter.MaxFilter(9)) - - generator = torch.Generator(device="cuda").manual_seed(seed) - - print(f" Generating with Strength {strength}...") - with torch.inference_mode(): - output = pipe( - prompt=prompt, - negative_prompt=negative_prompt, - image=img_drawn, - mask_image=mask, - strength=strength, - guidance_scale=8.0, - num_inference_steps=50, - generator=generator - ).images[0] - - fig, axs = plt.subplots(1, 4, figsize=(20, 6)) - axs[0].imshow(img_clean); axs[0].set_title("Initial") - axs[1].imshow(img_drawn); axs[1].set_title("Sketch") - axs[2].imshow(mask, cmap="gray"); axs[2].set_title("Calculated Mask (Fixed)") - axs[3].imshow(output); axs[3].set_title("Result") - plt.tight_layout() - plt.show() - return output - -if __name__ == "__main__": - if 'pipeline' not in globals(): - pipeline = load_model() - - if pipeline: - run_sketch_to_image( - pipe=pipeline, - clean_path="/kaggle/input/sample/scenery.jpg.png", - drawn_path="/kaggle/input/sample/scenery_sketch.jpg", - prompt="The image shows a river running through a lush green valley surrounded by trees, plants, grass, and poles. In the background, the sky is filled with clouds, creating a peaceful atmosphere.", - negative_prompt="", - strength=0.85, - seed=100 - ) - - - -!pip install -q einops timm - -import torch -from transformers import AutoProcessor, AutoModelForCausalLM -from PIL import Image - -device = "cuda" if torch.cuda.is_available() else "cpu" -model_id = "microsoft/Florence-2-base" - -print(f" Loading Model: {model_id}...") - -model = AutoModelForCausalLM.from_pretrained( - model_id, - trust_remote_code=True, - torch_dtype=torch.float16 -).to(device) - -processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True) - -print(" Model Loaded & Moved to GPU. Ready for Inference.") - -import time -import torch -from PIL import Image - -def generate_concise_caption(image_path): - device = "cuda" if torch.cuda.is_available() else "cpu" - dtype = torch.float16 - - torch.cuda.reset_peak_memory_stats() - torch.cuda.synchronize() - start_time = time.time() - - image = Image.open(image_path).convert("RGB") - - - prompt_task = "<DETAILED_CAPTION>" - - inputs = processor(text=prompt_task, images=image, return_tensors="pt").to(device, dtype) - - generated_ids = model.generate( - input_ids=inputs["input_ids"], - pixel_values=inputs["pixel_values"], - max_new_tokens=64, - num_beams=1, - do_sample=False - ) - - generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0] - caption = processor.post_process_generation( - generated_text, - task=prompt_task, - image_size=(image.width, image.height) - )[prompt_task] - - caption = caption.strip() - if not caption.endswith("."): - last_period_index = caption.rfind(".") - if last_period_index != -1: - caption = caption[:last_period_index + 1] - - torch.cuda.synchronize() - end_time = time.time() - latency = end_time - start_time - mem_gb = torch.cuda.max_memory_allocated() / (1024**3) - - print("-" * 30) - print(f"📝 Result: {caption}") - print(f"📏 Tokens: ~{len(caption.split())} words") # Approx count - print("-" * 30) - print(f"⚡ Latency: {latency:.4f}s") - print(f"💾 VRAM: {mem_gb:.2f} GB") - print("-" * 30) - - return caption - -text_output = generate_concise_caption("/kaggle/input/test1/tree_sktech.jpg") - diff --git a/models/lighting.py b/models/lighting.py @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- -"""lighting (1).ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1edtbuTtWprRBw_lOvGJuEEDu5HGc-KA3 +""" +Lighting +Original file: https://colab.research.google.com/drive/1edtbuTtWprRBw_lOvGJuEEDu5HGc-KA3 """ !pip install git+https://github.com/openai/CLIP.git --q !pip install torch torchvision pillow --q +## Build Text Bank + import torch import clip from PIL import Image @@ -48,13 +46,11 @@ PROMPT_TEMPLATES = [ "a professional photography setup using {}", ] -import torch -import clip -import numpy as np device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load("ViT-B/32", device=device) + def build_text_bank(output_path="lighting_text_bank.npz"): all_prompt_texts = [] all_prompt_labels = [] @@ -80,8 +76,8 @@ def build_text_bank(output_path="lighting_text_bank.npz"): centroid = centroid / centroid.norm() class_centroids.append(centroid.cpu().numpy()) - all_prompt_features = np.concatenate(all_prompt_features, axis=0); - class_centroids = np.stack(class_centroids, axis=0); + all_prompt_features = np.concatenate(all_prompt_features, axis=0) + class_centroids = np.stack(class_centroids, axis=0) np.savez( output_path, @@ -97,29 +93,15 @@ def build_text_bank(output_path="lighting_text_bank.npz"): if __name__ == "__main__": build_text_bank() -import torch -import clip -from PIL import Image - -device = "cuda" if torch.cuda.is_available() else "cpu" -model, preprocess = clip.load("ViT-B/32", device=device) - -def get_image_embedding(image_path: str) -> torch.Tensor: - image = Image.open(image_path).convert("RGB") - img_tensor = preprocess(image).unsqueeze(0).to(device) - - with torch.no_grad(): - img_feat = model.encode_image(img_tensor) - img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) - return img_feat.squeeze(0) +## Classify Lighting import numpy as np import torch - import clip from PIL import Image + def get_image_embedding(image_path: str) -> torch.Tensor: image = Image.open(image_path).convert("RGB") img_tensor = preprocess(image).unsqueeze(0).to(device) @@ -130,6 +112,7 @@ def get_image_embedding(image_path: str) -> torch.Tensor: return img_feat.squeeze(0) + def load_text_bank(path="lighting_text_bank.npz"): data = np.load(path, allow_pickle=True) centroids = data["class_centroids"] @@ -137,9 +120,10 @@ def load_text_bank(path="lighting_text_bank.npz"): centroids = centroids / np.linalg.norm(centroids, axis=1, keepdims=True) return centroids, class_names -def classify_lighting(image_path: str, - text_bank_path="lighting_text_bank.npz", - threshold: float = 0.25): + +def classify_lighting( + image_path: str, text_bank_path="lighting_text_bank.npz", threshold: float = 0.25 +): centroids, class_names = load_text_bank(text_bank_path) img_feat = get_image_embedding(image_path) @@ -160,11 +144,14 @@ def classify_lighting(image_path: str, return results + if __name__ == "__main__": - img_path = "/content/Screenshot 2025-11-24 194825.png" + img_path = "/content/test.png" result = classify_lighting(img_path) print(result) +## Demo Runner + !pip install ftfy regex tqdm import torch @@ -212,6 +199,7 @@ PROMPT_TEMPLATES = [ "a professional photography setup using {}", ] + def build_text_bank(output_path="lighting_text_bank.npz"): all_prompt_texts = [] all_prompt_labels = [] @@ -254,6 +242,7 @@ def build_text_bank(output_path="lighting_text_bank.npz"): build_text_bank() + def get_image_embedding(image_path: str) -> torch.Tensor: image = Image.open(image_path).convert("RGB") img_tensor = preprocess(image).unsqueeze(0).to(device) @@ -265,6 +254,7 @@ def get_image_embedding(image_path: str) -> torch.Tensor: return img_feat.squeeze(0) + DEFAULT_THRESHOLD = 0.28 @@ -292,10 +282,7 @@ def classify_lighting(image_path, threshold=DEFAULT_THRESHOLD): best_score = float(best_score.detach().cpu()) sims_list = sims.detach().cpu().tolist() - sims_dict = { - str(cls): float(s) - for cls, s in zip(class_names, sims_list) - } + sims_dict = {str(cls): float(s) for cls, s in zip(class_names, sims_list)} if best_score < threshold: predicted = "no_specific_lighting" @@ -304,7 +291,11 @@ def classify_lighting(image_path, threshold=DEFAULT_THRESHOLD): return predicted, best_score, sims_dict + +# Runner + from google.colab import files + uploaded = files.upload() image_path = list(uploaded.keys())[0] @@ -316,4 +307,3 @@ print("Best score:", score) print("\nAll similarities:") for k, v in sims.items(): print(f"{k:20s}: {v:.3f}") - diff --git a/models/sketch2img.py b/models/sketch2img.py @@ -0,0 +1,57 @@ +""" +Sketch to Image +Original file: https://colab.research.google.com/drive/1rE5rC71QRvdyxlhnjZUZ320xOV0xrdgx +""" + +## Stable Diffusion + +!pip install diffusers==0.30.0 transformers accelerate safetensors xformers torch --upgrade + +from diffusers import StableDiffusionPipeline +import torch +from PIL import Image + +model_id = "runwayml/stable-diffusion-v1-5" + +pipe = StableDiffusionPipeline.from_pretrained( + model_id, torch_dtype=torch.float16, use_safetensors=True +).to("cuda") + +pipe.enable_xformers_memory_efficient_attention() +pipe.enable_attention_slicing() + +import time +from PIL import Image +import subprocess + +prompt = "A luxury wristwatch placed on a black velvet pedestal, enclosed within a subtle metallic frame. Low-key dramatic lighting with controlled highlights. High-end jewelry product atmosphere. Deep neutral palette with soft contrast transitions." +negative_prompt = "" + +height, width = 512, 512 +steps = 30 +cfg = 7 + +torch.cuda.empty_cache() +start = time.time() + +image = pipe( + prompt=prompt, + negative_prompt=negative_prompt, + height=height, + width=width, + guidance_scale=cfg, + num_inference_steps=steps, +).images[0] + +end = time.time() + +image.save("output.png") + +print(f"Generation Time: {end - start:.2f} seconds") + +## GPU Usage + +!nvidia-smi + +gpu_usage = subprocess.getoutput("nvidia-smi").split("\n") +print("\n".join(gpu_usage[:10])) diff --git a/models/sketch2img_local_server.py b/models/sketch2img_local_server.py @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -"""sketch2img_local_server (1).ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1rE5rC71QRvdyxlhnjZUZ320xOV0xrdgx -""" - -!pip install diffusers==0.30.0 transformers accelerate safetensors xformers torch --upgrade - -from diffusers import StableDiffusionPipeline -import torch -from PIL import Image - -model_id = "runwayml/stable-diffusion-v1-5" - -pipe = StableDiffusionPipeline.from_pretrained( - model_id, - torch_dtype=torch.float16, - use_safetensors=True -).to("cuda") - -pipe.enable_xformers_memory_efficient_attention() -pipe.enable_attention_slicing() - -import time -from PIL import Image -import subprocess - -prompt = "A luxury wristwatch placed on a black velvet pedestal, enclosed within a subtle metallic frame. Low-key dramatic lighting with controlled highlights. High-end jewelry product atmosphere. Deep neutral palette with soft contrast transitions." -negative_prompt = "" - -height, width = 512, 512 -steps = 30 -cfg = 7 - -torch.cuda.empty_cache() -start = time.time() - -image = pipe( - prompt=prompt, - negative_prompt=negative_prompt, - height=height, - width=width, - guidance_scale=cfg, - num_inference_steps=steps -).images[0] - -end = time.time() - -image.save("output.png") - -print(f"⏱️ Generation Time: {end - start:.2f} seconds") - -image - -!nvidia-smi - -gpu_usage = subprocess.getoutput('nvidia-smi').split('\n') -print("\n".join(gpu_usage[:10])) -\ No newline at end of file diff --git a/models/style.py b/models/style.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- -"""style.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1YFqRnpIrbZiN2fwOVcr3FUMdHS3WZdwf +""" +Style +Original file: https://colab.research.google.com/drive/1YFqRnpIrbZiN2fwOVcr3FUMdHS3WZdwf """ !pip install git+https://github.com/openai/CLIP.git --q @@ -23,14 +20,13 @@ print(f"Using device: {device}") model, preprocess = clip.load("ViT-B/32", device=device) STYLE_CLASSES = [ - # --- Pop / Comic / Print --- + # Pop / Comic / Print "pop-art", "halftone-print", "comic-book", "graphic-novel", "newspaper-comic-strip", - - # --- Cartoon / Animation --- + # Cartoon / Animation "cartoon-style", "minimal-flat-cartoon", "rubber-hose", @@ -41,8 +37,7 @@ STYLE_CLASSES = [ "ghibli-style", "superhero-comic", "disney-style", - - # --- Line-Based --- + # Line-Based "line-art", "continuous-line", "blind-contour", @@ -52,8 +47,7 @@ STYLE_CLASSES = [ "technical-blueprint", "engraving", "monoline-icons", - - # --- Traditional Media --- + # Traditional Media "watercolor", "gouache", "oil-painting", @@ -65,8 +59,7 @@ STYLE_CLASSES = [ "ink-wash", "colored-pencil", "mixed-media", - - # --- Realism / Naturalism --- + # Realism / Naturalism "realism", "naturalism", "photorealism", @@ -76,8 +69,7 @@ STYLE_CLASSES = [ "wildlife-realism", "scientific-illustration", "documentary-illustration", - - # --- Abstract / Modernist --- + # Abstract / Modernist "abstract-expressionism", "minimal-abstract", "geometric-abstraction", @@ -90,14 +82,12 @@ STYLE_CLASSES = [ "magical-realism", "op-art", "escher-impossible", - - # --- Collage / Cut --- + # Collage / Cut "paper-cutout", "analog-collage", "digital-collage", "photomontage", - - # --- Historical / Cultural --- + # Historical / Cultural "ukiyo-e", "indian-miniature", "persian-miniature", @@ -105,8 +95,7 @@ STYLE_CLASSES = [ "art-deco", "medieval-illustration", "byzantine-icon", - - # --- Retro / Vintage --- + # Retro / Vintage "vintage", "mid-century-modern", "psychedelic-60s", @@ -116,15 +105,13 @@ STYLE_CLASSES = [ "retro-futurism", "risograph", "screenprint", - - # --- Street / Subculture --- + # Street / Subculture "graffiti", "street-art", "lowbrow-pop", "sticker-bomb", "zine-grunge", - - # --- Digital / 3D / Tech --- + # Digital / 3D / Tech "pixel-art", "isometric-pixel", "voxel-art", @@ -136,20 +123,18 @@ STYLE_CLASSES = [ "glitch-art", "chromatic-aberration", "datamosh", - "vhs-analog" + "vhs-analog", ] STYLE_DESCRIPTIONS = { - - # --- Pop / Comic / Print --- + # Pop / Comic / Print "pop-art": "pop art, comic book style, bold colors, halftone dots, thick outlines", "halftone-print": "halftone dot shading, CMYK print texture, vintage comic printing", "comic-book": "dynamic panels, bold inking, action lines, superhero comic style", "graphic-novel": "moody shadows, dramatic black ink, narrative panels, serious tone", "newspaper-comic-strip": "simple line work, flat shading, classic newspaper strip style", - - # --- Cartoon / Animation --- + # Cartoon / Animation "cartoon-style": "bold outlines, flat colors, exaggerated characters and expressions", "minimal-flat-cartoon": "simple vector shapes, flat color fills, minimal details", "rubber-hose": "1930s animation, noodle limbs, vintage rubber hose characters", @@ -160,8 +145,7 @@ STYLE_DESCRIPTIONS = { "ghibli-style": "soft painterly shading, warm palettes, whimsical nature scenes", "superhero-comic": "muscular characters, dramatic shading, intense action poses", "disney-style": "smooth polished shading, expressive eyes, classic animation aesthetic", - - # --- Line-Based --- + # Line-Based "line-art": "clean linear outlines, no shading, monochrome strokes", "continuous-line": "one unbroken flowing line forming the subject", "blind-contour": "distorted outlines, drawn without looking, loose sketch style", @@ -171,8 +155,7 @@ STYLE_DESCRIPTIONS = { "technical-blueprint": "precise technical diagrams, blue background, white lines", "engraving": "fine parallel carved lines, antique print look", "monoline-icons": "uniform line weight, minimal shapes, iconographic design", - - # --- Traditional Media --- + # Traditional Media "watercolor": "transparent washes, soft gradients, bleeding pigment, paper texture", "gouache": "opaque matte colors, flat coverage, velvety paint texture", "oil-painting": "rich color blends, heavy brushstrokes, textured canvas feel", @@ -184,8 +167,7 @@ STYLE_DESCRIPTIONS = { "ink-wash": "monochrome fluid washes, sumi-e brush style, soft gradients", "colored-pencil": "fine pencil texture, layered shading, visible grain", "mixed-media": "combined materials, layered textures, hybrid collage effects", - - # --- Realism / Naturalism --- + # Realism / Naturalism "realism": "accurate representation, natural lighting, lifelike detail", "naturalism": "organic colors, soft shading, true-to-life textures", "photorealism": "photo-like accuracy, crisp details, polished lighting", @@ -195,8 +177,7 @@ STYLE_DESCRIPTIONS = { "wildlife-realism": "accurate animal anatomy, natural fur/feather rendering", "scientific-illustration": "precise labeled diagrams, technical accuracy", "documentary-illustration": "real-life scenes, muted tones, reportage style", - - # --- Abstract / Modernist --- + # Abstract / Modernist "abstract-expressionism": "gestural strokes, energetic splashes, emotional color", "minimal-abstract": "simple geometric forms, large negative space, clarity", "geometric-abstraction": "strict shapes, patterns, bold geometry", @@ -209,14 +190,12 @@ STYLE_DESCRIPTIONS = { "magical-realism": "ordinary scenes with subtle fantastical elements", "op-art": "optical illusions, vibrating lines, high-contrast patterns", "escher-impossible": "paradoxical architecture, infinite loops, tessellations", - - # --- Collage / Cut --- + # Collage / Cut "paper-cutout": "flat colored shapes, crisp edges, layered paper look", "analog-collage": "torn paper edges, magazine scraps, glue texture", "digital-collage": "layered photography fragments, digital assembly", "photomontage": "multiple photos combined into surreal compositions", - - # --- Historical / Cultural --- + # Historical / Cultural "ukiyo-e": "japanese woodblock style, flat colors, bold outlines, wave patterns", "indian-miniature": "fine detailing, ornate patterns, traditional storytelling", "persian-miniature": "vibrant colors, intricate decoration, ornate scenes", @@ -224,8 +203,7 @@ STYLE_DESCRIPTIONS = { "art-deco": "geometric symmetry, metallic accents, luxury aesthetic", "medieval-illustration": "flat perspective, gold leaf accents, illuminated style", "byzantine-icon": "gold backgrounds, frontal figures, religious icon style", - - # --- Retro / Vintage --- + # Retro / Vintage "vintage": "aged tones, film grain, retro print texture, nostalgic aesthetic", "mid-century-modern": "retro shapes, muted tones, playful geometric forms", "psychedelic-60s": "vivid gradients, swirling patterns, trippy colors", @@ -235,15 +213,13 @@ STYLE_DESCRIPTIONS = { "retro-futurism": "futuristic style imagined in the past, old sci-fi aesthetic", "risograph": "duotone neon inks, grainy overlays, misaligned color layers", "screenprint": "flat inks, bold shapes, layered silkscreen texture", - - # --- Street / Subculture --- + # Street / Subculture "graffiti": "spray paint texture, drips, tags, bold lettering", "street-art": "urban wall murals, stencil work, mixed street media", "lowbrow-pop": "quirky surrealism, outsider cartoon style, punk influence", "sticker-bomb": "dense overlapping stickers, colorful chaotic layers", "zine-grunge": "photocopy texture, rough cut edges, DIY punk style", - - # --- Digital / 3D / Tech --- + # Digital / 3D / Tech "pixel-art": "pixelated blocks, low resolution, retro game style", "isometric-pixel": "45-degree pixel perspective, 3D pixel illusion", "voxel-art": "3D cubes, blocky shapes, volumetric pixel aesthetic", @@ -255,7 +231,7 @@ STYLE_DESCRIPTIONS = { "glitch-art": "broken pixels, digital errors, RGB misalignment", "chromatic-aberration": "red-blue fringing, lens distortion edges", "datamosh": "compression smears, frame artifacts, melting visuals", - "vhs-analog": "scanlines, tape noise, retro analog glitch" + "vhs-analog": "scanlines, tape noise, retro analog glitch", } @@ -289,7 +265,7 @@ PROMPT_TEMPLATES = [ "a rendered scene with {} visual identity", "an artistic study done with {} aesthetics", "an experiment in {} visual style", - "a fully rendered image designed around {} style" + "a fully rendered image designed around {} style", ] @@ -340,18 +316,20 @@ class StyleClassifier: img_feat = model.encode_image(img_tensor) img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) - similarity = (100.0 * img_feat @ self.centroids.T) + similarity = 100.0 * img_feat @ self.centroids.T probs = similarity.softmax(dim=-1).cpu().numpy()[0] raw_scores = similarity.cpu().numpy()[0] / 100.0 results = [] for i, style in enumerate(self.class_names): - results.append({ - "style": style, - "score": float(raw_scores[i]), - "confidence_pct": float(probs[i] * 100) - }) + results.append( + { + "style": style, + "score": float(raw_scores[i]), + "confidence_pct": float(probs[i] * 100), + } + ) results.sort(key=lambda x: x["score"], reverse=True) @@ -364,7 +342,8 @@ class StyleClassifier: return prediction, results -# EXECUTION + +# Runner classifier = StyleClassifier() @@ -379,4 +358,6 @@ for filename in uploaded.keys(): print("-" * 30) print("Full Breakdown:") for res in details[:5]: - print(f"{res['style']:<20} | Score: {res['score']:.4f} | Conf: {res['confidence_pct']:.2f}%") -\ No newline at end of file + print( + f"{res['style']:<20} | Score: {res['score']:.4f} | Conf: {res['confidence_pct']:.2f}%" + ) diff --git a/models/stylesheet.py b/models/stylesheet.py @@ -0,0 +1,1173 @@ +# -*- coding: utf-8 -*- +""" +Stylesheet & Prompt Generation +Original file: https://colab.research.google.com/drive/1eZLWqhB0KKFLhCA0E3qDHXpdvEIH44yE +""" + +## Stylesheet Generation + +import json +import math +import re +import numpy as np +from collections import defaultdict +from typing import List, Dict, Tuple, Any, Optional +from sklearn.cluster import KMeans + +RANK_DECAY_FACTOR = 0.85 +MIN_CONSENSUS_THRESHOLD = 0.05 +PALETTE_SIZE = 5 + + +def hex_to_rgb(hex_str: str) -> List[int]: + """Converts '#FF5733' to [255, 87, 51] for math operations.""" + try: + hex_str = hex_str.strip().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])) + + +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 "Display/Other" + + +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.material_filter = { + "glossy", + "matte", + "metallic", + "organic", + "wood", + "laminated", + "porcelain", + "wet-look", + "denim", + "fabric", + "leather", + } + + self.aliases = { + "Colour Palette": "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 + + source_data = {} + if "data" in data and isinstance(data["data"], dict): + 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 + + for category, payload in source_data.items(): + if category in ["filename", "meta"]: + continue + + std_category = self.aliases.get(category, category) + + if std_category == "Color Palette": + if isinstance(payload, dict) and "palette" in payload: + for hex_code in payload["palette"]: + if isinstance(hex_code, str) and hex_code.startswith("#"): + self.color_pool.append(hex_to_rgb(hex_code)) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str) and item.startswith("#"): + self.color_pool.append(hex_to_rgb(item)) + + elif std_category == "Typography": + if isinstance(payload, dict) and "scores" in payload: + payload = payload["scores"] + vectors = self._normalize(payload) + + for rank, (font_name, score) in enumerate(vectors): + 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) + + else: + if isinstance(payload, dict) and "scores" in payload: + payload = payload["scores"] + vectors = self._normalize(payload) + + 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(): + 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 + + 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": {}} + + for category, tags_map in self.feature_registry.items(): + candidates = [] + for tag, occurrences in tags_map.items(): + 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[:5] + + if "Background/Texture" in final_json["results"]: + textures = [] + materials = [] + + for item in final_json["results"]["Background/Texture"]: + label = item["label"].lower().replace("_", " ").replace("-", " ") + is_material = False + if item["label"] in self.material_filter: + is_material = True + else: + for mat in self.material_filter: + if mat in label: + is_material = True + break + + if is_material: + materials.append(item) + else: + textures.append(item) + + if textures: + final_json["results"]["Background/Texture"] = textures + else: + del final_json["results"]["Background/Texture"] + + if materials: + if "Material Look" in final_json["results"]: + final_json["results"]["Material Look"].extend(materials) + final_json["results"]["Material Look"].sort( + key=lambda x: x["score"], reverse=True + ) + else: + final_json["results"]["Material Look"] = materials + + 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 + + if len(self.color_pool) > 0: + try: + k_clusters = min(len(self.color_pool), PALETTE_SIZE) + + kmeans = KMeans(n_clusters=k_clusters, n_init=1, 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 + + +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: + for json_str in json_strings_list: + if not json_str: + continue + try: + data = json.loads(json_str) + engine.ingest_data(data) + except: + continue + + final_output = engine.compute_final_stylesheet() + return json.dumps(final_output) + + +## Prompt Generation + +DEFAULT_OPTIONS = { + "width": 1024, + "height": 1024, + "steps": 30, + "guidance_scale": 7.5, + "sampler": "k_lms", + "seed": "random", +} + + +def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: + """ + Organizes the flat stylesheet tags into Primary and Secondary categories. + """ + results = sheet.get("results", sheet) + norm: Dict[str, Dict[str, List[str]]] = {} + + for cat, val in results.items(): + if cat in ("filename", "meta", "Color Palette"): + continue + + items = [] + if isinstance(val, list): + items = val + elif isinstance(val, dict) and "scores" in val: + items = val["scores"] + + try: + items_sorted = sorted( + items, key=lambda x: float(x.get("score", 0)), reverse=True + ) + except Exception: + items_sorted = items + + labels = [ + str(it.get("label")).strip() for it in items_sorted if it.get("label") + ] + if labels: + norm[cat] = {"Primary": labels[0], "Secondary": labels[1:]} + + return norm + + +STYLE_WORDS = [ + "hand-drawn", + "hand drawn", + "sketch", + "thin lines", + "minimalistic", + "minimal", + "flat", + "cartoon", + "comic", + "pop-art", + "surreal", + "photorealistic", + "realistic", + "painting", + "oil painting", + "pastel", + "artistic", + "illustration", + "line drawing", + "line art", + "3d effect", + "texture", + "pattern", + "soft light", + "studio light", + "dramatic", + "cinematic", + "moody", + "vintage", + "retro", + "poster", + "render", + "octane render", + "unreal engine", + "digital illustration", +] + +style_words_regex = re.compile( + r"\b(" + "|".join(re.escape(w) for w in STYLE_WORDS) + r")\b", + flags=re.IGNORECASE, +) + + +def remove_style_words(caption: str) -> str: + """ + Removes specific artistic style descriptions from the caption so they + don't conflict with the Stylesheet. + """ + if not caption: + return caption + clean = style_words_regex.sub(" ", caption) + clean = re.sub(r"\s{2,}", " ", clean).strip() + return clean + + +def extract_subject(caption_text: str) -> str: + """ + Heuristic to guess the main subject (first few words of the first sentence). + """ + txt = caption_text.strip() + if not txt: + return "subject" + first_clause = re.split(r"[.?!]\s*", txt)[0] + return " ".join(first_clause.split()[:12]) + + +def pick_style_phrases( + norm: Dict[str, Dict[str, List[str]]], max_secondaries=3 +) -> List[str]: + """ + Extracts non-color features from the Stylesheet. + """ + cats = [ + "Style", + "Background/Texture", + "Lighting", + "Composition", + "Era/Cultural Reference", + "Material Look", + "Typography", + ] + key_map = { + "Style": "art style", + "Background/Texture": "background texture", + "Lighting": "lighting", + "Composition": "composition", + "Era/Cultural Reference": "era", + "Material Look": "material", + "Typography": "typography", + } + + out: List[str] = [] + for cat in cats: + entry = norm.get(cat) + if not entry: + continue + + prim = entry.get("Primary") + secs = entry.get("Secondary", []) + chosen: List[str] = [] + if prim: + chosen.append(prim) + for s in secs: + if s and len(chosen) < (1 + max_secondaries): + chosen.append(s) + if chosen: + out.append(f"{key_map.get(cat, cat)}: {', '.join(chosen)}") + return out + + +def style_to_tone(primary: str) -> str: + """ + Infers a general tone based on the primary style. + """ + s = (primary or "").lower() + if any(k in s for k in ["retro", "collage", "poster", "pop-art", "surreal"]): + return "illustrative, textured, poster-like" + if any(k in s for k in ["photoreal", "realistic", "film"]): + return "photorealistic, high-detail" + return "high detail, sharp focus" + + +def generate_magic_prompt( + stylesheet_json_str: str, caption: str, user_prompt: str +) -> str: + try: + sheet = json.loads(stylesheet_json_str) + except Exception as e: + return f"{user_prompt}. The image features: {caption}" + + cleaned_caption = remove_style_words(caption) + + norm = normalize_sheet(sheet) + subject = extract_subject(cleaned_caption) + + style_phrases = pick_style_phrases(norm) + + style_primary = (norm.get("Style") or {}).get("Primary", "") + tone_hint = style_to_tone(style_primary) + + style_instruction = "" + if style_phrases: + style_instruction = "Style elements: " + "; ".join(style_phrases) + + prompt_lines: List[str] = [ + f"Main subject: {subject}.", + f"Description: {cleaned_caption}.", + ] + + if user_prompt: + prompt_lines.append(f"User instruction: {user_prompt}.") + + if style_instruction: + prompt_lines.append(f"{style_instruction}.") + + prompt_lines += [ + f"Tone: {tone_hint}.", + "Note: Colors should be derived strictly from the Description.", + "Avoid: blurry, deformed, text, watermark, extra limbs, artifacts.", + ] + + prompt = "\n".join(prompt_lines) + + payload = { + "prompt": prompt, + "negative_prompt": "blurry, low resolution, deformed, text, watermark, extra limbs, artifacts", + "options": DEFAULT_OPTIONS, + "provenance": {"generated_from": "final_output"}, + "weights": {"caption": 0.7, "style_sheet": 0.2, "user": 0.1}, + } + + return prompt + " " + json.dumps(payload) diff --git a/models/stylesheet_gen&prompt_gen.py b/models/stylesheet_gen&prompt_gen.py @@ -1,671 +0,0 @@ -# -*- coding: utf-8 -*- -"""Untitled6.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1eZLWqhB0KKFLhCA0E3qDHXpdvEIH44yE -""" - -import json -import math -import re -import numpy as np -from collections import defaultdict -from typing import List, Dict, Tuple, Any, Optional -from sklearn.cluster import KMeans - -RANK_DECAY_FACTOR = 0.85 -MIN_CONSENSUS_THRESHOLD = 0.05 -PALETTE_SIZE = 5 - - -def hex_to_rgb(hex_str: str) -> List[int]: - """Converts '#FF5733' to [255, 87, 51] for math operations.""" - try: - hex_str = hex_str.strip().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])) - - -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 "Display/Other" - - -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.material_filter = { - "glossy", "matte", "metallic", "organic", - "wood", "laminated", "porcelain", "wet-look", - "denim", "fabric", "leather" - } - - self.aliases = { - "Colour Palette": "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 - - source_data = {} - if "data" in data and isinstance(data["data"], dict): - 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 - - for category, payload in source_data.items(): - if category in ["filename", "meta"]: continue - - std_category = self.aliases.get(category, category) - - if std_category == "Color Palette": - if isinstance(payload, dict) and "palette" in payload: - for hex_code in payload["palette"]: - if isinstance(hex_code, str) and hex_code.startswith('#'): - self.color_pool.append(hex_to_rgb(hex_code)) - elif isinstance(payload, list): - for item in payload: - if isinstance(item, str) and item.startswith('#'): - self.color_pool.append(hex_to_rgb(item)) - - elif std_category == "Typography": - if isinstance(payload, dict) and "scores" in payload: - payload = payload["scores"] - vectors = self._normalize(payload) - - for rank, (font_name, score) in enumerate(vectors): - 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) - - else: - if isinstance(payload, dict) and "scores" in payload: - payload = payload["scores"] - vectors = self._normalize(payload) - - 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(): - 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 - - 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": {}} - - for category, tags_map in self.feature_registry.items(): - candidates = [] - for tag, occurrences in tags_map.items(): - 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[:5] - - if "Background/Texture" in final_json["results"]: - textures = [] - materials = [] - - for item in final_json["results"]["Background/Texture"]: - label = item["label"].lower().replace("_", " ").replace("-", " ") - is_material = False - if item["label"] in self.material_filter: - is_material = True - else: - for mat in self.material_filter: - if mat in label: - is_material = True - break - - if is_material: - materials.append(item) - else: - textures.append(item) - - if textures: - final_json["results"]["Background/Texture"] = textures - else: - del final_json["results"]["Background/Texture"] - - if materials: - if "Material Look" in final_json["results"]: - final_json["results"]["Material Look"].extend(materials) - final_json["results"]["Material Look"].sort(key=lambda x: x['score'], reverse=True) - else: - final_json["results"]["Material Look"] = materials - - 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 - - if len(self.color_pool) > 0: - try: - k_clusters = min(len(self.color_pool), PALETTE_SIZE) - - kmeans = KMeans(n_clusters=k_clusters, n_init=1, 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 - - -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: - for json_str in json_strings_list: - if not json_str: continue - try: - data = json.loads(json_str) - engine.ingest_data(data) - except: continue - - final_output = engine.compute_final_stylesheet() - return json.dumps(final_output) - - -# PROMPT GENERATION LOGIC - - -DEFAULT_OPTIONS = { - "width": 1024, - "height": 1024, - "steps": 30, - "guidance_scale": 7.5, - "sampler": "k_lms", - "seed": "random", -} - -def normalize_sheet(sheet: Dict[str, Any]) -> Dict[str, Dict[str, List[str]]]: - """ - Organizes the flat stylesheet tags into Primary and Secondary categories. - """ - results = sheet.get("results", sheet) - norm: Dict[str, Dict[str, List[str]]] = {} - - for cat, val in results.items(): - if cat in ("filename", "meta", "Color Palette"): - continue - - items = [] - if isinstance(val, list): - items = val - elif isinstance(val, dict) and "scores" in val: - items = val["scores"] - - try: - items_sorted = sorted( - items, key=lambda x: float(x.get("score", 0)), reverse=True - ) - except Exception: - items_sorted = items - - labels = [ - str(it.get("label")).strip() for it in items_sorted if it.get("label") - ] - if labels: - norm[cat] = {"Primary": labels[0], "Secondary": labels[1:]} - - return norm - -STYLE_WORDS = [ - "hand-drawn", "hand drawn", "sketch", "thin lines", "minimalistic", - "minimal", "flat", "cartoon", "comic", "pop-art", "surreal", "photorealistic", - "realistic", "painting", "oil painting", "pastel", "artistic", "illustration", - "line drawing", "line art", "3d effect", "texture", "pattern", "soft light", - "studio light", "dramatic", "cinematic", "moody", "vintage", "retro", "poster", - "render", "octane render", "unreal engine","digital illustration", -] - -style_words_regex = re.compile( - r"\b(" + "|".join(re.escape(w) for w in STYLE_WORDS) + r")\b", - flags=re.IGNORECASE, -) - -def remove_style_words(caption: str) -> str: - """ - Removes specific artistic style descriptions from the caption so they - don't conflict with the Stylesheet. - """ - if not caption: return caption - clean = style_words_regex.sub(" ", caption) - clean = re.sub(r"\s{2,}", " ", clean).strip() - return clean - -def extract_subject(caption_text: str) -> str: - """ - Heuristic to guess the main subject (first few words of the first sentence). - """ - txt = caption_text.strip() - if not txt: return "subject" - first_clause = re.split(r"[.?!]\s*", txt)[0] - return " ".join(first_clause.split()[:12]) - -def pick_style_phrases(norm: Dict[str, Dict[str, List[str]]], max_secondaries=3) -> List[str]: - """ - Extracts non-color features from the Stylesheet. - """ - cats = [ - "Style", "Background/Texture", "Lighting", "Composition", - "Era/Cultural Reference", "Material Look", "Typography", - ] - key_map = { - "Style": "art style", - "Background/Texture": "background texture", - "Lighting": "lighting", - "Composition": "composition", - "Era/Cultural Reference": "era", - "Material Look": "material", - "Typography": "typography", - } - - out: List[str] = [] - for cat in cats: - entry = norm.get(cat) - if not entry: continue - - prim = entry.get("Primary") - secs = entry.get("Secondary", []) - chosen: List[str] = [] - if prim: chosen.append(prim) - for s in secs: - if s and len(chosen) < (1 + max_secondaries): - chosen.append(s) - if chosen: - out.append(f"{key_map.get(cat, cat)}: {', '.join(chosen)}") - return out - -def style_to_tone(primary: str) -> str: - """ - Infers a general tone based on the primary style. - """ - s = (primary or "").lower() - if any(k in s for k in ["retro", "collage", "poster", "pop-art", "surreal"]): - return "illustrative, textured, poster-like" - if any(k in s for k in ["photoreal", "realistic", "film"]): - return "photorealistic, high-detail" - return "high detail, sharp focus" - -def generate_magic_prompt(stylesheet_json_str: str, caption: str, user_prompt: str) -> str: - try: - sheet = json.loads(stylesheet_json_str) - except Exception as e: - return f"{user_prompt}. The image features: {caption}" - - cleaned_caption = remove_style_words(caption) - - norm = normalize_sheet(sheet) - subject = extract_subject(cleaned_caption) - - style_phrases = pick_style_phrases(norm) - - style_primary = (norm.get("Style") or {}).get("Primary", "") - tone_hint = style_to_tone(style_primary) - - - style_instruction = "" - if style_phrases: - style_instruction = "Style elements: " + "; ".join(style_phrases) - - prompt_lines: List[str] = [ - f"Main subject: {subject}.", - f"Description: {cleaned_caption}.", - ] - - if user_prompt: - prompt_lines.append(f"User instruction: {user_prompt}.") - - if style_instruction: - prompt_lines.append(f"{style_instruction}.") - - prompt_lines += [ - f"Tone: {tone_hint}.", - "Note: Colors should be derived strictly from the Description.", - "Avoid: blurry, deformed, text, watermark, extra limbs, artifacts.", - ] - - prompt = "\n".join(prompt_lines) - - payload = { - "prompt": prompt, - "negative_prompt": "blurry, low resolution, deformed, text, watermark, extra limbs, artifacts", - "options": DEFAULT_OPTIONS, - "provenance": {"generated_from": "final_output"}, - "weights": {"caption": 0.7, "style_sheet": 0.2, "user": 0.1}, - } - - return prompt + " " + json.dumps(payload) -\ No newline at end of file diff --git a/models/subject.py b/models/subject.py @@ -1,22 +1,14 @@ -# -*- coding: utf-8 -*- -"""BiRefNet.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1izs_-Hld8X4wtGylu8y-96PJb04Nc3kz +""" +Subject +Original file: https://colab.research.google.com/drive/1izs_-Hld8X4wtGylu8y-96PJb04Nc3kz """ -# Commented out IPython magic to ensure Python compatibility. !pip install -r https://raw.githubusercontent.com/ZhengPeng7/BiRefNet/main/requirements.txt !git clone https://github.com/ZhengPeng7/BiRefNet.git # %cd BiRefNet -"""Run This To Create The .pt file""" - -# Commented out IPython magic to ensure Python compatibility. -# %cd BiRefNet +## Create .pt file import torch from torchvision import transforms @@ -37,16 +29,18 @@ torch.save(model_fp16_cpu.state_dict(), "birefnet_fp16.pt") print("🔥 Saved fp16 model to birefnet_fp16.pt") image_size = (1024, 1024) -transform_image = transforms.Compose([ - transforms.Resize(image_size), - transforms.ToTensor(), - transforms.Normalize( - mean=[0.485, 0.456, 0.406], - std=[0.229, 0.224, 0.225], - ), -]) - -"""Inference Code""" +transform_image = transforms.Compose( + [ + transforms.Resize(image_size), + transforms.ToTensor(), + transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + ] +) + +## Inference Code import os import io @@ -61,7 +55,7 @@ from torchvision import transforms from google.colab import files -device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("Using device:", device) @@ -71,17 +65,19 @@ print("Cutouts will be saved in:", OUTPUT_DIR) image_size = (1024, 1024) -transform_image = transforms.Compose([ - transforms.Resize(image_size), - transforms.ToTensor(), - transforms.Normalize( - mean=[0.485, 0.456, 0.406], - std=[0.229, 0.224, 0.225], - ), -]) - - -if 'birefnet' not in globals(): +transform_image = transforms.Compose( + [ + transforms.Resize(image_size), + transforms.ToTensor(), + transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + ] +) + + +if "birefnet" not in globals(): raise RuntimeError( "Model `birefnet` not found. Load your BiRefNet model and weights first, e.g.:\n" " birefnet = BiRefNet(...)\n" @@ -102,7 +98,6 @@ def run_birefnet_on_image(pil_img: Image.Image, thresh: float = 0.5): """ orig_w, orig_h = pil_img.size - inp = transform_image(pil_img).unsqueeze(0).to(device) try: inp = inp.half() @@ -136,7 +131,7 @@ def make_binary_cutout(img_rgb: np.ndarray, mask_bin: np.ndarray): rgba = np.dstack([img_rgb, alpha_u8]) return rgba -#upload + print("🔼 Upload one or more images to segment with BiRefNet...") uploaded = files.upload() @@ -153,7 +148,9 @@ for fname, file_data in uploaded.items(): print(f"BiRefNet inference time: {t_inf:.3f} s") if KEEP_LARGEST: - num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_bin, connectivity=8) + num_labels, labels, stats, _ = cv2.connectedComponentsWithStats( + mask_bin, connectivity=8 + ) if num_labels > 1: areas = stats[1:, cv2.CC_STAT_AREA] max_idx = 1 + np.argmax(areas) @@ -167,14 +164,21 @@ for fname, file_data in uploaded.items(): print(" Saved final model cutout:", out_path) fig, axs = plt.subplots(1, 3, figsize=(12, 4)) - axs[0].imshow(img_np); axs[0].set_title("Original"); axs[0].axis("off") - axs[1].imshow(mask_bin, cmap="gray"); axs[1].set_title("Final model mask"); axs[1].axis("off") - axs[2].imshow(rgba_cutout); axs[2].set_title("Cutout (PNG, no background)"); axs[2].axis("off") - plt.tight_layout(); plt.show() + axs[0].imshow(img_np) + axs[0].set_title("Original") + axs[0].axis("off") + axs[1].imshow(mask_bin, cmap="gray") + axs[1].set_title("Final model mask") + axs[1].axis("off") + axs[2].imshow(rgba_cutout) + axs[2].set_title("Cutout (PNG, no background)") + axs[2].axis("off") + plt.tight_layout() + plt.show() files.download(out_path) except Exception as e: print(f" Error with {fname}: {e}") -print("\n Done! All cutouts saved in:", OUTPUT_DIR) -\ No newline at end of file +print("\n Done! All cutouts saved in:", OUTPUT_DIR) diff --git a/models/testing.py b/models/testing.py @@ -0,0 +1,156 @@ +""" +Testing Data Generation Template +Original file: https://colab.research.google.com/drive/1iDu3v3m3iOKyRs6IggnOPbN6tKKa9SRV +""" + +## Setup Environment + +!pip install requests tqdm icrawler + +import numpy as np +import pandas as pd +import os +import shutil +import urllib3 +import logging +from icrawler import ImageDownloader +from icrawler.builtin import BingImageCrawler + +OUTPUT_ROOT = "Feature_Dataset_HQ" +TARGET_COUNT = 100 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class No403Filter(logging.Filter): + def filter(self, record): + return "403" not in record.getMessage() + + +logging.getLogger("downloader").addFilter(No403Filter()) + + +class RobustDownloader(ImageDownloader): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.session.verify = False + self.session.headers.update( + { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Referer": "https://www.bing.com/", + } + ) + + +SEARCH_MAPPING = { + "soft-light": "soft light, gentle illumination, low contrast, wrap-around light", + "specular-highlights": "strong specular highlights, shiny skin, glossy reflections, bright hotspots", + "backlit": "backlit lighting from behind the subject, rim light, silhouette effect, sun flare", + "studio-lighting": "professional studio lighting, controlled illumination, three-point lighting setup", + "flat-lighting": "flat lighting, low contrast, even illumination, no shadows, cloudy day look", + "dramatic-contrast": "dramatic high-contrast lighting, chiaroscuro, hard shadows, noir style", + "diffused": "soft diffused lighting, cloudy sky light, light box look, scattered light, no hard edges", +} + + +def count_files(folder): + if not os.path.exists(folder): + return 0 + return len([f for f in os.listdir(folder) if f.endswith((".jpg", ".png", ".jpeg"))]) + + +def trim_excess(folder, limit): + files = sorted( + [f for f in os.listdir(folder) if f.endswith((".jpg", ".png", ".jpeg"))] + ) + if len(files) > limit: + print(f" -> Overshot! Trimming {len(files) - limit} extra images...") + for f in files[limit:]: + try: + os.remove(os.path.join(folder, f)) + except: + pass + + +if __name__ == "__main__": + if not os.path.exists(OUTPUT_ROOT): + os.makedirs(OUTPUT_ROOT) + + print(f"Starting Recursive Download (Target: {TARGET_COUNT})...") + + for category, query in SEARCH_MAPPING.items(): + print(f"\n--- Processing: {category} ---") + save_dir = os.path.join(OUTPUT_ROOT, category) + + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + # Initial Search Depth + search_depth = 200 + + while True: + current_count = count_files(save_dir) + + # 1. CHECK CONDITION + if current_count >= TARGET_COUNT: + print(f" -> Success! Found {current_count} images.") + break + + # 2. INCREASE DEPTH + print( + f" -> Have {current_count}/{TARGET_COUNT}. Increasing search depth to {search_depth}..." + ) + + crawler = BingImageCrawler( + downloader_cls=RobustDownloader, + feeder_threads=1, + parser_threads=2, + downloader_threads=4, + storage={"root_dir": save_dir}, + log_level="ERROR", + ) + + # 3. SAFETY OFFSET + if current_count == 0: + offset_param = 0 + else: + offset_param = "auto" + + crawler.crawl( + keyword=query, + max_num=search_depth, + filters={"type": "photo"}, + file_idx_offset=offset_param, + ) + + # Increase the search buffer for the next loop + search_depth += 500 + + # Safety break + if search_depth > 5000: + print(" -> Hit safety limit (5000). Stopping.") + break + + # Final Trim + trim_excess(save_dir, TARGET_COUNT) + print(f" Final Count: {count_files(save_dir)}") + + print(f"\n Material Look Dataset (HQ) Download Complete!") + + +## Compress dataset folder into .zip + +import shutil +import os + +FOLDER_TO_ZIP = "MaterialLook_Dataset_HQ" +OUTPUT_FILENAME = "MaterialLook_Dataset" # .zip will be added automatically + +if __name__ == "__main__": + if os.path.exists(FOLDER_TO_ZIP): + print(f"Compressing '{FOLDER_TO_ZIP}'...") + shutil.make_archive(OUTPUT_FILENAME, "zip", FOLDER_TO_ZIP) + zip_size = os.path.getsize(f"{OUTPUT_FILENAME}.zip") / (1024 * 1024) + print(f" Success! Created {OUTPUT_FILENAME}.zip ({zip_size:.2f} MB)") + else: + print(f" Error: Folder '{FOLDER_TO_ZIP}' not found.") diff --git a/models/testing_data_gen_template.py b/models/testing_data_gen_template.py @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -"""testing_data_gen_template.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1iDu3v3m3iOKyRs6IggnOPbN6tKKa9SRV -""" - -import numpy as np -import pandas as pd - -!pip install requests tqdm - -pip install icrawler - -import os -import shutil -import urllib3 -import logging -from icrawler import ImageDownloader -from icrawler.builtin import BingImageCrawler - -# --- CONFIGURATION --- -OUTPUT_ROOT = "Feature_Dataset_HQ" -TARGET_COUNT = 100 - -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - -class No403Filter(logging.Filter): - def filter(self, record): - return "403" not in record.getMessage() - -logging.getLogger("downloader").addFilter(No403Filter()) - -class RobustDownloader(ImageDownloader): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.session.verify = False - self.session.headers.update({ - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'https://www.bing.com/' - }) -#classes according to the feature -SEARCH_MAPPING = { - "soft-light": "soft light, gentle illumination, low contrast, wrap-around light", - "specular-highlights": "strong specular highlights, shiny skin, glossy reflections, bright hotspots", - "backlit": "backlit lighting from behind the subject, rim light, silhouette effect, sun flare", - "studio-lighting": "professional studio lighting, controlled illumination, three-point lighting setup", - "flat-lighting": "flat lighting, low contrast, even illumination, no shadows, cloudy day look", - "dramatic-contrast": "dramatic high-contrast lighting, chiaroscuro, hard shadows, noir style", - "diffused": "soft diffused lighting, cloudy sky light, light box look, scattered light, no hard edges" -} - -def count_files(folder): - if not os.path.exists(folder): return 0 - return len([f for f in os.listdir(folder) if f.endswith(('.jpg', '.png', '.jpeg'))]) - -def trim_excess(folder, limit): - files = sorted([f for f in os.listdir(folder) if f.endswith(('.jpg', '.png', '.jpeg'))]) - if len(files) > limit: - print(f" -> Overshot! Trimming {len(files) - limit} extra images...") - for f in files[limit:]: - try: os.remove(os.path.join(folder, f)) - except: pass - -if __name__ == "__main__": - if not os.path.exists(OUTPUT_ROOT): - os.makedirs(OUTPUT_ROOT) - - print(f"Starting Recursive Download (Target: {TARGET_COUNT})...") - - for category, query in SEARCH_MAPPING.items(): - print(f"\n--- Processing: {category} ---") - save_dir = os.path.join(OUTPUT_ROOT, category) - - # Ensure folder exists to prevent FileNotFoundError - if not os.path.exists(save_dir): - os.makedirs(save_dir) - - # Initial Search Depth - search_depth = 200 - - while True: - current_count = count_files(save_dir) - - # 1. CHECK CONDITION - if current_count >= TARGET_COUNT: - print(f" -> Success! Found {current_count} images.") - break - - # 2. INCREASE DEPTH - # If we already have some images, we need to search deeper to find NEW ones - print(f" -> Have {current_count}/{TARGET_COUNT}. Increasing search depth to {search_depth}...") - - crawler = BingImageCrawler( - downloader_cls=RobustDownloader, - feeder_threads=1, - parser_threads=2, - downloader_threads=4, - storage={'root_dir': save_dir}, - log_level='ERROR' - ) - - # 3. SAFETY OFFSET - # If folder is empty, offset=0. If files exist, offset='auto' to append. - if current_count == 0: - offset_param = 0 - else: - offset_param = 'auto' - - crawler.crawl( - keyword=query, - max_num=search_depth, - filters={'type': 'photo'}, - file_idx_offset=offset_param - ) - - # Increase the search buffer for the next loop - # If 200 wasn't enough, next time try 700, then 1200... - search_depth += 500 - - # Safety break - if search_depth > 5000: - print(" -> Hit safety limit (5000). Stopping.") - break - - # Final Trim - trim_excess(save_dir, TARGET_COUNT) - print(f" Final Count: {count_files(save_dir)}") - - print(f"\n Material Look Dataset (HQ) Download Complete!") - -import shutil -import os - -# --- CONFIGURATION --- -FOLDER_TO_ZIP = "MaterialLook_Dataset_HQ" -OUTPUT_FILENAME = "MaterialLook_Dataset" # .zip will be added automatically - -if __name__ == "__main__": - if os.path.exists(FOLDER_TO_ZIP): - print(f"Compressing '{FOLDER_TO_ZIP}'...") - - # This creates the zip file - shutil.make_archive(OUTPUT_FILENAME, 'zip', FOLDER_TO_ZIP) - - # Check size for confirmation - zip_size = os.path.getsize(f"{OUTPUT_FILENAME}.zip") / (1024 * 1024) - print(f" Success! Created {OUTPUT_FILENAME}.zip ({zip_size:.2f} MB)") - else: - print(f" Error: Folder '{FOLDER_TO_ZIP}' not found.") -\ No newline at end of file diff --git a/models/text&mat/texture&mat.py b/models/text&mat/texture&mat.py @@ -1,441 +0,0 @@ -# -*- coding: utf-8 -*- -"""Untitled7.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/1u-5cHHckvZPMjIFJ5YJcCAGzDWVPRl91 -""" - -import numpy as np -import pandas as pd - -import torch -import torch.nn as nn -import numpy as np -import os - -CENTROIDS_PATH = 'texture_centroids_augmented.pth_path' -OUTPUT_PATH = 'texture_centroids_augmented.pt_path' - -class DataWrapper(nn.Module): - def __init__(self, data_dict): - super().__init__() - self.data = torch.jit.Attribute(data_dict, dict[str, torch.Tensor]) - - def forward(self): - return self.data - -if os.path.exists(CENTROIDS_PATH): - print("Loading .pth file...") - - raw_data = torch.load(CENTROIDS_PATH, map_location='cpu', weights_only=False) - - print("Processing data...") - clean_dict = {} - for k, v in raw_data.items(): - if isinstance(v, np.ndarray): - clean_dict[k] = torch.from_numpy(v).float() - else: - clean_dict[k] = v.float() - - wrapper = DataWrapper(clean_dict) - scripted_wrapper = torch.jit.script(wrapper) - - scripted_wrapper.save(OUTPUT_PATH) - print(f"Success! Saved as {OUTPUT_PATH}") -else: - print("File not found.") - -import torch -import torchvision.transforms as T -from PIL import Image -import numpy as np -import os -import glob -from typing import Dict, List - -DATASET_ROOT = 'Texture_Dataset_path' -OUTPUT_PATH = 'texture_centroids_s14.pt' -DEVICE = torch.device('cuda' if torch.cuda.is_available() else "cpu") - - -print(f"Loading DINOv2-S14 on {DEVICE}...") -model = torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14') -model.to(DEVICE) -model.eval() - -transform = T.Compose([ - T.Resize(256, interpolation=T.InterpolationMode.BICUBIC), - T.CenterCrop(224), - T.ToTensor(), - T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), -]) - - - -def get_folder_centroid(folder_path): - image_files = glob.glob(os.path.join(folder_path, "*")) - valid_exts = ('.jpg', '.jpeg', '.png', '.bmp', '.webp') - image_files = [f for f in image_files if f.lower().endswith(valid_exts)] - - if not image_files: return None - print(f"Processing '{os.path.basename(folder_path)}' ({len(image_files)} images)...") - - vectors = [] - for img_path in image_files: - try: - img = Image.open(img_path).convert('RGB') - img_t = transform(img).unsqueeze(0).to(DEVICE) - with torch.no_grad(): - output = model.forward_features(img_t) - vectors.append(output['x_norm_clstoken'].cpu()) - except Exception: continue - - if not vectors: return None - - all_vecs = torch.cat(vectors, dim=0) - centroid = torch.mean(all_vecs, dim=0) - centroid = torch.nn.functional.normalize(centroid, p=2, dim=0) - return centroid - - - -class CentroidsWrapper(torch.nn.Module): - def __init__(self, data_dict): - super().__init__() - self.keys: List[str] = list(data_dict.keys()) - self.tensors = torch.nn.ParameterList([ - torch.nn.Parameter(data_dict[k]) for k in self.keys - ]) - - def forward(self) -> Dict[str, torch.Tensor]: - result: Dict[str, torch.Tensor] = {} - for i, tensor in enumerate(self.tensors): - result[self.keys[i]] = tensor - return result - - -# EXECUTION - - -if not os.path.exists(DATASET_ROOT): - print(f" Error: Dataset path not found: {DATASET_ROOT}") -else: - centroids_dict = {} - folders = sorted([d for d in os.listdir(DATASET_ROOT) if os.path.isdir(os.path.join(DATASET_ROOT, d))]) - - print(f"Found {len(folders)} texture categories.") - - for category in folders: - full_path = os.path.join(DATASET_ROOT, category) - centroid = get_folder_centroid(full_path) - if centroid is not None: - centroids_dict[category] = centroid - - if centroids_dict: - print(f"\nSaving {len(centroids_dict)} centroids to {OUTPUT_PATH}...") - - wrapper = CentroidsWrapper(centroids_dict) - scripted_wrapper = torch.jit.script(wrapper) - scripted_wrapper.save(OUTPUT_PATH) - - print(" SUCCESS! File saved.") - print(f"Output: {OUTPUT_PATH}") - else: - print(" Failed to generate any centroids.") - -import torch -import torchvision.transforms as T -import torch.nn.functional as F -from PIL import Image -import numpy as np -import os - -DEVICE = torch.device('cuda' if torch.cuda.is_available() else "cpu") -PATCH_SIZE = 14 - -CENTROIDS_PATH = 'CENTROIDS_PATH' -TEST_IMAGE_PATH = 'TEST_IMAGE_PATH' - -class TextureScoreScanner: - def __init__(self, centroids_dict): - self.device = DEVICE - print(f"Loading DINOv2 (S14 - Small) on {self.device}...") - self.model = torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14') - self.model = self.model.half().to(self.device) - self.model.eval() - - self.texture_names = [] - matrix_list = [] - print("Processing centroids...") - for name, tensor in centroids_dict.items(): - if tensor.dim() > 1: tensor = tensor.squeeze() - tensor = tensor.half().to(self.device) - - self.texture_names.append(name) - matrix_list.append(tensor) - - if len(matrix_list) > 0: - matrix = torch.stack(matrix_list) - self.centroid_matrix = F.normalize(matrix, p=2, dim=1) - - if self.centroid_matrix.shape[1] != 384: - print(f" WARNING: Dimension Mismatch! S14 expects 384, got {self.centroid_matrix.shape[1]}") - else: - raise ValueError("No valid centroids found in the .pt file.") - - def preprocess(self, img_path): - if not os.path.exists(img_path): raise FileNotFoundError(f"Image not found: {img_path}") - img = Image.open(img_path).convert('RGB') - w, h = img.size - - new_w = (w // PATCH_SIZE) * PATCH_SIZE - new_h = (h // PATCH_SIZE) * PATCH_SIZE - - transform = T.Compose([ - T.Resize((new_h, new_w)), - T.ToTensor(), - T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), - ]) - return transform(img).unsqueeze(0).half().to(self.device) - - def get_scores(self, img_path): - input_tensor = self.preprocess(img_path) - - with torch.no_grad(): - features = self.model.forward_features(input_tensor) - - patches = features["x_norm_patchtokens"].squeeze(0) - patches_norm = F.normalize(patches, p=2, dim=1) - - similarity_matrix = torch.mm(patches_norm, self.centroid_matrix.T) - - class_scores = torch.mean(similarity_matrix, dim=0).cpu().numpy() - - results = {} - for i, score in enumerate(class_scores): - results[self.texture_names[i]] = float(score) - - return sorted(results.items(), key=lambda item: item[1], reverse=True) - - def get_filtered_predictions(self, sorted_scores, max_count=5, relative_thresh=0.85, absolute_floor=0.15): - if not sorted_scores: return [] - - final_list = [] - top_score = sorted_scores[0][1] - - for name, score in sorted_scores: - if len(final_list) >= max_count: break - - if score < absolute_floor: continue - if score < (top_score * relative_thresh): break - - final_list.append((name, score)) - - return final_list - -# --- MAIN EXECUTION --- - -if os.path.exists(CENTROIDS_PATH): - print(f"Loading dictionary from {CENTROIDS_PATH}...") - - loaded_container = torch.jit.load(CENTROIDS_PATH, map_location='cpu') - centroids_data = loaded_container() - - scanner = TextureScoreScanner(centroids_data) - - print(f"\nAnalyzing: {TEST_IMAGE_PATH}") - if os.path.exists(TEST_IMAGE_PATH): - raw_scores = scanner.get_scores(TEST_IMAGE_PATH) - - smart_results = scanner.get_filtered_predictions( - raw_scores, - max_count=3, # Show max 3 textures - relative_thresh=0.85, # Must be 85% as good as the top match - absolute_floor=0.15 # Minimum similarity score - ) - - print("\n" + "="*40) - print(f"DETECTED TEXTURES ({len(smart_results)})") - print("="*40) - - if len(smart_results) > 0: - top_score_val = raw_scores[0][1] - for name, score in smart_results: - percentage = (score / top_score_val) * 100 - print(f"• {name.upper().ljust(15)} : Score {score:.4f} (Conf: {percentage:.0f}%)") - else: - print("No significant texture match found.") - else: - print(f"Error: Test image not found at {TEST_IMAGE_PATH}") - -else: - print(f" Error: Centroids file '{CENTROIDS_PATH}' not found.") - print("Please run the training/generation script first.") - -import torch -import os - -MODEL_NAME = 'dinov2_vits14' -OUTPUT_FILENAME = "dinov2_vits14_fp16.pt" - -print(f" Downloading {MODEL_NAME} (FP32) from Torch Hub...") -model = torch.hub.load('facebookresearch/dinov2', MODEL_NAME) - -print(" Converting model to Half Precision (FP16)...") -model = model.half() - -print(f" Saving weights to {OUTPUT_FILENAME}...") -torch.save(model.state_dict(), OUTPUT_FILENAME) - -# Verify -file_size_mb = os.path.getsize(OUTPUT_FILENAME) / (1024 * 1024) -print("-" * 30) -print(f" Done! Saved to: {os.path.abspath(OUTPUT_FILENAME)}") -print(f" File Size: {file_size_mb:.2f} MB") -print("-" * 30) - -import torch -import torchvision.transforms as T -import torch.nn.functional as F -from PIL import Image -import numpy as np -import os - -DEVICE = torch.device('cuda' if torch.cuda.is_available() else "cpu") -PATCH_SIZE = 14 - -CENTROIDS_PATH = 'centroids_path' -TEST_IMAGE_PATH = 'test_image_path' - -MODEL_WEIGHTS_PATH = '/kaggle/working/dinov2_vits14_fp16.pt' - -class TextureScoreScanner: - def __init__(self, centroids_dict): - self.device = DEVICE - print(f"Loading DINOv2 (S14 - Small) on {self.device}...") - - self.model = torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14', pretrained=False) - - self.model = self.model.half().to(self.device) - - if os.path.exists(MODEL_WEIGHTS_PATH): - print(f"Loading local weights from: {MODEL_WEIGHTS_PATH}") - state_dict = torch.load(MODEL_WEIGHTS_PATH, map_location=self.device) - self.model.load_state_dict(state_dict) - else: - raise FileNotFoundError(f"Weights file not found at: {MODEL_WEIGHTS_PATH}") - - self.model.eval() - - self.texture_names = [] - matrix_list = [] - - print("Processing centroids...") - for name, tensor in centroids_dict.items(): - if tensor.dim() > 1: tensor = tensor.squeeze() - - tensor = tensor.half().to(self.device) - - self.texture_names.append(name) - matrix_list.append(tensor) - - if len(matrix_list) > 0: - matrix = torch.stack(matrix_list) - self.centroid_matrix = F.normalize(matrix, p=2, dim=1) - - if self.centroid_matrix.shape[1] != 384: - print(f"⚠️ WARNING: Dimension Mismatch! S14 expects 384, got {self.centroid_matrix.shape[1]}") - else: - raise ValueError("No valid centroids found in the .pt file.") - - def preprocess(self, img_path): - if not os.path.exists(img_path): raise FileNotFoundError(f"Image not found: {img_path}") - img = Image.open(img_path).convert('RGB') - w, h = img.size - - new_w = (w // PATCH_SIZE) * PATCH_SIZE - new_h = (h // PATCH_SIZE) * PATCH_SIZE - - transform = T.Compose([ - T.Resize((new_h, new_w)), - T.ToTensor(), - T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), - ]) - - return transform(img).unsqueeze(0).half().to(self.device) - - def get_scores(self, img_path): - input_tensor = self.preprocess(img_path) - - with torch.no_grad(): - features = self.model.forward_features(input_tensor) - - patches = features["x_norm_patchtokens"].squeeze(0) - patches_norm = F.normalize(patches, p=2, dim=1) - - similarity_matrix = torch.mm(patches_norm, self.centroid_matrix.T) - - class_scores = torch.mean(similarity_matrix, dim=0).float().cpu().numpy() - - results = {} - for i, score in enumerate(class_scores): - results[self.texture_names[i]] = float(score) - - return sorted(results.items(), key=lambda item: item[1], reverse=True) - - def get_filtered_predictions(self, sorted_scores, max_count=5, relative_thresh=0.85, absolute_floor=0.15): - if not sorted_scores: return [] - - final_list = [] - top_score = sorted_scores[0][1] - - for name, score in sorted_scores: - if len(final_list) >= max_count: break - - if score < absolute_floor: continue - if score < (top_score * relative_thresh): break - - final_list.append((name, score)) - - return final_list - -# --- MAIN EXECUTION --- - -if os.path.exists(CENTROIDS_PATH): - print(f"Loading dictionary from {CENTROIDS_PATH}...") - - loaded_container = torch.jit.load(CENTROIDS_PATH, map_location='cpu') - centroids_data = loaded_container() - - scanner = TextureScoreScanner(centroids_data) - - print(f"\nAnalyzing: {TEST_IMAGE_PATH}") - if os.path.exists(TEST_IMAGE_PATH): - raw_scores = scanner.get_scores(TEST_IMAGE_PATH) - - smart_results = scanner.get_filtered_predictions( - raw_scores, - max_count=3, - relative_thresh=0.85, - absolute_floor=0.05 # Kept at 0.05 as discussed - ) - - print("\n" + "="*40) - print(f"DETECTED TEXTURES ({len(smart_results)})") - print("="*40) - - if len(smart_results) > 0: - top_score_val = raw_scores[0][1] - for name, score in smart_results: - percentage = (score / top_score_val) * 100 - print(f"• {name.upper().ljust(15)} : Score {score:.4f} (Conf: {percentage:.0f}%)") - else: - print("No significant texture match found.") - else: - print(f"Error: Test image not found at {TEST_IMAGE_PATH}") - -else: - print(f" Error: Centroids file '{CENTROIDS_PATH}' not found.") - diff --git a/models/text&mat/dinov2_vits14_fp16.pt b/models/texture/dinov2_vits14_fp16.pt Binary files differ. diff --git a/models/texture/texture.py b/models/texture/texture.py @@ -0,0 +1,486 @@ +# -*- coding: utf-8 -*- +""" +Texture and Material +Original file: https://colab.research.google.com/drive/1u-5cHHckvZPMjIFJ5YJcCAGzDWVPRl91 +""" + +## .pth -> .pt + +import numpy as np +import pandas as pd +import torch +import torch.nn as nn +import numpy as np +import os + +CENTROIDS_PATH = "texture_centroids_augmented.pth_path" +OUTPUT_PATH = "texture_centroids_augmented.pt_path" + + +class DataWrapper(nn.Module): + def __init__(self, data_dict): + super().__init__() + self.data = torch.jit.Attribute(data_dict, dict[str, torch.Tensor]) + + def forward(self): + return self.data + + +if os.path.exists(CENTROIDS_PATH): + print("Loading .pth file...") + raw_data = torch.load(CENTROIDS_PATH, map_location="cpu", weights_only=False) + + print("Processing data...") + clean_dict = {} + for k, v in raw_data.items(): + if isinstance(v, np.ndarray): + clean_dict[k] = torch.from_numpy(v).float() + else: + clean_dict[k] = v.float() + + wrapper = DataWrapper(clean_dict) + scripted_wrapper = torch.jit.script(wrapper) + scripted_wrapper.save(OUTPUT_PATH) + print(f"Success! Saved as {OUTPUT_PATH}") +else: + print("File not found.") + +## Generate centroids + +import torch +import torchvision.transforms as T +from PIL import Image +import numpy as np +import os +import glob +from typing import Dict, List + +DATASET_ROOT = "Texture_Dataset_path" +OUTPUT_PATH = "texture_centroids_s14.pt" +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + +print(f"Loading DINOv2-S14 on {DEVICE}...") +model = torch.hub.load("facebookresearch/dinov2", "dinov2_vits14") +model.to(DEVICE) +model.eval() + +transform = T.Compose( + [ + T.Resize(256, interpolation=T.InterpolationMode.BICUBIC), + T.CenterCrop(224), + T.ToTensor(), + T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), + ] +) + + +def get_folder_centroid(folder_path): + image_files = glob.glob(os.path.join(folder_path, "*")) + valid_exts = (".jpg", ".jpeg", ".png", ".bmp", ".webp") + image_files = [f for f in image_files if f.lower().endswith(valid_exts)] + + if not image_files: + return None + print( + f"Processing '{os.path.basename(folder_path)}' ({len(image_files)} images)..." + ) + + vectors = [] + for img_path in image_files: + try: + img = Image.open(img_path).convert("RGB") + img_t = transform(img).unsqueeze(0).to(DEVICE) + with torch.no_grad(): + output = model.forward_features(img_t) + vectors.append(output["x_norm_clstoken"].cpu()) + except Exception: + continue + + if not vectors: + return None + + all_vecs = torch.cat(vectors, dim=0) + centroid = torch.mean(all_vecs, dim=0) + centroid = torch.nn.functional.normalize(centroid, p=2, dim=0) + return centroid + + +class CentroidsWrapper(torch.nn.Module): + def __init__(self, data_dict): + super().__init__() + self.keys: List[str] = list(data_dict.keys()) + self.tensors = torch.nn.ParameterList( + [torch.nn.Parameter(data_dict[k]) for k in self.keys] + ) + + def forward(self) -> Dict[str, torch.Tensor]: + result: Dict[str, torch.Tensor] = {} + for i, tensor in enumerate(self.tensors): + result[self.keys[i]] = tensor + return result + + +# Runner + +if not os.path.exists(DATASET_ROOT): + print(f" Error: Dataset path not found: {DATASET_ROOT}") +else: + centroids_dict = {} + folders = sorted( + [ + d + for d in os.listdir(DATASET_ROOT) + if os.path.isdir(os.path.join(DATASET_ROOT, d)) + ] + ) + + print(f"Found {len(folders)} texture categories.") + + for category in folders: + full_path = os.path.join(DATASET_ROOT, category) + centroid = get_folder_centroid(full_path) + if centroid is not None: + centroids_dict[category] = centroid + + if centroids_dict: + print(f"\nSaving {len(centroids_dict)} centroids to {OUTPUT_PATH}...") + + wrapper = CentroidsWrapper(centroids_dict) + scripted_wrapper = torch.jit.script(wrapper) + scripted_wrapper.save(OUTPUT_PATH) + + print(" SUCCESS! File saved.") + print(f"Output: {OUTPUT_PATH}") + else: + print(" Failed to generate any centroids.") + + +## Texture Scoring +import torch +import torchvision.transforms as T +import torch.nn.functional as F +from PIL import Image +import numpy as np +import os + +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") +PATCH_SIZE = 14 +CENTROIDS_PATH = "CENTROIDS_PATH" +TEST_IMAGE_PATH = "TEST_IMAGE_PATH" + + +class TextureScoreScanner: + def __init__(self, centroids_dict): + self.device = DEVICE + print(f"Loading DINOv2 (S14 - Small) on {self.device}...") + self.model = torch.hub.load("facebookresearch/dinov2", "dinov2_vits14") + self.model = self.model.half().to(self.device) + self.model.eval() + + self.texture_names = [] + matrix_list = [] + print("Processing centroids...") + for name, tensor in centroids_dict.items(): + if tensor.dim() > 1: + tensor = tensor.squeeze() + tensor = tensor.half().to(self.device) + + self.texture_names.append(name) + matrix_list.append(tensor) + + if len(matrix_list) > 0: + matrix = torch.stack(matrix_list) + self.centroid_matrix = F.normalize(matrix, p=2, dim=1) + + if self.centroid_matrix.shape[1] != 384: + print( + f" WARNING: Dimension Mismatch! S14 expects 384, got {self.centroid_matrix.shape[1]}" + ) + else: + raise ValueError("No valid centroids found in the .pt file.") + + def preprocess(self, img_path): + if not os.path.exists(img_path): + raise FileNotFoundError(f"Image not found: {img_path}") + img = Image.open(img_path).convert("RGB") + w, h = img.size + + new_w = (w // PATCH_SIZE) * PATCH_SIZE + new_h = (h // PATCH_SIZE) * PATCH_SIZE + + transform = T.Compose( + [ + T.Resize((new_h, new_w)), + T.ToTensor(), + T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), + ] + ) + return transform(img).unsqueeze(0).half().to(self.device) + + def get_scores(self, img_path): + input_tensor = self.preprocess(img_path) + + with torch.no_grad(): + features = self.model.forward_features(input_tensor) + + patches = features["x_norm_patchtokens"].squeeze(0) + patches_norm = F.normalize(patches, p=2, dim=1) + + similarity_matrix = torch.mm(patches_norm, self.centroid_matrix.T) + + class_scores = torch.mean(similarity_matrix, dim=0).cpu().numpy() + + results = {} + for i, score in enumerate(class_scores): + results[self.texture_names[i]] = float(score) + + return sorted(results.items(), key=lambda item: item[1], reverse=True) + + def get_filtered_predictions( + self, sorted_scores, max_count=5, relative_thresh=0.85, absolute_floor=0.15 + ): + if not sorted_scores: + return [] + + final_list = [] + top_score = sorted_scores[0][1] + + for name, score in sorted_scores: + if len(final_list) >= max_count: + break + + if score < absolute_floor: + continue + if score < (top_score * relative_thresh): + break + + final_list.append((name, score)) + + return final_list + + +# Runner + +if os.path.exists(CENTROIDS_PATH): + print(f"Loading dictionary from {CENTROIDS_PATH}...") + + loaded_container = torch.jit.load(CENTROIDS_PATH, map_location="cpu") + centroids_data = loaded_container() + + scanner = TextureScoreScanner(centroids_data) + + print(f"\nAnalyzing: {TEST_IMAGE_PATH}") + if os.path.exists(TEST_IMAGE_PATH): + raw_scores = scanner.get_scores(TEST_IMAGE_PATH) + + smart_results = scanner.get_filtered_predictions( + raw_scores, + max_count=3, # Show max 3 textures + relative_thresh=0.85, # Must be 85% as good as the top match + absolute_floor=0.15, # Minimum similarity score + ) + + print("\n" + "=" * 40) + print(f"DETECTED TEXTURES ({len(smart_results)})") + print("=" * 40) + + if len(smart_results) > 0: + top_score_val = raw_scores[0][1] + for name, score in smart_results: + percentage = (score / top_score_val) * 100 + print( + f"• {name.upper().ljust(15)} : Score {score:.4f} (Conf: {percentage:.0f}%)" + ) + else: + print("No significant texture match found.") + else: + print(f"Error: Test image not found at {TEST_IMAGE_PATH}") + +else: + print(f" Error: Centroids file '{CENTROIDS_PATH}' not found.") + print("Please run the training/generation script first.") + +## Convert DINOv2-S14 to FP16 + +import torch +import os + +MODEL_NAME = "dinov2_vits14" +OUTPUT_FILENAME = "dinov2_vits14_fp16.pt" + +print(f" Downloading {MODEL_NAME} (FP32) from Torch Hub...") +model = torch.hub.load("facebookresearch/dinov2", MODEL_NAME) + +print(" Converting model to Half Precision (FP16)...") +model = model.half() + +print(f" Saving weights to {OUTPUT_FILENAME}...") +torch.save(model.state_dict(), OUTPUT_FILENAME) + +# Verify +file_size_mb = os.path.getsize(OUTPUT_FILENAME) / (1024 * 1024) +print("-" * 30) +print(f" Done! Saved to: {os.path.abspath(OUTPUT_FILENAME)}") +print(f" File Size: {file_size_mb:.2f} MB") +print("-" * 30) + +## Texture Scoring with FP16 model +import torch +import torchvision.transforms as T +import torch.nn.functional as F +from PIL import Image +import numpy as np +import os + +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") +PATCH_SIZE = 14 +CENTROIDS_PATH = "centroids_path" +TEST_IMAGE_PATH = "test_image_path" +MODEL_WEIGHTS_PATH = "/kaggle/working/dinov2_vits14_fp16.pt" + + +class TextureScoreScanner: + def __init__(self, centroids_dict): + self.device = DEVICE + print(f"Loading DINOv2 (S14 - Small) on {self.device}...") + + self.model = torch.hub.load( + "facebookresearch/dinov2", "dinov2_vits14", pretrained=False + ) + + self.model = self.model.half().to(self.device) + + if os.path.exists(MODEL_WEIGHTS_PATH): + print(f"Loading local weights from: {MODEL_WEIGHTS_PATH}") + state_dict = torch.load(MODEL_WEIGHTS_PATH, map_location=self.device) + self.model.load_state_dict(state_dict) + else: + raise FileNotFoundError(f"Weights file not found at: {MODEL_WEIGHTS_PATH}") + + self.model.eval() + + self.texture_names = [] + matrix_list = [] + + print("Processing centroids...") + for name, tensor in centroids_dict.items(): + if tensor.dim() > 1: + tensor = tensor.squeeze() + + tensor = tensor.half().to(self.device) + + self.texture_names.append(name) + matrix_list.append(tensor) + + if len(matrix_list) > 0: + matrix = torch.stack(matrix_list) + self.centroid_matrix = F.normalize(matrix, p=2, dim=1) + + if self.centroid_matrix.shape[1] != 384: + print( + f"⚠️ WARNING: Dimension Mismatch! S14 expects 384, got {self.centroid_matrix.shape[1]}" + ) + else: + raise ValueError("No valid centroids found in the .pt file.") + + def preprocess(self, img_path): + if not os.path.exists(img_path): + raise FileNotFoundError(f"Image not found: {img_path}") + img = Image.open(img_path).convert("RGB") + w, h = img.size + + new_w = (w // PATCH_SIZE) * PATCH_SIZE + new_h = (h // PATCH_SIZE) * PATCH_SIZE + + transform = T.Compose( + [ + T.Resize((new_h, new_w)), + T.ToTensor(), + T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), + ] + ) + + return transform(img).unsqueeze(0).half().to(self.device) + + def get_scores(self, img_path): + input_tensor = self.preprocess(img_path) + + with torch.no_grad(): + features = self.model.forward_features(input_tensor) + + patches = features["x_norm_patchtokens"].squeeze(0) + patches_norm = F.normalize(patches, p=2, dim=1) + + similarity_matrix = torch.mm(patches_norm, self.centroid_matrix.T) + + class_scores = torch.mean(similarity_matrix, dim=0).float().cpu().numpy() + + results = {} + for i, score in enumerate(class_scores): + results[self.texture_names[i]] = float(score) + + return sorted(results.items(), key=lambda item: item[1], reverse=True) + + def get_filtered_predictions( + self, sorted_scores, max_count=5, relative_thresh=0.85, absolute_floor=0.15 + ): + if not sorted_scores: + return [] + + final_list = [] + top_score = sorted_scores[0][1] + + for name, score in sorted_scores: + if len(final_list) >= max_count: + break + + if score < absolute_floor: + continue + if score < (top_score * relative_thresh): + break + + final_list.append((name, score)) + + return final_list + + +# Runner + +if os.path.exists(CENTROIDS_PATH): + print(f"Loading dictionary from {CENTROIDS_PATH}...") + + loaded_container = torch.jit.load(CENTROIDS_PATH, map_location="cpu") + centroids_data = loaded_container() + + scanner = TextureScoreScanner(centroids_data) + + print(f"\nAnalyzing: {TEST_IMAGE_PATH}") + if os.path.exists(TEST_IMAGE_PATH): + raw_scores = scanner.get_scores(TEST_IMAGE_PATH) + + smart_results = scanner.get_filtered_predictions( + raw_scores, + max_count=3, + relative_thresh=0.85, + absolute_floor=0.05, # Kept at 0.05 as discussed + ) + + print("\n" + "=" * 40) + print(f"DETECTED TEXTURES ({len(smart_results)})") + print("=" * 40) + + if len(smart_results) > 0: + top_score_val = raw_scores[0][1] + for name, score in smart_results: + percentage = (score / top_score_val) * 100 + print( + f"• {name.upper().ljust(15)} : Score {score:.4f} (Conf: {percentage:.0f}%)" + ) + else: + print("No significant texture match found.") + else: + print(f"Error: Test image not found at {TEST_IMAGE_PATH}") + +else: + print(f" Error: Centroids file '{CENTROIDS_PATH}' not found.") diff --git a/models/text&mat/texture_centroids_s14.pt b/models/texture/texture_centroids_s14.pt Binary files differ. diff --git a/models/typo.py b/models/typo.py @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -"""Untitled5.ipynb - -Automatically generated by Colab. - -Original file is located at - https://colab.research.google.com/drive/11sdpsNaYwetetOotIX3GKtxgaQOt7S0Y -""" - -!pip install easyocr -q - -import numpy as np -from PIL import Image -import easyocr -import pickle -import os -import json -from tensorflow.keras.models import model_from_json, Model -from tensorflow.keras.utils import get_custom_objects - -TEST_IMAGE_PATH = 'test image.png' -MODEL_JSON = 'fannet.json' -MODEL_WEIGHTS = 'fannet_weights.h5' -FONT_DB_PKL = 'font_database.pkl' - -if os.path.exists(FONT_DB_PKL) and os.path.exists(MODEL_JSON): - with open(FONT_DB_PKL, 'rb') as f: - font_db = pickle.load(f) - - get_custom_objects()['Model'] = Model - - with open(MODEL_JSON, 'r') as f: - model_json_content = f.read() - - fannet_model = model_from_json(model_json_content) - fannet_model.load_weights(MODEL_WEIGHTS) - - encoder = Model(inputs=fannet_model.input, outputs=fannet_model.get_layer('dense_1').output) - reader = easyocr.Reader(['en'], gpu=True) - - img_pil = Image.open(TEST_IMAGE_PATH) - detections = reader.readtext(TEST_IMAGE_PATH) - - print(f"{'TEXT':<20} | {'PREDICTED FONT':<25} | {'CONFIDENCE'}") - print("-" * 65) - - for bbox, text, conf in detections: - tl, tr, br, bl = bbox - x_min = max(0, int(min(tl[0], bl[0]))) - y_min = max(0, int(min(tl[1], tr[1]))) - x_max = min(img_pil.width, int(max(tr[0], br[0]))) - y_max = min(img_pil.height, int(max(bl[1], br[1]))) - - if x_max - x_min > 5 and y_max - y_min > 5: - crop = img_pil.crop((x_min, y_min, x_max, y_max)) - - img = crop.convert('L').resize((64, 64), Image.LANCZOS) - arr = np.array(img, dtype=np.float32) - arr = (arr < np.mean(arr)).astype(np.float32) - processed = np.expand_dims(arr, axis=(0, -1)) - - dummy_char = np.zeros((1, 26, 1)) - features = encoder.predict([processed, dummy_char], verbose=0)[0] - - best_font = "Unknown" - min_dist = float('inf') - - for name, db_feat in font_db.items(): - dist = np.linalg.norm(features - db_feat) - if dist < min_dist: - min_dist = dist - best_font = name - - font_conf = 1.0 / (1.0 + min_dist) - print(f"{text:<20} | {best_font:<25} | {font_conf:.4f}") -else: - print("Files not found. Please upload test image.png, fannet.json, fannet_weights.h5, and font_database.pkl") -\ No newline at end of file diff --git a/models/typography.py b/models/typography.py @@ -0,0 +1,77 @@ +""" +Typography +Original file: https://colab.research.google.com/drive/11sdpsNaYwetetOotIX3GKtxgaQOt7S0Y +""" + +!pip install easyocr -q + +import numpy as np +from PIL import Image +import easyocr +import pickle +import os +import json +from tensorflow.keras.models import model_from_json, Model +from tensorflow.keras.utils import get_custom_objects + +TEST_IMAGE_PATH = "test image.png" +MODEL_JSON = "fannet.json" +MODEL_WEIGHTS = "fannet_weights.h5" +FONT_DB_PKL = "font_database.pkl" + +if os.path.exists(FONT_DB_PKL) and os.path.exists(MODEL_JSON): + with open(FONT_DB_PKL, "rb") as f: + font_db = pickle.load(f) + + get_custom_objects()["Model"] = Model + + with open(MODEL_JSON, "r") as f: + model_json_content = f.read() + + fannet_model = model_from_json(model_json_content) + fannet_model.load_weights(MODEL_WEIGHTS) + + encoder = Model( + inputs=fannet_model.input, outputs=fannet_model.get_layer("dense_1").output + ) + reader = easyocr.Reader(["en"], gpu=True) + + img_pil = Image.open(TEST_IMAGE_PATH) + detections = reader.readtext(TEST_IMAGE_PATH) + + print(f"{'TEXT':<20} | {'PREDICTED FONT':<25} | {'CONFIDENCE'}") + print("-" * 65) + + for bbox, text, conf in detections: + tl, tr, br, bl = bbox + x_min = max(0, int(min(tl[0], bl[0]))) + y_min = max(0, int(min(tl[1], tr[1]))) + x_max = min(img_pil.width, int(max(tr[0], br[0]))) + y_max = min(img_pil.height, int(max(bl[1], br[1]))) + + if x_max - x_min > 5 and y_max - y_min > 5: + crop = img_pil.crop((x_min, y_min, x_max, y_max)) + + img = crop.convert("L").resize((64, 64), Image.LANCZOS) + arr = np.array(img, dtype=np.float32) + arr = (arr < np.mean(arr)).astype(np.float32) + processed = np.expand_dims(arr, axis=(0, -1)) + + dummy_char = np.zeros((1, 26, 1)) + features = encoder.predict([processed, dummy_char], verbose=0)[0] + + best_font = "Unknown" + min_dist = float("inf") + + for name, db_feat in font_db.items(): + dist = np.linalg.norm(features - db_feat) + if dist < min_dist: + min_dist = dist + best_font = name + + font_conf = 1.0 / (1.0 + min_dist) + print(f"{text:<20} | {best_font:<25} | {font_conf:.4f}") +else: + print( + "Files not found. Please upload test image.png, fannet.json, fannet_weights.h5, and font_database.pkl" + )