Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +44 -3
- index.html +42 -8
app.py
CHANGED
|
@@ -12,6 +12,7 @@ import sys
|
|
| 12 |
import tempfile
|
| 13 |
import threading
|
| 14 |
import time
|
|
|
|
| 15 |
import uuid
|
| 16 |
import zipfile
|
| 17 |
from pathlib import Path
|
|
@@ -641,7 +642,10 @@ MODEL = AutoModel.from_pretrained(
|
|
| 641 |
torch.set_grad_enabled(False)
|
| 642 |
print("Unlimited-OCR model ready.", flush=True)
|
| 643 |
|
| 644 |
-
app = Server(title="Unlimited-OCR Hugging Face Space")
|
|
|
|
|
|
|
|
|
|
| 645 |
_MODEL_LOCK = threading.Lock()
|
| 646 |
_THREAD_CONTEXT = threading.local()
|
| 647 |
_ACTIVE_REQUESTS: dict[str, threading.Event] = {}
|
|
@@ -726,7 +730,19 @@ def _run_model_worker(
|
|
| 726 |
except GenerationCancelled:
|
| 727 |
emit("cancelled", None)
|
| 728 |
except BaseException as exc:
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
finally:
|
| 731 |
ring_window = getattr(MODEL.config, "_ring_window", None)
|
| 732 |
if ring_window is not None:
|
|
@@ -759,6 +775,12 @@ def run_ocr_batch(
|
|
| 759 |
) -> Iterator[dict[str, Any]]:
|
| 760 |
"""Run one ZeroGPU batch and stream UI events through Gradio's SSE queue."""
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
session_dir = _session_dir(session_id)
|
| 763 |
pages = _read_metadata(session_dir).get("pages") or []
|
| 764 |
if not _SAFE_REQUEST_ID.fullmatch(request_id):
|
|
@@ -837,9 +859,22 @@ def run_ocr_batch(
|
|
| 837 |
elif kind == "final":
|
| 838 |
final_output = str(value or "")
|
| 839 |
elif kind == "error":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
yield {
|
| 841 |
"event": "error",
|
| 842 |
-
"data": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
}
|
| 844 |
return
|
| 845 |
elif kind == "cancelled":
|
|
@@ -887,6 +922,12 @@ def run_ocr_batch(
|
|
| 887 |
"mapped_pages": len(pairs),
|
| 888 |
},
|
| 889 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
finally:
|
| 891 |
cancel.set()
|
| 892 |
if worker.is_alive():
|
|
|
|
| 12 |
import tempfile
|
| 13 |
import threading
|
| 14 |
import time
|
| 15 |
+
import traceback
|
| 16 |
import uuid
|
| 17 |
import zipfile
|
| 18 |
from pathlib import Path
|
|
|
|
| 642 |
torch.set_grad_enabled(False)
|
| 643 |
print("Unlimited-OCR model ready.", flush=True)
|
| 644 |
|
| 645 |
+
app = Server(debug=True, title="Unlimited-OCR Hugging Face Space")
|
| 646 |
+
# Hugging Face's Gradio hot-reload launcher looks for the conventional `demo`
|
| 647 |
+
# symbol. Keep both names pointing at the same Server instance.
|
| 648 |
+
demo = app
|
| 649 |
_MODEL_LOCK = threading.Lock()
|
| 650 |
_THREAD_CONTEXT = threading.local()
|
| 651 |
_ACTIVE_REQUESTS: dict[str, threading.Event] = {}
|
|
|
|
| 730 |
except GenerationCancelled:
|
| 731 |
emit("cancelled", None)
|
| 732 |
except BaseException as exc:
|
| 733 |
+
error_traceback = traceback.format_exc()
|
| 734 |
+
print(
|
| 735 |
+
f"[OCR ERROR] request={settings.request_id} "
|
| 736 |
+
f"pages={settings.page_indices}: {exc}\n{error_traceback}",
|
| 737 |
+
flush=True,
|
| 738 |
+
)
|
| 739 |
+
emit(
|
| 740 |
+
"error",
|
| 741 |
+
{
|
| 742 |
+
"message": str(exc) or exc.__class__.__name__,
|
| 743 |
+
"exception_type": exc.__class__.__name__,
|
| 744 |
+
},
|
| 745 |
+
)
|
| 746 |
finally:
|
| 747 |
ring_window = getattr(MODEL.config, "_ring_window", None)
|
| 748 |
if ring_window is not None:
|
|
|
|
| 775 |
) -> Iterator[dict[str, Any]]:
|
| 776 |
"""Run one ZeroGPU batch and stream UI events through Gradio's SSE queue."""
|
| 777 |
|
| 778 |
+
print(
|
| 779 |
+
f"[OCR START] request={request_id} batch={batch_number}/{total_batches} "
|
| 780 |
+
f"session={session_id} pages={page_indices} mode={mode} "
|
| 781 |
+
f"max_tokens={max_tokens}",
|
| 782 |
+
flush=True,
|
| 783 |
+
)
|
| 784 |
session_dir = _session_dir(session_id)
|
| 785 |
pages = _read_metadata(session_dir).get("pages") or []
|
| 786 |
if not _SAFE_REQUEST_ID.fullmatch(request_id):
|
|
|
|
| 859 |
elif kind == "final":
|
| 860 |
final_output = str(value or "")
|
| 861 |
elif kind == "error":
|
| 862 |
+
if isinstance(value, dict):
|
| 863 |
+
error_message = value.get("message") or "Unknown inference error"
|
| 864 |
+
exception_type = value.get("exception_type") or "Error"
|
| 865 |
+
else:
|
| 866 |
+
error_message = str(value) or "Unknown inference error"
|
| 867 |
+
exception_type = type(value).__name__
|
| 868 |
yield {
|
| 869 |
"event": "error",
|
| 870 |
+
"data": {
|
| 871 |
+
"message": (
|
| 872 |
+
f"Hugging Face inference failed "
|
| 873 |
+
f"({exception_type}): {error_message}"
|
| 874 |
+
),
|
| 875 |
+
"request_id": request_id,
|
| 876 |
+
"batch": batch_number,
|
| 877 |
+
},
|
| 878 |
}
|
| 879 |
return
|
| 880 |
elif kind == "cancelled":
|
|
|
|
| 922 |
"mapped_pages": len(pairs),
|
| 923 |
},
|
| 924 |
}
|
| 925 |
+
print(
|
| 926 |
+
f"[OCR DONE] request={request_id} batch={batch_number}/{total_batches} "
|
| 927 |
+
f"characters={len(raw_output)} mapped_pages={len(pairs)} "
|
| 928 |
+
f"latency={latency:.3f}s",
|
| 929 |
+
flush=True,
|
| 930 |
+
)
|
| 931 |
finally:
|
| 932 |
cancel.set()
|
| 933 |
if worker.is_alive():
|
index.html
CHANGED
|
@@ -327,6 +327,14 @@
|
|
| 327 |
requestAnimationFrame(() => { state.rawScrollScheduled = false; viewer.scrollTop = viewer.scrollHeight; });
|
| 328 |
}
|
| 329 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
function semanticColor(label) {
|
| 331 |
const key = String(label || 'box').trim().toLowerCase();
|
| 332 |
if (colors[key]) return colors[key];
|
|
@@ -766,30 +774,53 @@
|
|
| 766 |
$('#latencyText').textContent = `${data.elapsed_seconds}s`; $('#downloadButton').disabled = false; renderAllPages(); selectPage(state.currentPage);
|
| 767 |
}
|
| 768 |
if (name === 'cancelled') { setRunning(false); setStatus('STOPPED'); }
|
| 769 |
-
if (name === 'error') {
|
|
|
|
|
|
|
|
|
|
| 770 |
}
|
| 771 |
async function consumeGradioJob(eventId, signal) {
|
| 772 |
const response = await fetch(`/gradio_api/call/run_ocr_batch/${encodeURIComponent(eventId)}`, {signal});
|
| 773 |
-
if (!response.ok) {
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
while (true) {
|
| 776 |
const {value, done} = await reader.read(); buffer += decoder.decode(value || new Uint8Array(), {stream: !done}).replace(/\r\n/g, '\n');
|
| 777 |
let boundary;
|
| 778 |
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
| 779 |
const block = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); let name = 'message'; const dataLines = [];
|
| 780 |
for (const line of block.split('\n')) { if (line.startsWith('event:')) name = line.slice(6).trim(); if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart()); }
|
| 781 |
-
if (name === 'error')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
if (dataLines.length && (name === 'generating' || name === 'complete')) {
|
| 783 |
-
|
| 784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 785 |
}
|
| 786 |
}
|
| 787 |
if (done) break;
|
| 788 |
}
|
|
|
|
| 789 |
}
|
| 790 |
async function callZeroGpuBatch(args, signal) {
|
| 791 |
const response = await fetch('/gradio_api/call/run_ocr_batch', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({data:args}), signal});
|
| 792 |
-
const
|
|
|
|
|
|
|
| 793 |
await consumeGradioJob(data.event_id, signal);
|
| 794 |
}
|
| 795 |
async function startGeneration() {
|
|
@@ -815,7 +846,10 @@
|
|
| 815 |
}
|
| 816 |
if (state.running) processEvent('done', {completed_pages:Object.keys(state.results).length, selected_pages:pageIndices.length, elapsed_seconds:Math.round((performance.now() - startedAt) / 100) / 10});
|
| 817 |
} catch (error) {
|
| 818 |
-
if (error.name !== 'AbortError') {
|
|
|
|
|
|
|
|
|
|
| 819 |
setRunning(false);
|
| 820 |
}
|
| 821 |
}
|
|
|
|
| 327 |
requestAnimationFrame(() => { state.rawScrollScheduled = false; viewer.scrollTop = viewer.scrollHeight; });
|
| 328 |
}
|
| 329 |
}
|
| 330 |
+
function appendStreamDiagnostic(message) {
|
| 331 |
+
const output = $('#rawOutput');
|
| 332 |
+
const text = String(message || 'Unknown streaming error');
|
| 333 |
+
if (output.textContent && !output.textContent.endsWith('\n')) output.appendChild(document.createTextNode('\n'));
|
| 334 |
+
output.appendChild(document.createTextNode(`\n===== STREAM ERROR =====\n${text}\n`));
|
| 335 |
+
output.parentElement.scrollTop = output.parentElement.scrollHeight;
|
| 336 |
+
console.error('[Unlimited-OCR stream]', text);
|
| 337 |
+
}
|
| 338 |
function semanticColor(label) {
|
| 339 |
const key = String(label || 'box').trim().toLowerCase();
|
| 340 |
if (colors[key]) return colors[key];
|
|
|
|
| 774 |
$('#latencyText').textContent = `${data.elapsed_seconds}s`; $('#downloadButton').disabled = false; renderAllPages(); selectPage(state.currentPage);
|
| 775 |
}
|
| 776 |
if (name === 'cancelled') { setRunning(false); setStatus('STOPPED'); }
|
| 777 |
+
if (name === 'error') {
|
| 778 |
+
const detail = data.message || 'Streaming failed';
|
| 779 |
+
setRunning(false); setStatus('ERROR', 'error'); appendStreamDiagnostic(detail); showToast(detail);
|
| 780 |
+
}
|
| 781 |
}
|
| 782 |
async function consumeGradioJob(eventId, signal) {
|
| 783 |
const response = await fetch(`/gradio_api/call/run_ocr_batch/${encodeURIComponent(eventId)}`, {signal});
|
| 784 |
+
if (!response.ok) {
|
| 785 |
+
const body = await response.text().catch(() => '');
|
| 786 |
+
let detail = body; try { const parsed = JSON.parse(body); detail = parsed.detail || parsed.error || body; } catch (_) {}
|
| 787 |
+
throw new Error(`Gradio result HTTP ${response.status}${detail ? `: ${detail}` : ''} [job ${eventId}]`);
|
| 788 |
+
}
|
| 789 |
+
if (!response.body) throw new Error(`Gradio returned an empty stream [job ${eventId}]`);
|
| 790 |
+
const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let lastPayload = ''; let completed = false;
|
| 791 |
while (true) {
|
| 792 |
const {value, done} = await reader.read(); buffer += decoder.decode(value || new Uint8Array(), {stream: !done}).replace(/\r\n/g, '\n');
|
| 793 |
let boundary;
|
| 794 |
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
| 795 |
const block = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); let name = 'message'; const dataLines = [];
|
| 796 |
for (const line of block.split('\n')) { if (line.startsWith('event:')) name = line.slice(6).trim(); if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart()); }
|
| 797 |
+
if (name === 'error') {
|
| 798 |
+
const rawError = dataLines.join('\n'); let detail = rawError;
|
| 799 |
+
try { const parsedError = JSON.parse(rawError); detail = parsedError.message || parsedError.error || parsedError.detail || rawError; } catch (_) {}
|
| 800 |
+
throw new Error(`${detail || 'ZeroGPU job failed without an error payload'} [job ${eventId}]`);
|
| 801 |
+
}
|
| 802 |
if (dataLines.length && (name === 'generating' || name === 'complete')) {
|
| 803 |
+
let parsed;
|
| 804 |
+
try { parsed = JSON.parse(dataLines.join('\n')); }
|
| 805 |
+
catch (error) { throw new Error(`Invalid Gradio SSE JSON: ${error.message} [job ${eventId}]`); }
|
| 806 |
+
if (name === 'complete') completed = true;
|
| 807 |
+
const update = Array.isArray(parsed) ? parsed[0] : parsed; const signature = JSON.stringify(update);
|
| 808 |
+
if (update && update.event && signature !== lastPayload) {
|
| 809 |
+
lastPayload = signature;
|
| 810 |
+
if (update.event === 'error') throw new Error(`${(update.data && update.data.message) || 'OCR failed'} [job ${eventId}]`);
|
| 811 |
+
processEvent(update.event, update.data || {});
|
| 812 |
+
}
|
| 813 |
}
|
| 814 |
}
|
| 815 |
if (done) break;
|
| 816 |
}
|
| 817 |
+
if (!completed) throw new Error(`Gradio stream closed before the job completed [job ${eventId}]`);
|
| 818 |
}
|
| 819 |
async function callZeroGpuBatch(args, signal) {
|
| 820 |
const response = await fetch('/gradio_api/call/run_ocr_batch', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({data:args}), signal});
|
| 821 |
+
const body = await response.text().catch(() => ''); let data = {};
|
| 822 |
+
try { data = JSON.parse(body); } catch (_) {}
|
| 823 |
+
if (!response.ok || !data.event_id) throw new Error(`${data.detail || data.error || body || `Unable to queue ZeroGPU job (${response.status})`} [queue HTTP ${response.status}]`);
|
| 824 |
await consumeGradioJob(data.event_id, signal);
|
| 825 |
}
|
| 826 |
async function startGeneration() {
|
|
|
|
| 846 |
}
|
| 847 |
if (state.running) processEvent('done', {completed_pages:Object.keys(state.results).length, selected_pages:pageIndices.length, elapsed_seconds:Math.round((performance.now() - startedAt) / 100) / 10});
|
| 848 |
} catch (error) {
|
| 849 |
+
if (error.name !== 'AbortError') {
|
| 850 |
+
const detail = error && error.message ? error.message : String(error);
|
| 851 |
+
setStatus('STREAM ERROR', 'error'); appendStreamDiagnostic(detail); showToast(detail);
|
| 852 |
+
}
|
| 853 |
setRunning(false);
|
| 854 |
}
|
| 855 |
}
|