youkii-xr commited on
Commit
5a4c8d4
ยท
verified ยท
1 Parent(s): 56c3591

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -157
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import gradio as gr
2
  from ultralytics import YOLO
3
- from PIL import Image
4
  from google import genai
5
  import os
6
  import json
@@ -9,6 +9,8 @@ import re
9
  from huggingface_hub import hf_hub_download
10
  import tempfile
11
  import numpy as np
 
 
12
  from typing import List, Tuple, Dict, Any
13
 
14
  # --- 1. CONFIGURATION & SECRETS ---
@@ -20,7 +22,6 @@ MODEL_FILENAME = "best.pt"
20
  JSON_DB_PATH = "gardiner_codes.json"
21
 
22
  os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
23
- # Ensure temp directory exists for plot saving
24
  os.makedirs("/tmp/gradio_results", exist_ok=True)
25
 
26
  # --- 2. DATA LOADING ---
@@ -41,7 +42,65 @@ def load_gardiner_database():
41
  gardiner_data = load_gardiner_database()
42
  gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()}
43
 
44
- # --- 3. CORE LOGIC FUNCTIONS (Reusable) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def core_detect(image, conf_threshold):
47
  """Core YOLO detection logic."""
@@ -72,7 +131,7 @@ def core_detect(image, conf_threshold):
72
  }
73
  detections.append(detection)
74
 
75
- # Create crop
76
  crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3]))
77
  crops.append((crop_img, f"{code}\n({int(conf*100)}%)"))
78
 
@@ -166,77 +225,68 @@ except Exception as e:
166
  model = None
167
 
168
  # --- 5. MAIN UI PIPELINE (Orchestrator) ---
169
- # Used *only* by the Web Interface (Returns complex Objects like gr.Plot)
170
 
171
  def process_pipeline(image, conf_threshold):
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
  img_w, img_h = image.size
 
177
  annotated_img, detections, crops = core_detect(image, conf_threshold)
 
 
 
 
 
 
 
 
 
 
178
  unique_codes = list(set([d['code'] for d in detections]))
179
  mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes]
180
  mystical, academic = core_translate(mapped_words)
 
 
181
  analytics_plot = core_analytics(detections, img_w, img_h)
 
 
182
  text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}"
183
  json_output = {"count": len(detections), "detections": detections}
184
  formatted_mystical = f"""<div class="mystical-container"><h3>โœจ THE ANCIENT WHISPER</h3><p>{mystical}</p></div>"""
185
 
186
- return annotated_img, formatted_mystical, academic, analytics_plot, text_report, json_output, crops
187
 
188
  except Exception as e:
189
- return None, f"System Failure: {str(e)}", "", None, str(e), None, []
 
190
 
191
- # --- 6. MCP API FUNCTIONS (Optimized for Claude) ---
192
- # These functions use TYPE HINTS so Claude knows what arguments to provide.
193
- # They return FILE PATHS (strings) for images, not base64 objects.
194
 
195
  def detect_hieroglyphs_api(image: Image.Image, conf: float = 0.25) -> Tuple[str, Dict[str, Any]]:
196
- """
197
- Scans an image for Egyptian hieroglyphs.
198
- Returns a tuple containing:
199
- 1. The file path to the annotated image result.
200
- 2. A JSON summary of detected codes and their bounding boxes.
201
- """
202
  ann_img, dets, _ = core_detect(image, conf)
203
-
204
- # Save to a temp file and return the PATH string
205
- # Using a fixed temp dir ensures the path is accessible
206
  with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".jpg", delete=False) as t:
207
  ann_img.save(t.name)
208
  path = t.name
209
-
210
  return path, {"count": len(dets), "detections": dets}
211
 
212
  def translate_codes_api(keywords_text: str) -> Tuple[str, str]:
213
- """
214
- Takes a comma-separated string of Gardiner codes (e.g., 'G43, X1, N5').
215
- Returns two translations: a mystical interpretation and an academic translation.
216
- """
217
  if isinstance(keywords_text, str):
218
  keywords = [k.strip() for k in keywords_text.split(',')]
219
  else:
220
  keywords = ["Unknown"]
