Upload app.py with huggingface_hub
Browse files
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,
|
| 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 |
-
|
| 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 =
|
| 198 |
-
if "zerogpu" in text and (
|
| 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 |
-
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|