Marcin-XStudio commited on
Commit
7e915f9
·
1 Parent(s): 68c73f1

inital commit

Browse files
Files changed (2) hide show
  1. app.py +157 -0
  2. requirements.txt +86 -0
app.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import torch
3
+ import time
4
+ from docling.document_converter import DocumentConverter
5
+ from transformers import AutoModelForCausalLM, AutoTokenizer
6
+ from pathlib import Path
7
+ from fastapi import FastAPI, File, UploadFile, HTTPException
8
+ import os
9
+ from dotenv import load_dotenv
10
+ import tempfile
11
+ from supabase import create_client
12
+
13
+
14
+
15
+ load_dotenv()
16
+ app = FastAPI()
17
+
18
+ model_name = "numind/NuExtract-1.5-tiny"
19
+ device = "mps" if torch.backends.mps.is_available() else "cpu"
20
+ dtype = torch.float16 if device=="mps" else torch.float32
21
+
22
+ @app.on_event("startup")
23
+ def startup_supabase():
24
+ global supabase
25
+ supabase = create_client(
26
+ os.getenv("DATABASE_URL"),
27
+ os.getenv("SUPABASE_SERVICE_ROLE_KEY")
28
+ )
29
+
30
+ @app.on_event("startup")
31
+ def load_model():
32
+ print("Loading model and tokenizer...", flush=True)
33
+ global model, tokenizer
34
+ model = AutoModelForCausalLM.from_pretrained(
35
+ model_name, torch_dtype=dtype, trust_remote_code=True
36
+ ).to(device).eval()
37
+ tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
38
+ print("Model and tokenizer loaded.", flush=True)
39
+
40
+ def predict_NuExtract(texts, template, batch_size=10, max_length=5096, max_new_tokens=1024):
41
+ print("Starting NuExtract prediction...", flush=True)
42
+ start_time = time.perf_counter()
43
+ template_str = json.dumps(json.loads(template), indent=4)
44
+ prompts = [
45
+ "<|input|>\n"
46
+ "### Instruction:\n"
47
+ "Remplis la template JSON avec les informations extraits du texte.\n"
48
+ "Exemples types de formations : CAP Boucherie, Licence Pro Métiers de l’Énergétique, Baccalauréat Général\n"
49
+ "Exemples catégories de formations : Transport, énergie, langues, esthétique\n"
50
+ "Exemples mobilités : permis B, permis C, permis D. si y'a juste la mention de permis on considère que c'est le permis B\n"
51
+ "Output *only* the completed JSON.\n"
52
+ "### Template:\n"
53
+ f"{template_str}\n"
54
+ "### Text:\n"
55
+ f"{text}\n\n"
56
+ "<|output|>"
57
+ for text in texts
58
+ ]
59
+ print("Prompts prepared.", flush=True)
60
+ outputs = []
61
+ with torch.no_grad():
62
+ for i in range(0, len(prompts), batch_size):
63
+ batch = prompts[i : i+batch_size]
64
+ enc = tokenizer(
65
+ batch,
66
+ return_tensors="pt",
67
+ truncation=True,
68
+ padding=True,
69
+ max_length=max_length
70
+ ).to(device)
71
+ print(f"Generating outputs with model for batch {i//batch_size+1}...", flush=True)
72
+ ids = model.generate(**enc, max_new_tokens=max_new_tokens, use_cache=False)
73
+ outputs += tokenizer.batch_decode(ids, skip_special_tokens=True)
74
+ print("Outputs generated.", flush=True)
75
+ elapsed = time.perf_counter() - start_time
76
+ print(f"NuExtract prediction completed in {elapsed:.2f} seconds.", flush=True)
77
+ return [out.split("<|output|>")[1] for out in outputs]
78
+
79
+ template = """{
80
+ "nom": "", "email": "", "telephone": "",
81
+ "education": [{"type_de_formation": "", "categorie_de_formation": "", "annee_debut": "", "annee_fin": ""}],
82
+ "experience": [{"position": "", "entreprise": "", "annee_debut": "", "annee_fin": ""}],
83
+ "types_des_permis_de_conduire": [""]
84
+ }"""
85
+
86
+ data_model = {
87
+ "experience": [{"start_date": "", "end_date": "", "job_category_id": ""}],
88
+ "education": [{"training_type_id": "", "training_category_id": "", "start_date": "", "end_date": ""}],
89
+ "email": "",
90
+ "phone": "",
91
+ "mobility": [{"id": "", "title": ""}]
92
+ }
93
+
94
+ @app.post("/extract")
95
+ async def extract(file: UploadFile = File(...)):
96
+ suffix = Path(file.filename).suffix or ".pdf"
97
+ try:
98
+ # Create one global client; reused across calls
99
+ # supabase = create_client(os.environ.get("DATABASE_URL"), os.environ.get("SUPABASE_SERVICE_ROLE_KEY"))
100
+
101
+ # Use Supabase client to query tables, map names to IDs
102
+ job_categories = supabase.table("Job_category").select("id, title").execute()
103
+
104
+ print("Job Categories: ", job_categories)
105
+
106
+ training_types = supabase.table("Training_type").select("id, title").execute()
107
+
108
+ print("Training Types: ", training_types)
109
+
110
+ training_categories = supabase.table("Training_category").select("id, title").execute()
111
+
112
+ print("Training Categories: ", training_categories)
113
+
114
+ mobility = supabase.table("Mobility").select("id, title").execute()
115
+
116
+ print("Mobility: ", mobility)
117
+
118
+ # … your LLM logic …
119
+
120
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
121
+ data = await file.read()
122
+ tmp.write(data)
123
+ tmp_path = tmp.name
124
+ print(f"Upload saved to {tmp_path}", flush=True)
125
+ except Exception as e:
126
+ print(f"Cannot save upload: {e}", flush=True)
127
+ raise HTTPException(400, f"Cannot save upload: {e}")
128
+
129
+ try:
130
+ converter = DocumentConverter()
131
+ result = converter.convert(tmp_path)
132
+ raw_text = result.document.export_to_text()
133
+ print("Docling conversion complete.", flush=True)
134
+ except Exception as e:
135
+ print(f"Docling error: {e}", flush=True)
136
+ raise HTTPException(500, f"Docling error: {e}")
137
+ finally:
138
+ try: os.remove(tmp_path)
139
+ except OSError: pass
140
+
141
+ try:
142
+ extracted_json_str = predict_NuExtract([raw_text], template)[0]
143
+ print("Extraction with NuExtract complete.", flush=True)
144
+ print("⏺ RAW MODEL OUTPUT:\n", extracted_json_str)
145
+ print("⏺ RAW MODEL OUTPUT (repr):\n", repr(extracted_json_str))
146
+
147
+ print("Clearing Cache", flush=True)
148
+ if device == "mps":
149
+ torch.mps.empty_cache()
150
+ elif device == "cuda":
151
+ torch.cuda.empty_cache()
152
+ elif device == "cpu":
153
+ torch.cpu.empty_cache()
154
+ return {"result": json.loads(extracted_json_str)}
155
+ except Exception as e:
156
+ print(f"Extraction error: {e}", flush=True)
157
+ raise HTTPException(500, f"Extraction error: {e}")
requirements.txt ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ attrs==25.3.0
3
+ beautifulsoup4==4.13.4
4
+ certifi==2025.4.26
5
+ charset-normalizer==3.4.2
6
+ click==8.1.8
7
+ dill==0.4.0
8
+ docling==2.32.0
9
+ docling-core==2.31.0
10
+ docling-ibm-models==3.4.3
11
+ docling-parse==4.0.1
12
+ easyocr==1.7.2
13
+ et_xmlfile==2.0.0
14
+ filelock==3.18.0
15
+ filetype==1.2.0
16
+ fsspec==2025.3.2
17
+ huggingface-hub==0.31.4
18
+ idna==3.10
19
+ imageio==2.37.0
20
+ Jinja2==3.1.6
21
+ jsonlines==3.1.0
22
+ jsonref==1.1.0
23
+ jsonschema==4.23.0
24
+ jsonschema-specifications==2025.4.1
25
+ latex2mathml==3.78.0
26
+ lazy_loader==0.4
27
+ lxml==5.4.0
28
+ markdown-it-py==3.0.0
29
+ marko==2.1.3
30
+ MarkupSafe==3.0.2
31
+ mdurl==0.1.2
32
+ mpire==2.10.2
33
+ mpmath==1.3.0
34
+ multiprocess==0.70.18
35
+ networkx==3.4.2
36
+ ninja==1.11.1.4
37
+ numpy==2.2.6
38
+ opencv-python-headless==4.11.0.86
39
+ openpyxl==3.1.5
40
+ packaging==25.0
41
+ pandas==2.2.3
42
+ pillow==11.2.1
43
+ pluggy==1.6.0
44
+ pyclipper==1.3.0.post6
45
+ pydantic==2.11.4
46
+ pydantic-settings==2.9.1
47
+ pydantic_core==2.33.2
48
+ Pygments==2.19.1
49
+ pylatexenc==2.10
50
+ pypdfium2==4.30.1
51
+ python-bidi==0.6.6
52
+ python-dateutil==2.9.0.post0
53
+ python-docx==1.1.2
54
+ python-dotenv==1.1.0
55
+ python-pptx==1.0.2
56
+ pytz==2025.2
57
+ PyYAML==6.0.2
58
+ referencing==0.36.2
59
+ regex==2024.11.6
60
+ requests==2.32.3
61
+ rich==14.0.0
62
+ rpds-py==0.25.0
63
+ rtree==1.4.0
64
+ safetensors==0.5.3
65
+ scikit-image==0.25.2
66
+ scipy==1.15.3
67
+ semchunk==2.2.2
68
+ setuptools==80.8.0
69
+ shapely==2.1.1
70
+ shellingham==1.5.4
71
+ six==1.17.0
72
+ soupsieve==2.7
73
+ sympy==1.14.0
74
+ tabulate==0.9.0
75
+ tifffile==2025.5.10
76
+ tokenizers==0.21.1
77
+ torch==2.7.0
78
+ torchvision==0.22.0
79
+ tqdm==4.67.1
80
+ transformers==4.51.3
81
+ typer==0.15.4
82
+ typing-inspection==0.4.0
83
+ typing_extensions==4.13.2
84
+ tzdata==2025.2
85
+ urllib3==2.4.0
86
+ XlsxWriter==3.2.3