221
  mystical, academic = core_translate(keywords)
222
- # Strip HTML tags for clean text return to Claude
223
  clean_mystical = re.sub('<[^<]+?>', '', mystical)
224
  return clean_mystical, academic
225
 
226
  def get_analytics_chart_api(json_data: Dict[str, Any]) -> str:
227
- """
228
- Generates statistical charts based on detection data.
229
- Input: The JSON output from the 'detect' tool.
230
- Returns: The file path to the generated chart image.
231
- """
232
  dets = json_data.get("detections", [])
233
- # Use fixed dimensions for consistency
234
  fig = core_analytics(dets, 640, 640)
235
-
236
- if fig is None:
237
- return "No data to plot."
238
-
239
- # Save to a temp file and return the PATH string
240
  with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".png", delete=False) as t:
241
  fig.savefig(t.name, format='png', facecolor='#0f0f23')
242
  path = t.name
@@ -244,10 +294,6 @@ def get_analytics_chart_api(json_data: Dict[str, Any]) -> str:
244
  return path
245
 
246
  def list_all_codes_api() -> Dict[str, Any]:
247
- """
248
- Returns the complete database of supported Gardiner codes and their descriptions.
249
- Useful for looking up specific symbol meanings.
250
- """
251
  return gardiner_data
252
 
253
  # --- 7. HTML GENERATORS ---
@@ -267,7 +313,6 @@ CATEGORIES = {
267
  def generate_gardiner_html():
268
  if not gardiner_data:
269
  return "<tr><td colspan='4'>No data loaded. Please upload gardiner_codes.json.</td></tr>"
270
-
271
  html_rows = ""
272
  grouped = {}
273
  for key, data in gardiner_data.items():
@@ -275,23 +320,13 @@ def generate_gardiner_html():
275
  prefix = match.group(1) if match else "Unk"
276
  if prefix not in grouped: grouped[prefix] = []
277
  grouped[prefix].append(data)
278
-
279
  sorted_prefixes = sorted(grouped.keys(), key=lambda x: (len(x), x))
280
-
281
  for prefix in sorted_prefixes:
282
  cat_name = CATEGORIES.get(prefix, f"Category {prefix}")
283
  html_rows += f"<tr><td colspan='4' class='category-header'>{cat_name}</td></tr>"
284
  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)
285
-
286
  for item in items:
287
- html_rows += f"""
288
- <tr>
289
- <td style="font-weight:bold; color: #fff;">{item.get("Code", "?")}</td>
290
- <td>{item.get("Description", "-")}</td>
291
- <td style="font-family:serif; font-size:1.1em;">{item.get("Transliteration", "-")}</td>
292
- <td><span class="type-badge">{item.get("Type", "-")}</span></td>
293
- </tr>
294
- """
295
  return html_rows
296
 
297
  GARDINER_TABLE_CONTENT = generate_gardiner_html()
