OpceanAI commited on
Commit
486b7c2
·
verified ·
1 Parent(s): 1a33400

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +145 -100
app.py CHANGED
@@ -78,10 +78,16 @@ def _estimate_duration(text, num_candidates=3, structured_fallback=True, *args,
78
  return min(30, max(12, 8 + len(text or "") // 200 + 4 * (candidates - 1)))
79
 
80
 
81
- def _run_harness(text: str | None, num_candidates: int | None, structured_fallback: bool | None) -> dict:
82
  text, num_candidates, structured_fallback = _normalize(text, num_candidates, structured_fallback)
83
  if not text:
84
  raise ValueError("Paste a document to convert.")
 
 
 
 
 
 
85
  started = time.perf_counter()
86
  result = convert_to_markdown(
87
  model,
@@ -93,6 +99,8 @@ def _run_harness(text: str | None, num_candidates: int | None, structured_fallba
93
  top_p=0.95,
94
  seed=0,
95
  use_cache=True,
 
 
96
  structured_fallback=structured_fallback,
97
  skip_model_for_structured=True,
98
  )
@@ -101,7 +109,7 @@ def _run_harness(text: str | None, num_candidates: int | None, structured_fallba
101
 
102
 
103
  @spaces.GPU(duration=_estimate_duration)
104
- def convert(text: str | None = None, num_candidates: int | None = None, structured_fallback: bool | None = None) -> tuple[str, str, str]:
105
  """Convert a plain-text document into faithful GitHub-Flavored Markdown.
106
 
107
  PASITA samples the document several times, every candidate is post-processed and
@@ -113,6 +121,7 @@ def convert(text: str | None = None, num_candidates: int | None = None, structur
113
  text: The document to convert, for example OCR output, pasted HTML, meeting notes or a report.
114
  num_candidates: How many PASITA samples to generate and rerank, from 1 to 3.
115
  structured_fallback: Use the deterministic converter for HTML and delimited data.
 
116
  Returns:
117
  A tuple of the rendered Markdown, the raw Markdown and a status line.
118
  """
@@ -121,7 +130,7 @@ def convert(text: str | None = None, num_candidates: int | None = None, structur
121
  return EMPTY_MD, "", _status("READY / PASTE A DOCUMENT TO CONVERT")
122
 
123
  try:
124
- result = _run_harness(text, num_candidates, structured_fallback)
125
  except Exception as exc:
126
  message = html.escape(f"{type(exc).__name__}: {exc}")
127
  return (
@@ -145,7 +154,7 @@ def convert(text: str | None = None, num_candidates: int | None = None, structur
145
 
146
 
147
  @spaces.GPU(duration=_estimate_duration)
148
- def gpu_convert(text: str | None = None, num_candidates: int | None = None, structured_fallback: bool | None = None) -> dict:
149
  """Convert a document and return the full conversion result.
150
 
151
  Same pipeline as the UI conversion. Used by the /v1 OpenAI-style API routes.
@@ -154,10 +163,11 @@ def gpu_convert(text: str | None = None, num_candidates: int | None = None, stru
154
  text: The document to convert.
155
  num_candidates: How many PASITA samples to generate and rerank, from 1 to 3.
156
  structured_fallback: Use the deterministic converter for HTML and delimited data.
 
157
  Returns:
158
  The full result dict: markdown, route, metrics, timing and candidate info.
159
  """
160
- return _run_harness(text, num_candidates, structured_fallback)
161
 
162
 
163
  COUNTER_JS = """(text) => {
@@ -190,12 +200,12 @@ def _exception_to_error(exc: Exception) -> JSONResponse:
190
  return _api_error(message, "server_error", status=500)
191
 
192
 
193
- def _with_request_context(request: fastapi.Request, text: str, candidates: int, fallback: bool) -> dict:
194
  if GradioRequest is not None:
195
  gr_request = GradioRequest(request=request, session_hash="api")
196
  token = LocalContext.request.set(gr_request)
197
  try:
198
- return gpu_convert(text, candidates, fallback)
199
  finally:
200
  LocalContext.request.reset(token)
201
  return gpu_convert(text, candidates, fallback)
@@ -216,6 +226,7 @@ class ChatCompletionRequest(BaseModel):
216
  model: str | None = "pasita-v1"
217
  messages: list[ChatMessage]
218
  stream: bool = False
 
219
  candidates: int = 3
220
  structured_fallback: bool = True
221
 
@@ -224,11 +235,47 @@ class ConvertRequest(BaseModel):
224
  text: str
225
  candidates: int = 3
226
  structured_fallback: bool = True
 
227
 
228
 
229
  server = Server(title="PASITA API", version="1.0.0", description=API_DESCRIPTION)
230
 
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  @server.exception_handler(fastapi.exceptions.RequestValidationError)
233
  async def validation_error_handler(request: fastapi.Request, exc: fastapi.exceptions.RequestValidationError):
234
  issues = "; ".join(f"{'.'.join(str(l) for l in e.get('loc', []))}: {e.get('msg', '')}" for e in exc.errors()[:3])
@@ -287,7 +334,7 @@ def v1_convert(body: ConvertRequest, request: fastapi.Request) -> dict | JSONRes
287
  return error
288
  started = time.perf_counter()
289
  try:
290
- result = _with_request_context(request, body.text.strip(), int(body.candidates), bool(body.structured_fallback))
291
  except Exception as exc:
292
  return _exception_to_error(exc)
293
  payload = _result_payload(result, body.text.strip(), started)
@@ -327,7 +374,7 @@ def chat_completions(body: ChatCompletionRequest, request: fastapi.Request) -> d
327
 
328
  started = time.perf_counter()
329
  try:
330
- result = _with_request_context(request, text.strip(), int(body.candidates), bool(body.structured_fallback))
331
  except Exception as exc:
332
  return _exception_to_error(exc)
333
 
@@ -475,187 +522,185 @@ Open questions
475
  ],
476
  ]
477
 
478
- CSS = """
479
- @import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
480
 
481
  :root, .gradio-container {
482
- --bg:#050608; --ink:#f4f5f7; --muted:#9aa0ab; --muted-2:#7f8698;
483
- --warm:#ff7a30; --cool:#7aa0ff; --obsidian:#0a0a0c;
484
- --hair:rgba(255,255,255,0.14); --hair-soft:rgba(255,255,255,0.08);
485
- --panel:rgba(255,255,255,0.03); --panel-hover:rgba(255,255,255,0.06);
486
- --danger:#ff7a6b;
487
- --font-display:"Hanken Grotesk", system-ui, sans-serif;
488
  --font-mono:"JetBrains Mono", ui-monospace, monospace;
489
 
490
- --body-background-fill:transparent;
491
- --background-fill-primary:transparent;
492
- --background-fill-secondary:transparent;
493
  --block-background-fill:transparent;
494
  --block-border-color:transparent;
495
  --block-border-width:0px;
496
- --block-radius:0px;
497
  --border-color-primary:var(--hair);
498
  --body-text-color:var(--ink);
499
  --body-text-color-subdued:var(--muted);
500
  --input-background-fill:transparent;
501
  --input-border-color:transparent;
502
- --input-radius:12px;
503
- --color-accent:var(--warm);
504
- --color-accent-soft:rgba(255,122,48,0.15);
505
- --button-primary-background-fill:var(--warm);
506
- --button-primary-background-fill-hover:#ff9757;
507
- --button-primary-text-color:#0b0705;
508
  --button-secondary-background-fill:transparent;
509
  --button-secondary-text-color:var(--muted);
510
  --button-secondary-border-color:var(--hair);
511
- --button-large-radius:100px;
512
- --button-small-radius:100px;
 
 
513
  --shadow-drop:none; --shadow-drop-lg:none;
514
  --font:var(--font-display); --font-mono:var(--font-mono);
515
  }
516
 
517
  body, .gradio-container {
518
- background:
519
- radial-gradient(120% 80% at 50% 118%, rgba(255,110,40,0.10), rgba(255,110,40,0) 55%),
520
- radial-gradient(90% 70% at 88% -10%, rgba(80,120,230,0.10), rgba(80,120,230,0) 55%),
521
- radial-gradient(140% 120% at 50% 50%, #0a0c11 0%, #050608 55%, #030305 100%) !important;
522
- background-attachment: fixed !important;
523
  color: var(--ink);
524
  font-family: var(--font-display);
 
525
  -webkit-font-smoothing: antialiased;
526
  }
527
- .gradio-container { max-width: 1240px !important; margin: 0 auto; padding: 0 20px 26px; }
528
  footer[aria-label="Gradio footer navigation"] { display: none !important; }
529
- :focus-visible { outline: 2px solid var(--cool) !important; outline-offset: 2px; }
530
 
531
  .p-head {
532
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
533
- padding: 22px 0 16px; border-bottom: 1px solid var(--hair-soft); margin-bottom: 20px;
534
  }
535
  .p-brand { display: flex; align-items: center; gap: 10px; }
536
- .p-glyph { display: inline-flex; width: 22px; height: 22px; color: var(--warm); }
537
  .p-glyph svg { width: 100%; height: 100%; }
538
- .p-name { font-weight: 700; letter-spacing: 0.14em; font-size: 1.02rem; }
539
- .p-tag { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.14em; color: var(--muted-2); margin-top: 2px; }
540
  .p-actions { display: flex; align-items: center; gap: 10px; }
541
  .p-btn {
542
- font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.1em;
543
- color: var(--ink); text-decoration: none; padding: 0 18px; min-height: 44px;
544
- display: inline-flex; align-items: center; border-radius: 999px;
545
- background: rgba(255,255,255,0.04); border: 1px solid var(--hair);
546
- transition: background .25s ease, color .25s ease, border-color .25s ease;
547
  }
548
- .p-btn:hover { background: var(--ink); color: #08090c; border-color: var(--ink); }
549
 
550
  .p-status {
551
  display: flex; align-items: center; gap: 9px; margin: -6px 0 16px;
552
- font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
553
  text-transform: uppercase; color: var(--muted); min-height: 20px;
 
554
  }
555
  .p-dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,0.2); flex-shrink: 0; }
556
- .p-status.ready .p-dot { background: var(--warm); box-shadow: 0 0 10px rgba(255,138,77,0.55); }
557
- .p-status.busy .p-dot { background: #ffcd6b; animation: p-pulse 1.1s ease-in-out infinite; }
558
- .p-status.error { color: var(--danger); }
559
- .p-status.error .p-dot { background: var(--danger); box-shadow: 0 0 10px rgba(255,122,107,0.5); }
560
  .p-detail { margin-left: auto; color: var(--muted-2); }
561
- @keyframes p-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
562
 
563
- #p-shell { gap: 18px; align-items: stretch; }
564
  .p-panel {
565
- background: var(--panel); border: 1px solid var(--hair); border-radius: 16px;
566
  padding: 14px 14px 12px; min-width: 0;
567
  }
568
- .p-label { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
569
- .p-count { float: right; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.04em; color: var(--muted-2); }
570
 
571
  #p-text, #p-text > div { background: transparent !important; border: 0 !important; box-shadow: none !important; }
572
  #p-text textarea {
573
  background: transparent !important; border: 0 !important; color: var(--ink) !important;
574
- font-family: var(--font-display) !important; font-size: 0.95rem; line-height: 1.65;
575
  min-height: 360px; padding: 12px 2px 0;
576
  }
577
  #p-text textarea::placeholder { color: var(--muted-2) !important; }
578
 
579
- #p-run, #p-clear { font-family: var(--font-mono) !important; font-size: 0.75rem !important; letter-spacing: 0.12em; min-height: 46px; }
580
- #p-run { flex: 2; }
581
  #p-clear { flex: 1; background: transparent !important; color: var(--muted) !important; border: 1px solid var(--hair) !important; }
582
- #p-clear:hover { color: var(--ink) !important; border-color: rgba(255,255,255,0.3) !important; }
583
 
584
- #p-examples .label { font-family: var(--font-mono) !important; font-size: 0.66rem !important; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted) !important; }
585
  #p-examples .label svg { display: none; }
586
  #p-examples .gallery-item {
587
- background: var(--panel) !important; border: 1px solid var(--hair) !important; border-radius: 999px !important;
588
  color: var(--muted) !important; font-size: 12px !important; min-height: 44px; padding: 8px 14px !important;
589
  text-align: left; transition: background .2s ease, color .2s ease, border-color .2s ease;
590
  }
591
- #p-examples .gallery-item:hover { border-color: rgba(255,255,255,0.3) !important; color: var(--ink) !important; background: var(--panel-hover) !important; }
592
 
593
  #p-tabs .tab-container[role="tablist"] {
594
- display: inline-flex; gap: 4px; background: var(--panel); border: 1px solid var(--hair);
595
- border-radius: 100px; padding: 3px; margin-bottom: 12px;
596
  }
597
  #p-tabs .tab-container[role="tablist"] button {
598
- appearance: none; border: 0 !important; background: transparent !important; border-radius: 100px !important;
599
  padding: 7px 14px !important; font-family: var(--font-mono) !important; font-size: 10.5px !important;
600
- letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted) !important;
601
  }
602
  #p-tabs .tab-container[role="tablist"] button.selected,
603
  #p-tabs .tab-container[role="tablist"] button[aria-selected="true"] {
604
- background: rgba(255,255,255,0.08) !important; color: var(--warm) !important;
605
  }
606
  #p-tabs .tab-container::after,
607
  #p-tabs .tab-container button.selected::after { content: none !important; }
608
 
609
- #p-rendered { min-height: 360px; font-size: 0.95rem; line-height: 1.72; color: var(--ink); }
610
- #p-rendered .prose { font-size: 0.95rem; line-height: 1.72; }
611
- #p-rendered h1, #p-rendered h2, #p-rendered h3, #p-rendered h4 { color: #fff; font-weight: 600; line-height: 1.25; margin: 1.25em 0 0.55em; }
612
- #p-rendered h1 { font-size: 1.5em; }
613
- #p-rendered h2 { font-size: 1.28em; }
614
- #p-rendered h3 { font-size: 1.12em; }
615
- #p-rendered p { margin: 0 0 0.9em; }
616
- #p-rendered ul, #p-rendered ol { margin: 0 0 0.9em; padding-left: 1.5em; }
617
- #p-rendered li { margin: 0.25em 0; }
618
  #p-rendered li::marker { color: var(--muted-2); }
619
- #p-rendered a { color: #8ab4ff; text-decoration: underline; text-underline-offset: 2px; }
620
- #p-rendered a:hover { color: #aecbff; }
621
- #p-rendered blockquote { margin: 0 0 0.9em; padding: 2px 0 2px 16px; border-left: 2px solid var(--hair); color: var(--muted); }
622
- #p-rendered hr { border: 0; border-top: 1px solid var(--hair); margin: 1.3em 0; }
623
- #p-rendered code { font-family: var(--font-mono); font-size: 0.85em; background: var(--panel); border: 1px solid var(--hair); border-radius: 5px; padding: 1px 5px; }
624
- #p-rendered pre { margin: 0 0 0.9em; padding: 14px 16px; background: var(--obsidian); border: 1px solid var(--hair); border-radius: 12px; overflow-x: auto; }
625
  #p-rendered pre code { background: none; border: 0; padding: 0; font-size: 0.82em; line-height: 1.6; }
626
- #p-rendered table { border-collapse: collapse; margin: 0 0 0.9em; font-size: 0.92em; display: block; overflow-x: auto; }
627
- #p-rendered th, #p-rendered td { border: 1px solid var(--hair); padding: 6px 11px; text-align: left; }
628
- #p-rendered th { background: var(--panel); color: var(--ink); font-weight: 600; }
629
 
630
  #p-source {
631
  background: var(--obsidian) !important; border: 1px solid var(--hair-soft) !important;
632
- border-radius: 12px !important; font-family: var(--font-mono) !important; min-height: 360px;
633
  }
634
  #p-source label[data-testid="block-label"] { display: none; }
635
  #p-source .cm-editor { background: transparent !important; }
636
- #p-source .cm-content, #p-source .cm-scroller { color: var(--ink) !important; caret-color: var(--warm) !important; }
637
  #p-source .cm-gutters { background: transparent !important; color: var(--muted-2) !important; border-right: 1px solid var(--hair-soft) !important; }
638
 
639
- #p-settings { background: transparent !important; border: 1px solid var(--hair-soft) !important; border-radius: 12px !important; margin-top: 4px; }
640
- #p-settings > .label-wrap { font-family: var(--font-mono) !important; font-size: 0.68rem !important; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted) !important; }
641
  #p-settings > .label-wrap .icon { color: var(--muted-2) !important; }
642
  #p-settings .info-text { font-size: 0.72rem !important; color: var(--muted-2) !important; }
643
 
644
- #p-candidates .wrap { display: inline-flex; gap: 4px; background: var(--panel); border: 1px solid var(--hair); border-radius: 100px; padding: 3px; }
645
- #p-candidates label { border-radius: 100px; padding: 7px 16px; margin: 0 !important; cursor: pointer; transition: background .2s ease, color .2s ease; }
646
- #p-candidates label span { font-family: var(--font-mono) !important; font-size: 11px !important; letter-spacing: 0.08em; color: var(--muted) !important; }
647
- #p-candidates label.selected { background: rgba(255,255,255,0.08) !important; }
648
- #p-candidates label.selected span { color: var(--warm) !important; }
649
- #p-candidates input[type="radio"] { accent-color: var(--warm); }
650
- #p-settings .p-label { margin-bottom: 4px; }
651
 
652
  .p-foot {
653
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 14px;
654
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--hair-soft);
655
- font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.05em; line-height: 1.7; color: var(--muted-2);
656
  }
657
  .p-foot a { color: var(--muted); text-decoration: none; }
658
- .p-foot a:hover { color: var(--warm); }
659
 
660
  @media (max-width: 820px) {
661
  .gradio-container { padding: 0 14px 20px; }
@@ -676,7 +721,7 @@ footer[aria-label="Gradio footer navigation"] { display: none !important; }
676
  theme = gr.themes.Base(
677
  primary_hue=gr.themes.colors.orange,
678
  neutral_hue=gr.themes.colors.gray,
679
- font=[gr.themes.GoogleFont("Hanken Grotesk"), "system-ui", "sans-serif"],
680
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
681
  )
682
 
 
78
  return min(30, max(12, 8 + len(text or "") // 200 + 4 * (candidates - 1)))
79
 
80
 
81
+ def _run_harness(text: str | None, num_candidates: int | None, structured_fallback: bool | None, max_new_cap: int | None = None) -> dict:
82
  text, num_candidates, structured_fallback = _normalize(text, num_candidates, structured_fallback)
83
  if not text:
84
  raise ValueError("Paste a document to convert.")
85
+ hard_cap = 512
86
+ if max_new_cap is not None:
87
+ try:
88
+ hard_cap = max(16, min(512, int(max_new_cap)))
89
+ except (TypeError, ValueError):
90
+ hard_cap = 512
91
  started = time.perf_counter()
92
  result = convert_to_markdown(
93
  model,
 
99
  top_p=0.95,
100
  seed=0,
101
  use_cache=True,
102
+ max_new_tokens="auto",
103
+ hard_cap=hard_cap,
104
  structured_fallback=structured_fallback,
105
  skip_model_for_structured=True,
106
  )
 
109
 
110
 
111
  @spaces.GPU(duration=_estimate_duration)
112
+ def convert(text: str | None = None, num_candidates: int | None = None, structured_fallback: bool | None = None, max_output_tokens: int | None = None) -> tuple[str, str, str]:
113
  """Convert a plain-text document into faithful GitHub-Flavored Markdown.
114
 
115
  PASITA samples the document several times, every candidate is post-processed and
 
121
  text: The document to convert, for example OCR output, pasted HTML, meeting notes or a report.
122
  num_candidates: How many PASITA samples to generate and rerank, from 1 to 3.
123
  structured_fallback: Use the deterministic converter for HTML and delimited data.
124
+ max_output_tokens: Optional cap on generated tokens. When omitted, the length is chosen automatically.
125
  Returns:
126
  A tuple of the rendered Markdown, the raw Markdown and a status line.
127
  """
 
130
  return EMPTY_MD, "", _status("READY / PASTE A DOCUMENT TO CONVERT")
131
 
132
  try:
133
+ result = _run_harness(text, num_candidates, structured_fallback, max_output_tokens)
134
  except Exception as exc:
135
  message = html.escape(f"{type(exc).__name__}: {exc}")
136
  return (
 
154
 
155
 
156
  @spaces.GPU(duration=_estimate_duration)
157
+ def gpu_convert(text: str | None = None, num_candidates: int | None = None, structured_fallback: bool | None = None, max_output_tokens: int | None = None) -> dict:
158
  """Convert a document and return the full conversion result.
159
 
160
  Same pipeline as the UI conversion. Used by the /v1 OpenAI-style API routes.
 
163
  text: The document to convert.
164
  num_candidates: How many PASITA samples to generate and rerank, from 1 to 3.
165
  structured_fallback: Use the deterministic converter for HTML and delimited data.
166
+ max_output_tokens: Optional cap on generated tokens. When omitted, the length is chosen automatically.
167
  Returns:
168
  The full result dict: markdown, route, metrics, timing and candidate info.
169
  """
170
+ return _run_harness(text, num_candidates, structured_fallback, max_output_tokens)
171
 
172
 
173
  COUNTER_JS = """(text) => {
 
200
  return _api_error(message, "server_error", status=500)
201
 
202
 
203
+ def _with_request_context(request: fastapi.Request, text: str, candidates: int, fallback: bool, max_tokens: int | None = None) -> dict:
204
  if GradioRequest is not None:
205
  gr_request = GradioRequest(request=request, session_hash="api")
206
  token = LocalContext.request.set(gr_request)
207
  try:
208
+ return gpu_convert(text, candidates, fallback, max_tokens)
209
  finally:
210
  LocalContext.request.reset(token)
211
  return gpu_convert(text, candidates, fallback)
 
226
  model: str | None = "pasita-v1"
227
  messages: list[ChatMessage]
228
  stream: bool = False
229
+ max_tokens: int | None = None
230
  candidates: int = 3
231
  structured_fallback: bool = True
232
 
 
235
  text: str
236
  candidates: int = 3
237
  structured_fallback: bool = True
238
+ max_tokens: int | None = None
239
 
240
 
241
  server = Server(title="PASITA API", version="1.0.0", description=API_DESCRIPTION)
242
 
243
 
244
+ _V2_CONVERT_PATHS = frozenset({"/gradio_api/call/v2/convert", "/gradio_api/call/v2/convert/"})
245
+
246
+
247
+ @server.middleware("http")
248
+ async def _legacy_convert_body(request: fastapi.Request, call_next):
249
+ if request.method == "POST" and request.url.path in _V2_CONVERT_PATHS:
250
+ try:
251
+ body = await request.json()
252
+ except Exception:
253
+ body = None
254
+ if isinstance(body, dict) and "data" in body and "text" not in body:
255
+ data = body.get("data")
256
+ if isinstance(data, list):
257
+ named: dict = {}
258
+ for name, value in zip(("text", "num_candidates", "structured_fallback"), data):
259
+ named[name] = value
260
+ for key in ("session_hash", "event_id", "fn_index", "trigger_id", "batched"):
261
+ if key in body:
262
+ named[key] = body[key]
263
+ raw = json.dumps(named, default=str).encode("utf-8")
264
+ scope = dict(request.scope)
265
+ headers = [(k, v) for k, v in scope.get("headers", []) if k.lower() != b"content-length"]
266
+ headers.append((b"content-length", str(len(raw)).encode("latin-1")))
267
+ scope["headers"] = headers
268
+ messages = [{"type": "http.request", "body": raw, "more_body": False}]
269
+
270
+ async def receive_once():
271
+ if messages:
272
+ return messages.pop(0)
273
+ return {"type": "http.request", "body": b"", "more_body": False}
274
+
275
+ request = fastapi.Request(scope, receive_once)
276
+ return await call_next(request)
277
+
278
+
279
  @server.exception_handler(fastapi.exceptions.RequestValidationError)
280
  async def validation_error_handler(request: fastapi.Request, exc: fastapi.exceptions.RequestValidationError):
281
  issues = "; ".join(f"{'.'.join(str(l) for l in e.get('loc', []))}: {e.get('msg', '')}" for e in exc.errors()[:3])
 
334
  return error
335
  started = time.perf_counter()
336
  try:
337
+ result = _with_request_context(request, body.text.strip(), int(body.candidates), bool(body.structured_fallback), body.max_tokens)
338
  except Exception as exc:
339
  return _exception_to_error(exc)
340
  payload = _result_payload(result, body.text.strip(), started)
 
374
 
375
  started = time.perf_counter()
376
  try:
377
+ result = _with_request_context(request, text.strip(), int(body.candidates), bool(body.structured_fallback), body.max_tokens)
378
  except Exception as exc:
379
  return _exception_to_error(exc)
380
 
 
522
  ],
523
  ]
524
 
525
+ CSS = """@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
 
526
 
527
  :root, .gradio-container {
528
+ --bg:#0a0a0b; --panel:#101013; --panel-2:#16161a; --obsidian:#0d0d10;
529
+ --ink:#f4f4f5; --muted:#a1a1aa; --muted-2:#8e8e96;
530
+ --gold:#e8b34c; --green:#4cc38a; --red:#f87171;
531
+ --hair:rgba(255,255,255,0.09); --hair-soft:rgba(255,255,255,0.06);
532
+ --panel-hover:rgba(255,255,255,0.05);
533
+ --font-display:"Inter", system-ui, sans-serif;
534
  --font-mono:"JetBrains Mono", ui-monospace, monospace;
535
 
536
+ --body-background-fill:var(--bg);
537
+ --background-fill-primary:var(--panel);
538
+ --background-fill-secondary:var(--panel-2);
539
  --block-background-fill:transparent;
540
  --block-border-color:transparent;
541
  --block-border-width:0px;
542
+ --block-radius:12px;
543
  --border-color-primary:var(--hair);
544
  --body-text-color:var(--ink);
545
  --body-text-color-subdued:var(--muted);
546
  --input-background-fill:transparent;
547
  --input-border-color:transparent;
548
+ --input-radius:8px;
549
+ --color-accent:var(--gold);
550
+ --color-accent-soft:rgba(232,179,76,0.12);
551
+ --button-primary-background-fill:#fafafa;
552
+ --button-primary-background-fill-hover:#e4e4e7;
553
+ --button-primary-text-color:#0a0a0b;
554
  --button-secondary-background-fill:transparent;
555
  --button-secondary-text-color:var(--muted);
556
  --button-secondary-border-color:var(--hair);
557
+ --button-large-radius:8px;
558
+ --button-small-radius:8px;
559
+ --checkbox-background-color-selected:var(--gold);
560
+ --checkbox-border-color-focus:var(--gold);
561
  --shadow-drop:none; --shadow-drop-lg:none;
562
  --font:var(--font-display); --font-mono:var(--font-mono);
563
  }
564
 
565
  body, .gradio-container {
566
+ background: var(--bg) !important;
 
 
 
 
567
  color: var(--ink);
568
  font-family: var(--font-display);
569
+ font-variant-numeric: tabular-nums;
570
  -webkit-font-smoothing: antialiased;
571
  }
572
+ .gradio-container { max-width: 1200px !important; margin: 0 auto; padding: 0 20px 26px; }
573
  footer[aria-label="Gradio footer navigation"] { display: none !important; }
574
+ :focus-visible { outline: 2px solid var(--gold) !important; outline-offset: 2px; }
575
 
576
  .p-head {
577
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
578
+ padding: 20px 0 14px; border-bottom: 1px solid var(--hair-soft); margin-bottom: 20px;
579
  }
580
  .p-brand { display: flex; align-items: center; gap: 10px; }
581
+ .p-glyph { display: inline-flex; width: 20px; height: 20px; color: var(--gold); }
582
  .p-glyph svg { width: 100%; height: 100%; }
583
+ .p-name { font-weight: 600; letter-spacing: 0.12em; font-size: 0.95rem; }
584
+ .p-tag { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.12em; color: var(--muted-2); margin-top: 2px; }
585
  .p-actions { display: flex; align-items: center; gap: 10px; }
586
  .p-btn {
587
+ font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
588
+ color: var(--ink); text-decoration: none; padding: 0 16px; min-height: 44px;
589
+ display: inline-flex; align-items: center; border-radius: 8px;
590
+ background: var(--panel); border: 1px solid var(--hair);
591
+ transition: background .2s ease, color .2s ease, border-color .2s ease;
592
  }
593
+ .p-btn:hover { background: var(--panel-2); border-color: rgba(255,255,255,0.22); }
594
 
595
  .p-status {
596
  display: flex; align-items: center; gap: 9px; margin: -6px 0 16px;
597
+ font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em;
598
  text-transform: uppercase; color: var(--muted); min-height: 20px;
599
+ font-variant-numeric: tabular-nums;
600
  }
601
  .p-dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,0.2); flex-shrink: 0; }
602
+ .p-status.ready .p-dot { background: var(--green); }
603
+ .p-status.busy .p-dot { background: var(--gold); animation: p-pulse 1.1s ease-in-out infinite; }
604
+ .p-status.error { color: var(--red); }
605
+ .p-status.error .p-dot { background: var(--red); }
606
  .p-detail { margin-left: auto; color: var(--muted-2); }
607
+ @keyframes p-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
608
 
609
+ #p-shell { gap: 16px; align-items: stretch; }
610
  .p-panel {
611
+ background: var(--panel); border: 1px solid var(--hair); border-radius: 12px;
612
  padding: 14px 14px 12px; min-width: 0;
613
  }
614
+ .p-label { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
615
+ .p-count { float: right; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.02em; color: var(--muted-2); font-variant-numeric: tabular-nums; }
616
 
617
  #p-text, #p-text > div { background: transparent !important; border: 0 !important; box-shadow: none !important; }
618
  #p-text textarea {
619
  background: transparent !important; border: 0 !important; color: var(--ink) !important;
620
+ font-family: var(--font-display) !important; font-size: 0.94rem; line-height: 1.65;
621
  min-height: 360px; padding: 12px 2px 0;
622
  }
623
  #p-text textarea::placeholder { color: var(--muted-2) !important; }
624
 
625
+ #p-run, #p-clear { font-family: var(--font-mono) !important; font-size: 0.75rem !important; letter-spacing: 0.1em; min-height: 46px; }
626
+ #p-run { flex: 2; font-weight: 500; }
627
  #p-clear { flex: 1; background: transparent !important; color: var(--muted) !important; border: 1px solid var(--hair) !important; }
628
+ #p-clear:hover { color: var(--ink) !important; border-color: rgba(255,255,255,0.25) !important; }
629
 
630
+ #p-examples .label { font-family: var(--font-mono) !important; font-size: 0.66rem !important; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted) !important; }
631
  #p-examples .label svg { display: none; }
632
  #p-examples .gallery-item {
633
+ background: var(--panel-2) !important; border: 1px solid var(--hair) !important; border-radius: 8px !important;
634
  color: var(--muted) !important; font-size: 12px !important; min-height: 44px; padding: 8px 14px !important;
635
  text-align: left; transition: background .2s ease, color .2s ease, border-color .2s ease;
636
  }
637
+ #p-examples .gallery-item:hover { border-color: rgba(255,255,255,0.25) !important; color: var(--ink) !important; }
638
 
639
  #p-tabs .tab-container[role="tablist"] {
640
+ display: inline-flex; gap: 2px; background: var(--panel-2); border: 1px solid var(--hair);
641
+ border-radius: 10px; padding: 3px; margin-bottom: 12px;
642
  }
643
  #p-tabs .tab-container[role="tablist"] button {
644
+ appearance: none; border: 0 !important; background: transparent !important; border-radius: 7px !important;
645
  padding: 7px 14px !important; font-family: var(--font-mono) !important; font-size: 10.5px !important;
646
+ letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted) !important;
647
  }
