Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from ultralytics import YOLO | |
| from PIL import Image, ImageDraw, ImageFont | |
| from google import genai | |
| import os | |
| import json | |
| import matplotlib.pyplot as plt | |
| import re | |
| from huggingface_hub import hf_hub_download | |
| import tempfile | |
| import numpy as np | |
| import shutil | |
| import zipfile | |
| from typing import List, Tuple, Dict, Any | |
| # --- 1. CONFIGURATION & SECRETS --- | |
| GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY") | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| MODEL_REPO = "youkii-xr/hieroglyphic-detection" | |
| MODEL_FILENAME = "best.pt" | |
| JSON_DB_PATH = "gardiner_codes.json" | |
| os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics" | |
| os.makedirs("/tmp/gradio_results", exist_ok=True) | |
| # --- 2. DATA LOADING --- | |
| def load_gardiner_database(): | |
| if os.path.exists(JSON_DB_PATH): | |
| try: | |
| print(f"System: Loading Gardiner codes from {JSON_DB_PATH}...") | |
| with open(JSON_DB_PATH, "r", encoding='utf-8') as f: | |
| return json.load(f) | |
| except Exception as e: | |
| print(f"โ ๏ธ Error reading JSON: {e}") | |
| return {} | |
| else: | |
| print(f"โ ๏ธ Warning: {JSON_DB_PATH} not found.") | |
| return {} | |
| gardiner_data = load_gardiner_database() | |
| gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()} | |
| # --- 3. CORE LOGIC FUNCTIONS --- | |
| def create_labeled_zip(image, detections): | |
| """ | |
| Crops glyphs, draws label (Code + Conf) on bottom right, and zips them. | |
| """ | |
| if not detections: return None | |
| zip_dir = tempfile.mkdtemp() | |
| zip_path = os.path.join(tempfile.gettempdir(), "rosetta_glyphs.zip") | |
| try: | |
| # Load a font (try-except block for system compatibility) | |
| try: | |
| # Try loading a standard font, fallback to default if fails | |
| font = ImageFont.truetype("arial.ttf", 16) | |
| except IOError: | |
| font = ImageFont.load_default() | |
| for i, d in enumerate(detections): | |
| # Crop | |
| box = d['box'] | |
| crop = image.crop((box[0], box[1], box[2], box[3])) | |
| # Prepare Label | |
| label_text = f"{d['code']} {int(d['confidence']*100)}%" | |
| draw = ImageDraw.Draw(crop) | |
| # Calculate text size using textbbox (newer PIL versions) | |
| left, top, right, bottom = draw.textbbox((0, 0), label_text, font=font) | |
| text_w = right - left | |
| text_h = bottom - top | |
| img_w, img_h = crop.size | |
| # Draw background rectangle (bottom right) | |
| # Check if image is too small for label, if so, skip drawing to avoid crash | |
| if img_w > text_w and img_h > text_h: | |
| rect_x0 = img_w - text_w - 4 | |
| rect_y0 = img_h - text_h - 4 | |
| rect_x1 = img_w | |
| rect_y1 = img_h | |
| draw.rectangle([rect_x0, rect_y0, rect_x1, rect_y1], fill="black") | |
| draw.text((rect_x0 + 2, rect_y0), label_text, fill="white", font=font) | |
| # Save crop | |
| filename = f"{d['code']}_{i}.png" | |
| crop.save(os.path.join(zip_dir, filename)) | |
| # Create Zip | |
| shutil.make_archive(zip_path.replace('.zip', ''), 'zip', zip_dir) | |
| return zip_path | |
| except Exception as e: | |
| print(f"Zip creation error: {e}") | |
| return None | |
| finally: | |
| shutil.rmtree(zip_dir, ignore_errors=True) | |
| def core_detect(image, conf_threshold): | |
| """Core YOLO detection logic.""" | |
| if image is None or model is None: | |
| return None, [], [] | |
| results = model.predict(source=image, conf=conf_threshold, iou=0.45, imgsz=640, verbose=False, device='cpu', max_det=300) | |
| annotated_array = results[0].plot() | |
| annotated_image = Image.fromarray(annotated_array[..., ::-1]) | |
| detections = [] | |
| crops = [] | |
| for box in results[0].boxes: | |
| if box.cls.numel() > 0: | |
| cls_id = int(box.cls[0]) | |
| if 0 <= cls_id < len(model.names): | |
| code = model.names[cls_id] | |
| conf = float(box.conf[0]) | |
| xyxy = box.xyxy[0].tolist() | |
| # Create detection object | |
| detection = { | |
| "code": code, | |
| "description": gardiner_map.get(code, "Unknown"), | |
| "confidence": round(conf, 2), | |
| "box": xyxy | |
| } | |
| detections.append(detection) | |
| # Create crop (Clean crop for Gallery display) | |
| crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3])) | |
| crops.append((crop_img, f"{code}\n({int(conf*100)}%)")) | |
| return annotated_image, detections, crops | |
| def core_translate(keywords_list): | |
| """Core Gemini translation logic.""" | |
| if not GOOGLE_API_KEY: | |
| return "Error: API Key Missing", "Error: API Key Missing" | |
| if not keywords_list: | |
| return "No symbols detected", "No symbols detected" | |
| keywords_str = ", ".join(keywords_list) | |
| prompt = f""" | |
| You are an expert Egyptologist AI. I have detected these symbols: [{keywords_str}]. | |
| Please provide 2 distinct outputs separated by "|||SEPARATOR|||". | |
| 1. A Mystical Story: Highly atmospheric, sounding like an ancient prophecy. | |
| Do NOT number this section. Use HTML tags <b> for bolding keywords and <br> for new lines. | |
| 2. An Academic Translation: Direct, linguistic, focusing on grammar. Use standard text. | |
| """ | |
| try: | |
| client = genai.Client(api_key=GOOGLE_API_KEY) | |
| response = client.models.generate_content(model="gemini-2.5-flash", contents=prompt) | |
| parts = response.text.split("|||SEPARATOR|||") | |
| def clean(t): return t.replace("**", "").strip() | |
| if len(parts) < 2: return clean(response.text), "Could not parse academic style." | |
| return clean(parts[0]), clean(parts[1]) | |
| except Exception as e: | |
| return f"Error: {str(e)}", f"Error: {str(e)}" | |
| def core_analytics(detections, img_w, img_h): | |
| """Core Matplotlib logic.""" | |
| if not detections: return None | |
| codes = [d['code'] for d in detections] | |
| confs = [d['confidence'] for d in detections] | |
| x_centers = [d['box'][0] + (d['box'][2] - d['box'][0])/2 for d in detections] | |
| y_centers = [d['box'][1] + (d['box'][3] - d['box'][1])/2 for d in detections] | |
| fig = plt.figure(figsize=(10, 15)) | |
| fig.patch.set_facecolor('#0f0f23') | |
| # 1. Frequency | |
| ax1 = plt.subplot(3, 1, 1) | |
| unique_codes = list(set(codes)) | |
| counts = [codes.count(c) for c in unique_codes] | |
| ax1.bar(unique_codes, counts, color='#d4af37') | |
| ax1.set_title('Symbol Frequency', color='white', fontsize=12, pad=10) | |
| ax1.tick_params(colors='white') | |
| ax1.set_facecolor('none') | |
| for spine in ax1.spines.values(): spine.set_color('#d4af37') | |
| # 2. Confidence | |
| ax2 = plt.subplot(3, 1, 2) | |
| ax2.scatter(range(len(confs)), confs, color='#d4af37', alpha=0.7, s=50) | |
| ax2.set_title('AI Confidence Levels', color='white', fontsize=12, pad=10) | |
| ax2.set_ylim(0, 1.1) | |
| ax2.tick_params(colors='white') | |
| ax2.set_facecolor('none') | |
| for spine in ax2.spines.values(): spine.set_color('#d4af37') | |
| # 3. Heatmap | |
| ax3 = plt.subplot(3, 1, 3) | |
| h = ax3.hist2d(x_centers, y_centers, bins=[20, 20], range=[[0, img_w], [0, img_h]], cmap='inferno') | |
| ax3.set_title('Glyph Spatial Heatmap', color='white', fontsize=12, pad=10) | |
| ax3.set_xlim(0, img_w) | |
| ax3.set_ylim(img_h, 0) | |
| ax3.tick_params(colors='white') | |
| cbar = plt.colorbar(h[3], ax=ax3) | |
| cbar.ax.yaxis.set_tick_params(color='white') | |
| plt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white') | |
| plt.tight_layout(pad=4.0) | |
| return fig | |
| # --- 4. LOAD MODEL --- | |
| print("System: Initializing Rosetta Decoder Core...") | |
| try: | |
| model_path = hf_hub_download( | |
| repo_id=MODEL_REPO, | |
| filename=MODEL_FILENAME, | |
| token=HF_TOKEN | |
| ) | |
| model = YOLO(model_path) | |
| print("System: Model loaded successfully.") | |
| except Exception as e: | |
| print(f"Error loading model: {e}") | |
| model = None | |
| # --- 5. MAIN UI PIPELINE (Orchestrator) --- | |
| def process_pipeline(image, conf_threshold): | |
| """ | |
| Main function used by the Web UI. | |
| """ | |
| if image is None: return None, None, None, "", "", None, "", "", "", [] | |
| if model is None: return None, None, None, "Error: Model not loaded.", "", None, "", "", "", [] | |
| try: | |
| img_w, img_h = image.size | |
| # 1. Detect | |
| annotated_img, detections, crops = core_detect(image, conf_threshold) | |
| # 2. Prepare Downloads | |
| # A. Annotated Image | |
| ann_path = os.path.join(tempfile.gettempdir(), "annotated_hieroglyphs.jpg") | |
| annotated_img.save(ann_path) | |
| # B. Zip File with Labels | |
| zip_path = create_labeled_zip(image, detections) | |
| # 3. Extract Keywords & Translate | |
| unique_codes = list(set([d['code'] for d in detections])) | |
| mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes] | |
| mystical, academic = core_translate(mapped_words) | |
| # 4. Analytics | |
| analytics_plot = core_analytics(detections, img_w, img_h) | |
| # 5. Reports | |
| text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}" | |
| json_output = {"count": len(detections), "detections": detections} | |
| formatted_mystical = f"""<div class="mystical-container"><h3>โจ THE ANCIENT WHISPER</h3><p>{mystical}</p></div>""" | |
| return annotated_img, ann_path, zip_path, formatted_mystical, academic, analytics_plot, text_report, json_output, crops | |
| except Exception as e: | |
| print(f"Pipeline Error: {e}") | |
| return None, None, None, f"System Failure: {str(e)}", "", None, str(e), None, [] | |
| # --- 6. MCP API FUNCTIONS --- | |
| def detect_hieroglyphs_api(image: Image.Image, conf: float = 0.25) -> Tuple[str, Dict[str, Any]]: | |
| ann_img, dets, _ = core_detect(image, conf) | |
| with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".jpg", delete=False) as t: | |
| ann_img.save(t.name) | |
| path = t.name | |
| return path, {"count": len(dets), "detections": dets} | |
| def translate_codes_api(keywords_text: str) -> Tuple[str, str]: | |
| if isinstance(keywords_text, str): | |
| keywords = [k.strip() for k in keywords_text.split(',')] | |
| else: | |
| keywords = ["Unknown"] | |
| mystical, academic = core_translate(keywords) | |
| clean_mystical = re.sub('<[^<]+?>', '', mystical) | |
| return clean_mystical, academic | |
| def get_analytics_chart_api(json_data: Dict[str, Any]) -> str: | |
| dets = json_data.get("detections", []) | |
| fig = core_analytics(dets, 640, 640) | |
| if fig is None: return "No data." | |
| with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".png", delete=False) as t: | |
| fig.savefig(t.name, format='png', facecolor='#0f0f23') | |
| path = t.name | |
| plt.close(fig) | |
| return path | |
| def list_all_codes_api() -> Dict[str, Any]: | |
| return gardiner_data | |
| # --- 7. HTML GENERATORS --- | |
| CATEGORIES = { | |
| 'A': "Men & Monarchs", 'B': "Women & Human Activities", 'C': "Deities", | |
| 'D': "Parts of Human Body", 'E': "Mammals", 'F': "Parts of Mammals", | |
| 'G': "Birds", 'H': "Parts of Birds", 'I': "Reptiles & Amphibians", | |
| 'K': "Fishes", 'L': "Invertebrates", 'M': "Trees & Plants", | |
| 'N': "Sky, Earth, Water", 'O': "Buildings", 'P': "Ships", | |
| 'Q': "Furniture", 'R': "Temple Furniture", 'S': "Crowns & Dress", | |
| 'T': "Warfare & Hunting", 'U': "Agriculture & Crafts", 'V': "Rope & Baskets", | |
| 'W': "Vessels", 'X': "Loaves & Cakes", 'Y': "Writings & Games", | |
| 'Z': "Strokes & Figures", 'Aa': "Unclassified" | |
| } | |
| def generate_gardiner_html(): | |
| if not gardiner_data: | |
| return "<tr><td colspan='4'>No data loaded. Please upload gardiner_codes.json.</td></tr>" | |
| html_rows = "" | |
| grouped = {} | |
| for key, data in gardiner_data.items(): | |
| match = re.match(r"([A-Za-z]+)", data.get("Code", key)) | |
| prefix = match.group(1) if match else "Unk" | |
| if prefix not in grouped: grouped[prefix] = [] | |
| grouped[prefix].append(data) | |
| sorted_prefixes = sorted(grouped.keys(), key=lambda x: (len(x), x)) | |
| for prefix in sorted_prefixes: | |
| cat_name = CATEGORIES.get(prefix, f"Category {prefix}") | |
| html_rows += f"<tr><td colspan='4' class='category-header'>{cat_name}</td></tr>" | |
| items = sorted(grouped[prefix], key=lambda x: int(re.search(r'\d+', x.get("Code", "0")).group()) if re.search(r'\d+', x.get("Code", "0")) else 0) | |
| for item in items: | |
| html_rows += f"""<tr><td style="font-weight:bold; color: #fff;">{item.get("Code", "?")}</td><td>{item.get("Description", "-")}</td><td style="font-family:serif; font-size:1.1em;">{item.get("Transliteration", "-")}</td><td><span class="type-badge">{item.get("Type", "-")}</span></td></tr>""" | |
| return html_rows | |
| GARDINER_TABLE_CONTENT = generate_gardiner_html() | |
| # --- 8. UI STYLING & ASSETS --- | |
| cursor_url = "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGFmMzciIHN0cm9rZS13aWR0aD0iMS41Ij4KICAgIDxwYXRoIGQ9Ik0xNiw4IEM2LDIwIDI2LDIwIDE2LDggWiIgZmlsbD0icmdiYSgyMTIsIDE3NSwgNTUsIDAuMSkiLz4KICAgIDxjaXJjbGUgY3g9IjE2IiBjeT0iMTUiIHI9IjMiIGZpbGw9IiNkNGFmMzciLz4KICAgIDxwYXRoIGQ9Ik0xNiwyMiBMMTYsMjggTDEwLDI4Ii8+CiAgPC9nPgo8L3N2Zz4=')" | |
| custom_css = f""" | |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap'); | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'); | |
| :root, .dark, body {{ --bg-gradient: radial-gradient(circle at 50% 0%, #0a0a2e 0%, #000000 100%); --card-bg: rgba(15, 15, 35, 0.7); --text-primary: #e0e7ff; --text-accent: #d4af37; --border-color: #d4af37; --btn-grad: linear-gradient(135deg, #b8860b 0%, #d4af37 100%); --info-bg: rgba(212, 175, 55, 0.08); --info-border: #d4af37; --glow-color: rgba(212, 175, 55, 0.4); }} | |
| body.light-mode, .gradio-container.light-mode {{ --bg-gradient: linear-gradient(135deg, #f0e6d2 0%, #e6dcc3 100%) !important; --card-bg: rgba(255, 255, 255, 0.6) !important; --text-primary: #3d342b !important; --text-accent: #8b4513 !important; --border-color: #8b4513 !important; --btn-grad: linear-gradient(135deg, #cd853f 0%, #8b4513 100%) !important; --info-bg: rgba(139, 69, 19, 0.05) !important; --info-border: #8b4513 !important; --glow-color: rgba(139, 69, 19, 0.3) !important; color: var(--text-primary) !important; }} | |
| body, .gradio-container {{ background: var(--bg-gradient) !important; font-family: 'Cairo', sans-serif !important; color: var(--text-primary) !important; cursor: {cursor_url} 16 16, auto !important; transition: background 0.5s ease; }} | |
| .gold-dust {{ position: fixed; width: 6px; height: 6px; background: var(--text-accent); border-radius: 50%; pointer-events: none; z-index: 9999; animation: fadeDust 0.6s linear forwards; box-shadow: 0 0 5px var(--text-accent); }} | |
| @keyframes fadeDust {{ 0% {{ opacity: 1; transform: scale(1); }} 100% {{ opacity: 0; transform: scale(0); }} }} | |
| button, a, .cursor-pointer {{ cursor: {cursor_url} 16 16, pointer !important; }} | |
| .tabs button {{ padding: 5px 10px !important; font-size: 14px !important; min-width: auto !important; }} | |
| .card {{ background: var(--card-bg) !important; border: 1px solid rgba(128, 128, 128, 0.2) !important; border-radius: 12px; padding: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); backdrop-filter: blur(12px); margin-bottom: 24px; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); }} | |
| .card:hover {{ transform: translateY(-4px); border-color: var(--border-color) !important; box-shadow: 0 10px 40px rgba(0,0,0,0.2), 0 0 20px var(--glow-color); }} | |
| .card-title {{ font-family: 'Cairo', sans-serif; font-size: 20px; font-weight: 700; color: var(--text-accent) !important; text-transform: uppercase; letter-spacing: 2px; border-bottom: 1px solid rgba(128,128,128, 0.2); padding-bottom: 15px; margin-bottom: 20px; display: flex; align-items: center; justify-content: center; gap: 8px; }} | |
| .guide-step {{ background: rgba(255, 255, 255, 0.03); border-left: 4px solid #d4af37; padding: 16px; margin-bottom: 16px; border-radius: 0 6px 6px 0; }} | |
| .step-title {{ color: #d4af37; font-family: 'Space Mono', monospace; font-weight: bold; display: block; margin-bottom: 8px; font-size: 14px; }} | |
| .path-highlight {{ background: rgba(212, 175, 55, 0.15); border: 1px solid #d4af37; padding: 2px 6px; border-radius: 4px; color: #fff; font-family: 'Space Mono', monospace; }} | |
| code {{ font-family: 'Space Mono', monospace; background: rgba(0,0,0,0.3); padding: 2px 5px; border-radius: 4px; color: #e0e7ff; }} | |
| .gardiner-table {{ width: 100%; border-collapse: collapse; font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 10px; border: 1px solid #d4af37; }} | |
| .gardiner-table th {{ color: #ffffff; text-align: left; padding: 12px; border-bottom: 2px solid #d4af37; text-transform: uppercase; letter-spacing: 1px; background: rgba(212, 175, 55, 0.1); }} | |
| .gardiner-table td {{ padding: 10px; border-bottom: 1px solid rgba(212, 175, 55, 0.2); color: #e0e0e0; }} | |
| .gardiner-table tr:hover {{ background: rgba(212, 175, 55, 0.1); }} | |
| .category-header {{ background: rgba(212, 175, 55, 0.2); color: #d4af37; font-weight: bold; text-align: center; padding: 8px; text-transform: uppercase; letter-spacing: 2px; }} | |
| .type-badge {{ border: 1px solid #d4af37; color: #d4af37; padding: 2px 6px; border-radius: 4px; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }} | |
| .mystical-container {{ font-family: 'Cairo', serif; font-size: 18px; line-height: 1.8; color: #fff8e1; padding: 20px; border: 1px solid var(--border-color); background: rgba(212, 175, 55, 0.05); border-radius: 8px; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); }} | |
| .mystical-container:hover {{ transform: translateY(-4px); box-shadow: 0 10px 40px rgba(0,0,0,0.2), 0 0 20px var(--glow-color); }} | |
| .mystical-container h3 {{ color: var(--text-accent); text-align: center; border-bottom: 1px dashed var(--border-color); padding-bottom: 10px; }} | |
| .mystical-container b {{ color: #d4af37; text-shadow: 0 0 5px rgba(212, 175, 55, 0.5); }} | |
| .scrollable-box textarea {{ overflow-y: auto !important; max-height: 400px !important; background-color: rgba(0,0,0,0.3) !important; }} | |
| button.primary-btn {{ background: var(--btn-grad) !important; border: 1px solid var(--border-color) !important; color: #000 !important; font-weight: 700 !important; font-size: 16px !important; }} | |
| .gradio-image, .gradio-json {{ background: transparent !important; border: none !important; }} | |
| button.toggle-btn {{ background: #0a0a2e !important; border: 1px solid var(--border-color) !important; color: var(--text-accent) !important; padding: 5px 15px !important; font-family: 'Space Mono', monospace; box-shadow: none !important; }} | |
| button.toggle-btn:hover {{ background: var(--info-bg) !important; }} | |
| """ | |
| header_html = """ | |
| <div style="padding: 20px 0; border-bottom: 1px solid rgba(128,128,128,0.2); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;"> | |
| <div style="display: flex; align-items: center; gap: 20px;"> | |
| <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#d4af37" stroke-width="2"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" /> | |
| <path d="M7 7h10" /> | |
| <path d="M7 12h10" /> | |
| <path d="M7 17h10" /> | |
| <circle cx="12" cy="12" r="3" stroke="#d4af37" fill="none"/> | |
| </svg> | |
| <div> | |
| <h1 style="margin: 0; font-size: 36px; font-weight: 700; color: var(--text-primary); text-shadow: 0 0 10px rgba(212, 175, 55, 0.3);">ROSETTA DECODER</h1> | |
| <p style="margin: 0; font-size: 14px; color: var(--text-accent); letter-spacing: 3px; font-weight: 600;">HIEROGLYPHIC DETECTOR AND TRANSLATOR</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # UPDATED VISION STATEMENT | |
| mission_html = """ | |
| <div class="card"><div class="card-title">๐ก VISION STATEMENT</div><p style="opacity: 0.9; font-size: 16px; line-height: 1.8; color: var(--text-primary);"> | |
| <b>Echoes of Humanity, Decoded.</b><br> | |
| It's not just about code; it's about connection. For millennia, the voices of ancient Egypt have been locked in stone, waiting to be heard. | |
| Rosetta Decoder isn't just a toolโit's a bridge across time. We are using modern AI to re-awaken these silent stories, allowing us to listen | |
| to the hopes, prayers, and daily lives of those who walked before us. We are decoding history to understand our shared humanity. | |
| </p></div> | |
| """ | |
| guide_html = """ | |
| <div class="card" style="border-color: #d4af37;"> | |
| <div class="card-title" style="color: #d4af37;">๐ค CLAUDE DESKTOP SETUP GUIDE</div> | |
| <div class="guide-step"><span class="step-title">STEP 0: PREREQUISITE</span><p>Ensure you have <b>Node.js</b> installed.</p></div> | |
| <div class="guide-step"><span class="step-title">STEP 1: PREPARE WORKSPACE</span>1. Create: <span class="path-highlight">C:\\Claude_Work</span><br>2. Move images inside.</div> | |
| <div class="guide-step"><span class="step-title">STEP 2: CONFIGURE CLAUDE</span>1. Edit: <code>%APPDATA%\\Claude\\claude_desktop_config.json</code><br>2. Paste the JSON below.<br>3. Restart Claude.</div> | |
| </div> | |
| """ | |
| # URL UPDATED TO: youkii-xr/hieroglyph-mcp-server | |
| claude_json_content = """{ | |
| "mcpServers": { | |
| "gradio": { | |
| "command": "npx", | |
| "args": [ | |
| "mcp-remote", | |
| "https://youkii-xr-hieroglyph-mcp-server.hf.space/gradio_api/mcp/", | |
| "--transport", | |
| "streamable-http" | |
| ] | |
| }, | |
| "upload_helper": { | |
| "command": "C:\\\\Python313\\\\python.exe", | |
| "args": [ | |
| "-m", | |
| "gradio", | |
| "upload-mcp", | |
| "https://youkii-xr-hieroglyph-mcp-server.hf.space/", | |
| "C:\\\\Claude_Work" | |
| ] | |
| } | |
| } | |
| }""" | |
| trail_script = """<script>document.addEventListener('DOMContentLoaded', () => { document.addEventListener('mousemove', (e) => { if (Math.random() > 0.7) return; const dust = document.createElement('div'); dust.classList.add('gold-dust'); dust.style.left = e.clientX + 'px'; dust.style.top = e.clientY + 'px'; document.body.appendChild(dust); setTimeout(() => dust.remove(), 600); }); });</script>""" | |
| # --- 9. MAIN APP ASSEMBLY --- | |
| with gr.Blocks(title="Rosetta Decoder Ultimate") as demo: | |
| gr.HTML(f"<style>{custom_css}</style>") | |
| gr.HTML(trail_script) | |
| # --- MCP TOOL REGISTRATION LAYER (Hidden) --- | |
| with gr.Row(visible=False): | |
| btn_detect = gr.Button("Detect") | |
| btn_detect.click(fn=detect_hieroglyphs_api, inputs=[gr.Image(label="img"), gr.Number(label="conf")], outputs=[gr.Textbox(label="path"), gr.JSON(label="json")], api_name="detect") | |
| btn_trans = gr.Button("Translate") | |
| btn_trans.click(fn=translate_codes_api, inputs=[gr.Textbox(label="text")], outputs=[gr.Textbox(label="mystic"), gr.Textbox(label="academic")], api_name="translate") | |
| btn_anal = gr.Button("Analytics") | |
| btn_anal.click(fn=get_analytics_chart_api, inputs=[gr.JSON(label="data")], outputs=[gr.Textbox(label="chart_path")], api_name="analytics") | |
| btn_list = gr.Button("List") | |
| btn_list.click(fn=list_all_codes_api, inputs=[], outputs=[gr.JSON(label="data")], api_name="list_codes") | |
| # --- Visible UI --- | |
| with gr.Row(elem_classes="header-row"): | |
| with gr.Column(scale=4): gr.HTML(header_html) | |
| with gr.Column(scale=1): btn_toggle = gr.Button("๐ Day / Night", elem_classes="toggle-btn") | |
| with gr.Tabs(): | |
| # TAB 1: DECODER | |
| with gr.TabItem("๐ฎ DECODER WORKSTATION"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML('<div class="card"><div class="card-title">Input Source</div>') | |
| with gr.Tabs(): | |
| with gr.TabItem("๐ Upload File"): | |
| img_upload = gr.Image(type="pil", sources=["upload", "clipboard"], label="Upload", height=280) | |
| slider_conf = gr.Slider(0.1, 1.0, 0.25, label="Scan Sensitivity") | |
| btn_upload = gr.Button("โจ START DECODING", elem_classes="primary-btn") | |
| with gr.TabItem("๐ฅ Live Camera"): | |
| img_cam = gr.Image(type="pil", sources=["webcam"], label="Camera", height=280) | |
| slider_conf_cam = gr.Slider(0.1, 1.0, 0.25, label="Scan Sensitivity") | |
| btn_cam = gr.Button("โจ START DECODING", elem_classes="primary-btn") | |
| gr.HTML('</div>') | |
| with gr.Column(scale=1): | |
| gr.HTML('<div class="card"><div class="card-title">Result</div>') | |
| with gr.Tabs(): | |
| with gr.TabItem("๐ฎ Mystical"): out_mystical = gr.HTML(label="Prophecy") | |
| with gr.TabItem("๐๏ธ Academic"): out_academic = gr.Textbox(label="Scientific Translation", lines=15, show_label=False, elem_classes="scrollable-box") | |
| with gr.TabItem("๐ผ๏ธ Visuals"): | |
| out_image = gr.Image(label="Annotated Result", interactive=False) | |
| with gr.Row(): | |
| btn_download_img = gr.DownloadButton("๐พ Download Annotated Image") | |
| btn_download_zip = gr.DownloadButton("๐ฆ Download Glyphs (ZIP)") | |
| out_gallery = gr.Gallery(label="Extracted Glyphs", columns=4, height="auto") | |
| with gr.TabItem("๐ Analytics"): out_plot = gr.Plot(label="Analysis Charts") | |
| with gr.TabItem("๐ ๏ธ Logs"): | |
| out_report = gr.Textbox(label="Detection Log", lines=5) | |
| out_json = gr.JSON(label="JSON Data") | |
| gr.HTML('</div>') | |
| # TAB 2: ABOUT | |
| with gr.TabItem("๐ VISION & ABOUT"): gr.HTML(mission_html) | |
| # TAB 3: SETUP | |
| with gr.TabItem("๐ค SYSTEM SETUP"): | |
| gr.HTML(guide_html) | |
| gr.Code(value=claude_json_content, language="json", label="claude_desktop_config.json", interactive=False, lines=30) | |
| # TAB 4: GARDINER CODES | |
| with gr.TabItem("๐ GARDINER CODES"): | |
| gr.HTML(f"""<div class="card"><div class="card-title">๐ SUPPORTED GARDINER CODES</div><div style="overflow-x: auto; max-height: 600px; overflow-y: auto;"><table class="gardiner-table"><thead><tr><th>Code</th><th>Description</th><th>Transliteration</th><th>Type</th></tr></thead><tbody>{GARDINER_TABLE_CONTENT}</tbody></table></div></div>""") | |
| gr.HTML('<div style="text-align: center; color: var(--text-accent); opacity: 0.5; padding: 20px;"></div>') | |
| # Events | |
| btn_toggle.click(None, None, None, js="() => { document.body.classList.toggle('light-mode'); const container = document.querySelector('.gradio-container'); if(container) container.classList.toggle('light-mode'); }") | |
| # OUTPUTS: Image, ImgPath, ZipPath, Mystical, Academic, Plot, TextReport, JSON, Crops | |
| outputs = [out_image, btn_download_img, btn_download_zip, out_mystical, out_academic, out_plot, out_report, out_json, out_gallery] | |
| btn_upload.click(fn=process_pipeline, inputs=[img_upload, slider_conf], outputs=outputs) | |
| btn_cam.click(fn=process_pipeline, inputs=[img_cam, slider_conf_cam], outputs=outputs) | |
| if __name__ == "__main__": | |
| demo.launch(mcp_server=True, ssr_mode=False, allowed_paths=["/tmp", "/tmp/gradio_results", "."]) |