@@ -303,62 +338,26 @@ cursor_url = "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5
303
  custom_css = f"""
304
  @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap');
305
  @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
306
-
307
- :root, .dark, body {{
308
- --bg-gradient: radial-gradient(circle at 50% 0%, #0a0a2e 0%, #000000 100%);
309
- --card-bg: rgba(15, 15, 35, 0.7);
310
- --text-primary: #e0e7ff;
311
- --text-accent: #d4af37;
312
- --border-color: #d4af37;
313
- --btn-grad: linear-gradient(135deg, #b8860b 0%, #d4af37 100%);
314
- --info-bg: rgba(212, 175, 55, 0.08);
315
- --info-border: #d4af37;
316
- --glow-color: rgba(212, 175, 55, 0.4);
317
- }}
318
-
319
- body.light-mode, .gradio-container.light-mode {{
320
- --bg-gradient: linear-gradient(135deg, #f0e6d2 0%, #e6dcc3 100%) !important;
321
- --card-bg: rgba(255, 255, 255, 0.6) !important;
322
- --text-primary: #3d342b !important;
323
- --text-accent: #8b4513 !important;
324
- --border-color: #8b4513 !important;
325
- --btn-grad: linear-gradient(135deg, #cd853f 0%, #8b4513 100%) !important;
326
- --info-bg: rgba(139, 69, 19, 0.05) !important;
327
- --info-border: #8b4513 !important;
328
- --glow-color: rgba(139, 69, 19, 0.3) !important;
329
- color: var(--text-primary) !important;
330
- }}
331
-
332
- body, .gradio-container {{
333
- background: var(--bg-gradient) !important;
334
- font-family: 'Cairo', sans-serif !important;
335
- color: var(--text-primary) !important;
336
- cursor: {cursor_url} 16 16, auto !important;
337
- transition: background 0.5s ease;
338
- }}
339
-
340
  .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); }}
341
  @keyframes fadeDust {{ 0% {{ opacity: 1; transform: scale(1); }} 100% {{ opacity: 0; transform: scale(0); }} }}
342
-
343
  button, a, .cursor-pointer {{ cursor: {cursor_url} 16 16, pointer !important; }}
344
  .tabs button {{ padding: 5px 10px !important; font-size: 14px !important; min-width: auto !important; }}
345
-
346
  .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); }}
347
  .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); }}
348
-
349
  .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; }}
350
  .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; }}
351
  .step-title {{ color: #d4af37; font-family: 'Space Mono', monospace; font-weight: bold; display: block; margin-bottom: 8px; font-size: 14px; }}
352
  .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; }}
353
  code {{ font-family: 'Space Mono', monospace; background: rgba(0,0,0,0.3); padding: 2px 5px; border-radius: 4px; color: #e0e7ff; }}
354
-
355
  .gardiner-table {{ width: 100%; border-collapse: collapse; font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 10px; border: 1px solid #d4af37; }}
356
  .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); }}
357
  .gardiner-table td {{ padding: 10px; border-bottom: 1px solid rgba(212, 175, 55, 0.2); color: #e0e0e0; }}
358
  .gardiner-table tr:hover {{ background: rgba(212, 175, 55, 0.1); }}
359
  .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; }}
360
  .type-badge {{ border: 1px solid #d4af37; color: #d4af37; padding: 2px 6px; border-radius: 4px; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }}
361
-
362
  .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); }}
363
  .mystical-container:hover {{ transform: translateY(-4px); box-shadow: 0 10px 40px rgba(0,0,0,0.2), 0 0 20px var(--glow-color); }}
364
  .mystical-container h3 {{ color: var(--text-accent); text-align: center; border-bottom: 1px dashed var(--border-color); padding-bottom: 10px; }}
@@ -366,19 +365,8 @@ code {{ font-family: 'Space Mono', monospace; background: rgba(0,0,0,0.3); paddi
366
  .scrollable-box textarea {{ overflow-y: auto !important; max-height: 400px !important; background-color: rgba(0,0,0,0.3) !important; }}
367
  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; }}
368
  .gradio-image, .gradio-json {{ background: transparent !important; border: none !important; }}
369
-
370
- /* FIXED TOGGLE BUTTON */
371
- button.toggle-btn {{
372
- background: #0a0a2e !important;
373
- border: 1px solid var(--border-color) !important;
374
- color: var(--text-accent) !important;
375
- padding: 5px 15px !important;
376
- font-family: 'Space Mono', monospace;
377
- box-shadow: none !important;
378
- }}
379
- button.toggle-btn:hover {{
380
- background: var(--info-bg) !important;
381
- }}
382
  """
383
 
384
  header_html = """
@@ -399,22 +387,26 @@ header_html = """
399
  </div>
400
  """
401
 
 
402
  mission_html = """
403
- <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.</p></div>
 
 
 
 
 
404
  """
405
 
406
  guide_html = """
407
  <div class="card" style="border-color: #d4af37;">
408
  <div class="card-title" style="color: #d4af37;">๐Ÿค– CLAUDE DESKTOP SETUP GUIDE</div>