648
  #p-tabs .tab-container[role="tablist"] button.selected,
649
  #p-tabs .tab-container[role="tablist"] button[aria-selected="true"] {
650
+ background: rgba(255,255,255,0.09) !important; color: var(--gold) !important;
651
  }
652
  #p-tabs .tab-container::after,
653
  #p-tabs .tab-container button.selected::after { content: none !important; }
654
 
655
+ #p-rendered { min-height: 360px; font-size: 0.94rem; line-height: 1.7; color: var(--ink); }
656
+ #p-rendered .prose { font-size: 0.94rem; line-height: 1.7; }
657
+ #p-rendered h1, #p-rendered h2, #p-rendered h3, #p-rendered h4 { color: var(--ink); font-weight: 600; line-height: 1.3; margin: 1.2em 0 0.5em; letter-spacing: -0.01em; }
658
+ #p-rendered h1 { font-size: 1.45em; }
659
+ #p-rendered h2 { font-size: 1.22em; }
660
+ #p-rendered h3 { font-size: 1.08em; }
661
+ #p-rendered p { margin: 0 0 0.85em; }
662
+ #p-rendered ul, #p-rendered ol { margin: 0 0 0.85em; padding-left: 1.5em; }
663
+ #p-rendered li { margin: 0.22em 0; }
664
  #p-rendered li::marker { color: var(--muted-2); }
