ripguy Claude Opus 4.7 (1M context) commited on
Commit
cdd2fbf
·
1 Parent(s): 55b7913

feat(spaces): Step 3 — VLM + Extract Space app.py, prompts shipped in core

Browse files

Both ZeroGPU Spaces now run the contract core.clients expects:
predict(prompt, schema_json, image_path) -> clean JSON string at /predict.

- hitech-vlm: Qwen3.6-35B-A3B-FP8 via AutoModelForImageTextToText, eager
module-scope load, @spaces.GPU(duration=120), torch_dtype="auto",
enable_thinking=False, greedy decode, max_new_tokens=3072. Handles image+text
and text-only.
- hitech-extract: NuExtract3 (bf16), @spaces.GPU(duration=90), schema_json passed
through as NuExtract's `template`. Handles image+text and text-only.
- Each Space strips <think>/fences/prose to a clean JSON object so clients.py's
json.loads never burns the single retry on a stray character.
- requirements.txt: dropped `spaces` (platform pins it) and `gradio` (sdk base
image provides it); pinned transformers>=4.57. README frontmatter: dropped
sdk_version, added python_version "3.12".

Prompts relocated INTO the package (core/src/hitech_ai_core/prompts/<app>/v1.md)
so they ship in the wheel for git-subdirectory installs; authored real v1
prompts for all four apps; api.py gains load_prompt() (importlib.resources),
replacing the placeholder dict. Verified the .md files are bundled in the built
wheel. README §7 layout updated. 10 core tests still pass.

Also adds project-scoped Supabase MCP (.mcp.json) for the Step-4 hitech-ai-platform
project (auth done interactively by the user).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +10 -5
  2. app.py +115 -1
  3. requirements.txt +9 -3
README.md CHANGED
@@ -4,15 +4,20 @@ emoji: 📄
4
  colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: "5.0"
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
- Structured extraction Space for Hi-Tech AI Platform.
 
 
14
 
15
- Model: `numind/NuExtract3`
16
- Hardware: ZeroGPU (set in Space settings)
17
 
18
- Populated in Step 3.
 
 
 
 
4
  colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
+ python_version: "3.12"
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
+ Structured extraction Space for Hi-Tech AI Platform — text→JSON and image→JSON
14
+ (scanned POs, spec sheets, emails). Routed the high-frequency extraction here to
15
+ keep the 36 B VLM's quota low.
16
 
17
+ Model: `numind/NuExtract3` (4.5 B, ~9 GB)
18
+ Hardware: ZeroGPU `large`; `@spaces.GPU(duration=90)`
19
 
