yonilev commited on
Commit
c3728fa
Β·
verified Β·
1 Parent(s): ead4894

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -118
app.py CHANGED
@@ -337,6 +337,60 @@ def render_recommendations(recs):
337
  # ═════════════════════════════════════════════════════════════════════════════
338
  DEMO_ISSUER = {"name": "ΧžΧ™Χ Χ©Χ§Χ˜Χ™Χ", "tax_id": "962569844", "status": "authorized_dealer"}
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  _DOC_CHOICE_MAP = {
341
  "Χ§Χ‘ΧœΧ”": "receipt",
342
  "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב": "tax_invoice",
@@ -345,19 +399,19 @@ _DOC_CHOICE_MAP = {
345
 
346
  def generate(note, doc_choice):
347
  """Single-shot, stateless pipeline:
348
- note (Textbox) + doc_choice (Radio) β†’ (status_md, doc_html, recs_html).
349
  No gr.State, no second handler β€” the input value cannot be lost.
350
  """
351
  note = (note or "").strip()
352
  if not note:
353
- return "✏️ Χ”Χ§ΧœΧ“ Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך.", "", ""
354
 
355
  _lazy_init()
356
  parse = model_parse(note)
357
 
358
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
359
  return ("πŸ€” לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ— ΧžΧ—Χ“Χ© β€” ΧœΧ“Χ•Χ’ΧžΧ”: "
360
- "'Χ§Χ™Χ‘ΧœΧͺΧ™ 500β‚ͺ ΧžΧžΧ©Χ” גל Χ™Χ™Χ’Χ•Χ₯'.", "", "")
361
 
362
  # ---- agentic clarification: only the doc-type can be genuinely ambiguous ----
363
  answers = {}
@@ -369,16 +423,16 @@ def generate(note, doc_choice):
369
  if _needs_allocation_clarification(parse):
370
  hint = " (בכום Χ’Χ‘Χ•Χ” ΧœΧœΧ§Χ•Χ— Χ’Χ‘Χ§Χ™ β€” ΧœΧ¨Χ•Χ‘ Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב Χ•Χ§Χ‘ΧœΧ”)"
371
  return (f"❓ Χ¦Χ¨Χ™Χš ׀רט אחד Χ Χ•Χ‘Χ£: **Χ‘Χ—Χ¨ Χ‘Χ•Χ’ מבמך** ΧžΧ”Χ¨Χ©Χ™ΧžΧ” ΧœΧžΧ’ΧœΧ”{hint} "
372
- "Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך Χ©Χ•Χ‘.", "", "")
373
 
374
  final = apply_answers(parse, answers)
375
  try:
376
  completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
377
  except Exception as e:
378
- return (f"⚠️ שגיאה Χ‘Χ’Χ™Χ‘Χ•Χ“: {e}", "", "")
379
 
380
- client = final.get("client_name", "β€”")
381
  return ("βœ… Χ”ΧžΧ‘ΧžΧš Χ”Χ•Χ€Χ§ Χ‘Χ”Χ¦ΧœΧ—Χ”.",
 
382
  render_document(completed),
383
  render_recommendations(recommend(note)))
384
 
@@ -387,104 +441,16 @@ def generate(note, doc_choice):
387
  # 7. UI β€” Gradio 6.17 compatible, ChatGPT/Claude style
388
  # ═════════════════════════════════════════════════════════════════════════════
389
  CSS = """
390
- *, *::before, *::after { box-sizing: border-box; }
391
-
392
- .gradio-container {
393
- max-width: 760px !important;
394
- margin: 0 auto !important;
395
- padding: 0 16px !important;
396
- background: #0a0f1e !important;
397
- }
398
- body, .main { background: #0a0f1e !important; }
399
  footer { display: none !important; }
400
-
401
- .chatbot-area .bubble-wrap { gap: 12px !important; }
402
-
403
- .chatbot-area [data-testid="user"] .bubble,
404
- .chatbot-area .user .bubble {
405
- background: #0d9488 !important;
406
- color: #fff !important;
407
- border-radius: 18px 18px 4px 18px !important;
408
- padding: 12px 16px !important;
409
- font-size: 14px !important;
410
- line-height: 1.6 !important;
411
- max-width: 80% !important;
412
- border: none !important;
413
- }
414
-
415
- .chatbot-area [data-testid="bot"] .bubble,
416
- .chatbot-area .bot .bubble {
417
- background: #1e293b !important;
418
- color: #e2e8f0 !important;
419
- border-radius: 18px 18px 18px 4px !important;
420
- padding: 12px 16px !important;
421
- font-size: 14px !important;
422
- line-height: 1.6 !important;
423
- max-width: 80% !important;
424
- border: 1px solid #334155 !important;
425
- }
426
-
427
- .note-box textarea {
428
- background: #1e293b !important;
429
- color: #e2e8f0 !important;
430
- border: 1px solid #334155 !important;
431
- border-radius: 14px !important;
432
- font-size: 15px !important;
433
- padding: 14px 18px !important;
434
- resize: none !important;
435
- }
436
- .note-box textarea:focus {
437
- border-color: #0d9488 !important;
438
- box-shadow: 0 0 0 3px rgba(13,148,136,.18) !important;
439
- outline: none !important;
440
- }
441
- .note-box label { display: none !important; }
442
-
443
- .answer-box textarea {
444
- background: #1e293b !important;
445
- color: #e2e8f0 !important;
446
- border: 1px solid #0d9488 !important;
447
- border-radius: 10px !important;
448
- font-size: 14px !important;
449
- padding: 10px 14px !important;
450
- resize: none !important;
451
- }
452
- .answer-box label { color: #475569 !important; font-size: 11px !important; }
453
-
454
- .btn-send button {
455
- background: #0d9488 !important;
456
- color: #fff !important;
457
- border: none !important;
458
- border-radius: 12px !important;
459
- font-size: 15px !important;
460
- font-weight: 600 !important;
461
- height: 52px !important;
462
- }
463
- .btn-send button:hover { background: #0f766e !important; }
464
-
465
- .btn-clear button {
466
- background: transparent !important;
467
- color: #475569 !important;
468
- border: 1px solid #1e293b !important;
469
- border-radius: 10px !important;
470
- font-size: 13px !important;
471
- }
472
- .btn-clear button:hover { border-color: #334155 !important; color: #94a3b8 !important; }
473
-
474
- .qs-btn button {
475
- background: #111827 !important;
476
- color: #94a3b8 !important;
477
- border: 1px solid #1e293b !important;
478
- border-radius: 10px !important;
479
- font-size: 12px !important;
480
- padding: 8px 12px !important;
481
- width: 100% !important;
482
- }
483
- .qs-btn button:hover {
484
- background: #1e293b !important;
485
- border-color: #0d9488 !important;
486
- color: #e2e8f0 !important;
487
  }
 
 
488
  """
489
 
490
  QUICK_STARTERS = [
@@ -493,20 +459,22 @@ QUICK_STARTERS = [
493
  ("πŸ“¦ Χ›ΧžΧ” Χ€Χ¨Χ™Χ˜Χ™Χ", 'Χ Χ•Χ’Χ” Χ›Χ”ΧŸ Χ©Χ™ΧœΧžΧ” 2Γ—350 Χ©"Χ— Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™ + 180 Χ©"Χ— Χ”Χ›Χ Χͺ ΧžΧ¦Χ’Χͺ'),
494
  ]
495
 
496
- with gr.Blocks(title="Text2Receipt") as demo:
 
497
 
498
  gr.HTML("""
499
- <div dir="rtl" style="text-align:center;padding:28px 0 16px;">
500
- <div style="font-size:30px;margin-bottom:8px;">🧾</div>
501
- <div style="font-size:26px;font-weight:700;color:#2dd4bf;letter-spacing:-.4px;">Text2Receipt</div>
502
- <div style="font-size:13px;color:#334155;margin-top:6px;">
503
  Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ—Χ•Χ€Χ©Χ™Χͺ Χ‘Χ’Χ‘Χ¨Χ™Χͺ &nbsp;β†’&nbsp; מבמך Χ€Χ™Χ‘Χ§ΧœΧ™ Χ™Χ©Χ¨ΧΧœΧ™ ΧͺΧ§Χ™ΧŸ
504
  </div>
505
  </div>""")
506
 
507
  note_input = gr.Textbox(
508
- label="Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ”", lines=2, elem_classes=["note-box"],
509
  placeholder='ΧœΧ“Χ•Χ’ΧžΧ”: Χ§Χ™Χ‘ΧœΧͺΧ™ 1,200 Χ©"Χ— ΧžΧžΧ©Χ” Χ›Χ”ΧŸ גל Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™',
 
510
  )
511
 
512
  doc_choice = gr.Radio(
@@ -515,30 +483,33 @@ with gr.Blocks(title="Text2Receipt") as demo:
515
  )
516
 
517
  with gr.Row():
518
- submit_btn = gr.Button("Χ”Χ€Χ§ מבמך ⚑", variant="primary", elem_classes=["btn-send"])
519
- clear_btn = gr.Button("Χ Χ§Χ” πŸ—‘", elem_classes=["btn-clear"])
 
520
 
521
- status_output = gr.Markdown("", elem_classes=["status-area"])
522
 
523
- gr.HTML('<div dir="rtl" style="font-size:10px;color:#1e293b;margin:14px 0 6px;'
524
  'letter-spacing:.07em;text-transform:uppercase;">Χ“Χ•Χ’ΧžΧΧ•Χͺ ΧžΧ”Χ™Χ¨Χ•Χͺ</div>')
525
  with gr.Row():
526
  for lbl, ex in QUICK_STARTERS:
527
- gr.Button(lbl, elem_classes=["qs-btn"]).click(
528
  fn=lambda e=ex: e, outputs=note_input)
529
 
530
- doc_output = gr.HTML()
531
- recs_output = gr.HTML()
 
532
 
533
  # ── single, stateless handler β€” value cannot be lost ──
534
  submit_btn.click(
535
  fn=generate,
536
  inputs=[note_input, doc_choice],
537
- outputs=[status_output, doc_output, recs_output],
538
  )
539
  clear_btn.click(
540
- fn=lambda: ("", "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™", "", "", ""),
541
- outputs=[note_input, doc_choice, status_output, doc_output, recs_output],
 
542
  )
543
 
544
  # ═════════════════════════════════════════════════════════════════════════════
 
337
  # ═════════════════════════════════════════════════════════════════════════════
338
  DEMO_ISSUER = {"name": "ΧžΧ™Χ Χ©Χ§Χ˜Χ™Χ", "tax_id": "962569844", "status": "authorized_dealer"}
339
 
340
+
341
+ def render_parse_panel(parse, completed):
342
+ """'Behind the scenes' panel: what the MODEL extracted vs what the
343
+ DETERMINISTIC engine computed. Demonstrates the model/rules separation β€”
344
+ the model predicts only the extraction; all arithmetic is rule-based."""
345
+ items = parse.get("items", [])
346
+ item_rows = "".join(
347
+ f'<div style="display:flex;justify-content:space-between;font-size:12px;'
348
+ f'color:#334155;padding:3px 0;"><span>{it.get("description","")}</span>'
349
+ f'<span style="color:#64748b;">{it.get("quantity",1)} Γ— β‚ͺ{it.get("unit_price",0):,.0f}</span></div>'
350
+ for it in items)
351
+
352
+ chips = (
353
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
354
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">ΧœΧ§Χ•Χ—: {parse.get("client_name","β€”")}</span>'
355
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
356
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">'
357
+ f'{"Χ’Χ‘Χ§" if parse.get("client_is_business") else "Χ€Χ¨Χ˜Χ™"}</span>'
358
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
359
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">{len(items)} Χ€Χ¨Χ™Χ˜Χ™Χ</span>')
360
+
361
+ alloc_line = ""
362
+ if completed.get("allocation_required"):
363
+ alloc_line = ('<div style="font-size:12px;color:#334155;padding:3px 0;">'
364
+ 'מב׀ר הקצאה <span style="color:#0d9488;">βœ“ Χ Χ“Χ¨Χ©</span> '
365
+ '(Χͺנאי: Χ—Χ©Χ‘Χ•Χ Χ™Χͺ Β· Χ’Χ‘Χ§ Β· β‰₯ Χ‘Χ£)</div>')
366
+
367
+ return f"""
368
+ <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
369
+ margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
370
+
371
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
372
+ <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
373
+ text-transform:uppercase;margin-bottom:10px;">🧠 Χ”ΧžΧ•Χ“Χœ Χ—Χ™ΧœΧ₯</div>
374
+ <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px;">{chips}</div>
375
+ {item_rows}
376
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
377
+ padding-top:8px;">gemma-2-2b + LoRA Β· Χ—Χ•Χ–Χ” Χ¨Χ§ <code>parse</code></div>
378
+ </div>
379
+
380
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
381
+ <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
382
+ text-transform:uppercase;margin-bottom:10px;">βš™οΈ Χ”ΧžΧ Χ•Χ’ Χ—Χ™Χ©Χ‘</div>
383
+ <div style="font-size:12px;color:#334155;padding:3px 0;">מג"מ {int(round(completed.get("vat_rate",0)*100))}% ·
384
+ β‚ͺ{completed.get("vat_amount",0):,.2f}</div>
385
+ <div style="font-size:12px;color:#334155;padding:3px 0;">Χ‘Χ”"Χ› β‚ͺ{completed.get("total",0):,.2f}</div>
386
+ <div style="font-size:12px;color:#334155;padding:3px 0;">מב׳ מבמך {completed.get("serial_number","")}</div>
387
+ {alloc_line}
388
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
389
+ padding-top:8px;">Χ“Χ˜Χ¨ΧžΧ™Χ Χ™Χ‘Χ˜Χ™ Β· א׀ב Χ”Χ–Χ™Χ•Χͺ Χ‘Χ—Χ©Χ‘Χ•ΧŸ</div>
390
+ </div>
391
+ </div>"""
392
+
393
+
394
  _DOC_CHOICE_MAP = {
395
  "Χ§Χ‘ΧœΧ”": "receipt",
396
  "Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב": "tax_invoice",
 
399
 
400
  def generate(note, doc_choice):
401
  """Single-shot, stateless pipeline:
402
+ note (Textbox) + doc_choice (Radio) β†’ (status, parse_html, doc_html, recs_html).
403
  No gr.State, no second handler β€” the input value cannot be lost.
404
  """
405
  note = (note or "").strip()
406
  if not note:
407
+ return "✏️ Χ”Χ§ΧœΧ“ Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך.", "", "", ""
408
 
409
  _lazy_init()
410
  parse = model_parse(note)
411
 
412
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
413
  return ("πŸ€” לא Χ”Χ¦ΧœΧ—ΧͺΧ™ ΧœΧ—ΧœΧ₯ Χ€Χ¨Χ˜Χ™Χ. Χ Χ‘Χ— ΧžΧ—Χ“Χ© β€” ΧœΧ“Χ•Χ’ΧžΧ”: "
414
+ "'Χ§Χ™Χ‘ΧœΧͺΧ™ 500β‚ͺ ΧžΧžΧ©Χ” גל Χ™Χ™Χ’Χ•Χ₯'.", "", "", "")
415
 
416
  # ---- agentic clarification: only the doc-type can be genuinely ambiguous ----
417
  answers = {}
 
423
  if _needs_allocation_clarification(parse):
424
  hint = " (בכום Χ’Χ‘Χ•Χ” ΧœΧœΧ§Χ•Χ— Χ’Χ‘Χ§Χ™ β€” ΧœΧ¨Χ•Χ‘ Χ—Χ©Χ‘Χ•Χ Χ™Χͺ מב Χ•Χ§Χ‘ΧœΧ”)"
425
  return (f"❓ Χ¦Χ¨Χ™Χš ׀רט אחד Χ Χ•Χ‘Χ£: **Χ‘Χ—Χ¨ Χ‘Χ•Χ’ מבמך** ΧžΧ”Χ¨Χ©Χ™ΧžΧ” ΧœΧžΧ’ΧœΧ”{hint} "
426
+ "Χ•ΧœΧ—Χ₯ Χ”Χ€Χ§ מבמך Χ©Χ•Χ‘.", "", "", "")
427
 
428
  final = apply_answers(parse, answers)
429
  try:
430
  completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
431
  except Exception as e:
432
+ return (f"⚠️ שגיאה Χ‘Χ’Χ™Χ‘Χ•Χ“: {e}", "", "", "")
433
 
 
434
  return ("βœ… Χ”ΧžΧ‘ΧžΧš Χ”Χ•Χ€Χ§ Χ‘Χ”Χ¦ΧœΧ—Χ”.",
435
+ render_parse_panel(final, completed),
436
  render_document(completed),
437
  render_recommendations(recommend(note)))
438
 
 
441
  # 7. UI β€” Gradio 6.17 compatible, ChatGPT/Claude style
442
  # ═════════════════════════════════════════════════════════════════════════════
443
  CSS = """
444
+ .gradio-container { max-width: 880px !important; margin: 0 auto !important; }
 
 
 
 
 
 
 
 
445
  footer { display: none !important; }
446
+ .qs-btn {
447
+ font-size: 13px !important;
448
+ background: #f1f5f9 !important;
449
+ border: 1px solid #e2e8f0 !important;
450
+ color: #0f172a !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  }
452
+ .qs-btn:hover { background: #e2e8f0 !important; }
453
+ .btn-send { font-weight: 600 !important; }
454
  """
455
 
456
  QUICK_STARTERS = [
 
459
  ("πŸ“¦ Χ›ΧžΧ” Χ€Χ¨Χ™Χ˜Χ™Χ", 'Χ Χ•Χ’Χ” Χ›Χ”ΧŸ Χ©Χ™ΧœΧžΧ” 2Γ—350 Χ©"Χ— Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™ + 180 Χ©"Χ— Χ”Χ›Χ Χͺ ΧžΧ¦Χ’Χͺ'),
460
  ]
461
 
462
+ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
463
+ primary_hue="teal", secondary_hue="slate")) as demo:
464
 
465
  gr.HTML("""
466
+ <div dir="rtl" style="text-align:center;padding:24px 0 8px;">
467
+ <div style="font-size:30px;margin-bottom:6px;">🧾</div>
468
+ <div style="font-size:26px;font-weight:800;color:#0d9488;letter-spacing:-.4px;">Text2Receipt</div>
469
+ <div style="font-size:14px;color:#475569;margin-top:6px;">
470
  Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ” Χ—Χ•Χ€Χ©Χ™Χͺ Χ‘Χ’Χ‘Χ¨Χ™Χͺ &nbsp;β†’&nbsp; מבמך Χ€Χ™Χ‘Χ§ΧœΧ™ Χ™Χ©Χ¨ΧΧœΧ™ ΧͺΧ§Χ™ΧŸ
471
  </div>
472
  </div>""")
473
 
474
  note_input = gr.Textbox(
475
+ label="Χ”Χ’Χ¨Χͺ Χ”Χ›Χ Χ‘Χ”", lines=2,
476
  placeholder='ΧœΧ“Χ•Χ’ΧžΧ”: Χ§Χ™Χ‘ΧœΧͺΧ™ 1,200 Χ©"Χ— ΧžΧžΧ©Χ” Χ›Χ”ΧŸ גל Χ™Χ™Χ’Χ•Χ₯ Χ’Χ‘Χ§Χ™',
477
+ rtl=True, autofocus=True,
478
  )
479
 
480
  doc_choice = gr.Radio(
 
483
  )
484
 
485
  with gr.Row():
486
+ submit_btn = gr.Button("Χ”Χ€Χ§ מבמך ⚑", variant="primary", scale=2,
487
+ elem_classes=["btn-send"])
488
+ clear_btn = gr.Button("Χ Χ§Χ” πŸ—‘", scale=1)
489
 
490
+ status_output = gr.Markdown("")
491
 
492
+ gr.HTML('<div dir="rtl" style="font-size:11px;color:#94a3b8;margin:10px 0 6px;'
493
  'letter-spacing:.07em;text-transform:uppercase;">Χ“Χ•Χ’ΧžΧΧ•Χͺ ΧžΧ”Χ™Χ¨Χ•Χͺ</div>')
494
  with gr.Row():
495
  for lbl, ex in QUICK_STARTERS:
496
+ gr.Button(lbl, elem_classes=["qs-btn"], size="sm").click(
497
  fn=lambda e=ex: e, outputs=note_input)
498
 
499
+ parse_output = gr.HTML()
500
+ doc_output = gr.HTML()
501
+ recs_output = gr.HTML()
502
 
503
  # ── single, stateless handler β€” value cannot be lost ──
504
  submit_btn.click(
505
  fn=generate,
506
  inputs=[note_input, doc_choice],
507
+ outputs=[status_output, parse_output, doc_output, recs_output],
508
  )
509
  clear_btn.click(
510
+ fn=lambda: ("", "Χ–Χ™Χ”Χ•Χ™ ΧΧ•Χ˜Χ•ΧžΧ˜Χ™", "", "", "", ""),
511
+ outputs=[note_input, doc_choice, status_output,
512
+ parse_output, doc_output, recs_output],
513
  )
514
 
515
  # ═════════════════════════════════════════════════════════════════════════════