cstr commited on
Commit
a327ae0
Β·
verified Β·
1 Parent(s): c639378

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -61
app.py CHANGED
@@ -2,12 +2,12 @@ import gradio as gr
2
  import sqlite3
3
  import pandas as pd
4
  from huggingface_hub import hf_hub_download, HfApi
5
- from fastapi import FastAPI, HTTPException
6
  from fastapi.responses import JSONResponse
7
  import os
8
  import time
9
  import json
10
- from typing import Optional, List, Dict
11
 
12
  # ===== CONFIGURATION =====
13
  TARGET_LANGUAGES = ['de', 'en', 'es', 'fr', 'it', 'ja', 'nl', 'pl', 'pt', 'ru', 'zh']
@@ -57,7 +57,6 @@ def get_db_connection():
57
  conn.execute("PRAGMA mmap_size = 4294967296")
58
  return conn
59
 
60
- # Define relations with full URLs
61
  RELATIONS = [
62
  ("IsA", f"{CONCEPTNET_BASE}/r/IsA"),
63
  ("PartOf", f"{CONCEPTNET_BASE}/r/PartOf"),
@@ -73,8 +72,8 @@ RELATIONS = [
73
  ("DerivedFrom", f"{CONCEPTNET_BASE}/r/DerivedFrom"),
74
  ]
75
 
76
- def get_semantic_profile_json(word: str, lang: str = 'en', max_per_relation: int = 10) -> Dict:
77
- """Get semantic profile as JSON - for API"""
78
  if not word or lang not in TARGET_LANGUAGES:
79
  return {"error": "Invalid input"}
80
 
@@ -93,19 +92,16 @@ def get_semantic_profile_json(word: str, lang: str = 'en', max_per_relation: int
93
  with get_db_connection() as conn:
94
  cursor = conn.cursor()
95
 
96
- # Find nodes
97
  cursor.execute("SELECT id, label FROM node WHERE id LIKE ? LIMIT 5", (like_path,))
98
  result["nodes"] = [{"id": nid, "label": label} for nid, label in cursor.fetchall()]
99
 
100
  if not result["nodes"]:
101
  return {"error": "Word not found"}
102
 
103
- # Get relations
104
  for rel_name, rel_url in RELATIONS:
105
  outgoing = []
106
  incoming = []
107
 
108
- # Outgoing
109
  cursor.execute("""
110
  SELECT en.label, e.weight, en.id
111
  FROM edge e
@@ -118,7 +114,6 @@ def get_semantic_profile_json(word: str, lang: str = 'en', max_per_relation: int
118
  outgoing = [{"target": label, "weight": weight, "target_id": eid}
119
  for label, weight, eid in cursor.fetchall()]
120
 
121
- # Incoming
122
  cursor.execute("""
123
  SELECT s.label, e.weight, s.id
124
  FROM edge e
@@ -145,14 +140,13 @@ def get_semantic_profile_json(word: str, lang: str = 'en', max_per_relation: int
145
  return {"error": str(e)}
146
 
147
  def get_semantic_profile(word, lang='en', progress=gr.Progress()):
148
- """Get semantic profile - for Gradio UI with better progress"""
149
  log_progress(f"Profile: {word} ({lang})", "INFO")
150
 
151
  if not word or lang not in TARGET_LANGUAGES:
152
  return "⚠️ Invalid input"
153
 
154
- # Update progress immediately
155
- progress(0, desc="πŸ” Starting search...")
156
 
157
  word = word.strip().lower().replace(' ', '_')
158
  like_path = f"{CONCEPTNET_BASE}/c/{lang}/{word}%"
@@ -165,7 +159,6 @@ def get_semantic_profile(word, lang='en', progress=gr.Progress()):
165
 
166
  progress(0.05, desc="πŸ“ Finding nodes...")
167
 
168
- # Find nodes
169
  cursor.execute("SELECT id, label FROM node WHERE id LIKE ? LIMIT 5", (like_path,))
170
  nodes = cursor.fetchall()
171
 
@@ -182,13 +175,11 @@ def get_semantic_profile(word, lang='en', progress=gr.Progress()):
182
  num_relations = len(RELATIONS)
183
 
184
  for i, (rel_name, rel_url) in enumerate(RELATIONS):
185
- # Update progress with relation name
186
  progress((i + 0.1) / num_relations, desc=f"πŸ”Ž {rel_name}...")
187
 
188
  output_md += f"## {rel_name}\n\n"
189
  found = False
190
 
191
- # Outgoing
192
  start_time = time.time()
193
  cursor.execute("""
194
  SELECT en.label, e.weight
@@ -209,7 +200,6 @@ def get_semantic_profile(word, lang='en', progress=gr.Progress()):
209
  found = True
210
  total += 1
211
 
212
- # Incoming
213
  cursor.execute("""
214
  SELECT s.label, e.weight
215
  FROM edge e
@@ -230,15 +220,12 @@ def get_semantic_profile(word, lang='en', progress=gr.Progress()):
230
  output_md += "*No results*\n"
231
 
232
  output_md += "\n"
233
-
234
- # Update progress after each relation
235
- progress((i + 1) / num_relations, desc=f"βœ“ {rel_name} done")
236
 
237
  progress(1.0, desc="βœ… Complete!")
238
 
239
  output_md += f"---\n**Total relations:** {total}\n"
240
-
241
- log_progress(f"Profile complete: {total} relations", "SUCCESS")
242
 
243
  return output_md
244
 
@@ -251,8 +238,8 @@ def get_semantic_profile(word, lang='en', progress=gr.Progress()):
251
  def query_edges_json(start_node: Optional[str] = None,
252
  relation: Optional[str] = None,
253
  end_node: Optional[str] = None,
254
- limit: int = 50) -> Dict:
255
- """Query edges - JSON for API"""
256
  query = """
257
  SELECT
258
  e.id, s.id, r.label, en.id, e.weight, s.label, en.label
@@ -267,7 +254,6 @@ def query_edges_json(start_node: Optional[str] = None,
267
 
268
  try:
269
  with get_db_connection() as conn:
270
- # Start node
271
  if start_node:
272
  if start_node.startswith('http://'):
273
  pattern = f"{start_node}%"
@@ -276,7 +262,6 @@ def query_edges_json(start_node: Optional[str] = None,
276
  query += " AND s.id LIKE ?"
277
  params.append(pattern)
278
 
279
- # Relation
280
  if relation:
281
  if relation.startswith('http://'):
282
  rel_value = relation
@@ -287,7 +272,6 @@ def query_edges_json(start_node: Optional[str] = None,
287
  query += " AND r.id = ?"
288
  params.append(rel_value)
289
 
290
- # End node
291
  if end_node:
292
  if end_node.startswith('http://'):
293
  pattern = f"{end_node}%"
@@ -315,10 +299,10 @@ def query_edges_json(start_node: Optional[str] = None,
315
  return {"error": str(e)}
316
 
317
  def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
318
- """Query builder - Gradio UI with progress"""
319
  log_progress(f"Query: start={start_node}, rel={relation}, end={end_node}", "INFO")
320
 
321
- progress(0, desc="πŸ” Building query...")
322
 
323
  query = """
324
  SELECT
@@ -334,9 +318,8 @@ def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
334
 
335
  try:
336
  with get_db_connection() as conn:
337
- progress(0.3, desc="πŸ“ Adding filters...")
338
 
339
- # Start node
340
  if start_node and start_node.strip():
341
  if start_node.startswith('http://'):
342
  pattern = f"{start_node}%"
@@ -345,7 +328,6 @@ def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
345
  query += " AND s.id LIKE ?"
346
  params.append(pattern)
347
 
348
- # Relation
349
  if relation and relation.strip():
350
  if relation.startswith('http://'):
351
  rel_value = relation
@@ -356,7 +338,6 @@ def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
356
  query += " AND r.id = ?"
357
  params.append(rel_value)
358
 
359
- # End node
360
  if end_node and end_node.strip():
361
  if end_node.startswith('http://'):
362
  pattern = f"{end_node}%"
@@ -368,7 +349,7 @@ def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
368
  query += " ORDER BY e.weight DESC LIMIT ?"
369
  params.append(limit)
370
 
371
- progress(0.6, desc="⚑ Executing...")
372
 
373
  start_time = time.time()
374
  df = pd.read_sql_query(query, conn, params=params)
@@ -376,7 +357,7 @@ def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
376
 
377
  progress(1.0, desc="βœ… Done!")
378
 
379
- log_progress(f"Query done: {len(df)} rows in {elapsed:.2f}s", "SUCCESS")
380
 
381
  if df.empty:
382
  return pd.DataFrame(), f"⚠️ No results ({elapsed:.2f}s)"
@@ -405,16 +386,12 @@ def run_raw_query(sql_query):
405
  def get_schema_info():
406
  md = f"# πŸ“š Schema\n\n"
407
  md += f"**Repo:** [{INDEXED_REPO_ID}](https://huggingface.co/datasets/{INDEXED_REPO_ID})\n\n"
408
- md += "## Key Facts\n\n"
409
- md += f"- **Base URL:** `{CONCEPTNET_BASE}`\n"
410
- md += "- **Relations:** Use full URLs (e.g., `http://conceptnet.io/r/IsA`)\n"
411
- md += "- **Nodes:** Use full URLs (e.g., `http://conceptnet.io/c/en/dog`)\n\n"
412
 
413
  try:
414
  with get_db_connection() as conn:
415
  cursor = conn.cursor()
416
 
417
- md += "## Common Relations\n\n"
418
  cursor.execute("SELECT id, label FROM relation ORDER BY label LIMIT 20")
419
  for rel_id, label in cursor.fetchall():
420
  md += f"- **{label}:** `{rel_id}`\n"
@@ -430,12 +407,12 @@ def get_schema_info():
430
 
431
  return md
432
 
433
- # ===== FASTAPI FOR JSON API =====
434
  app = FastAPI(title="ConceptNet API", version="1.0")
435
 
436
  @app.get("/api/profile/{word}")
437
  def api_profile(word: str, lang: str = "en", limit: int = 10):
438
- """Get semantic profile as JSON"""
439
  return JSONResponse(get_semantic_profile_json(word, lang, limit))
440
 
441
  @app.get("/api/query")
@@ -443,30 +420,30 @@ def api_query(start: Optional[str] = None,
443
  relation: Optional[str] = None,
444
  end: Optional[str] = None,
445
  limit: int = 50):
446
- """Query edges as JSON"""
447
  return JSONResponse(query_edges_json(start, relation, end, limit))
448
 
449
  @app.get("/api/relations")
450
  def api_relations():
451
- """Get list of available relations"""
452
  return JSONResponse({"relations": [{"name": name, "url": url} for name, url in RELATIONS]})
453
 
454
  @app.get("/api/languages")
455
  def api_languages():
456
- """Get supported languages"""
457
  return JSONResponse({"languages": TARGET_LANGUAGES})
458
 
459
  @app.get("/")
460
  def api_root():
461
- """API documentation"""
462
  return {
463
  "name": "ConceptNet API",
464
  "version": "1.0",
465
  "endpoints": {
466
- "/api/profile/{word}": "Get semantic profile (params: lang, limit)",
467
  "/api/query": "Query edges (params: start, relation, end, limit)",
468
- "/api/relations": "List available relations",
469
- "/api/languages": "List supported languages"
470
  },
471
  "examples": {
472
  "profile": "/api/profile/dog?lang=en&limit=10",
@@ -480,7 +457,7 @@ with gr.Blocks(title="ConceptNet Explorer", theme=gr.themes.Soft()) as demo:
480
  gr.Markdown(
481
  f"**Multi-language semantic network** | "
482
  f"**Languages:** {', '.join([l.upper() for l in TARGET_LANGUAGES])} | "
483
- f"**API:** `/api/profile/{{word}}` `/api/query`"
484
  )
485
 
486
  with gr.Tabs():
@@ -526,18 +503,27 @@ with gr.Blocks(title="ConceptNet Explorer", theme=gr.themes.Soft()) as demo:
526
  schema_output = gr.Markdown()
527
 
528
  with gr.TabItem("πŸ”Œ API"):
529
- gr.Markdown("## JSON API Endpoints\n\n")
530
- gr.Markdown("### Get Semantic Profile\n```\nGET /api/profile/{word}?lang=en&limit=10\n```")
531
- gr.Markdown("### Query Edges\n```\nGET /api/query?start=dog&relation=IsA&limit=50\n```")
532
- gr.Markdown("### List Relations\n```\nGET /api/relations\n```")
533
- gr.Markdown("### Examples\n")
534
- gr.Code("curl http://localhost:7860/api/profile/dog?lang=en", language="bash")
535
- gr.Code("curl 'http://localhost:7860/api/query?start=dog&relation=IsA&limit=10'", language="bash")
 
 
 
 
 
 
 
 
 
536
 
537
  gr.Markdown(
538
  "---\n"
539
- "**Performance:** Using exact match on rel_id for fast queries | "
540
- "**API:** RESTful JSON endpoints available"
541
  )
542
 
543
  semantic_btn.click(get_semantic_profile, [word_input, lang_input], semantic_output)
@@ -545,14 +531,14 @@ with gr.Blocks(title="ConceptNet Explorer", theme=gr.themes.Soft()) as demo:
545
  raw_btn.click(run_raw_query, raw_sql_input, [raw_results, raw_status])
546
  schema_btn.click(get_schema_info, None, schema_output)
547
 
548
- # Mount Gradio app to FastAPI
549
  app = gr.mount_gradio_app(app, demo, path="/")
550
 
551
  if __name__ == "__main__":
552
  log_progress("="*60, "SUCCESS")
553
- log_progress("APP READY WITH JSON API!", "SUCCESS")
554
  log_progress("="*60, "SUCCESS")
555
- log_progress("UI: http://localhost:7860", "INFO")
556
  log_progress("API: http://localhost:7860/api/profile/dog", "INFO")
557
  log_progress("="*60, "SUCCESS")
558
 
 
2
  import sqlite3
3
  import pandas as pd
4
  from huggingface_hub import hf_hub_download, HfApi
5
+ from fastapi import FastAPI
6
  from fastapi.responses import JSONResponse
7
  import os
8
  import time
9
  import json
10
+ from typing import Optional
11
 
12
  # ===== CONFIGURATION =====
13
  TARGET_LANGUAGES = ['de', 'en', 'es', 'fr', 'it', 'ja', 'nl', 'pl', 'pt', 'ru', 'zh']
 
57
  conn.execute("PRAGMA mmap_size = 4294967296")
58
  return conn
59
 
 
60
  RELATIONS = [
61
  ("IsA", f"{CONCEPTNET_BASE}/r/IsA"),
62
  ("PartOf", f"{CONCEPTNET_BASE}/r/PartOf"),
 
72
  ("DerivedFrom", f"{CONCEPTNET_BASE}/r/DerivedFrom"),
73
  ]
74
 
75
+ def get_semantic_profile_json(word: str, lang: str = 'en', max_per_relation: int = 10):
76
+ """Get semantic profile as JSON"""
77
  if not word or lang not in TARGET_LANGUAGES:
78
  return {"error": "Invalid input"}
79
 
 
92
  with get_db_connection() as conn:
93
  cursor = conn.cursor()
94
 
 
95
  cursor.execute("SELECT id, label FROM node WHERE id LIKE ? LIMIT 5", (like_path,))
96
  result["nodes"] = [{"id": nid, "label": label} for nid, label in cursor.fetchall()]
97
 
98
  if not result["nodes"]:
99
  return {"error": "Word not found"}
100
 
 
101
  for rel_name, rel_url in RELATIONS:
102
  outgoing = []
103
  incoming = []
104
 
 
105
  cursor.execute("""
106
  SELECT en.label, e.weight, en.id
107
  FROM edge e
 
114
  outgoing = [{"target": label, "weight": weight, "target_id": eid}
115
  for label, weight, eid in cursor.fetchall()]
116
 
 
117
  cursor.execute("""
118
  SELECT s.label, e.weight, s.id
119
  FROM edge e
 
140
  return {"error": str(e)}
141
 
142
  def get_semantic_profile(word, lang='en', progress=gr.Progress()):
143
+ """Get semantic profile with progress"""
144
  log_progress(f"Profile: {word} ({lang})", "INFO")
145
 
146
  if not word or lang not in TARGET_LANGUAGES:
147
  return "⚠️ Invalid input"
148
 
149
+ progress(0, desc="πŸ” Starting...")
 
150
 
151
  word = word.strip().lower().replace(' ', '_')
152
  like_path = f"{CONCEPTNET_BASE}/c/{lang}/{word}%"
 
159
 
160
  progress(0.05, desc="πŸ“ Finding nodes...")
161
 
 
162
  cursor.execute("SELECT id, label FROM node WHERE id LIKE ? LIMIT 5", (like_path,))
163
  nodes = cursor.fetchall()
164
 
 
175
  num_relations = len(RELATIONS)
176
 
177
  for i, (rel_name, rel_url) in enumerate(RELATIONS):
 
178
  progress((i + 0.1) / num_relations, desc=f"πŸ”Ž {rel_name}...")
179
 
180
  output_md += f"## {rel_name}\n\n"
181
  found = False
182
 
 
183
  start_time = time.time()
184
  cursor.execute("""
185
  SELECT en.label, e.weight
 
200
  found = True
201
  total += 1
202
 
 
203
  cursor.execute("""
204
  SELECT s.label, e.weight
205
  FROM edge e
 
220
  output_md += "*No results*\n"
221
 
222
  output_md += "\n"
223
+ progress((i + 1) / num_relations, desc=f"βœ“ {rel_name}")
 
 
224
 
225
  progress(1.0, desc="βœ… Complete!")
226
 
227
  output_md += f"---\n**Total relations:** {total}\n"
228
+ log_progress(f"Complete: {total} relations", "SUCCESS")
 
229
 
230
  return output_md
231
 
 
238
  def query_edges_json(start_node: Optional[str] = None,
239
  relation: Optional[str] = None,
240
  end_node: Optional[str] = None,
241
+ limit: int = 50):
242
+ """Query edges JSON"""
243
  query = """
244
  SELECT
245
  e.id, s.id, r.label, en.id, e.weight, s.label, en.label
 
254
 
255
  try:
256
  with get_db_connection() as conn:
 
257
  if start_node:
258
  if start_node.startswith('http://'):
259
  pattern = f"{start_node}%"
 
262
  query += " AND s.id LIKE ?"
263
  params.append(pattern)
264
 
 
265
  if relation:
266
  if relation.startswith('http://'):
267
  rel_value = relation
 
272
  query += " AND r.id = ?"
273
  params.append(rel_value)
274
 
 
275
  if end_node:
276
  if end_node.startswith('http://'):
277
  pattern = f"{end_node}%"
 
299
  return {"error": str(e)}
300
 
301
  def run_query(start_node, relation, end_node, limit, progress=gr.Progress()):
302
+ """Query builder"""
303
  log_progress(f"Query: start={start_node}, rel={relation}, end={end_node}", "INFO")
304
 
305
+ progress(0, desc="πŸ” Building...")
306
 
307
  query = """
308
  SELECT
 
318
 
319
  try:
320
  with get_db_connection() as conn:
321
+ progress(0.3, desc="πŸ“ Filters...")
322
 
 
323
  if start_node and start_node.strip():
324
  if start_node.startswith('http://'):
325
  pattern = f"{start_node}%"
 
328
  query += " AND s.id LIKE ?"
329
  params.append(pattern)
330
 
 
331
  if relation and relation.strip():
332
  if relation.startswith('http://'):
333
  rel_value = relation
 
338
  query += " AND r.id = ?"
339
  params.append(rel_value)
340
 
 
341
  if end_node and end_node.strip():
342
  if end_node.startswith('http://'):
343
  pattern = f"{end_node}%"
 
349
  query += " ORDER BY e.weight DESC LIMIT ?"
350
  params.append(limit)
351
 
352
+ progress(0.6, desc="⚑ Running...")
353
 
354
  start_time = time.time()
355
  df = pd.read_sql_query(query, conn, params=params)
 
357
 
358
  progress(1.0, desc="βœ… Done!")
359
 
360
+ log_progress(f"Done: {len(df)} rows in {elapsed:.2f}s", "SUCCESS")
361
 
362
  if df.empty:
363
  return pd.DataFrame(), f"⚠️ No results ({elapsed:.2f}s)"
 
386
  def get_schema_info():
387
  md = f"# πŸ“š Schema\n\n"
388
  md += f"**Repo:** [{INDEXED_REPO_ID}](https://huggingface.co/datasets/{INDEXED_REPO_ID})\n\n"
 
 
 
 
389
 
390
  try:
391
  with get_db_connection() as conn:
392
  cursor = conn.cursor()
393
 
394
+ md += "## Relations\n\n"
395
  cursor.execute("SELECT id, label FROM relation ORDER BY label LIMIT 20")
396
  for rel_id, label in cursor.fetchall():
397
  md += f"- **{label}:** `{rel_id}`\n"
 
407
 
408
  return md
409
 
410
+ # ===== FASTAPI =====
411
  app = FastAPI(title="ConceptNet API", version="1.0")
412
 
413
  @app.get("/api/profile/{word}")
414
  def api_profile(word: str, lang: str = "en", limit: int = 10):
415
+ """Get semantic profile"""
416
  return JSONResponse(get_semantic_profile_json(word, lang, limit))
417
 
418
  @app.get("/api/query")
 
420
  relation: Optional[str] = None,
421
  end: Optional[str] = None,
422
  limit: int = 50):
423
+ """Query edges"""
424
  return JSONResponse(query_edges_json(start, relation, end, limit))
425
 
426
  @app.get("/api/relations")
427
  def api_relations():
428
+ """List relations"""
429
  return JSONResponse({"relations": [{"name": name, "url": url} for name, url in RELATIONS]})
430
 
431
  @app.get("/api/languages")
432
  def api_languages():
433
+ """List languages"""
434
  return JSONResponse({"languages": TARGET_LANGUAGES})
435
 
436
  @app.get("/")
437
  def api_root():
438
+ """API docs"""
439
  return {
440
  "name": "ConceptNet API",
441
  "version": "1.0",
442
  "endpoints": {
443
+ "/api/profile/{word}": "Semantic profile (params: lang, limit)",
444
  "/api/query": "Query edges (params: start, relation, end, limit)",
445
+ "/api/relations": "List relations",
446
+ "/api/languages": "List languages"
447
  },
448
  "examples": {
449
  "profile": "/api/profile/dog?lang=en&limit=10",
 
457
  gr.Markdown(
458
  f"**Multi-language semantic network** | "
459
  f"**Languages:** {', '.join([l.upper() for l in TARGET_LANGUAGES])} | "
460
+ f"**API:** RESTful JSON endpoints"
461
  )
462
 
463
  with gr.Tabs():
 
503
  schema_output = gr.Markdown()
504
 
505
  with gr.TabItem("πŸ”Œ API"):
506
+ gr.Markdown("## JSON API Endpoints\n")
507
+ gr.Markdown("### Get Semantic Profile")
508
+ gr.Markdown("```\nGET /api/profile/{word}?lang=en&limit=10\n```")
509
+ gr.Markdown("**Example:**")
510
+ gr.Markdown("```\ncurl http://localhost:7860/api/profile/dog?lang=en\n```\n")
511
+
512
+ gr.Markdown("### Query Edges")
513
+ gr.Markdown("```\nGET /api/query?start=dog&relation=IsA&limit=50\n```")
514
+ gr.Markdown("**Example:**")
515
+ gr.Markdown("```\ncurl 'http://localhost:7860/api/query?start=dog&relation=IsA&limit=10'\n```\n")
516
+
517
+ gr.Markdown("### List Relations")
518
+ gr.Markdown("```\nGET /api/relations\n```\n")
519
+
520
+ gr.Markdown("### List Languages")
521
+ gr.Markdown("```\nGET /api/languages\n```")
522
 
523
  gr.Markdown(
524
  "---\n"
525
+ "**Performance:** Exact match on rel_id for fast queries | "
526
+ "**API:** Full REST API with JSON responses"
527
  )
528
 
529
  semantic_btn.click(get_semantic_profile, [word_input, lang_input], semantic_output)
 
531
  raw_btn.click(run_raw_query, raw_sql_input, [raw_results, raw_status])
532
  schema_btn.click(get_schema_info, None, schema_output)
533
 
534
+ # Mount Gradio to FastAPI
535
  app = gr.mount_gradio_app(app, demo, path="/")
536
 
537
  if __name__ == "__main__":
538
  log_progress("="*60, "SUCCESS")
539
+ log_progress("πŸš€ APP READY WITH JSON API!", "SUCCESS")
540
  log_progress("="*60, "SUCCESS")
541
+ log_progress("UI: http://localhost:7860", "INFO")
542
  log_progress("API: http://localhost:7860/api/profile/dog", "INFO")
543
  log_progress("="*60, "SUCCESS")
544