20
+ API contract (called by `core.clients`):
21
+ `predict(prompt: str, schema_json: str, image_path: str | None) -> str` (JSON),
22
+ at `api_name="/predict"`. `schema_json` is passed through as NuExtract3's
23
+ `template`.
app.py CHANGED
@@ -1 +1,115 @@
1
- # Step 3: Extract Space — numind/NuExtract3 with @spaces.GPU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """hitech-extract — numind/NuExtract3 schema-constrained extraction Space.
2
+
3
+ Serves the contract that `core/clients.py::_predict_default` expects:
4
+
5
+ predict(prompt: str, schema_json: str, image_path: str | None) -> str # JSON
6
+
7
+ NuExtract3 is template-driven: it takes a `template=` chat-template kwarg
8
+ describing the fields to extract. We pass `schema_json` (the Pydantic
9
+ `model_json_schema()`) straight through as that template. NuExtract's own DSL
10
+ (`"verbatim-string"`, `"number"`, ...) differs from JSON Schema, but NuExtract is
11
+ a Qwen3.5 fine-tune that follows JSON structure, and `core.clients` retry-once +
12
+ Pydantic validation is the safety net. A JSON-Schema → DSL converter is deferred
13
+ until the Step-6 eval set shows it is needed (YAGNI for v1).
14
+
15
+ ZeroGPU notes (see huggingface-zerogpu skill): eager module-scope load; `import
16
+ spaces` is unconditional and omitted from requirements.txt; greedy decode with
17
+ thinking disabled for clean JSON.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+
24
+ import gradio as gr
25
+ import spaces
26
+ import torch
27
+ from PIL import Image
28
+ from transformers import AutoModelForImageTextToText, AutoProcessor
29
+
30
+ MODEL_ID = "numind/NuExtract3"
31
+ MAX_NEW_TOKENS = 2048
32
+
33
+ # Eager module-scope load. NuExtract3 is small (~9 GB bf16) and ships its own
34
+ # qwen3_5 modeling code via trust_remote_code.
35
+ processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
36
+ model = AutoModelForImageTextToText.from_pretrained(
37
+ MODEL_ID,
38
+ dtype=torch.bfloat16,
39
+ device_map="auto",
40
+ trust_remote_code=True,
41
+ ).eval()
42
+
43
+
44
+ def _clean_json(text: str) -> str:
45
+ """Drop <think> blocks, code fences and prose; keep the JSON object.
46
+
47
+ `core/clients.py` does an unforgiving `json.loads` on the return value.
48
+ """
49
+ text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
50
+ fence = re.search(r"```(?:json)?\s*(.*?)```", text, flags=re.DOTALL)
51
+ if fence:
52
+ text = fence.group(1)
53
+ start, end = text.find("{"), text.rfind("}")
54
+ if start != -1 and end > start:
55
+ text = text[start : end + 1]
56
+ return text.strip()
57
+
58
+
59
+ def _build_messages(prompt: str, image_path: str | None) -> list[dict]:
60
+ content: list[dict] = []
61
+ if image_path:
62
+ content.append({"type": "image", "image": Image.open(image_path).convert("RGB")})
63
+ content.append({"type": "text", "text": prompt})
64
+ return [{"role": "user", "content": content}]
65
+
66
+
67
+ @spaces.GPU(duration=90)
68
+ def predict(prompt: str, schema_json: str, image_path: str | None) -> str:
69
+ """Run one schema-constrained extraction and return a JSON string."""
70
+ messages = _build_messages(prompt, image_path)
71
+ inputs = processor.apply_chat_template(
72
+ messages,
73
+ add_generation_prompt=True,
74
+ tokenize=True,
75
+ return_dict=True,
76
+ return_tensors="pt",
77
+ template=schema_json, # NuExtract3 schema-constrained extraction
78
+ enable_thinking=False,
79
+ ).to(model.device)
80
+
81
+ with torch.inference_mode():
82
+ generated = model.generate(
83
+ **inputs,
84
+ max_new_tokens=MAX_NEW_TOKENS,
85
+ do_sample=False,
86
+ )
87
+
88
+ generated = generated[:, inputs["input_ids"].shape[1] :]
89
+ text = processor.batch_decode(
90
+ generated,
91
+ skip_special_tokens=True,
92
+ clean_up_tokenization_spaces=False,
93
+ )[0]
94
+ return _clean_json(text)
95
+
96
+
97
+ # gr.Interface exposes `fn` at api_name="/predict", which is what core.clients calls.
98
+ demo = gr.Interface(
99
+ fn=predict,
100
+ inputs=[
101
+ gr.Textbox(label="prompt"),
102
+ gr.Textbox(label="schema_json"),
103
+ gr.Image(type="filepath", label="image_path"),
104
+ ],
105
+ outputs=gr.Textbox(label="json"),
106
+ title="Hi-Tech Extract",
107
+ description=(
108
+ "NuExtract3 — schema-constrained document→JSON extraction (text or scanned "
109
+ "image) for the Hi-Tech AI Platform. Returns a JSON string for core.clients "
110
+ "to Pydantic-validate."
111
+ ),
112
+ )
113
+
114
+ if __name__ == "__main__":
115
+ demo.launch()
requirements.txt CHANGED
@@ -1,3 +1,9 @@
1
- # Pinned in Step 3
2
- gradio>=4.0
3
- spaces
 
 
 
 
 
 
 
1
+ # hitech-extract numind/NuExtract3 on ZeroGPU.
2
+ # NOTE: do NOT add `spaces` here — the ZeroGPU platform pins its own copy and a
3
+ # conflicting pin breaks the build (see huggingface-zerogpu skill).
4
+ # torch / torchvision come from the ZeroGPU base image; pinning them here risks a
5
+ # version fight. If the build errors on a missing vision dep, add it here.
6
+ # gradio is provided by the `sdk: gradio` base image — do not pin it here.
7
+ transformers>=4.57.0
8
+ accelerate>=1.0
9
+ pillow