import html import json import os import tempfile from pathlib import Path from dotenv import load_dotenv load_dotenv() import gradio as gr from gradio.themes import Base from gallery.gallery import append_exhibit, load_exhibits, count_exhibits # ── Palette ──────────────────────────────────────────────────────────────────── ACCENT = "#52B788" ACCENT_DARK = "#3D9B70" ACCENT_BRIGHT= "#6EE8A4" BG = "#0D1A12" # dark forest SURFACE = "#162A1C" # card / panel SURFACE2 = "#1E3526" # slightly raised surface BORDER = "#2D4A36" # borders TEXT = "#D6F5E3" # primary bright text TEXT_DIM = "#6BA882" # secondary / muted text # ── Theme ────────────────────────────────────────────────────────────────────── MUSEUM_THEME = Base( font=[gr.themes.GoogleFont("DM Sans"), "system-ui", "sans-serif"], ).set( body_background_fill=BG, body_text_color=TEXT, background_fill_primary=SURFACE, background_fill_secondary=BG, border_color_primary=BORDER, button_primary_background_fill=ACCENT, button_primary_background_fill_hover=ACCENT_DARK, button_primary_text_color="#FFFFFF", button_primary_border_color=ACCENT, button_secondary_background_fill=SURFACE2, button_secondary_text_color=TEXT_DIM, button_secondary_border_color=BORDER, input_background_fill=SURFACE, input_border_color=BORDER, block_border_color="transparent", block_background_fill="transparent", shadow_drop="none", shadow_spread="0px", ) APP_CSS = """ /* ── Base ── */ .gradio-container { max-width: 1400px !important; margin: 0 auto !important; padding: 0 0 48px 0 !important; background: #0D1A12 !important; } body { background: #0D1A12 !important; } footer { display: none !important; } .block { padding: 0 !important; border: none !important; background: transparent !important; box-shadow: none !important; } .gap, .gr-group { gap: 0 !important; } .label-wrap { display: none !important; } /* ── Hide Gradio tab nav (single-page, no tabs) ── */ .tabs > .tab-nav { display: none !important; } /* ── App bar ── */ .mot-appbar { display: flex; align-items: center; justify-content: space-between; padding: 16px 40px 14px; background: rgba(13,26,18,0.97); border-bottom: 1px solid #2D4A36; position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); font-family: 'DM Sans', sans-serif; } .mot-logo { font-size: 20px; font-weight: 700; color: #D6F5E3; letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; } .mot-logo-dot { width: 10px; height: 10px; border-radius: 50%; background: #52B788; } /* ── Hero + upload row ── */ .mot-hero-col { padding: 0 !important; } .mot-upload-col { padding: 36px 40px !important; background: #111F16 !important; border-left: 1px solid #2D4A36 !important; } /* ── Upload zone ── */ .mot-upload { padding: 0 !important; } .mot-upload .upload-container, .mot-upload .image-frame, .mot-upload .wrap { border: 2px dashed #2D4A36 !important; border-radius: 18px !important; background: #0D1A12 !important; min-height: 200px !important; } .mot-upload img { border-radius: 16px !important; } /* ── Buttons ── */ .mot-submit { padding: 20px 0 0 !important; } .mot-submit button { border-radius: 16px !important; font-size: 16px !important; font-weight: 600 !important; min-height: 54px !important; box-shadow: 0 4px 20px rgba(82,183,136,0.25) !important; } .mot-share { padding: 12px 0 0 !important; } .mot-share button { border-radius: 14px !important; min-height: 46px !important; font-weight: 600 !important; } /* ── Results row ── */ .mot-results-row { border-top: 1px solid #2D4A36 !important; border-bottom: 1px solid #2D4A36 !important; background: #111F16 !important; } .mot-slider-col { padding: 36px 20px 36px 40px !important; } .mot-phases-col { padding: 36px 40px 36px 20px !important; } /* ── Gallery section ── */ .mot-gallery-section { padding: 0 40px !important; } /* ── Image slider ── */ .mot-slider .image-slider { border-radius: 16px !important; overflow: hidden !important; } /* ── Accordion ── */ .gr-accordion { background: #0D1A12 !important; border: 1px solid #2D4A36 !important; border-radius: 12px !important; } /* ── Refresh ── */ .mot-refresh { padding: 16px 0 !important; } .mot-refresh button { border-radius: 12px !important; font-size: 13px !important; } .mot-spacer { height: 12px !important; } """ # ── Static HTML ──────────────────────────────────────────────────────────────── APPBAR_HTML = """
""" STEP1_HTML = """1 — Upload a photo
""" STEP2_HTML = """2 — What should it become?
""" def _status_html(active: int) -> str: steps = [ "Analyzing your object", "Designing the transformation", "Rendering the new life", ] rows = "" for i, label in enumerate(steps, 1): if i < active: rows += ( f'Working on it
' f'{rows}' f'{tagline}
' + ( f'' f'Identified: {html.escape(orig_name)} · {html.escape(orig_material)}
' if orig_name else "" ) + f'Recent ideas
' f'AI Upcycling
' '' 'Upload any object you\'d throw away. Our AI identifies the parts, ' 'picks a transformation, and shows you how to build it.
' 'No transformations yet. Be the first.
' ) return ( f'Things people gave a second life
' f'{icon} {title}
' f'' 'No step-by-step guide available for this item.
' ) return ( f'Click any card to see the transformation.
' ) if not exhibit_json: return gr.update(visible=False), gr.update(visible=False), placeholder, gr.update(visible=False) try: d = json.loads(exhibit_json) except Exception: return gr.update(visible=False), gr.update(visible=False), placeholder, gr.update(visible=False) portrait = d.get("img", "") original = d.get("original", "") if not portrait: return gr.update(visible=False), gr.update(visible=False), placeholder, gr.update(visible=False) before = gr.update(value=original if original else portrait, visible=True) after = gr.update(value=portrait, visible=True) name = d.get("name", "") tname = d.get("tname", "") tagline = d.get("tagline", "") p1, p2, p3 = d.get("p1", ""), d.get("p2", ""), d.get("p3", "") header = ( f'{html.escape(name)} →
' f'{html.escape(tagline)}
' f'' f'Vision error: {html.escape(str(e))}
', _no_change, _no_change, "", "", gr.update(visible=False), gr.update(visible=True), ) return name = vision.get("name", "—") material = vision.get("material", "—") components = vision.get("components", []) # ── Step 2: Transform ──────────────────────────────────────────────────── yield _status_html(2), _no_change, _no_change, _no_change, _no_change, gr.update(visible=False), _no_change try: transformation_text = _transform(vision, category or "Lamp") except Exception as e: transformation_text = f"Transformation error: {e}" parsed = _parse_transformation(transformation_text) new_name = parsed.get("new_name") or "New creation" tagline = parsed.get("tagline") or "" phase_1 = parsed.get("phase_1") or "" phase_2 = parsed.get("phase_2") or "" phase_3 = parsed.get("phase_3") or "" # ── Step 3: Render ─────────────────────────────────────────────────────── yield _status_html(3), _no_change, _no_change, _no_change, _no_change, gr.update(visible=False), _no_change render_path = None image_prompt = _build_image_prompt(vision, transformation_text) try: render_path, image_prompt = _render(image_path, vision, transformation_text) except Exception as e: phase_3 += f"\n\n[Render error: {e}]" state = json.dumps({ "name": name, "material": material, "components": components, "transformation_name": new_name, "tagline": tagline, "instructions": transformation_text, "portrait_path": render_path or "", "original_path": image_path, }) results = _results_html(new_name, tagline, phase_1, phase_2, phase_3, orig_name=name, orig_material=material) debug = ( f"=== VISION ===\n{json.dumps(vision, indent=2, ensure_ascii=False)}\n\n" f"=== LLM OUTPUT ===\n{transformation_text}\n\n" f"=== IMAGE PROMPT ===\n{image_prompt}" ) before = gr.update(value=image_path, visible=True) after = gr.update(value=render_path, visible=True) if render_path else gr.update(visible=False) yield results, before, after, debug, state, gr.update(visible=True), gr.update(visible=True) def save_to_gallery(state_json): if not state_json: return "Generate a transformation first." try: s = json.loads(state_json) append_exhibit( name=s["name"], material=s["material"], components=s["components"], transformation_name=s["transformation_name"], tagline=s["tagline"], instructions=s["instructions"], portrait_path=s.get("portrait_path") or None, original_path=s.get("original_path") or None, ) return "✓ Added to the gallery!" except Exception as e: return f"Error: {e}" # ── App ──────────────────────────────────────────────────────────────────────── with gr.Blocks(title="Upcycle Things") as demo: exhibit_state = gr.State("") gr.HTML(APPBAR_HTML) # ── Hero + upload ────────────────────────────────────────────────────────── with gr.Row(equal_height=True): with gr.Column(scale=1, elem_classes=["mot-hero-col"]): gr.HTML(value=_home_hero_html()) with gr.Column(scale=1, elem_classes=["mot-upload-col"]): gr.HTML(STEP1_HTML) image_input = gr.Image(type="filepath", show_label=False, elem_classes=["mot-upload"]) gr.HTML(STEP2_HTML) category_input = gr.Radio( choices=[("🧸 Kids toy", "Kids toy"), ("🖼️ Wall art", "Wall art"), ("🪴 Flowerpot", "Flowerpot"), ("💡 Lamp", "Lamp"), ("👕 Clothes", "new clothing item or fashion accessory")], value=None, show_label=False, ) submit_btn = gr.Button("Find its second life →", variant="primary", elem_classes=["mot-submit"], interactive=False) # ── Results (hidden until scan completes) ────────────────────────────────── with gr.Row(visible=False, elem_id="mot-results", elem_classes=["mot-results-row"]) as results_row: with gr.Column(scale=1, elem_classes=["mot-slider-col"]): with gr.Row(): before_img = gr.Image(label="Original", interactive=False, type="filepath", show_label=True, visible=False) after_img = gr.Image(label="AI Vision", interactive=False, type="filepath", show_label=True, visible=False) with gr.Column(scale=1, elem_classes=["mot-phases-col"]): results_display = gr.HTML("") save_btn = gr.Button("Share to Gallery", variant="primary", visible=False, elem_classes=["mot-share"]) save_status = gr.Markdown("") with gr.Accordion("Debug info", open=True): prompt_out = gr.Textbox(show_label=False, lines=8, interactive=False) # ── Gallery ──────────────────────────────────────────────────────────────── with gr.Row(elem_classes=["mot-gallery-section"]): gallery_html = gr.HTML(value=_gallery_html()) with gr.Row(): refresh_btn = gr.Button("↻ Refresh gallery", size="sm", variant="secondary", elem_classes=["mot-refresh"]) exhibit_trigger = gr.Textbox(visible=False, elem_id="exhibit-select") # ── Events ───────────────────────────────────────────────────────────────── submit_btn.click( fn=process_object, inputs=[image_input, category_input], outputs=[results_display, before_img, after_img, prompt_out, exhibit_state, save_btn, results_row], ) category_input.change( fn=lambda cat: gr.update(interactive=bool(cat)), inputs=[category_input], outputs=[submit_btn], ) save_btn.click(fn=save_to_gallery, inputs=[exhibit_state], outputs=[save_status]) refresh_btn.click(fn=_gallery_html, outputs=[gallery_html]) exhibit_trigger.change( fn=load_exhibit_detail, inputs=[exhibit_trigger], outputs=[before_img, after_img, results_display, results_row], ) if __name__ == "__main__": demo.launch(css=APP_CSS, theme=MUSEUM_THEME)