youkii-xr commited on
Commit
9030808
·
verified ·
1 Parent(s): 6383950

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -164
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. HTML GENERATORS ---
41
 
42
- CATEGORIES = {
43
- 'A': "Men & Monarchs", 'B': "Women & Human Activities", 'C': "Deities",
44
- 'D': "Parts of Human Body", 'E': "Mammals", 'F': "Parts of Mammals",
45
- 'G': "Birds", 'H': "Parts of Birds", 'I': "Reptiles & Amphibians",
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
- sorted_prefixes = sorted(grouped.keys(), key=lambda x: (len(x), x))
 
 
67
 
68
- for prefix in sorted_prefixes:
69
- cat_name = CATEGORIES.get(prefix, f"Category {prefix}")
70
- html_rows += f"<tr><td colspan='4' class='category-header'>{cat_name}</td></tr>"
71
- 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- for item in items:
74
- html_rows += f"""
75
- <tr>
76
- <td style="font-weight:bold; color: #fff;">{item.get("Code", "?")}</td>
77
- <td>{item.get("Description", "-")}</td>
78
- <td style="font-family:serif; font-size:1.1em;">{item.get("Transliteration", "-")}</td>
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 generate_analytics_plots(detections, img_width, img_height):
 
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, img_width], [0, img_height]], cmap='inferno')
138
  ax3.set_title('Glyph Spatial Heatmap', color='white', fontsize=12, pad=10)
139
- ax3.set_xlim(0, img_width)
140
- ax3.set_ylim(img_height, 0)
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
- # --- 6. MCP EXPOSED FUNCTIONS ---
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
- @gr.tool
199
- def get_supported_codes_mcp():
200
- """Returns list of supported codes."""
201
- return list(gardiner_data.keys())
 
 
 
 
 
 
 
 
202
 
203
- # --- 7. UI PROCESSING PIPELINE ---
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
- results = model.predict(source=image, conf=conf_threshold, iou=0.45, imgsz=640, verbose=False, device='cpu', max_det=300)
211
- annotated_array = results[0].plot()
212
- annotated_image = Image.fromarray(annotated_array[..., ::-1])
213
  img_w, img_h = image.size
 
214
 
215
- detections = []
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
- # Translation
234
- if not GOOGLE_API_KEY:
235
- mystical, academic = "API Key Missing", "API Key Missing"
236
- else:
237
- keywords_str = ", ".join(mapped_words)
238
- prompt = f"""
239
- You are an expert Egyptologist AI. I have detected these symbols: [{keywords_str}].
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 annotated_image, formatted_mystical, academic, analytics_plot, text_report, json_output, crops
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
- # --- 8. MAIN APP ASSEMBLY ---
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")