Spaces:
Sleeping
Sleeping
| # Import modular components | |
| import os | |
| os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "false" | |
| from agno.models.google import Gemini | |
| import base64 | |
| import time | |
| import cv2 | |
| import numpy as np | |
| import streamlit as st | |
| from components.sidebar import render_sidebar | |
| from services.gemini_service import ( | |
| analyze_feasibility_with_gemini, | |
| refine_svg_with_gemini, | |
| ) | |
| from services.image_processor import skeletonize_image | |
| from services.pdf_processor import process_pdf | |
| from services.vectorizer import generate_svg | |
| # Import rembg for background isolation | |
| try: | |
| from rembg import remove | |
| REMBG_AVAILABLE = True | |
| except ImportError: | |
| REMBG_AVAILABLE = False | |
| # Attempt to import PyMuPDF | |
| try: | |
| import fitz # PyMuPDF | |
| PDF_SUPPORT_AVAILABLE = True | |
| except ImportError: | |
| PDF_SUPPORT_AVAILABLE = False | |
| from config.session import init_session, reset_generation_state | |
| # ------------------------------------------------------------- | |
| # SAFE CALLBACK FUNCTIONS (Prevents State Exceptions) | |
| # ------------------------------------------------------------- | |
| def load_recommended_settings(params): | |
| """Stage 1: Safely sets baseline AI parameter recommendations.""" | |
| st.session_state.quality_val = params.get("quality_val", "Moderate (Balanced)") | |
| st.session_state.sharpen_val = float(params.get("sharpen_val", 0.0)) | |
| st.session_state.scale_val = float(params.get("scale_val", 2.0)) | |
| st.session_state.canny_low_val = int(params.get("canny_low_val", 10)) | |
| st.session_state.canny_high_val = int(params.get("canny_high_val", 70)) | |
| st.session_state.skeletonize_val = bool(params.get("skeletonize_val", False)) | |
| def load_refined_settings(params): | |
| """Stage 2: Safely sets fine-tuned refinement tweaks.""" | |
| st.session_state.quality_val = params.get("quality_val", st.session_state.quality_val) | |
| st.session_state.sharpen_val = float(params.get("sharpen_val", st.session_state.sharpen_val)) | |
| st.session_state.scale_val = float(params.get("scale_val", st.session_state.scale_val)) | |
| st.session_state.canny_low_val = int(params.get("canny_low_val", st.session_state.canny_low_val)) | |
| st.session_state.canny_high_val = int(params.get("canny_high_val", st.session_state.canny_high_val)) | |
| st.session_state.skeletonize_val = bool(params.get("skeletonize_val", st.session_state.skeletonize_val)) | |
| st.session_state.contour_thickness_val = int(params.get("contour_thickness_val", st.session_state.contour_thickness_val)) | |
| st.session_state.dilate_iter_val = int(params.get("dilate_iter_val", st.session_state.dilate_iter_val)) | |
| st.set_page_config(page_title="Handicraft Image Workspace", layout="wide") | |
| init_session() | |
| # Initialize State management if missing | |
| if "pipeline_triggered" not in st.session_state: | |
| st.session_state.pipeline_triggered = False | |
| if "advisor_insights" not in st.session_state: | |
| st.session_state.advisor_insights = None | |
| if "advisor_insights_dynamic" not in st.session_state: | |
| st.session_state.advisor_insights_dynamic = None | |
| # ------------------------------------------------------------- | |
| # CALL THE MODULAR SIDEBAR COMPONENT HERE | |
| # ------------------------------------------------------------- | |
| app_page = render_sidebar(reset_generation_state) | |
| # ============================================================= | |
| # PAGE 1: ORIGINAL IMAGE-TO-SVG VECTORIZER | |
| # ============================================================= | |
| if app_page == "🎨 Image-to-SVG Vectorizer": | |
| st.markdown("# 🎨 Advanced Handicraft Image & PDF Vectorizer") | |
| st.markdown("Convert photos, sketches, drawings, or multi-page PDF documents into clean, editable vector SVGs with strict AI guidance.") | |
| # PDF Processing Mode | |
| if PDF_SUPPORT_AVAILABLE: | |
| st.subheader("📄 PDF Processing Mode") | |
| pdf_mode = st.radio( | |
| "Select how files should be processed:", | |
| ["Extract Embedded Images", "Render Entire Pages"], | |
| index=0 if st.session_state.pdf_mode_val == "Extract Embedded Images" else 1, | |
| help="Extract Embedded Images scans inside the PDF to fetch actual photographic contents. Render Entire Pages snapshots each full page." | |
| ) | |
| if pdf_mode != st.session_state.pdf_mode_val: | |
| st.session_state.pdf_mode_val = pdf_mode | |
| st.session_state.current_file_name = "" | |
| reset_generation_state() | |
| st.session_state.pipeline_triggered = False | |
| # File Uploader - Configured to allow multiple uploads for target comparison | |
| supported_formats = ["png", "jpg", "jpeg"] | |
| if PDF_SUPPORT_AVAILABLE: | |
| supported_formats.append("pdf") | |
| file_help = "Upload one or more alternative photos of your handicraft (JPG, PNG, or PDF) to let AI evaluate the cleanest candidate." | |
| else: | |
| file_help = "Upload JPG/PNG images. Note: Install 'pymupdf' (pip install pymupdf) to unlock PDF support." | |
| uploaded_files = st.file_uploader(file_help, type=supported_formats, accept_multiple_files=True, key="vectorizer_uploader") | |
| if uploaded_files: | |
| # Check if the compilation batch changed | |
| batch_signature = "".join([f.name for f in uploaded_files]) | |
| if batch_signature != st.session_state.get("last_batch_signature", ""): | |
| st.session_state.last_batch_signature = batch_signature | |
| st.session_state.extracted_images = [] | |
| st.session_state.selected_img_index = 0 | |
| st.session_state.advisor_insights = None | |
| st.session_state.advisor_insights_dynamic = None | |
| reset_generation_state() | |
| st.session_state.pipeline_triggered = False | |
| # Process single or multiple uploaded items into session state assets | |
| for f in uploaded_files: | |
| file_name = f.name | |
| if file_name.lower().endswith(".pdf") and PDF_SUPPORT_AVAILABLE: | |
| pdf_data = f.read() | |
| st.session_state.extracted_images.extend(process_pdf(pdf_data, st.session_state.pdf_mode_val)) | |
| else: | |
| file_bytes_raw = f.read() | |
| mime_type = "image/png" if f.type == "image/png" else "image/jpeg" | |
| st.session_state.extracted_images.append({ | |
| "name": file_name, | |
| "bytes": file_bytes_raw, | |
| "mime_type": mime_type, | |
| "analysis": None, | |
| "params": None | |
| }) | |
| total_images = len(st.session_state.extracted_images) | |
| # ------------------------------------------------------------- | |
| # IMAGE SELECTION & SELECTIVE AI COMPARISON AGENT | |
| # ------------------------------------------------------------- | |
| if total_images > 1: | |
| st.info(f"📄 **Multiple Variants Detected:** Found **{total_images}** target variants for evaluation.") | |
| # Auto-run strict feasibility checks across all items to recommend the best asset | |
| with st.spinner("🤖 Strict AI Assessment Engine evaluating all files for trace compatibility..."): | |
| for idx, img in enumerate(st.session_state.extracted_images): | |
| if img["analysis"] is None: | |
| # Append instructions to enforce strict evaluation rules in the backend framework | |
| strict_result = analyze_feasibility_with_gemini(img["bytes"], img["mime_type"]) | |
| st.session_state.extracted_images[idx]["analysis"] = strict_result | |
| if isinstance(strict_result, dict) and "recommended_parameters" in strict_result: | |
| st.session_state.extracted_images[idx]["params"] = strict_result["recommended_parameters"] | |
| # Construct comparison board UI | |
| st.markdown("### 📊 AI Cross-Image Comparison Analysis") | |
| comparison_data = [] | |
| for img in st.session_state.extracted_images: | |
| an = img["analysis"] if isinstance(img["analysis"], dict) else {} | |
| score = an.get("suitability_score", "N/A") | |
| suit = "👍 High Pass" if an.get("suitable", False) else "⚠️ Low/Noisy (Rejected)" | |
| comparison_data.append({ | |
| "Asset Name": img["name"], | |
| "Traceability Score (Strict)": f"{score}/100", | |
| "Status Decision": suit, | |
| "Lighting/Contrast Note": an.get("lighting_critique", "N/A")[:90] + "..." | |
| }) | |
| st.table(comparison_data) | |
| selected_page_name = st.selectbox( | |
| "Select your preferred item to load into Workspace:", | |
| options=[img["name"] for img in st.session_state.extracted_images], | |
| index=st.session_state.selected_img_index | |
| ) | |
| new_idx = [img["name"] for img in st.session_state.extracted_images].index(selected_page_name) | |
| if new_idx != st.session_state.selected_img_index: | |
| st.session_state.selected_img_index = new_idx | |
| st.session_state.advisor_insights = None | |
| st.session_state.advisor_insights_dynamic = None | |
| reset_generation_state() | |
| st.session_state.pipeline_triggered = False | |
| st.rerun() | |
| else: | |
| # Single Image Upload: Automatically run strict analysis instantly without button gate | |
| if st.session_state.extracted_images[0]["analysis"] is None: | |
| with st.spinner("🤖 Initializing Strict Visual Feasibility Assessment..."): | |
| img = st.session_state.extracted_images[0] | |
| strict_result = analyze_feasibility_with_gemini(img["bytes"], img["mime_type"]) | |
| st.session_state.extracted_images[0]["analysis"] = strict_result | |
| if isinstance(strict_result, dict) and "recommended_parameters" in strict_result: | |
| st.session_state.extracted_images[0]["params"] = strict_result["recommended_parameters"] | |
| # Finalize context around the Active Target Item | |
| active_img = st.session_state.extracted_images[st.session_state.selected_img_index] | |
| active_bytes = active_img["bytes"] | |
| active_mime = active_img["mime_type"] | |
| st.session_state.original_img_b64 = f"data:{active_mime};base64," + base64.b64encode(active_bytes).decode("utf-8") | |
| file_bytes_np = np.asarray(bytearray(active_bytes), dtype=np.uint8) | |
| img_rgb = cv2.cvtColor(cv2.imdecode(file_bytes_np, cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB) | |
| cached_analysis = active_img["analysis"] | |
| cached_params = active_img["params"] | |
| # ------------------------------------------------------------- | |
| # HIGH-VISIBILITY AI HANDICRAFT ALERT / WARNING | |
| # ------------------------------------------------------------- | |
| if cached_analysis and isinstance(cached_analysis, dict): | |
| is_handicraft = cached_analysis.get("is_handicraft", True) | |
| if not is_handicraft: | |
| st.error( | |
| "🚨 **Invalid Image Alert: Non-Handicraft Detected!**\n\n" | |
| "Our AI Diagnostic Engine analyzed this image and classified it as **NOT a handicraft, drawing, sketch, or craft pattern**.\n\n" | |
| "Tracing models and configurations are optimized specifically for artwork outlines. Please upload a valid image for correct results." | |
| ) | |
| # ------------------------------------------------------------- | |
| # THREE-TAB WORKSPACE INTERFACE LAYOUT | |
| # ------------------------------------------------------------- | |
| tab_analysis, tab_workspace, tab_advisor = st.tabs([ | |
| "📊 AI Diagnostic Analysis", | |
| "🎨 Control Workspace", | |
| "💡 Interactive AI Advisor & Staging Pro" | |
| ]) | |
| # --- TAB 1: STRICT AI DIAGNOSTIC ANALYSIS --- | |
| with tab_analysis: | |
| st.subheader(f"🛡️ Strict AI Target Quality Verification: `{active_img['name']}`") | |
| if cached_analysis and isinstance(cached_analysis, dict): | |
| c_score, c_suit = st.columns([1, 2]) | |
| with c_score: | |
| score = cached_analysis.get("suitability_score", 0) | |
| st.metric(label="Traceability Rating (Strict Audit)", value=f"{score}/100") | |
| with c_suit: | |
| if cached_analysis.get("suitable", True): | |
| st.success("##### 👍 Acceptable Quality\nThe file passes edge-contrast guidelines and is suitable for trace conversion.") | |
| else: | |
| st.warning("##### ⚠️ Strict Review Advisory\nHigh ambient noise, shadows, or clutter detected. Expect some artifacts.") | |
| with st.expander("👁️ View Full Strict Structural Diagnostics Breakdown", expanded=True): | |
| col_det1, col_det2 = st.columns(2) | |
| with col_det1: | |
| st.markdown("**💡 Illumination & Shading Profile**") | |
| st.write(cached_analysis.get("lighting_critique", "N/A")) | |
| st.markdown("**📐 Geometric Foreground/Clutter Separation**") | |
| st.write(cached_analysis.get("contrast_critique", "N/A")) | |
| with col_det2: | |
| st.markdown("**🔍 Edge Resolution & Clarity**") | |
| st.write(cached_analysis.get("detail_clarity_critique", "N/A")) | |
| st.markdown("**🔮 Vectorization Fidelity Forecast**") | |
| st.write(cached_analysis.get("line_art_prediction", "N/A")) | |
| # Offer one-click baseline ingestion | |
| if cached_params: | |
| st.write("") | |
| st.button( | |
| "⚙️ Sync Diagnostic Parameters to Slider Configurations", | |
| on_click=load_recommended_settings, | |
| args=(cached_params,), | |
| use_container_width=True | |
| ) | |
| else: | |
| st.info("No active diagnostic metrics generated. Please make sure the uploaded image is evaluated properly.") | |
| # --- TAB 2: ARTWORK FILTERING & CONTROL WORKSPACE --- | |
| with tab_workspace: | |
| # Setup dynamic processing variables inside OpenCV pipeline memory maps | |
| h, w = img_rgb.shape[:2] | |
| # Only calculate mathematical visual conversions if explicitly requested by the button matrix | |
| if st.session_state.pipeline_triggered: | |
| # --- GRABCUT BACKGROUND SEPARATION --- | |
| if st.session_state.get("enable_grabcut", False): | |
| with st.spinner("Isolating foreground object..."): | |
| bbox_padding = st.session_state.get("bbox_padding", 10) | |
| gc_mask = np.zeros((h, w), np.uint8) | |
| bgdModel = np.zeros((1, 65), np.float64) | |
| fgdModel = np.zeros((1, 65), np.float64) | |
| rect = (bbox_padding, bbox_padding, w - (2 * bbox_padding), h - (2 * bbox_padding)) | |
| cv2.grabCut(img_rgb, gc_mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) | |
| binary_mask = np.where((gc_mask == 2) | (gc_mask == 0), 0, 1).astype("uint8") | |
| img_rgb = img_rgb * binary_mask[:, :, np.newaxis] | |
| # --- RESIZING & GRAYSCALE CONVERSION --- | |
| img_large = cv2.resize(img_rgb, (int(w * st.session_state.scale_val), int(h * st.session_state.scale_val)), interpolation=cv2.INTER_CUBIC) | |
| gray = cv2.cvtColor(img_large, cv2.COLOR_RGB2GRAY) | |
| # --- SHARPENING / DE-BLUR --- | |
| if st.session_state.sharpen_val > 0: | |
| blurred_temp = cv2.GaussianBlur(gray, (0, 0), 3.0) | |
| gray = cv2.addWeighted(gray, 1.0 + st.session_state.sharpen_val, blurred_temp, -st.session_state.sharpen_val, 0) | |
| gray = np.clip(gray, 0, 255).astype(np.uint8) | |
| # --- SMOOTHING & CANNY EDGES --- | |
| gray = cv2.bilateralFilter(gray, 9, 75, 75) | |
| edges = cv2.Canny(gray, st.session_state.canny_low_val, st.session_state.canny_high_val) | |
| # --- MORPHOLOGICAL CLOSING (Fills minor gaps in lines) --- | |
| kernel_close = np.ones((3, 3), np.uint8) | |
| edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel_close, iterations=1) | |
| # --- CONTOUR THICKNESS CONSTRAINTS --- | |
| if (not st.session_state.skeletonize_val and st.session_state.contour_thickness_val > 1): | |
| contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) | |
| edges_clean = np.zeros_like(edges) | |
| cv2.drawContours(edges_clean, contours, -1, 255, st.session_state.contour_thickness_val) | |
| edges = edges_clean | |
| if st.session_state.skeletonize_val: | |
| edges = skeletonize_image(edges) | |
| elif st.session_state.dilate_iter_val > 0: | |
| edges = cv2.dilate(edges, np.ones((2, 2), np.uint8), iterations=st.session_state.dilate_iter_val) | |
| # --- PREPARE BUFFERS FOR DISPLAY AND VECTORIZATION --- | |
| _, edge_buffer = cv2.imencode(".png", edges) | |
| st.session_state.line_art_bytes = edge_buffer.tobytes() | |
| else: | |
| # Default clean array state placeholder before generation trigger | |
| edges = np.zeros((h, w), dtype=np.uint8) | |
| # --- RENDER COLUMNS FOR SIDE-BY-SIDE MATCHING --- | |
| c1, c2, c3 = st.columns(3) | |
| with c1: | |
| st.subheader("Original Image") | |
| st.image(img_rgb, use_container_width=True) | |
| with c2: | |
| st.subheader("Line Art") | |
| if st.session_state.pipeline_triggered: | |
| st.image(edges, use_container_width=True, caption="Computed edge trace map state.") | |
| else: | |
| st.info("Line art rendering process is queued. Click 'Process Image Filters' above to visualize vectorization paths.") | |
| # Inline action button for generating Line Art | |
| if st.button("🚀 Process Line Art", type="primary", use_container_width=False): | |
| st.session_state.pipeline_triggered = True | |
| st.rerun() | |
| with c3: | |
| st.subheader("Vector Image") | |
| if st.session_state.get("svg_generated", False): | |
| b64 = base64.b64encode(st.session_state.svg_bytes).decode() | |
| st.markdown(f'<img src="data:image/svg+xml;base64,{b64}" width="100%"/>', unsafe_allow_html=True) | |
| st.download_button( | |
| label="📥 Download SVG", | |
| data=st.session_state.svg_bytes, | |
| file_name=f"primitive_vector_{active_img['name'].replace(' ', '_')}_{int(time.time())}.svg", | |
| mime="image/svg+xml", | |
| use_container_width=False, | |
| ) | |
| else: | |
| st.info("Awaiting geometric primitive resolution execution mapping steps.") | |
| # Inline action button for SVG output generation | |
| if st.session_state.pipeline_triggered: | |
| if st.button("🏆 Generate SVG", type="secondary", use_container_width=False): | |
| with st.spinner("Executing structural geometric primitive estimation..."): | |
| st.session_state.svg_bytes = generate_svg(edges, st.session_state.skeletonize_val) | |
| st.session_state.svg_generated = True | |
| st.rerun() | |
| # Post-Vectorization Refinement Routine Block | |
| if st.session_state.get("svg_generated", False): | |
| st.markdown("---") | |
| st.markdown("### 🔍 Advanced Post-Vectorization Refinement") | |
| st.write("Use Gemini Vision to analyze structural precision gaps between original artwork layout and vector output curves.") | |
| if st.button("🔍 Analyze Generated SVG for Improvements", use_container_width=True): | |
| with st.spinner("Executing side-by-side visual analysis..."): | |
| refine_result = refine_svg_with_gemini( | |
| active_bytes, | |
| active_mime, | |
| st.session_state.line_art_bytes, | |
| ) | |
| st.session_state.ai_refinement = refine_result | |
| if isinstance(refine_result, dict) and "recommended_parameters" in refine_result: | |
| st.session_state.ai_refined_params = refine_result["recommended_parameters"] | |
| else: | |
| st.session_state.ai_refined_params = None | |
| st.rerun() | |
| if st.session_state.get("ai_refinement", None): | |
| ref = st.session_state.ai_refinement | |
| if isinstance(ref, dict) and "error" in ref: | |
| st.error(ref["error"]) | |
| elif isinstance(ref, dict): | |
| if ref.get("is_perfect", False) or st.session_state.ai_refined_params is None: | |
| st.balloons() | |
| st.success("✨ **Perfect Vectorization Reached!**\n\nThe vector mapping looks pristine and fully optimized!") | |
| else: | |
| st.info("📋 **AI Vector Alignment Review**") | |
| col_ref1, col_ref2 = st.columns(2) | |
| with col_ref1: | |
| st.markdown("##### 📐 Line Continuity & Fidelity") | |
| st.write(ref.get("edge_fidelity_critique", "No structural flaws found.")) | |
| with col_ref2: | |
| st.markdown("##### 🧼 Noise & Background Mud") | |
| st.write(ref.get("noise_clutter_critique", "No background clutter found.")) | |
| st.markdown("##### 🛠️ Correction Roadmap") | |
| st.write(ref.get("the_fix", "Ready to optimize.")) | |
| p = st.session_state.ai_refined_params | |
| if p: | |
| st.write("---") | |
| p_cols = st.columns(4) | |
| p_cols[0].metric("Target Sharpen", f"{p.get('sharpen_val', 0.0)}") | |
| p_cols[1].metric("Target Scale", f"{p.get('scale_val', 2.0)}x") | |
| p_cols[2].metric("Target Canny", f"{p.get('canny_low_val', 10)}-{p.get('canny_high_val', 70)}") | |
| p_cols[3].metric("Skeletonize", "On" if p.get('skeletonize_val', False) else "Off") | |
| st.button( | |
| "🚀 Apply Refined Settings", | |
| type="primary", | |
| use_container_width=True, | |
| on_click=load_refined_settings, | |
| args=(p,), | |
| ) | |
| # --- TAB 3: INTERACTIVE AI ADVISOR & STAGING PRO --- | |
| with tab_advisor: | |
| st.subheader("💡 Interactive Quality Advisor & Staging Consultant") | |
| st.write("Is your raw photo struggling to convert into a clean SVG? Let's dynamically diagnose your handicraft image and design a perfect prompt for ChatGPT/DALL-E to generate a clean outline.") | |
| st.markdown("---") | |
| st.markdown("##### 🔍 Describe Your Preferences & Image Concerns") | |
| # Question 1: Common alternative photo questionnaire | |
| has_alt_photos = st.radio( | |
| "1. Do you have any alternative photos of this same handicraft?", | |
| ["No, this is my only photo", "Yes, I have alternative photos taken under different settings"], | |
| index=0, | |
| help="Uploading multiple variants allows our AI model to automatically isolate and select the highest contrast asset." | |
| ) | |
| # Question 2: Free-form outline style description | |
| preferred_style = st.text_input( | |
| "2. Describe your preferred outline style (e.g., bold and thick stencils, fine-line detailed sketches, simple clean cartoon outlines):", | |
| value="Crisp black and white vector outline with solid uniform lines and no inner details", | |
| help="Type exactly how you want the generated outlines to look." | |
| ) | |
| # Question 3: Free-form corrections / opinion input | |
| correction_priority = st.text_area( | |
| "3. Describe any specific concerns or trace corrections you want the model to resolve:", | |
| value="Please completely remove the noisy textured background, connect any faint/broken borders, and ignore all shadows and gray tones.", | |
| help="Let us know what parts of the image need adjusting or cleaning." | |
| ) | |
| st.markdown("---") | |
| # Make advisor key unique to the active state inputs to prevent cache collision | |
| advisor_key = f"adv_{active_img['name']}_{has_alt_photos[:5]}_{preferred_style[:10]}_{correction_priority[:10]}" | |
| if st.session_state.get("current_advisor_key") != advisor_key: | |
| st.session_state.advisor_insights = None | |
| if st.button("✨ Generate Custom ChatGPT Prompt & Staging Advice", type="primary", use_container_width=True): | |
| st.session_state.current_advisor_key = advisor_key | |
| with st.spinner("AI Studio analyzing visual diagnostics and custom preferences..."): | |
| from services.gemini_service import prepare_gemini_env, rotator | |
| prepare_gemini_env() | |
| score = 100 | |
| clutter_note = "Minimal clutter detected." | |
| lighting_note = "Uniform illumination." | |
| if cached_analysis and isinstance(cached_analysis, dict): | |
| score = cached_analysis.get("suitability_score", 75) | |
| clutter_note = cached_analysis.get("contrast_critique", "N/A") | |
| lighting_note = cached_analysis.get("lighting_critique", "N/A") | |
| system_instruction = ( | |
| "You are an expert handicraft design consultant, product photographer, and vector design specialist. " | |
| "You analyze physical image quality constraints and help users generate perfect digital templates using generative AI tools." | |
| ) | |
| structured_prompt = f""" | |
| The user uploaded a handicraft image named '{active_img['name']}'. | |
| Image Analysis: | |
| - Suitability Score: {score}/100 | |
| - Contrast: {clutter_note} | |
| - Lighting: {lighting_note} | |
| User Preferences: | |
| - Outline Style: {preferred_style} | |
| - Corrections: {correction_priority} | |
| - Alternative Photos: {has_alt_photos} | |
| Generate a SHORT and SIMPLE markdown response. | |
| Rules: | |
| - Maximum 250 words total. | |
| - Use short bullet points only. | |
| - Do NOT write long paragraphs. | |
| - Keep every bullet under 20 words. | |
| - Use simple English. | |
| - Focus only on useful information. | |
| - Avoid repeating the user's preferences. | |
| - Be concise. | |
| Return exactly these sections: | |
| ### ✅ Image Diagnosis | |
| • Overall quality (1 sentence) | |
| • Biggest issue (1 bullet) | |
| • Quick recommendation (1 bullet) | |
| ### 🎨 ChatGPT / DALL·E Prompt | |
| Provide ONE copy-paste prompt inside a markdown code block. | |
| ### 📸 Better Photo Tips | |
| Give ONLY 3 short bullets. | |
| Keep everything easy to scan. | |
| """ | |
| try: | |
| advisor_agent = Gemini( | |
| id="gemini-2.5-flash", | |
| system_prompt=system_instruction | |
| ) | |
| from agno.models.message import Message | |
| response_object = advisor_agent.response([Message(role="user", content=structured_prompt)]) | |
| st.session_state.advisor_insights = response_object.content if hasattr(response_object, 'content') else str(response_object) | |
| except Exception as chat_error: | |
| rotator.rotate() | |
| st.session_state.advisor_insights = f"⚠️ An anomaly occurred while synthesizing recommendations: {str(chat_error)}. Please try again." | |
| st.rerun() | |
| if st.session_state.get("advisor_insights"): | |
| st.markdown("---") | |
| st.markdown("### 🔮 Your Dynamic AI Advisor Blueprint") | |
| st.markdown(st.session_state.advisor_insights) | |
| # Render navigation context assistant if alternative images are active | |
| if has_alt_photos == "Yes, I have alternative photos taken under different settings": | |
| st.info( | |
| "💡 **Quick Navigation:** Drag and drop your alternate photos into the file uploader at the very top. " | |
| "The comparison board will automatically run diagnostics and show you which is the cleanest candidate." | |
| ) | |
| else: | |
| st.info("💡 Fill out your custom preferences above and click the button to generate a personalized ChatGPT prompt and physical staging recipes.") | |
| else: | |
| st.info("✨ Please upload one or more alternative photographs, drawings, or PDF documents containing your handicraft designs to begin!") | |
| if not PDF_SUPPORT_AVAILABLE: | |
| st.warning("ℹ️ **Unlock PDF Support:** Install PyMuPDF (`pip install pymupdf`) to extract pages from PDF files directly in this application.") | |
| # ============================================================= | |
| # NEW PAGE 2: TOOL EXTRACT (OPENCV GRABCUT & CLIPBOARD) | |
| # ============================================================= | |
| elif app_page == "🛠️ Tool Extract": | |
| st.markdown("# 🛠️ Handicraft Tool Isolator & Extractor") | |
| st.markdown("Upload pictures or PDFs of your craft items to isolate tools using standard Computer Vision (GrabCut), convert them into a transparent GIF asset, and copy them directly to your clipboard.") | |
| # PDF Processing Configuration for extraction | |
| if PDF_SUPPORT_AVAILABLE: | |
| st.subheader("📄 PDF Processing Mode") | |
| tool_pdf_mode = st.radio( | |
| "Select document extraction method:", | |
| ["Extract Embedded Images", "Render Entire Pages"], | |
| key="tool_pdf_mode_choice" | |
| ) | |
| # Unified File Input Formats | |
| tool_formats = ["png", "jpg", "jpeg"] | |
| if PDF_SUPPORT_AVAILABLE: | |
| tool_formats.append("pdf") | |
| tool_help = "Upload a JPG, PNG, or multi-page PDF document containing tools." | |
| else: | |
| tool_help = "Upload JPG/PNG image. Install 'pymupdf' to unpack PDF items." | |
| tool_file = st.file_uploader(tool_help, type=tool_formats, key="tool_extractor_uploader") | |
| if tool_file: | |
| t_images = [] | |
| if tool_file.name.lower().endswith(".pdf") and PDF_SUPPORT_AVAILABLE: | |
| with st.spinner("Deconstructing pages..."): | |
| t_pdf_data = tool_file.read() | |
| t_images = process_pdf(t_pdf_data, tool_pdf_mode) | |
| else: | |
| t_bytes = tool_file.read() | |
| t_mime = "image/png" if tool_file.type == "image/png" else "image/jpeg" | |
| t_images = [{"name": tool_file.name, "bytes": t_bytes, "mime_type": t_mime}] | |
| # Multiple elements selection handler | |
| if len(t_images) > 1: | |
| selected_tool_img = st.selectbox( | |
| "Target design catalog index:", | |
| options=[ti["name"] for ti in t_images], | |
| key="tool_img_selector" | |
| ) | |
| active_idx = [ti["name"] for ti in t_images].index(selected_tool_img) | |
| else: | |
| active_idx = 0 | |
| target_tool = t_images[active_idx] | |
| # --- GrabCut Custom Parameters --- | |
| st.sidebar.markdown("### 🎛️ Isolation Fine-Tuning") | |
| bbox_padding = st.sidebar.slider( | |
| "Bounding Box Padding", | |
| min_value=2, | |
| max_value=100, | |
| value=15, | |
| help="Tells the computer how close the tool is to the edges of the image. Lower values mean the tool spans nearly the whole frame." | |
| ) | |
| iter_count = st.sidebar.slider("Extraction Accuracy Iterations", min_value=1, max_value=10, value=5) | |
| # Draw split presentation boards | |
| tc1, tc2 = st.columns(2) | |
| with tc1: | |
| st.subheader("📸 Original Input") | |
| st.image(target_tool["bytes"], use_container_width=True) | |
| with tc2: | |
| st.subheader("✨ Isolated Tool Output") | |
| with st.spinner("Executing CV GrabCut background separation..."): | |
| try: | |
| from PIL import Image | |
| import io | |
| # Convert uploaded bytes to OpenCV image format | |
| raw_np = np.asarray(bytearray(target_tool["bytes"]), dtype=np.uint8) | |
| decoded_img = cv2.imdecode(raw_np, cv2.IMREAD_COLOR) | |
| h, w = decoded_img.shape[:2] | |
| # Convert to RGB for PIL compatibility later | |
| img_rgb = cv2.cvtColor(decoded_img, cv2.COLOR_BGR2RGB) | |
| # Initialize mask matrices for GrabCut | |
| mask = np.zeros((h, w), np.uint8) | |
| bgdModel = np.zeros((1, 65), np.float64) | |
| fgdModel = np.zeros((1, 65), np.float64) | |
| # Define a rectangle wrapping the foreground tool based on user padding | |
| rect = (bbox_padding, bbox_padding, w - (2 * bbox_padding), h - (2 * bbox_padding)) | |
| # Execute GrabCut directly using OpenCV | |
| cv2.grabCut(img_rgb, mask, rect, bgdModel, fgdModel, iter_count, cv2.GC_INIT_WITH_RECT) | |
| # Generate a clean binary mask where background = 0, foreground = 1 | |
| # (cv2.GC_PR_FGD and cv2.GC_FGD represent probable and definite foreground) | |
| binary_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8") | |
| # Apply the mask to isolate the image content | |
| isolated_rgb = img_rgb * binary_mask[:, :, np.newaxis] | |
| # Create an alpha channel (transparency map) based on the mask | |
| alpha_channel = (binary_mask * 255).astype("uint8") | |
| # Stack them together to make a clean 4-channel transparent RGBA image | |
| r, g, b = cv2.split(isolated_rgb) | |
| rgba_mat = cv2.merge([r, g, b, alpha_channel]) | |
| # Convert directly to a transparent PIL GIF asset | |
| pil_img = Image.fromarray(rgba_mat) | |
| gif_io = io.BytesIO() | |
| pil_img.save(gif_io, format="GIF", save_all=True, transparency=0, disposal=2) | |
| final_bytes = gif_io.getvalue() | |
| # Encode asset to Base64 for the browser clipboard handling injection | |
| b64_output = base64.b64encode(final_bytes).decode("utf-8") | |
| st.image(final_bytes, use_container_width=True, caption="Isolated tool asset ready") | |
| except Exception as ex: | |
| st.error(f"Failed processing background isolation details: {ex}") | |
| final_bytes = None | |
| if final_bytes: | |
| st.markdown("---") | |
| st.markdown("### 📋 Export & Distribution") | |
| # JavaScript injection to bind image blob string straight inside clipboard framework | |
| clipboard_html = f""" | |
| <button id="clip-btn" style=" | |
| background-color: #FF4B4B; | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| width: 100%; | |
| font-size: 16px;"> | |
| 📋 Copy Isolated Tool GIF to Clipboard | |
| </button> | |
| <script> | |
| document.getElementById('clip-btn').addEventListener('click', async () => {{ | |
| try {{ | |
| const response = await fetch("data:image/gif;base64,{b64_output}"); | |
| const blob = await response.blob(); | |
| await navigator.clipboard.write([ | |
| new ClipboardItem({{ [blob.type]: blob }}) | |
| ]); | |
| alert("Isolated tool GIF graphic copied successfully to your clipboard!"); | |
| }} catch (err) {{ | |
| console.error(err); | |
| alert("Could not automatically copy image. Verify site security permissions or use Download button."); | |
| }} | |
| }}); | |
| </script> | |
| """ | |
| st.components.v1.html(clipboard_html, height=60) | |
| # Standard physical download safety fallback | |
| st.download_button( | |
| label="📥 Save Transparent GIF Asset Instead", | |
| data=final_bytes, | |
| file_name=f"isolated_{target_tool['name'].rsplit('.', 1)[0]}.gif", | |
| mime="image/gif", | |
| use_container_width=True, | |
| key="tool_download_fallback" | |
| ) | |
| elif app_page == "⚡ Interactive Node Simplifier": | |
| st.markdown("# ⚡ Interactive Node Simplifier") | |
| if st.session_state.svg_bytes: | |
| svg_content = st.session_state.svg_bytes.decode('utf-8') | |
| vector_editor_html = """<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js"></script> | |
| <style> | |
| body { margin: 0; background: #12151c; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; overflow: hidden; } | |
| #canvas { width: 100vw; height: 100vh; display: block; cursor: crosshair; } | |
| .controls { | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| z-index: 10; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| background: rgba(30, 34, 43, 0.95); | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| border-left: 4px solid #ff4b4b; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.5); | |
| align-items: center; | |
| } | |
| button { | |
| padding: 8px 16px; | |
| cursor: pointer; | |
| border-radius: 5px; | |
| border: none; | |
| font-weight: bold; | |
| transition: all 0.2s ease; | |
| } | |
| .btn-export { background: #2ecc71; color: white; } | |
| .btn-export:hover { background: #27ae60; } | |
| .btn-simplify { background: #ff4b4b; color: white; } | |
| .btn-simplify:hover { background: #e04141; } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| color: #a3a8b4; | |
| font-size: 0.9rem; | |
| gap: 8px; | |
| } | |
| input[type="range"] { | |
| cursor: pointer; | |
| accent-color: #ff4b4b; | |
| } | |
| .hud { | |
| position: absolute; | |
| top: 15px; | |
| right: 15px; | |
| z-index: 10; | |
| background: rgba(30, 34, 43, 0.95); | |
| border-radius: 8px; | |
| padding: 12px 18px; | |
| border-left: 4px solid #2ecc71; | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.5); | |
| font-size: 0.85rem; | |
| min-width: 220px; | |
| } | |
| .hud-row { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 6px; | |
| } | |
| .hud-row:last-child { | |
| margin-bottom: 0; | |
| padding-top: 6px; | |
| border-top: 1px dashed #3a3f4d; | |
| } | |
| .hud-label { color: #808495; text-transform: uppercase; font-size: 0.75rem; font-weight: bold; } | |
| .hud-val { font-weight: bold; color: #ffffff; } | |
| .hud-highlight { color: #2ecc71; font-weight: 800; font-size: 1rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar"> | |
| <div class="controls"> | |
| <button class="btn-export" onclick="exportSVG()">Export Edited SVG</button> | |
| <div style="width: 1px; height: 20px; background: #3a3f4d;"></div> | |
| <div class="slider-container"> | |
| <label for="tolerance">Simplification Strength:</label> | |
| <input id="tolerance" type="range" min="0.5" max="15.0" step="0.5" value="3.0" oninput="updateToleranceLabel(this.value)"> | |
| <span id="tolerance-val" style="color: #ff4b4b; font-weight: bold;">3.0</span> | |
| </div> | |
| <button class="btn-simplify" onclick="applySimplification()">Simplify Node Mesh ✨</button> | |
| </div> | |
| <div class="hud"> | |
| <div class="hud-row"> | |
| <span class="hud-label">Original Vector Nodes:</span> | |
| <span id="orig-nodes" class="hud-val">-</span> | |
| </div> | |
| <div class="hud-row"> | |
| <span class="hud-label">Current Nodes:</span> | |
| <span id="curr-nodes" class="hud-val">-</span> | |
| </div> | |
| <div class="hud-row"> | |
| <span class="hud-label">Nodes Eliminated:</span> | |
| <span id="reduced-nodes" class="hud-highlight">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <canvas id="canvas" resize></canvas> | |
| <script type="text/javascript"> | |
| const canvas = document.getElementById('canvas'); | |
| paper.setup('canvas'); | |
| const tool = new paper.Tool(); | |
| let selectedSegment = null; | |
| let importedItem = null; | |
| let originalBackup = null; | |
| let largestPath = null; | |
| let initialNodeCount = 0; | |
| const svgData = `{{escaped_svg_content}}`; | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(svgData, "image/svg+xml"); | |
| const svgElement = doc.documentElement; | |
| svgElement.removeAttribute("width"); | |
| svgElement.removeAttribute("height"); | |
| function countTotalNodes(item) { | |
| let count = 0; | |
| if (item instanceof paper.Path) { | |
| count += item.segments.length; | |
| } else if (item.children) { | |
| item.children.forEach(child => { | |
| count += countTotalNodes(child); | |
| }); | |
| } | |
| return count; | |
| } | |
| function styleItems(obj) { | |
| if (obj instanceof paper.Path) { | |
| obj.strokeColor = '#3498db'; | |
| if (!obj.strokeWidth || obj.strokeWidth === 0) | |
| obj.strokeWidth = 2.5; | |
| obj.fullySelected = false; | |
| } | |
| if (obj.children) { | |
| obj.children.forEach(child => styleItems(child)); | |
| } | |
| } | |
| paper.project.importSVG(svgElement, function(item) { | |
| if (!item) { | |
| console.error("SVG load error."); | |
| return; | |
| } | |
| importedItem = item; | |
| findLargestPath(importedItem); | |
| originalBackup = item.clone({ insert: false }); | |
| const targetBounds = paper.view.bounds.clone().scale(0.85); | |
| importedItem.fitBounds(targetBounds); | |
| importedItem.position = new paper.Point( | |
| paper.view.center.x, | |
| paper.view.center.y + 80 | |
| ); | |
| styleItems(importedItem); | |
| initialNodeCount = countTotalNodes(importedItem); | |
| document.getElementById('orig-nodes').innerText = initialNodeCount; | |
| updateDiagnostics(); | |
| paper.view.update(); | |
| }); | |
| function findLargestPath(item) { | |
| let largest = null; | |
| let largestArea = 0; | |
| function traverse(obj) { | |
| if (obj instanceof paper.Path) { | |
| const area = obj.bounds.width * obj.bounds.height; | |
| if (area > largestArea) { | |
| largestArea = area; | |
| largest = obj; | |
| } | |
| } | |
| if (obj.children) { | |
| obj.children.forEach(traverse); | |
| } | |
| } | |
| traverse(item); | |
| if (largest) { | |
| largest.data.keepOriginal = true; | |
| } | |
| } | |
| function updateToleranceLabel(val) { | |
| document.getElementById('tolerance-val').innerText = val; | |
| } | |
| function updateDiagnostics() { | |
| if (!importedItem) return; | |
| const currentCount = countTotalNodes(importedItem); | |
| const reduced = initialNodeCount - currentCount; | |
| const percentage = initialNodeCount > 0 ? ((reduced / initialNodeCount) * 100).toFixed(1) : 0; | |
| document.getElementById('curr-nodes').innerText = currentCount; | |
| document.getElementById('reduced-nodes').innerText = reduced + ' (' + percentage + '%)'; | |
| } | |
| window.applySimplification = function() { | |
| if (!importedItem || !originalBackup) return; | |
| importedItem.remove(); | |
| importedItem = originalBackup.clone(); | |
| paper.project.activeLayer.addChild(importedItem); | |
| importedItem.fitBounds( | |
| paper.view.bounds.clone().scale(0.85) | |
| ); | |
| importedItem.position = paper.view.center; | |
| const tolerance = parseFloat( | |
| document.getElementById('tolerance').value | |
| ); | |
| function simplifyPathElements(obj) { | |
| if (obj instanceof paper.Path) { | |
| const wRatio = | |
| obj.bounds.width / | |
| importedItem.bounds.width; | |
| const hRatio = | |
| obj.bounds.height / | |
| importedItem.bounds.height; | |
| const isOuterFrame = | |
| wRatio > 0.85 && | |
| hRatio > 0.85; | |
| if (!obj.data.keepOriginal && !isOuterFrame) { | |
| obj.simplify(tolerance); | |
| } | |
| obj.fullySelected = false; | |
| } | |
| if (obj.children) { | |
| obj.children.forEach(child => | |
| simplifyPathElements(child) | |
| ); | |
| } | |
| } | |
| simplifyPathElements(importedItem); | |
| styleItems(importedItem); | |
| updateDiagnostics(); | |
| paper.view.update(); | |
| }; | |
| tool.onMouseDown = function(event) { | |
| selectedSegment = null; | |
| const hitResult = paper.project.hitTest(event.point, { | |
| segments: true, | |
| tolerance: 15 | |
| }); | |
| if (hitResult && hitResult.segment) { | |
| selectedSegment = hitResult.segment; | |
| selectedSegment.selected = true; | |
| } | |
| }; | |
| tool.onMouseDrag = function(event) { | |
| if (!selectedSegment) return; | |
| selectedSegment.point = selectedSegment.point.add(event.delta); | |
| updateDiagnostics(); | |
| paper.view.update(); | |
| }; | |
| tool.onMouseUp = function() { | |
| if (selectedSegment) { | |
| selectedSegment.selected = false; | |
| } | |
| selectedSegment = null; | |
| }; | |
| window.exportSVG = function() { | |
| const svgString = paper.project.exportSVG({ | |
| asString: true, | |
| precision: 3 | |
| }); | |
| const blob = new Blob([svgString], { type: "image/svg+xml" }); | |
| const link = document.createElement("a"); | |
| link.href = URL.createObjectURL(blob); | |
| link.download = "handicraft_reduced_nodes.svg"; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| }; | |
| window.addEventListener("resize", () => { | |
| paper.view.viewSize = | |
| new paper.Size( | |
| canvas.clientWidth, | |
| canvas.clientHeight | |
| ); | |
| if (importedItem) { | |
| importedItem.fitBounds( | |
| paper.view.bounds.clone().scale(0.85) | |
| ); | |
| importedItem.position = paper.view.center; | |
| } | |
| paper.view.update(); | |
| }); | |
| </script> | |
| </body> | |
| </html>""".replace("{{escaped_svg_content}}", svg_content.replace("\\", "\\\\").replace("`", "\\`")) | |
| st.components.v1.html(vector_editor_html, height=750, scrolling=False) | |
| else: | |
| st.warning("Please process an image on the first page first.") |