409
- <div class="guide-step"><span class="step-title">STEP 0: PREREQUISITE</span><p>Ensure you have <b>Node.js</b> installed (Required for the `npx` command used by MCP).</p><p><a href="https://nodejs.org/" target="_blank" style="color: #d4af37; text-decoration: underline;">Download Node.js Official Website</a></p></div>
410
- <div class="guide-step"><span class="step-title">STEP 1: PREPARE WORKSPACE</span><p>Claude is sandboxed. It cannot see your Desktop. You must create a bridge.</p>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>
411
- <div class="guide-step"><span class="step-title">STEP 2: VERIFY PYTHON</span><p>The code below assumes Python is at: <code>C:\\Python313\\python.exe</code></p><p><b>Check your path:</b> Open CMD and type <code>where python</code>.</p><p><i>Note: If your path is different, replace the path in the JSON code block below before copying.</i></p></div>
412
- <div class="guide-step"><span class="step-title">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>
413
- <div class="guide-step"><span class="step-title">STEP 4: USAGE</span><p>Prompt Claude: <i>"Analyze the image at C:\\Claude_Work\\my_tablet.jpg"</i></p></div>
414
  </div>
415
  """
416
 
417
- # FIX 1: Reformatted JSON string with multiline triple-quotes so it displays expanded.
418
  claude_json_content = """{
419
  "mcpServers": {
420
  "gradio": {
@@ -447,47 +439,19 @@ with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
447
  gr.HTML(f"<style>{custom_css}</style>")
448
  gr.HTML(trail_script)
449
 
450
- # --- MCP TOOL REGISTRATION LAYER (Hidden from UI) ---
451
- # This is the correct, standard way to register tools in Gradio for MCP.
452
- # We use hidden buttons connected to our specific API functions.
453
- # The 'api_name' argument defines the short name you see in Claude.
454
-
455
  with gr.Row(visible=False):
456
- # 1. Detect Tool (Short name: "detect")
457
  btn_detect = gr.Button("Detect")
458
- btn_detect.click(
459
- fn=detect_hieroglyphs_api,
460
- inputs=[gr.Image(label="img"), gr.Number(label="conf")],
461
- outputs=[gr.Textbox(label="path"), gr.JSON(label="json")],
462
- api_name="detect" # << SHORT NAME HERE
463
- )
464
-
465
- # 2. Translate Tool (Short name: "translate")
466
  btn_trans = gr.Button("Translate")
467
- btn_trans.click(
468
- fn=translate_codes_api,
469
- inputs=[gr.Textbox(label="text")],
470
- outputs=[gr.Textbox(label="mystic"), gr.Textbox(label="academic")],
471
- api_name="translate" # << SHORT NAME HERE
472
- )
473
-
474
- # 3. Analytics Tool (Short name: "analytics")
475
  btn_anal = gr.Button("Analytics")
476
- btn_anal.click(
477
- fn=get_analytics_chart_api,
478
- inputs=[gr.JSON(label="data")],
479
- outputs=[gr.Textbox(label="chart_path")],
480
- api_name="analytics" # << SHORT NAME HERE
481
- )
482
-
483
- # 4. List Codes Tool (Short name: "list_codes")
484
  btn_list = gr.Button("List")
485
- btn_list.click(
486
- fn=list_all_codes_api,
487
- inputs=[],
488
- outputs=[gr.JSON(label="data")],
489
- api_name="list_codes" # << SHORT NAME HERE
490
- )
491
 
492
  # --- Visible UI ---
493
  with gr.Row(elem_classes="header-row"):
@@ -518,6 +482,9 @@ with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
518
  with gr.TabItem("๐Ÿ›๏ธ Academic"): out_academic = gr.Textbox(label="Scientific Translation", lines=15, show_label=False, elem_classes="scrollable-box")
519
  with gr.TabItem("๐Ÿ–ผ๏ธ Visuals"):
520
  out_image = gr.Image(label="Annotated Result", interactive=False)
 
 
 
521
  out_gallery = gr.Gallery(label="Extracted Glyphs", columns=4, height="auto")
522
  with gr.TabItem("๐Ÿ“Š Analytics"): out_plot = gr.Plot(label="Analysis Charts")
523
  with gr.TabItem("๐Ÿ› ๏ธ Logs"):
@@ -531,7 +498,6 @@ with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
531
  # TAB 3: SETUP
532
  with gr.TabItem("๐Ÿค– SYSTEM SETUP"):
533
  gr.HTML(guide_html)
534
- # JSON box will now be expanded due to multiline string above
535
  gr.Code(value=claude_json_content, language="json", label="claude_desktop_config.json", interactive=False, lines=30)
536
 
537
  # TAB 4: GARDINER CODES
@@ -542,14 +508,12 @@ with gr.Blocks(title="Rosetta Decoder Ultimate") as demo:
542
 
543
  # Events
544
  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'); }")
545
- outputs = [out_image, out_mystical, out_academic, out_plot, out_report, out_json, out_gallery]
 
 
 
546
  btn_upload.click(fn=process_pipeline, inputs=[img_upload, slider_conf], outputs=outputs)
547
  btn_cam.click(fn=process_pipeline, inputs=[img_cam, slider_conf_cam], outputs=outputs)
548
 
549
  if __name__ == "__main__":
550
- # Standard launch. Tools are registered via the hidden buttons above.
551
- demo.launch(
552
- mcp_server=True,
553
- ssr_mode=False,
554
- allowed_paths=["/tmp", "/tmp/gradio_results", "."]
555
- )
 
1
  import gradio as gr
2
  from ultralytics import YOLO
3
+ from PIL import Image, ImageDraw, ImageFont
4
  from google import genai
5
  import os
6
  import json
 
9
  from huggingface_hub import hf_hub_download
10
  import tempfile
11
  import numpy as np
12
+ import shutil
13
+ import zipfile
14
  from typing import List, Tuple, Dict, Any
15
 
16
  # --- 1. CONFIGURATION & SECRETS ---
 
22
  JSON_DB_PATH = "gardiner_codes.json"
23
 
24
  os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
 
25
  os.makedirs("/tmp/gradio_results", exist_ok=True)
26
 
27
  # --- 2. DATA LOADING ---
 
42
  gardiner_data = load_gardiner_database()
43
  gardiner_map = {k: v.get("Description", k) for k, v in gardiner_data.items()}
44
 
45
+ # --- 3. CORE LOGIC FUNCTIONS ---
46
+
47
+ def create_labeled_zip(image, detections):
48
+ """
49
+ Crops glyphs, draws label (Code + Conf) on bottom right, and zips them.
50
+ """
51
+ if not detections: return None
52
+
53
+ zip_dir = tempfile.mkdtemp()
54
+ zip_path = os.path.join(tempfile.gettempdir(), "rosetta_glyphs.zip")
55
+
56
+ try:
57
+ # Load a font (try-except block for system compatibility)
58
+ try:
59
+ # Try loading a standard font, fallback to default if fails
60
+ font = ImageFont.truetype("arial.ttf", 16)
61
+ except IOError:
62
+ font = ImageFont.load_default()
63
+
64
+ for i, d in enumerate(detections):
65
+ # Crop
66
+ box = d['box']
67
+ crop = image.crop((box[0], box[1], box[2], box[3]))
68
+
69
+ # Prepare Label
70
+ label_text = f"{d['code']} {int(d['confidence']*100)}%"
71
+ draw = ImageDraw.Draw(crop)
72
+
73
+ # Calculate text size using textbbox (newer PIL versions)
74
+ left, top, right, bottom = draw.textbbox((0, 0), label_text, font=font)
75
+ text_w = right - left
76
+ text_h = bottom - top
77
+
78
+ img_w, img_h = crop.size
79
+
80
+ # Draw background rectangle (bottom right)
81
+ # Check if image is too small for label, if so, skip drawing to avoid crash
82
+ if img_w > text_w and img_h > text_h:
83
+ rect_x0 = img_w - text_w - 4
84
+ rect_y0 = img_h - text_h - 4
85
+ rect_x1 = img_w
86
+ rect_y1 = img_h
87
+
88
+ draw.rectangle([rect_x0, rect_y0, rect_x1, rect_y1], fill="black")
89
+ draw.text((rect_x0 + 2, rect_y0), label_text, fill="white", font=font)
90
+
91
+ # Save crop
92
+ filename = f"{d['code']}_{i}.png"
93
+ crop.save(os.path.join(zip_dir, filename))
94
+
95
+ # Create Zip
96
+ shutil.make_archive(zip_path.replace('.zip', ''), 'zip', zip_dir)
97
+ return zip_path
98
+
99
+ except Exception as e:
100
+ print(f"Zip creation error: {e}")
101
+ return None
102
+ finally:
103
+ shutil.rmtree(zip_dir, ignore_errors=True)
104
 
105
  def core_detect(image, conf_threshold):
106
  """Core YOLO detection logic."""
 
131
  }
132
  detections.append(detection)
133
 
134
+ # Create crop (Clean crop for Gallery display)
135
  crop_img = image.crop((xyxy[0], xyxy[1], xyxy[2], xyxy[3]))
136
  crops.append((crop_img, f"{code}\n({int(conf*100)}%)"))
137
 
 
225
  model = None
226
 
227
  # --- 5. MAIN UI PIPELINE (Orchestrator) ---
 
228
 
229
  def process_pipeline(image, conf_threshold):
230
+ """
231
+ Main function used by the Web UI.
232
+ """
233
+ if image is None: return None, None, None, "", "", None, "", "", "", []
234
+ if model is None: return None, None, None, "Error: Model not loaded.", "", None, "", "", "", []
235
 
236
  try:
237
  img_w, img_h = image.size
238
+ # 1. Detect
239
  annotated_img, detections, crops = core_detect(image, conf_threshold)
240
+
241
+ # 2. Prepare Downloads
242
+ # A. Annotated Image
243
+ ann_path = os.path.join(tempfile.gettempdir(), "annotated_hieroglyphs.jpg")
244
+ annotated_img.save(ann_path)
245
+
246
+ # B. Zip File with Labels
247
+ zip_path = create_labeled_zip(image, detections)
248
+
249
+ # 3. Extract Keywords & Translate
250
  unique_codes = list(set([d['code'] for d in detections]))
251
  mapped_words = [gardiner_map.get(code, f"[{code}]") for code in unique_codes]
252
  mystical, academic = core_translate(mapped_words)
253
+
254
+ # 4. Analytics
255
  analytics_plot = core_analytics(detections, img_w, img_h)
256
+
257
+ # 5. Reports
258
  text_report = f"Total Symbols: {len(detections)}\nUnique Codes: {', '.join(unique_codes)}"
259
  json_output = {"count": len(detections), "detections": detections}
260
  formatted_mystical = f"""<div class="mystical-container"><h3>โœจ THE ANCIENT WHISPER</h3><p>{mystical}</p></div>"""
261
 
262
+ return annotated_img, ann_path, zip_path, formatted_mystical, academic, analytics_plot, text_report, json_output, crops
263
 
264
  except Exception as e:
265
+ print(f"Pipeline Error: {e}")
266
+ return None, None, None, f"System Failure: {str(e)}", "", None, str(e), None, []
267
 
268
+ # --- 6. MCP API FUNCTIONS ---
 
 
269
 
270
  def detect_hieroglyphs_api(image: Image.Image, conf: float = 0.25) -> Tuple[str, Dict[str, Any]]:
 
 
 
 
 
 
271
  ann_img, dets, _ = core_detect(image, conf)
 
 
 
272
  with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".jpg", delete=False) as t:
273
  ann_img.save(t.name)
274
  path = t.name
 
275
  return path, {"count": len(dets), "detections": dets}
276
 
277
  def translate_codes_api(keywords_text: str) -> Tuple[str, str]:
 
 
 
 
278
  if isinstance(keywords_text, str):
279
  keywords = [k.strip() for k in keywords_text.split(',')]
280
  else:
281
  keywords = ["Unknown"]
282
  mystical, academic = core_translate(keywords)
 
283
  clean_mystical = re.sub('<[^<]+?>', '', mystical)
284
  return clean_mystical, academic
285
 
286
  def get_analytics_chart_api(json_data: Dict[str, Any]) -> str:
 
 
 
 
 
287
  dets = json_data.get("detections", [])
 
288
  fig = core_analytics(dets, 640, 640)
289
+ if fig is None: return "No data."
 
 
 
 
290
  with tempfile.NamedTemporaryFile(dir="/tmp/gradio_results", suffix=".png", delete=False) as t:
291
  fig.savefig(t.name, format='png', facecolor='#0f0f23')
292
  path = t.name
 
294
  return path
295
 
296
  def list_all_codes_api() -> Dict[str, Any]:
 
 
 
 
297
  return gardiner_data
298
 
299
  # --- 7. HTML GENERATORS ---
 
313
  def generate_gardiner_html():
314
  if not gardiner_data:
315
  return "<tr><td colspan='4'>No data loaded. Please upload gardiner_codes.json.</td></tr>"
 
316
  html_rows = ""
317
  grouped = {}
318
  for key, data in gardiner_data.items():
 
320
  prefix = match.group(1) if match else "Unk"
321
  if prefix not in grouped: grouped[prefix] = []
322
  grouped[prefix].append(data)
 
323
  sorted_prefixes = sorted(grouped.keys(), key=lambda x: (len(x), x))
 
324
  for prefix in sorted_prefixes:
325
  cat_name = CATEGORIES.get(prefix, f"Category {prefix}")
326
  html_rows += f"<tr><td colspan='4' class='category-header'>{cat_name}</td></tr>"
327
  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)
 
