OpceanAI commited on
Commit
11c5e87
·
verified ·
1 Parent(s): 5beebb1

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +39 -24
app.py CHANGED
@@ -59,22 +59,26 @@ def _counter(text: str) -> str:
59
  )
60
 
61
 
 
 
 
 
 
 
 
 
 
62
  def _normalize(text: str | None, num_candidates: int | None, structured_fallback: bool | None) -> tuple[str, int, bool]:
63
  text = (text or "").strip()
64
- if num_candidates is None:
65
- num_candidates = 3
66
  if structured_fallback is None:
67
  structured_fallback = True
68
- return text, max(1, min(3, int(num_candidates))), bool(structured_fallback)
69
 
70
 
71
  def _estimate_duration(text, num_candidates=3, structured_fallback=True, *args, **kwargs):
72
  if structured_fallback and infer_kind((text or "").strip()) in ("html", "data"):
73
  return 10
74
- try:
75
- candidates = int(num_candidates)
76
- except (TypeError, ValueError):
77
- candidates = 3
78
  return min(30, max(12, 8 + len(text or "") // 200 + 4 * (candidates - 1)))
79
 
80
 
@@ -128,6 +132,12 @@ def convert(text: str | None = None, num_candidates: int | None = None, structur
128
  text = (text or "").strip()
129
  if not text:
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)
@@ -150,6 +160,8 @@ def convert(text: str | None = None, num_candidates: int | None = None, structur
150
  source_label = routes.get(result.get("route"), "PASITA + RERANK")
151
  taken = result["seconds"] if result["seconds"] else result["elapsed"]
152
  detail = f"{source_label} / {numbers} / {overlap} / {taken:.1f}S"
 
 
153
  return result["markdown"], result["markdown"], _status("DONE", "ready", detail)
154
 
155
 
@@ -170,13 +182,6 @@ def gpu_convert(text: str | None = None, num_candidates: int | None = None, stru
170
  return _run_harness(text, num_candidates, structured_fallback, max_output_tokens)
171
 
172
 
173
- COUNTER_JS = """(text) => {
174
- const t = (text || '').trim();
175
- const words = t ? t.split(/\\s+/).length : 0;
176
- const chars = (text || '').length;
177
- return '<span class="p-label">PLAIN TEXT</span><span class="p-count">' + chars.toLocaleString() + ' CHARS / ' + words.toLocaleString() + ' WORDS</span>';
178
- }"""
179
-
180
  API_DESCRIPTION = """
181
  OpenAI-style REST API for [PASITA](https://huggingface.co/OpceanAI/PASITA), an 88M
182
  from-scratch model that turns plain text into faithful Markdown. No API key is required.
@@ -194,9 +199,11 @@ def _api_error(message: str, error_type: str = "invalid_request_error", status:
194
 
195
  def _exception_to_error(exc: Exception) -> JSONResponse:
196
  message = f"{type(exc).__name__}: {exc}"
197
- text = str(exc).lower()
198
- if "zerogpu" in text and ("quota" in text or "limit" in text or "quota" in message.lower()):
199
  return _api_error(str(exc), "rate_limit_error", status=429, code="gpu_quota_exceeded")
 
 
200
  return _api_error(message, "server_error", status=500)
201
 
202
 
@@ -208,11 +215,12 @@ def _with_request_context(request: fastapi.Request, text: str, candidates: int,
208
  return gpu_convert(text, candidates, fallback, max_tokens)
209
  finally:
210
  LocalContext.request.reset(token)
211
- return gpu_convert(text, candidates, fallback)
212
 
213
 
214
- def _token_counts(text: str, markdown: str) -> tuple[int, int]:
215
- prompt_tokens = len(tokenizer(text, add_special_tokens=True)["input_ids"])
 
216
  completion_tokens = len(tokenizer(markdown, add_special_tokens=False)["input_ids"])
217
  return prompt_tokens, completion_tokens
218
 
@@ -249,7 +257,7 @@ class _LegacyConvertBodyMiddleware:
249
  self.app = app
250
 
251
  async def __call__(self, scope, receive, send):
252
- if scope["type"] != "http" or scope.get("method") != "POST":
253
  await self.app(scope, receive, send)
254
  return
255
  path = scope.get("path", "")
@@ -314,14 +322,18 @@ def _validate(body: ChatCompletionRequest | ConvertRequest, text: str | None) ->
314
  code="input_too_long",
315
  )
316
  candidates = getattr(body, "candidates", 3)
317
- if not 1 <= int(candidates) <= 3:
 
 
 
 
318
  return _api_error("candidates must be between 1 and 3.", code="invalid_candidates")
319
  return None
320
 
321
 
322
  def _result_payload(result: dict, text: str, started: float) -> dict:
323
  markdown = result["markdown"]
324
- prompt_tokens, completion_tokens = _token_counts(text, markdown)
325
  return {
326
  "markdown": markdown,
327
  "route": result.get("route", "model"),
@@ -366,8 +378,11 @@ def _extract_user_text(messages: list[ChatMessage]) -> str | None:
366
  parts.append(str(part.get("text", "")))
367
  elif isinstance(part, str):
368
  parts.append(part)
369
- return "\n".join(p for p in parts if p)
370
- return None
 
 
 
371
  return None
372
 
373
 
 
59
  )
60
 
61
 
62
+ def _clamp_candidates(num_candidates: int | None) -> int:
63
+ if num_candidates is None:
64
+ return 3
65
+ try:
66
+ return max(1, min(3, int(num_candidates)))
67
+ except (TypeError, ValueError):
68
+ return 3
69
+
70
+
71
  def _normalize(text: str | None, num_candidates: int | None, structured_fallback: bool | None) -> tuple[str, int, bool]:
72
  text = (text or "").strip()
 
 
73
  if structured_fallback is None:
74
  structured_fallback = True
75
+ return text, _clamp_candidates(num_candidates), bool(structured_fallback)
76
 
77
 
78
  def _estimate_duration(text, num_candidates=3, structured_fallback=True, *args, **kwargs):
79
  if structured_fallback and infer_kind((text or "").strip()) in ("html", "data"):
80
  return 10
81
+ candidates = _clamp_candidates(num_candidates if not isinstance(num_candidates, bool) else 3)
 
 
 
82
  return min(30, max(12, 8 + len(text or "") // 200 + 4 * (candidates - 1)))
83
 
84
 
 
132
  text = (text or "").strip()
133
  if not text:
134
  return EMPTY_MD, "", _status("READY / PASTE A DOCUMENT TO CONVERT")
135
+ if len(text) > MAX_INPUT_CHARS:
136
+ return (
137
+ f"> Input too long ({len(text):,} chars). The limit is {MAX_INPUT_CHARS:,} characters. Shorten the document and try again.",
138
+ "",
139
+ _status("ERROR / INPUT TOO LONG", "error"),
140
+ )
141
 
142
  try:
143
  result = _run_harness(text, num_candidates, structured_fallback, max_output_tokens)
 
160
  source_label = routes.get(result.get("route"), "PASITA + RERANK")
161
  taken = result["seconds"] if result["seconds"] else result["elapsed"]
162
  detail = f"{source_label} / {numbers} / {overlap} / {taken:.1f}S"
163
+ if result.get("strategy", {}).get("truncated_input"):
164
+ detail += " / INPUT TRUNCATED TO 2048 TOKENS"
165
  return result["markdown"], result["markdown"], _status("DONE", "ready", detail)
166
 
167
 
 
182
  return _run_harness(text, num_candidates, structured_fallback, max_output_tokens)
183
 
184
 
 
 
 
 
 
 
 
185
  API_DESCRIPTION = """
186
  OpenAI-style REST API for [PASITA](https://huggingface.co/OpceanAI/PASITA), an 88M
187
  from-scratch model that turns plain text into faithful Markdown. No API key is required.
 
199
 
200
  def _exception_to_error(exc: Exception) -> JSONResponse:
201
  message = f"{type(exc).__name__}: {exc}"
202
+ text = message.lower()
203
+ if "zerogpu" in text and any(k in text for k in ("quota", "exhausted", "insufficient", "limit")):
204
  return _api_error(str(exc), "rate_limit_error", status=429, code="gpu_quota_exceeded")
205
+ if any(k in text for k in ("illegal duration", "task aborted", "queue is full", "queue full")):
206
+ return _api_error(str(exc), "rate_limit_error", status=429, code="gpu_busy")
207
  return _api_error(message, "server_error", status=500)
208
 
209
 
 
215
  return gpu_convert(text, candidates, fallback, max_tokens)
216
  finally:
217
  LocalContext.request.reset(token)
218
+ return gpu_convert(text, candidates, fallback, max_tokens)
219
 
220
 
221
+ def _token_counts(text: str, markdown: str, prompt_tokens: int | None = None) -> tuple[int, int]:
222
+ if not prompt_tokens:
223
+ prompt_tokens = len(tokenizer(text, add_special_tokens=True)["input_ids"])
224
  completion_tokens = len(tokenizer(markdown, add_special_tokens=False)["input_ids"])
225
  return prompt_tokens, completion_tokens
226
 
 
257
  self.app = app
258
 
259
  async def __call__(self, scope, receive, send):
260
+ if scope.get("type") != "http" or scope.get("method") != "POST":
261
  await self.app(scope, receive, send)
262
  return
263
  path = scope.get("path", "")
 
322
  code="input_too_long",
323
  )
324
  candidates = getattr(body, "candidates", 3)
325
+ try:
326
+ candidates = int(candidates)
327
+ except (TypeError, ValueError):
328
+ return _api_error("candidates must be between 1 and 3.", code="invalid_candidates")
329
+ if not 1 <= candidates <= 3:
330
  return _api_error("candidates must be between 1 and 3.", code="invalid_candidates")
331
  return None
332
 
333
 
334
  def _result_payload(result: dict, text: str, started: float) -> dict:
335
  markdown = result["markdown"]
336
+ prompt_tokens, completion_tokens = _token_counts(text, markdown, result.get("prompt_tokens"))
337
  return {
338
  "markdown": markdown,
339
  "route": result.get("route", "model"),
 
378
  parts.append(str(part.get("text", "")))
379
  elif isinstance(part, str):
380
  parts.append(part)
381
+ joined = "\n".join(p for p in parts if p)
382
+ if joined:
383
+ return joined
384
+ continue
385
+ continue
386
  return None
387
 
388