Rosetta-Decoder / app.py
youkii-xr's picture
Update app.py
352c573 verified
Raw
History Blame
15.8 kB
import gradio as gr
from ultralytics import YOLO
from PIL import Image
import os
from huggingface_hub import hf_hub_download
import numpy as np
import tempfile
# --- 1. SETUP & MODEL LOADING ---
MODEL_REPO = "youkii-xr/hieroglyphic-detection"
MODEL_FILENAME = "best.pt"
os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
try:
print("System: Initializing Rosetta Decoder Core...")
model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILENAME,
token=os.environ.get("HF_TOKEN")
)
model = YOLO(model_path)
print("System: Model loaded successfully.")
except Exception as e:
print(f"Error: {e}")
model = None
# --- 2. LOGIC ---
def generate_human_report(detections, counts):
if not detections:
return "Rosetta Decoder detects no identifiable Gardiner codes in this image."
total = len(detections)
sorted_counts = sorted(counts.items(), key=lambda item: item[1], reverse=True)
report = f"πŸ”“ DECODING SEQUENCE COMPLETE\n"
report += f"===================================\n"
report += f"Glyph Density: {total} Symbols Identified\n\n"
report += "πŸ”£ GARDINER CODE INVENTORY:\n"
for code, count in sorted_counts:
report += f"β€’ Code [{code}]: {count} instance(s)\n"
report += f"\n===================================\n"
report += f"CONFIDENCE: High\n"
report += f"STATUS: Digitized & Ready for Translation."
return report
def detect_hieroglyphs(image: Image.Image, conf_threshold: float = 0.25):
if image is None:
return None, None, "Input source required.", None
if model is None:
return None, {"error": "Model failed"}, "Critical Error: Weights not loaded.", None
try:
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])
temp_dir = tempfile.gettempdir()
save_path = os.path.join(temp_dir, "rosetta_decoded_result.jpg")
annotated_image.save(save_path)
detections = []
gardiner_counts = {}
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])
if code not in gardiner_counts: gardiner_counts[code] = 0
gardiner_counts[code] += 1
detections.append({"code": code, "confidence": round(conf, 2)})
summary_json = {
"status": "success",
"total_found": len(detections),
"counts": gardiner_counts
}
text_report = generate_human_report(detections, gardiner_counts)
return annotated_image, summary_json, text_report, save_path
except Exception as e:
return None, {"error": str(e)}, f"System Failure: {str(e)}", None
# --- 3. UI STYLING ---
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');
/* --- 1. DEFAULT (DARK/NIGHT MODE) --- */
/* This applies when the page first loads or when 'light-mode' is NOT active */
: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%);
--btn-text: #000;
--info-bg: rgba(212, 175, 55, 0.08);
--info-border: #d4af37;
--glow-color: rgba(212, 175, 55, 0.4);
}}
/* --- 2. LIGHT MODE OVERRIDE (PAPYRUS) --- */
/* We use high specificity (.gradio-container.light-mode) to defeat the OS dark mode */
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; /* Forces Dark Text */
--text-accent: #8b4513 !important; /* Forces Brown Accent */
--border-color: #8b4513 !important;
--btn-grad: linear-gradient(135deg, #cd853f 0%, #8b4513 100%) !important;
--btn-text: #fff !important;
--info-bg: rgba(139, 69, 19, 0.05) !important;
--info-border: #8b4513 !important;
--glow-color: rgba(139, 69, 19, 0.3) !important;
color: #3d342b !important; /* Force Body Text Dark */
}}
/* --- GLOBAL LAYOUT --- */
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;
-webkit-font-smoothing: antialiased;
transition: background 0.5s ease, color 0.5s ease;
}}
button, a, .cursor-pointer {{
cursor: {cursor_url} 16 16, pointer !important;
}}
/* --- TRAIL --- */
.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); }}
}}
/* --- CARDS --- */
.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);
color: var(--text-primary) !important;
}}
.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);
}}
/* --- BUTTONS --- */
button.primary-btn {{
background: var(--btn-grad) !important;
border: 1px solid var(--border-color) !important;
color: var(--btn-text) !important;
font-weight: 700 !important;
font-family: 'Cairo', sans-serif !important;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
font-size: 16px !important;
}}
button.primary-btn:hover {{
transform: scale(1.02);
box-shadow: 0 0 25px var(--glow-color);
}}
button.toggle-btn {{
background: transparent !important;
border: 1px solid var(--border-color) !important;
color: var(--text-accent) !important;
padding: 5px 15px !important;
font-family: 'Space Mono', monospace;
}}
button.toggle-btn:hover {{
background: var(--info-bg) !important;
}}
/* --- GUIDE BOXES & TEXT COLORS --- */
.guide-step {{
background-color: var(--info-bg) !important;
border-left: 4px solid var(--info-border) !important;
padding: 16px;
margin: 12px 0;
border-radius: 0 8px 8px 0;
font-family: 'Space Mono', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text-primary) !important; /* STRICT COLOR ENFORCEMENT */
}}
.step-number {{
color: var(--text-accent) !important;
font-weight: bold;
text-transform: uppercase;
display: block;
margin-bottom: 6px;
font-size: 12px;
}}
.path-highlight {{
background: rgba(128,128,128,0.2);
padding: 3px 8px;
border-radius: 4px;
color: var(--text-accent) !important;
font-weight: bold;
border: 1px solid var(--border-color);
}}
/* --- FIXES --- */
.gradio-image, .gradio-json {{ background: transparent !important; border: none !important; }}
/* Fix Light Mode Upload Box Background */
body.light-mode .gradio-image button,
.gradio-container.light-mode .gradio-image button {{
background-color: rgba(255, 248, 240, 0.7) !important;
border: 2px dashed var(--border-color) !important;
color: var(--text-primary) !important;
}}
.report-box textarea {{
background-color: rgba(0,0,0,0.2) !important;
border: 1px solid var(--border-color) !important;
font-family: 'Space Mono', monospace !important;
color: var(--text-primary) !important; /* Ensure text matches theme */
font-size: 14px !important;
}}
.block-title {{ color: var(--text-accent) !important; }}
span {{ color: var(--text-primary); }}
"""
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;">AI HIEROGLYPHIC TRANSLATION SYSTEM</p>
</div>
</div>
</div>
"""
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>Bridging the Ancient and the Digital.</b><br>
The Rosetta Decoder project utilizes advanced computer vision to identify and catalog Ancient Egyptian hieroglyphs.
By automating the detection of Gardiner codes, we are creating a digital bridge that will eventually allow for instant,
context-aware translation of Pharaonic wisdom, making the voices of the past accessible to everyone.
</p>
</div>
"""
guide_instructions_html = """
<div class="card" style="border-color: var(--info-border) !important;">
<div class="card-title" style="color: var(--info-border) !important;">πŸ€– CLAUDE DESKTOP SETUP GUIDE</div>
<div class="guide-step">
<span class="step-number">STEP 1: PREPARE WORKSPACE</span>
Claude is sandboxed. It cannot see your Desktop. You must create a bridge.<br>
1. Create this EXACT folder on your PC: <span class="path-highlight">C:\\Claude_Work</span><br>
2. Move your hieroglyph images <b>INSIDE</b> this folder.
</div>
<div class="guide-step">
<span class="step-number">STEP 2: VERIFY PYTHON</span>
The code below assumes Python is at: <code>C:\\Python313\\python.exe</code><br>
<b>Check your path:</b> Open CMD and type <code>where python</code>.<br>
<i>Note: If your path is different, replace the path in the JSON code block below before copying.</i>
</div>
<div class="guide-step">
<span class="step-number">STEP 3: CONFIGURE CLAUDE</span>
1. Open Config: <code>%APPDATA%\\Claude\\claude_desktop_config.json</code><br>
2. Paste the JSON below into the <code>"mcpServers"</code> section.<br>
3. <b>IMPORTANT:</b> Close Claude from the System Tray (near the clock) and restart it.
</div>
<div class="guide-step">
<span class="step-number">STEP 4: USAGE</span>
Prompt Claude: <i>"Analyze the image at C:\\Claude_Work\\my_tablet.jpg"</i>
</div>
"""
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"
]
}
}
}"""
# --- 4. MAIN APP ASSEMBLY ---
trail_script = """
<script>
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('mousemove', (e) => {
if (Math.random() > 0.7) return; // limit density
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>
"""
with gr.Blocks(title="Rosetta Decoder") as demo:
# Inject CSS & Trail Script
gr.HTML(f"<style>{custom_css}</style>")
gr.HTML(trail_script)
# Top Section
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")
gr.HTML(mission_html)
# Workspace
with gr.Row():
with gr.Column(scale=1):
gr.HTML('<div class="card"><div class="card-title">INPUT IMAGE</div>')
with gr.Tabs():
with gr.TabItem("πŸ“œ Upload File"):
img_upload = gr.Image(type="pil", sources=["upload", "clipboard"], label="Upload", height=320)
slider_upload = 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=320)
slider_cam = gr.Slider(0.1, 1.0, 0.25, label="Scan Sensitivity")
btn_cam = gr.Button("πŸ” LIVE DECODE", elem_classes="primary-btn")
gr.HTML('</div>')
with gr.Column(scale=1):
gr.HTML('<div class="card"><div class="card-title">AI ANALYSIS RESULTS</div>')
out_image = gr.Image(label="Decoded Result", interactive=False)
btn_download = gr.DownloadButton("πŸ’Ύ DOWNLOAD RESULT", visible=True)
out_report = gr.Textbox(label="Analysis Log", lines=6, elem_classes="report-box", placeholder="Waiting for data stream...")
with gr.Accordion("Raw Glyph Data (JSON)", open=False):
out_json = gr.JSON(label="JSON Data")
gr.HTML('</div>')
# Footer
gr.HTML(guide_instructions_html)
gr.Code(value=claude_json_content, language="json", label="claude_desktop_config.json", interactive=False, lines=15)
gr.HTML("</div>")
# Wiring
# JS: Toggle 'light-mode' on body AND container to defeat OS dark mode
btn_toggle.click(None, None, None, js="() => { document.body.classList.toggle('light-mode'); document.querySelector('.gradio-container').classList.toggle('light-mode'); }")
btn_upload.click(fn=detect_hieroglyphs, inputs=[img_upload, slider_upload], outputs=[out_image, out_json, out_report, btn_download])
btn_cam.click(fn=detect_hieroglyphs, inputs=[img_cam, slider_cam], outputs=[out_image, out_json, out_report, btn_download])
if __name__ == "__main__":
demo.launch(mcp_server=True, ssr_mode=False, allowed_paths=["/tmp"])