328
  for item in items:
329
+ 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>"""
 
 
 
 
 
 
 
330
  return html_rows
331
 
332
  GARDINER_TABLE_CONTENT = generate_gardiner_html()
 
338
  custom_css = f"""
339
  @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap');
340
  @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
341
+ :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); }}
342
+ 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; }}
343
+ 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; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  .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); }}
345
  @keyframes fadeDust {{ 0% {{ opacity: 1; transform: scale(1); }} 100% {{ opacity: 0; transform: scale(0); }} }}
 
346
  button, a, .cursor-pointer {{ cursor: {cursor_url} 16 16, pointer !important; }}
347
  .tabs button {{ padding: 5px 10px !important; font-size: 14px !important; min-width: auto !important; }}
 
348
  .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); }}
349
  .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); }}
 
350
  .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; }}
351
  .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; }}
352
  .step-title {{ color: #d4af37; font-family: 'Space Mono', monospace; font-weight: bold; display: block; margin-bottom: 8px; font-size: 14px; }}
353
  .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; }}
354
  code {{ font-family: 'Space Mono', monospace; background: rgba(0,0,0,0.3); padding: 2px 5px; border-radius: 4px; color: #e0e7ff; }}
 
355
  .gardiner-table {{ width: 100%; border-collapse: collapse; font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 10px; border: 1px solid #d4af37; }}
356
  .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); }}
357
  .gardiner-table td {{ padding: 10px; border-bottom: 1px solid rgba(212, 175, 55, 0.2); color: #e0e0e0; }}
358
  .gardiner-table tr:hover {{ background: rgba(212, 175, 55, 0.1); }}
359
  .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; }}
360
  .type-badge {{ border: 1px solid #d4af37; color: #d4af37; padding: 2px 6px; border-radius: 4px; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }}
 
361
  .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); }}