665
+ #p-rendered a { color: var(--gold); text-decoration: underline; text-underline-offset: 2px; }
666
+ #p-rendered a:hover { color: #f2c578; }
667
+ #p-rendered blockquote { margin: 0 0 0.85em; padding: 2px 0 2px 14px; border-left: 2px solid var(--hair); color: var(--muted); }
668
+ #p-rendered hr { border: 0; border-top: 1px solid var(--hair); margin: 1.2em 0; }
669
+ #p-rendered code { font-family: var(--font-mono); font-size: 0.82em; background: var(--panel-2); border: 1px solid var(--hair); border-radius: 6px; padding: 1px 5px; }
670
+ #p-rendered pre { margin: 0 0 0.85em; padding: 14px 16px; background: var(--obsidian); border: 1px solid var(--hair); border-radius: 8px; overflow-x: auto; }
671
  #p-rendered pre code { background: none; border: 0; padding: 0; font-size: 0.82em; line-height: 1.6; }
672
+ #p-rendered table { border-collapse: collapse; margin: 0 0 0.85em; font-size: 0.9em; display: block; overflow-x: auto; }
673
+ #p-rendered th, #p-rendered td { border: 1px solid var(--hair); padding: 6px 11px; text-align: left; font-variant-numeric: tabular-nums; }
674
+ #p-rendered th { background: var(--panel-2); color: var(--ink); font-weight: 600; }
675
 
