Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ import matplotlib.pyplot as plt
|
|
| 8 |
import re
|
| 9 |
from huggingface_hub import hf_hub_download
|
| 10 |
import tempfile
|
|
|
|
| 11 |
|
| 12 |
# --- 1. CONFIGURATION & SECRETS ---
|
| 13 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
|
@@ -37,75 +38,72 @@ def load_gardiner_database():
|
|
| 37 |
gardiner_data = load_gardiner_database()
|
| 38 |
gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()}
|
| 39 |
|
| 40 |
-
# --- 3.
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
'K': "Fishes", 'L': "Invertebrates", 'M': "Trees & Plants",
|
| 47 |
-
'N': "Sky, Earth, Water", 'O': "Buildings", 'P': "Ships",
|
| 48 |
-
'Q': "Furniture", 'R': "Temple Furniture", 'S': "Crowns & Dress",
|
| 49 |
-
'T': "Warfare & Hunting", 'U': "Agriculture & Crafts", 'V': "Rope & Baskets",
|
| 50 |
-
'W': "Vessels", 'X': "Loaves & Cakes", 'Y': "Writings & Games",
|
| 51 |
-
'Z': "Strokes & Figures", 'Aa': "Unclassified"
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
def generate_gardiner_html():
|
| 55 |
-
if not gardiner_data:
|
| 56 |
-
return "<tr><td colspan='4'>No data loaded. Please upload gardiner_codes.json.</td></tr>"
|
| 57 |
-
|
| 58 |
-
html_rows = ""
|
| 59 |
-
grouped = {}
|
| 60 |
-
for key, data in gardiner_data.items():
|
| 61 |
-
match = re.match(r"([A-Za-z]+)", data.get("Code", key))
|
| 62 |
-
prefix = match.group(1) if match else "Unk"
|
| 63 |
-
if prefix not in grouped: grouped[prefix] = []
|
| 64 |
-
grouped[prefix].append(data)
|
| 65 |
|
| 66 |
-
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
<td><span class="type-badge">{item.get("Type", "-")}</span></td>
|
| 80 |
-
</tr>
|
| 81 |
-
"""
|
| 82 |
-
return html_rows
|
| 83 |
-
|
| 84 |
-
GARDINER_TABLE_CONTENT = generate_gardiner_html()
|
| 85 |
-
|
| 86 |
-
# --- 4. LOAD MODEL ---
|
| 87 |
-
|
| 88 |
-
print("System: Initializing Rosetta Decoder Core...")
|
| 89 |
-
try:
|
| 90 |
-
model_path = hf_hub_download(
|
| 91 |
-
repo_id=MODEL_REPO,
|
| 92 |
-
filename=MODEL_FILENAME,
|
| 93 |
-
token=HF_TOKEN
|
| 94 |
-
)
|
| 95 |
-
model = YOLO(model_path)
|
| 96 |
-
print("System: Model loaded successfully.")
|
| 97 |
-
except Exception as e:
|
| 98 |
-
print(f"Error loading model: {e}")
|
| 99 |
-
model = None
|
| 100 |
-
|
| 101 |
-
# --- 5. CORE LOGIC (Helpers) ---
|
| 102 |
-
|
| 103 |
-
def clean_ai_text(text):
|
| 104 |
-
text = re.sub(r'^\d+[\.\)]\s*', '', text)
|
| 105 |
-
text = text.replace("**", "")
|
| 106 |
-
return text
|
| 107 |
|
| 108 |
-
def
|
|
|
|
| 109 |
if not detections: return None
|
| 110 |
|
| 111 |
codes = [d['code'] for d in detections]
|
|
@@ -116,6 +114,7 @@ def generate_analytics_plots(detections, img_width, img_height):
|
|
| 116 |
fig = plt.figure(figsize=(10, 15))
|
| 117 |
fig.patch.set_facecolor('#0f0f23')
|
| 118 |
|
|
|
|
| 119 |
ax1 = plt.subplot(3, 1, 1)
|
| 120 |
unique_codes = list(set(codes))
|
| 121 |
counts = [codes.count(c) for c in unique_codes]
|
|
@@ -125,6 +124,7 @@ def generate_analytics_plots(detections, img_width, img_height):
|
|
| 125 |
ax1.set_facecolor('none')
|
| 126 |
for spine in ax1.spines.values(): spine.set_color('#d4af37')
|
| 127 |
|
|
|
|
| 128 |
ax2 = plt.subplot(3, 1, 2)
|
| 129 |
ax2.scatter(range(len(confs)), confs, color='#d4af37', alpha=0.7, s=50)
|
| 130 |
ax2.set_title('AI Confidence Levels', color='white', fontsize=12, pad=10)
|
|
@@ -133,11 +133,12 @@ def generate_analytics_plots(detections, img_width, img_height):
|
|
| 133 |
ax2.set_facecolor('none')
|
| 134 |
for spine in ax2.spines.values(): spine.set_color('#d4af37')
|
| 135 |
|
|
|
|
| 136 |
ax3 = plt.subplot(3, 1, 3)
|
| 137 |
-
h = ax3.hist2d(x_centers, y_centers, bins=[20, 20], range=[[0,
|
| 138 |
ax3.set_title('Glyph Spatial Heatmap', color='white', fontsize=12, pad=10)
|
| 139 |
-
ax3.set_xlim(0,
|
| 140 |
-
ax3.set_ylim(
|
| 141 |
ax3.tick_params(colors='white')
|
| 142 |
cbar = plt.colorbar(h[3], ax=ax3)
|
| 143 |
cbar.ax.yaxis.set_tick_params(color='white')
|
|
@@ -146,121 +147,130 @@ def generate_analytics_plots(detections, img_width, img_height):
|
|
| 146 |
plt.tight_layout(pad=4.0)
|
| 147 |
return fig
|
| 148 |
|
| 149 |
-
# ---
|
| 150 |
-
|
| 151 |
-
@gr.tool
|
| 152 |
-
def detect_hieroglyphs_mcp(image_path: str, conf_threshold: float = 0.25):
|
| 153 |
-
"""Detects hieroglyphs in an image file. Returns JSON data."""
|
| 154 |
-
if model is None: return {"error": "Model not loaded"}
|
| 155 |
-
try:
|
| 156 |
-
image = Image.open(image_path)
|
| 157 |
-
img_w, img_h = image.size
|
| 158 |
-
results = model.predict(source=image, conf=conf_threshold, iou=0.45, imgsz=640, verbose=False, device='cpu', max_det=300)
|
| 159 |
-
detections = []
|
| 160 |
-
unique_codes = set()
|
| 161 |
-
for box in results[0].boxes:
|
| 162 |
-
if box.cls.numel() > 0:
|
| 163 |
-
cls_id = int(box.cls[0])
|
| 164 |
-
if 0 <= cls_id < len(model.names):
|
| 165 |
-
code = model.names[cls_id]
|
| 166 |
-
conf = float(box.conf[0])
|
| 167 |
-
unique_codes.add(code)
|
| 168 |
-
xyxy = box.xyxy[0].tolist()
|
| 169 |
-
meaning = gardiner_map.get(code, "Unknown")
|
| 170 |
-
detections.append({"code": code, "meaning": meaning, "confidence": round(conf, 2), "box": xyxy})
|
| 171 |
-
return {"status": "success", "image_size": [img_w, img_h], "total_detected": len(detections), "unique_codes": list(unique_codes), "detections": detections}
|
| 172 |
-
except Exception as e: return {"error": str(e)}
|
| 173 |
-
|
| 174 |
-
@gr.tool
|
| 175 |
-
def translate_hieroglyphs_mcp(keywords: list[str], style: str = "Academic Literal"):
|
| 176 |
-
"""Translates a list of meanings using Gemini."""
|
| 177 |
-
if not GOOGLE_API_KEY: return "Error: Google API Key not configured."
|
| 178 |
-
keywords_str = ", ".join(keywords)
|
| 179 |
-
prompts = {
|
| 180 |
-
"Mystical Story": f"Create a coherent, mystical, and atmospheric short story using these ancient Egyptian concepts: [{keywords_str}]. Make it sound like a prophecy.",
|
| 181 |
-
"Academic Literal": f"Provide a direct, grammatical translation of these concepts: [{keywords_str}]. Focus on linguistic structure and sentence formation used in Egyptology.",
|
| 182 |
-
"Modern Interpretation": f"Translate the essence of these symbols: [{keywords_str}] into a modern, relatable piece of advice or horoscopic reading."
|
| 183 |
-
}
|
| 184 |
-
prompt = prompts.get(style, prompts["Academic Literal"])
|
| 185 |
-
try:
|
| 186 |
-
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 187 |
-
response = client.models.generate_content(model="gemini-2.5-flash", contents=f"You are an expert Egyptologist AI. {prompt}")
|
| 188 |
-
return response.text
|
| 189 |
-
except Exception as e: return f"Translation Error: {str(e)}"
|
| 190 |
-
|
| 191 |
-
@gr.tool
|
| 192 |
-
def get_gardiner_info_mcp(code: str):
|
| 193 |
-
"""Retrieves detailed info about a specific code."""
|
| 194 |
-
info = gardiner_data.get(code)
|
| 195 |
-
if info: return info
|
| 196 |
-
return {"error": f"Code {code} not found in database."}
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
# ---
|
| 204 |
|
| 205 |
def process_pipeline(image, conf_threshold):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
if image is None: return None, "", "", None, "", None, []
|
| 207 |
if model is None: return None, "Error: Model not loaded.", "", None, "", None, []
|
| 208 |
|
| 209 |
try:
|
| 210 |
-
|
| 211 |
-
annotated_array = results[0].plot()
|
| 212 |
-
annotated_image = Image.fromarray(annotated_array[..., ::-1])
|
| 213 |
img_w, img_h = image.size
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
unique_codes = set()
|
| 217 |
-
crops = []
|
| 218 |
-
|
| 219 |
-
for box in results[0].boxes:
|
| 220 |
-
if box.cls.numel() > 0:
|
| 221 |
-
cls_id = int(box.cls[0])
|
| 222 |
-
if 0 <= cls_id < len(model.names):
|
| 223 |
-
code = model.names[cls_id]
|
| 224 |
-
conf = float(box.conf[0])
|
| 225 |
-
unique_codes.add(code)
|
| 226 |
-
xyxy = box.xyxy[0].tolist()
|
| 227 |
-
detections.append({"code": code, "confidence": round(conf, 2), "box": xyxy})
|
| 228 |
-
crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3]))
|
| 229 |
-
crops.append((crop_img, f"{code}\n({int(conf*100)}%)"))
|
| 230 |
-
|
| 231 |
mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes]
|
| 232 |
|
| 233 |
-
#
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
Please provide 2 distinct outputs separated by "|||SEPARATOR|||".
|
| 241 |
-
1. A Mystical Story: Highly atmospheric, sounding like an ancient prophecy.
|
| 242 |
-
Do NOT number this section. Use HTML tags <b> for bolding keywords and <br> for new lines.
|
| 243 |
-
2. An Academic Translation: Direct, linguistic, focusing on grammar. Use standard text.
|
| 244 |
-
"""
|
| 245 |
-
try:
|
| 246 |
-
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 247 |
-
response = client.models.generate_content(model="gemini-2.5-flash", contents=prompt)
|
| 248 |
-
parts = response.text.split("|||SEPARATOR|||")
|
| 249 |
-
if len(parts) < 2: mystical, academic = clean_ai_text(response.text), "Could not parse academic style."
|
| 250 |
-
else: mystical, academic = clean_ai_text(parts[0].strip()), clean_ai_text(parts[1].strip())
|
| 251 |
-
except Exception as e:
|
| 252 |
-
mystical, academic = f"Error: {str(e)}", f"Error: {str(e)}"
|
| 253 |
-
|
| 254 |
-
analytics_plot = generate_analytics_plots(detections, img_w, img_h)
|
| 255 |
text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}"
|
| 256 |
-
formatted_mystical = f"""<div class="mystical-container"><h3>✨ THE ANCIENT WHISPER</h3><p>{mystical}</p></div>"""
|
| 257 |
json_output = {"count": len(detections), "detections": detections}
|
|
|
|
| 258 |
|
| 259 |
-
return
|
| 260 |
|
| 261 |
except Exception as e:
|
| 262 |
return None, f"System Failure: {str(e)}", "", None, str(e), None, []
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
# --- 8. UI STYLING & ASSETS ---
|
| 265 |
|
| 266 |
cursor_url = "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGFmMzciIHN0cm9rZS13aWR0aD0iMS41Ij4KICAgIDxwYXRoIGQ9Ik0xNiw4IEM2LDIwIDI2LDIwIDE2LDggWiIgZmlsbD0icmdiYSgyMTIsIDE3NSwgNTUsIDAuMSkiLz4KICAgIDxjaXJjbGUgY3g9IjE2IiBjeT0iMTUiIHI9IjMiIGZpbGw9IiNkNGFmMzciLz4KICAgIDxwYXRoIGQ9Ik0xNiwyMiBMMTYsMjggTDEwLDI4Ii8+CiAgPC9nPgo8L3N2Zz4=')"
|
|
@@ -383,12 +393,38 @@ claude_json_content = """{ "mcpServers": { "gradio": { "command": "npx", "args":
|
|
| 383 |
|
| 384 |
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>"""
|
| 385 |
|
| 386 |
-
# ---
|
| 387 |
|
| 388 |
with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
|
| 389 |
gr.HTML(f"<style>{custom_css}</style>")
|
| 390 |
gr.HTML(trail_script)
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
with gr.Row(elem_classes="header-row"):
|
| 393 |
with gr.Column(scale=4): gr.HTML(header_html)
|
| 394 |
with gr.Column(scale=1): btn_toggle = gr.Button("🌗 Day / Night", elem_classes="toggle-btn")
|
|
|
|
| 8 |
import re
|
| 9 |
from huggingface_hub import hf_hub_download
|
| 10 |
import tempfile
|
| 11 |
+
import numpy as np
|
| 12 |
|
| 13 |
# --- 1. CONFIGURATION & SECRETS ---
|
| 14 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
|
|
|
| 38 |
gardiner_data = load_gardiner_database()
|
| 39 |
gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()}
|
| 40 |
|
| 41 |
+
# --- 3. CORE LOGIC FUNCTIONS (Reusable) ---
|
| 42 |
|
| 43 |
+
def core_detect(image, conf_threshold):
|
| 44 |
+
"""Core YOLO detection logic."""
|
| 45 |
+
if image is None or model is None:
|
| 46 |
+
return None, [], []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
results = model.predict(source=image, conf=conf_threshold, iou=0.45, imgsz=640, verbose=False, device='cpu', max_det=300)
|
| 49 |
+
annotated_array = results[0].plot()
|
| 50 |
+
annotated_image = Image.fromarray(annotated_array[..., ::-1])
|
| 51 |
|
| 52 |
+
detections = []
|
| 53 |
+
crops = []
|
| 54 |
+
|
| 55 |
+
for box in results[0].boxes:
|
| 56 |
+
if box.cls.numel() > 0:
|
| 57 |
+
cls_id = int(box.cls[0])
|
| 58 |
+
if 0 <= cls_id < len(model.names):
|
| 59 |
+
code = model.names[cls_id]
|
| 60 |
+
conf = float(box.conf[0])
|
| 61 |
+
xyxy = box.xyxy[0].tolist()
|
| 62 |
+
|
| 63 |
+
# Create detection object
|
| 64 |
+
detection = {
|
| 65 |
+
"code": code,
|
| 66 |
+
"description": gardiner_map.get(code, "Unknown"),
|
| 67 |
+
"confidence": round(conf, 2),
|
| 68 |
+
"box": xyxy
|
| 69 |
+
}
|
| 70 |
+
detections.append(detection)
|
| 71 |
+
|
| 72 |
+
# Create crop
|
| 73 |
+
crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3]))
|
| 74 |
+
crops.append((crop_img, f"{code}\n({int(conf*100)}%)"))
|
| 75 |
+
|
| 76 |
+
return annotated_image, detections, crops
|
| 77 |
+
|
| 78 |
+
def core_translate(keywords_list):
|
| 79 |
+
"""Core Gemini translation logic."""
|
| 80 |
+
if not GOOGLE_API_KEY:
|
| 81 |
+
return "Error: API Key Missing", "Error: API Key Missing"
|
| 82 |
+
if not keywords_list:
|
| 83 |
+
return "No symbols detected", "No symbols detected"
|
| 84 |
+
|
| 85 |
+
keywords_str = ", ".join(keywords_list)
|
| 86 |
+
prompt = f"""
|
| 87 |
+
You are an expert Egyptologist AI. I have detected these symbols: [{keywords_str}].
|
| 88 |
+
Please provide 2 distinct outputs separated by "|||SEPARATOR|||".
|
| 89 |
+
1. A Mystical Story: Highly atmospheric, sounding like an ancient prophecy.
|
| 90 |
+
Do NOT number this section. Use HTML tags <b> for bolding keywords and <br> for new lines.
|
| 91 |
+
2. An Academic Translation: Direct, linguistic, focusing on grammar. Use standard text.
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 95 |
+
response = client.models.generate_content(model="gemini-2.5-flash", contents=prompt)
|
| 96 |
+
parts = response.text.split("|||SEPARATOR|||")
|
| 97 |
|
| 98 |
+
def clean(t): return t.replace("**", "").strip()
|
| 99 |
+
|
| 100 |
+
if len(parts) < 2: return clean(response.text), "Could not parse academic style."
|
| 101 |
+
return clean(parts[0]), clean(parts[1])
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return f"Error: {str(e)}", f"Error: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
+
def core_analytics(detections, img_w, img_h):
|
| 106 |
+
"""Core Matplotlib logic."""
|
| 107 |
if not detections: return None
|
| 108 |
|
| 109 |
codes = [d['code'] for d in detections]
|
|
|
|
| 114 |
fig = plt.figure(figsize=(10, 15))
|
| 115 |
fig.patch.set_facecolor('#0f0f23')
|
| 116 |
|
| 117 |
+
# 1. Frequency
|
| 118 |
ax1 = plt.subplot(3, 1, 1)
|
| 119 |
unique_codes = list(set(codes))
|
| 120 |
counts = [codes.count(c) for c in unique_codes]
|
|
|
|
| 124 |
ax1.set_facecolor('none')
|
| 125 |
for spine in ax1.spines.values(): spine.set_color('#d4af37')
|
| 126 |
|
| 127 |
+
# 2. Confidence
|
| 128 |
ax2 = plt.subplot(3, 1, 2)
|
| 129 |
ax2.scatter(range(len(confs)), confs, color='#d4af37', alpha=0.7, s=50)
|
| 130 |
ax2.set_title('AI Confidence Levels', color='white', fontsize=12, pad=10)
|
|
|
|
| 133 |
ax2.set_facecolor('none')
|
| 134 |
for spine in ax2.spines.values(): spine.set_color('#d4af37')
|
| 135 |
|
| 136 |
+
# 3. Heatmap
|
| 137 |
ax3 = plt.subplot(3, 1, 3)
|
| 138 |
+
h = ax3.hist2d(x_centers, y_centers, bins=[20, 20], range=[[0, img_w], [0, img_h]], cmap='inferno')
|
| 139 |
ax3.set_title('Glyph Spatial Heatmap', color='white', fontsize=12, pad=10)
|
| 140 |
+
ax3.set_xlim(0, img_w)
|
| 141 |
+
ax3.set_ylim(img_h, 0)
|
| 142 |
ax3.tick_params(colors='white')
|
| 143 |
cbar = plt.colorbar(h[3], ax=ax3)
|
| 144 |
cbar.ax.yaxis.set_tick_params(color='white')
|
|
|
|
| 147 |
plt.tight_layout(pad=4.0)
|
| 148 |
return fig
|
| 149 |
|
| 150 |
+
# --- 4. LOAD MODEL ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
+
print("System: Initializing Rosetta Decoder Core...")
|
| 153 |
+
try:
|
| 154 |
+
model_path = hf_hub_download(
|
| 155 |
+
repo_id=MODEL_REPO,
|
| 156 |
+
filename=MODEL_FILENAME,
|
| 157 |
+
token=HF_TOKEN
|
| 158 |
+
)
|
| 159 |
+
model = YOLO(model_path)
|
| 160 |
+
print("System: Model loaded successfully.")
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"Error loading model: {e}")
|
| 163 |
+
model = None
|
| 164 |
|
| 165 |
+
# --- 5. MAIN UI PIPELINE (Orchestrator) ---
|
| 166 |
|
| 167 |
def process_pipeline(image, conf_threshold):
|
| 168 |
+
"""
|
| 169 |
+
Main function used by the Web UI.
|
| 170 |
+
Chains detection -> analytics -> translation.
|
| 171 |
+
"""
|
| 172 |
if image is None: return None, "", "", None, "", None, []
|
| 173 |
if model is None: return None, "Error: Model not loaded.", "", None, "", None, []
|
| 174 |
|
| 175 |
try:
|
| 176 |
+
# 1. Detect
|
|
|
|
|
|
|
| 177 |
img_w, img_h = image.size
|
| 178 |
+
annotated_img, detections, crops = core_detect(image, conf_threshold)
|
| 179 |
|
| 180 |
+
# 2. Extract Keywords
|
| 181 |
+
unique_codes = list(set([d['code'] for d in detections]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes]
|
| 183 |
|
| 184 |
+
# 3. Translate
|
| 185 |
+
mystical, academic = core_translate(mapped_words)
|
| 186 |
+
|
| 187 |
+
# 4. Analytics
|
| 188 |
+
analytics_plot = core_analytics(detections, img_w, img_h)
|
| 189 |
+
|
| 190 |
+
# 5. Reports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}"
|
|
|
|
| 192 |
json_output = {"count": len(detections), "detections": detections}
|
| 193 |
+
formatted_mystical = f"""<div class="mystical-container"><h3>✨ THE ANCIENT WHISPER</h3><p>{mystical}</p></div>"""
|
| 194 |
|
| 195 |
+
return annotated_img, formatted_mystical, academic, analytics_plot, text_report, json_output, crops
|
| 196 |
|
| 197 |
except Exception as e:
|
| 198 |
return None, f"System Failure: {str(e)}", "", None, str(e), None, []
|
| 199 |
|
| 200 |
+
# --- 6. MCP API FUNCTIONS (Granular) ---
|
| 201 |
+
# These wrappers are exposed as API endpoints for Claude/MCP to use independently.
|
| 202 |
+
|
| 203 |
+
def api_detect_only(image, conf):
|
| 204 |
+
"""API: Returns JSON detection data and annotated image."""
|
| 205 |
+
ann_img, dets, _ = core_detect(image, conf)
|
| 206 |
+
return ann_img, {"count": len(dets), "detections": dets}
|
| 207 |
+
|
| 208 |
+
def api_translate_only(keywords_text):
|
| 209 |
+
"""API: Translates a comma-separated string of keywords."""
|
| 210 |
+
# Convert string input "sun, life" to list ["sun", "life"]
|
| 211 |
+
if isinstance(keywords_text, str):
|
| 212 |
+
keywords = [k.strip() for k in keywords_text.split(',')]
|
| 213 |
+
else:
|
| 214 |
+
keywords = keywords_text
|
| 215 |
+
mystical, academic = core_translate(keywords)
|
| 216 |
+
return mystical, academic
|
| 217 |
+
|
| 218 |
+
def api_get_supported_codes():
|
| 219 |
+
"""API: Returns the full Gardiner list."""
|
| 220 |
+
return gardiner_data
|
| 221 |
+
|
| 222 |
+
def api_get_analytics(json_data):
|
| 223 |
+
"""API: Generates plots from JSON detection data."""
|
| 224 |
+
# Mock image size if not provided, mostly for heatmap relative positions
|
| 225 |
+
dets = json_data.get("detections", [])
|
| 226 |
+
return core_analytics(dets, 1000, 1000)
|
| 227 |
+
|
| 228 |
+
# --- 7. HTML GENERATORS ---
|
| 229 |
+
|
| 230 |
+
CATEGORIES = {
|
| 231 |
+
'A': "Men & Monarchs", 'B': "Women & Human Activities", 'C': "Deities",
|
| 232 |
+
'D': "Parts of Human Body", 'E': "Mammals", 'F': "Parts of Mammals",
|
| 233 |
+
'G': "Birds", 'H': "Parts of Birds", 'I': "Reptiles & Amphibians",
|
| 234 |
+
'K': "Fishes", 'L': "Invertebrates", 'M': "Trees & Plants",
|
| 235 |
+
'N': "Sky, Earth, Water", 'O': "Buildings", 'P': "Ships",
|
| 236 |
+
'Q': "Furniture", 'R': "Temple Furniture", 'S': "Crowns & Dress",
|
| 237 |
+
'T': "Warfare & Hunting", 'U': "Agriculture & Crafts", 'V': "Rope & Baskets",
|
| 238 |
+
'W': "Vessels", 'X': "Loaves & Cakes", 'Y': "Writings & Games",
|
| 239 |
+
'Z': "Strokes & Figures", 'Aa': "Unclassified"
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
def generate_gardiner_html():
|
| 243 |
+
if not gardiner_data:
|
| 244 |
+
return "<tr><td colspan='4'>No data loaded. Please upload gardiner_codes.json.</td></tr>"
|
| 245 |
+
|
| 246 |
+
html_rows = ""
|
| 247 |
+
grouped = {}
|
| 248 |
+
for key, data in gardiner_data.items():
|
| 249 |
+
match = re.match(r"([A-Za-z]+)", data.get("Code", key))
|
| 250 |
+
prefix = match.group(1) if match else "Unk"
|
| 251 |
+
if prefix not in grouped: grouped[prefix] = []
|
| 252 |
+
grouped[prefix].append(data)
|
| 253 |
+
|
| 254 |
+
sorted_prefixes = sorted(grouped.keys(), key=lambda x: (len(x), x))
|
| 255 |
+
|
| 256 |
+
for prefix in sorted_prefixes:
|
| 257 |
+
cat_name = CATEGORIES.get(prefix, f"Category {prefix}")
|
| 258 |
+
html_rows += f"<tr><td colspan='4' class='category-header'>{cat_name}</td></tr>"
|
| 259 |
+
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)
|
| 260 |
+
|
| 261 |
+
for item in items:
|
| 262 |
+
html_rows += f"""
|
| 263 |
+
<tr>
|
| 264 |
+
<td style="font-weight:bold; color: #fff;">{item.get("Code", "?")}</td>
|
| 265 |
+
<td>{item.get("Description", "-")}</td>
|
| 266 |
+
<td style="font-family:serif; font-size:1.1em;">{item.get("Transliteration", "-")}</td>
|
| 267 |
+
<td><span class="type-badge">{item.get("Type", "-")}</span></td>
|
| 268 |
+
</tr>
|
| 269 |
+
"""
|
| 270 |
+
return html_rows
|
| 271 |
+
|
| 272 |
+
GARDINER_TABLE_CONTENT = generate_gardiner_html()
|
| 273 |
+
|
| 274 |
# --- 8. UI STYLING & ASSETS ---
|
| 275 |
|
| 276 |
cursor_url = "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGFmMzciIHN0cm9rZS13aWR0aD0iMS41Ij4KICAgIDxwYXRoIGQ9Ik0xNiw4IEM2LDIwIDI2LDIwIDE2LDggWiIgZmlsbD0icmdiYSgyMTIsIDE3NSwgNTUsIDAuMSkiLz4KICAgIDxjaXJjbGUgY3g9IjE2IiBjeT0iMTUiIHI9IjMiIGZpbGw9IiNkNGFmMzciLz4KICAgIDxwYXRoIGQ9Ik0xNiwyMiBMMTYsMjggTDEwLDI4Ii8+CiAgPC9nPgo8L3N2Zz4=')"
|
|
|
|
| 393 |
|
| 394 |
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>"""
|
| 395 |
|
| 396 |
+
# --- 9. MAIN APP ASSEMBLY ---
|
| 397 |
|
| 398 |
with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
|
| 399 |
gr.HTML(f"<style>{custom_css}</style>")
|
| 400 |
gr.HTML(trail_script)
|
| 401 |
|
| 402 |
+
# --- Hidden API Buttons for MCP (Granular Access) ---
|
| 403 |
+
# These buttons are not visible in the UI but expose the API endpoints for Claude
|
| 404 |
+
|
| 405 |
+
# 1. Detect Only
|
| 406 |
+
btn_mcp_detect = gr.Button(visible=False)
|
| 407 |
+
api_detect_out_img = gr.Image(visible=False)
|
| 408 |
+
api_detect_out_json = gr.JSON(visible=False)
|
| 409 |
+
btn_mcp_detect.click(fn=api_detect_only, inputs=[gr.Image(visible=False), gr.Number(visible=False)], outputs=[api_detect_out_img, api_detect_out_json], api_name="detect_hieroglyphs")
|
| 410 |
+
|
| 411 |
+
# 2. Translate Only
|
| 412 |
+
btn_mcp_trans = gr.Button(visible=False)
|
| 413 |
+
api_trans_out_mystical = gr.Textbox(visible=False)
|
| 414 |
+
api_trans_out_academic = gr.Textbox(visible=False)
|
| 415 |
+
btn_mcp_trans.click(fn=api_translate_only, inputs=[gr.Textbox(visible=False)], outputs=[api_trans_out_mystical, api_trans_out_academic], api_name="translate_hieroglyphs")
|
| 416 |
+
|
| 417 |
+
# 3. Analytics Only
|
| 418 |
+
btn_mcp_analytics = gr.Button(visible=False)
|
| 419 |
+
api_analytics_out = gr.Plot(visible=False)
|
| 420 |
+
btn_mcp_analytics.click(fn=api_get_analytics, inputs=[gr.JSON(visible=False)], outputs=[api_analytics_out], api_name="get_analytics")
|
| 421 |
+
|
| 422 |
+
# 4. Get List
|
| 423 |
+
btn_mcp_list = gr.Button(visible=False)
|
| 424 |
+
api_list_out = gr.JSON(visible=False)
|
| 425 |
+
btn_mcp_list.click(fn=api_get_supported_codes, inputs=[], outputs=[api_list_out], api_name="get_supported_codes")
|
| 426 |
+
|
| 427 |
+
# --- Visible UI ---
|
| 428 |
with gr.Row(elem_classes="header-row"):
|
| 429 |
with gr.Column(scale=4): gr.HTML(header_html)
|
| 430 |
with gr.Column(scale=1): btn_toggle = gr.Button("🌗 Day / Night", elem_classes="toggle-btn")
|