362
  .mystical-container:hover {{ transform: translateY(-4px); box-shadow: 0 10px 40px rgba(0,0,0,0.2), 0 0 20px var(--glow-color); }}
363
  .mystical-container h3 {{ color: var(--text-accent); text-align: center; border-bottom: 1px dashed var(--border-color); padding-bottom: 10px; }}
 
365
  .scrollable-box textarea {{ overflow-y: auto !important; max-height: 400px !important; background-color: rgba(0,0,0,0.3) !important; }}
366
  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; }}
367
  .gradio-image, .gradio-json {{ background: transparent !important; border: none !important; }}
368
+ 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; }}
369
+ button.toggle-btn:hover {{ background: var(--info-bg) !important; }}
 
 
 
 
 
 
 
 
 
 
 
370
  """
371
 
372
  header_html = """
 
387
  </div>
388
  """
389
 
390
+ # UPDATED VISION STATEMENT
391
  mission_html = """
392
+ <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);">
393
+ <b>Echoes of Humanity, Decoded.</b><br>
394
+ 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.
395
+ 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
396
+ to the hopes, prayers, and daily lives of those who walked before us. We are decoding history to understand our shared humanity.
397
+ </p></div>
398
  """
399
 
400
  guide_html = """
401
  <div class="card" style="border-color: #d4af37;">
