import gradio as gr
from ultralytics import YOLO
from PIL import Image
from google import genai
import os
import json
import matplotlib.pyplot as plt
import re
from huggingface_hub import hf_hub_download
import tempfile
# --- 1. CONFIGURATION & SECRETS ---
# Reading secrets from Hugging Face Environment Variables
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
HF_TOKEN = os.environ.get("HF_TOKEN")
# Model Configuration
MODEL_REPO = "youkii-xr/hieroglyphic-detection"
MODEL_FILENAME = "best.pt"
JSON_DB_PATH = "gardiner_codes.json"
# Configure YOLO to use a writable temp directory in HF Spaces
os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
# --- 2. DATA LOADING (GARDINER CODES) ---
def load_gardiner_database():
"""Loads the Gardiner Code database from the external JSON file."""
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. Gardiner Code tab will be empty.")
return {}
gardiner_data = load_gardiner_database()
gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()}
# --- 3. HTML TABLE GENERATOR ---
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 "
No data loaded. Please upload gardiner_codes.json to the Space.
"
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"
{cat_name}
"
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"""
{item.get("Code", "?")}
{item.get("Description", "-")}
{item.get("Transliteration", "-")}
{item.get("Type", "-")}
"""
return html_rows
GARDINER_TABLE_CONTENT = generate_gardiner_html()
# --- 4. LOAD MODEL (FROM PRIVATE REPO) ---
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. CORE PROCESSING LOGIC ---
def clean_ai_text(text):
text = re.sub(r'^\d+[\.\)]\s*', '', text)
text = text.replace("**", "")
return text
def generate_analytics_plots(detections, img_width, img_height):
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')
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')
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')
ax3 = plt.subplot(3, 1, 3)
h = ax3.hist2d(x_centers, y_centers, bins=[20, 20], range=[[0, img_width], [0, img_height]], cmap='inferno')
ax3.set_title('Glyph Spatial Heatmap', color='white', fontsize=12, pad=10)
ax3.set_xlim(0, img_width)
ax3.set_ylim(img_height, 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
def get_ai_translation_all_styles(keywords_list):
if not GOOGLE_API_KEY:
return "⚠️ Error: Google API Key not found in Secrets.", "Error"
if not keywords_list:
return "No hieroglyphs detected.", "No data."
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 for bolding keywords and 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|||")
if len(parts) < 2: return clean_ai_text(response.text), "Could not parse academic style."
return clean_ai_text(parts[0].strip()), clean_ai_text(parts[1].strip())
except Exception as e:
return f"Error: {str(e)}", "Error"
def process_pipeline(image, conf_threshold):
if image is None: return None, "", "", None, "", None, []
if model is None: return None, "Error: Model not loaded.", "", None, "", 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])
img_w, img_h = image.size
detections = []
unique_codes = set()
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])
unique_codes.add(code)
xyxy = box.xyxy[0].tolist()
detections.append({"code": code, "confidence": round(conf, 2), "box": xyxy})
crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3]))
crops.append((crop_img, f"{code}\n({int(conf*100)}%)"))
mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes]
analytics_plot = generate_analytics_plots(detections, img_w, img_h)
mystical, academic = get_ai_translation_all_styles(mapped_words)
text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}"
formatted_mystical = f"""
Bridging the Ancient and the Digital. 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.
"""
guide_html = """
🤖 CLAUDE DESKTOP SETUP GUIDE
STEP 0: PREREQUISITE
Ensure you have Node.js installed (Required for the `npx` command used by MCP).
Claude is sandboxed. It cannot see your Desktop. You must create a bridge.
1. Create this EXACT folder on your PC: C:\\Claude_Work 2. Move your hieroglyph images INSIDE this folder.
STEP 2: VERIFY PYTHON
The code below assumes Python is at: C:\\Python313\\python.exe
Check your path: Open CMD and type where python.
Note: If your path is different, replace the path in the JSON code block below before copying.
STEP 3: CONFIGURE CLAUDE1. Open Config: %APPDATA%\\Claude\\claude_desktop_config.json 2. Paste the JSON below into the "mcpServers" section. 3. IMPORTANT: Close Claude from the System Tray (near the clock) and restart it.
STEP 4: USAGE
Prompt Claude: "Analyze the image at C:\\Claude_Work\\my_tablet.jpg"
"""
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 = """"""
# --- 7. MAIN APP ASSEMBLY ---
with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
gr.HTML(f"")
gr.HTML(trail_script)
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")
with gr.Tabs():
# TAB 1: DECODER
with gr.TabItem("🔮 DECODER WORKSTATION"):
with gr.Row():
with gr.Column(scale=1):
gr.HTML('