676
  #p-source {
677
  background: var(--obsidian) !important; border: 1px solid var(--hair-soft) !important;
678
+ border-radius: 8px !important; font-family: var(--font-mono) !important; min-height: 360px;
679
  }
680
  #p-source label[data-testid="block-label"] { display: none; }
681
  #p-source .cm-editor { background: transparent !important; }
682
+ #p-source .cm-content, #p-source .cm-scroller { color: var(--ink) !important; caret-color: var(--gold) !important; }
683
  #p-source .cm-gutters { background: transparent !important; color: var(--muted-2) !important; border-right: 1px solid var(--hair-soft) !important; }
684
 
685
+ #p-settings { background: transparent !important; border: 1px solid var(--hair-soft) !important; border-radius: 8px !important; margin-top: 4px; }
686
+ #p-settings > .label-wrap { font-family: var(--font-mono) !important; font-size: 0.68rem !important; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted) !important; }
687
  #p-settings > .label-wrap .icon { color: var(--muted-2) !important; }
688
  #p-settings .info-text { font-size: 0.72rem !important; color: var(--muted-2) !important; }
689
 
690
+ #p-candidates .wrap { display: inline-flex; gap: 2px; background: var(--panel-2); border: 1px solid var(--hair); border-radius: 10px; padding: 3px; }
691
+ #p-candidates label { border-radius: 7px; padding: 7px 16px; margin: 0 !important; cursor: pointer; transition: background .2s ease, color .2s ease; }
692
+ #p-candidates label span { font-family: var(--font-mono) !important; font-size: 11px !important; letter-spacing: 0.06em; color: var(--muted) !important; font-variant-numeric: tabular-nums; }
693
+ #p-candidates label.selected { background: rgba(255,255,255,0.09) !important; }
694
+ #p-candidates label.selected span { color: var(--gold) !important; }
695
+ #p-candidates input[type="radio"] { accent-color: var(--gold); }
 
696
 
697
  .p-foot {
698
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 14px;
699
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--hair-soft);
700
+ font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; line-height: 1.7; color: var(--muted-2);
701
  }
702
  .p-foot a { color: var(--muted); text-decoration: none; }
703
+ .p-foot a:hover { color: var(--gold); }
704
 
705
  @media (max-width: 820px) {
706
  .gradio-container { padding: 0 14px 20px; }
 
721
  theme = gr.themes.Base(
722
  primary_hue=gr.themes.colors.orange,
723
  neutral_hue=gr.themes.colors.gray,
724
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
725
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
726
  )
727