402
  <div class="card-title" style="color: #d4af37;">๐Ÿค– CLAUDE DESKTOP SETUP GUIDE</div>
403
+ <div class="guide-step"><span class="step-title">STEP 0: PREREQUISITE</span><p>Ensure you have <b>Node.js</b> installed.</p></div>
404
+ <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>
405
+ <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>
 
 
406
  </div>
407
  """
408
 
409
+ # URL UPDATED TO: youkii-xr/hieroglyph-mcp-server
410
  claude_json_content = """{
411
  "mcpServers": {
412
  "gradio": {
 
439
  gr.HTML(f"<style>{custom_css}</style>")
440
  gr.HTML(trail_script)
441
 
442
+ # --- MCP TOOL REGISTRATION LAYER (Hidden) ---
 
 
 
 
443
  with gr.Row(visible=False):
 
444
  btn_detect = gr.Button("Detect")
445
+ 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")
446
+
 
 
 
 
 
 
447
  btn_trans = gr.Button("Translate")
448
+ btn_trans.click(fn=translate_codes_api, inputs=[gr.Textbox(label="text")], outputs=[gr.Textbox(label="mystic"), gr.Textbox(label="academic")], api_name="translate")
449
+
 
 
 
 
 
 
450
  btn_anal = gr.Button("Analytics")
451
+ btn_anal.click(fn=get_analytics_chart_api, inputs=[gr.JSON(label="data")], outputs=[gr.Textbox(label="chart_path")], api_name="analytics")
452
+
 
 
 
 
 
 
453
  btn_list = gr.Button("List")
454
+ btn_list.click(fn=list_all_codes_api, inputs=[], outputs=[gr.JSON(label="data")], api_name="list_codes")
 
 
 
 
 
455
 
456
  # --- Visible UI ---
457
  with gr.Row(elem_classes="header-row"):
 
482
  with gr.TabItem("๐Ÿ›๏ธ Academic"): out_academic = gr.Textbox(label="Scientific Translation", lines=15, show_label=False, elem_classes="scrollable-box")
483
  with gr.TabItem("๐Ÿ–ผ๏ธ Visuals"):
484
  out_image = gr.Image(label="Annotated Result", interactive=False)
485
+ with gr.Row():
486
+ btn_download_img = gr.DownloadButton("๐Ÿ’พ Download Annotated Image")
487
+ btn_download_zip = gr.DownloadButton("๐Ÿ“ฆ Download Glyphs (ZIP)")
488
  out_gallery = gr.Gallery(label="Extracted Glyphs", columns=4, height="auto")
489
  with gr.TabItem("๐Ÿ“Š Analytics"): out_plot = gr.Plot(label="Analysis Charts")
490
  with gr.TabItem("๐Ÿ› ๏ธ Logs"):
 
498
  # TAB 3: SETUP
499
  with gr.TabItem("๐Ÿค– SYSTEM SETUP"):
500
  gr.HTML(guide_html)
 
501
  gr.Code(value=claude_json_content, language="json", label="claude_desktop_config.json", interactive=False, lines=30)
502
 
503
  # TAB 4: GARDINER CODES
 
508
 
509
  # Events
510
  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'); }")
511
+
512
+ # OUTPUTS: Image, ImgPath, ZipPath, Mystical, Academic, Plot, TextReport, JSON, Crops
513
+ outputs = [out_image, btn_download_img, btn_download_zip, out_mystical, out_academic, out_plot, out_report, out_json, out_gallery]
514
+
515
  btn_upload.click(fn=process_pipeline, inputs=[img_upload, slider_conf], outputs=outputs)
516
  btn_cam.click(fn=process_pipeline, inputs=[img_cam, slider_conf_cam], outputs=outputs)
517
 
518
  if __name__ == "__main__":
519
+ demo.launch(mcp_server=True, ssr_mode=False, allowed_paths=["/tmp", "/tmp/gradio_results", "."])