CoolFace
Apppublic

Marcin-XStudio/CV_Exractor

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py217 linesDownload Raw Back to root
1import json2import torch3import time4from docling.document_converter import DocumentConverter5from transformers import AutoModelForCausalLM, AutoTokenizer6from pathlib import Path7from fastapi import FastAPI, File, UploadFile, HTTPException8import os9from dotenv import load_dotenv10import tempfile11from supabase import create_client12from huggingface_hub import snapshot_download13from transformers import BitsAndBytesConfig, AutoModelForCausalLM14 15load_dotenv()16app = FastAPI()17 18model_name = "numind/NuExtract-1.5"19 20# MODEL_CACHE = "/home/user/app/model_cache"21 22device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"23dtype = torch.float16 if device in ("mps", "cuda") else torch.float3224 25print("CUDA available:", torch.cuda.is_available())       # True26print("Device name:", torch.cuda.get_device_name(0)) 27print ("Model Running ", model_name)28 29# If lower memory usage needed: 30 31# bnb_config = BitsAndBytesConfig(32#     load_in_4bit=True,33#     bnb_4bit_use_double_quant=True,34#     bnb_4bit_quant_type="nf4",35#     bnb_4bit_compute_dtype=torch.float1636# )37 38@app.on_event("startup")39def startup_supabase():40    print("DEVICE:", device)41    global supabase42    supabase = create_client(43        os.getenv("DATABASE_URL"),44        os.getenv("SUPABASE_SERVICE_ROLE_KEY")45    )46 47@app.on_event("startup")48def load_model():49    print("Loading model and tokenizer...", flush=True)50    global model, tokenizer51    model = AutoModelForCausalLM.from_pretrained(52        # model_name,53        # cache_dir=MODEL_CACHE,54        MODEL_CACHE,55        local_files_only=True,56        torch_dtype=dtype,57        trust_remote_code=True,58        # quantization_config=bnb_config,59        device_map="auto"60    ).to(device).eval()61    tokenizer = AutoTokenizer.from_pretrained(62        # model_name,63        # cache_dir=MODEL_CACHE,64        MODEL_CACHE,65        local_files_only=True,66        trust_remote_code=True,67        device_map="auto"68    )69    # Check this optimization!70    # if torch.__version__ >= "2.0":71    #     model = torch.compile(model)72    print("✅ Model and tokenizer loaded from", MODEL_CACHE)73 74 75def predict_NuExtract(texts, template, batch_size=1, max_length=5096, max_new_tokens=1024):76    print("Starting NuExtract prediction...", flush=True)77    start_time = time.perf_counter()78    template_str = json.dumps(json.loads(template), indent=4)79    prompts = [80        "<|input|>\n"81        "### Instruction:\n"82        "Remplis la template JSON avec les informations extraits du texte.\n"83        "Extraire le nom du candidat tel qu’il apparaît sur la première ligne du document (souvent en majuscules ou en plus gros), et le mettre dans nom. Si le nom n’est pas trouvé, renvoyer une chaîne vide.\n"84        "Si le text contient des mentions de diplomes ou de titres de formations, certificats ou Attestation on les considère comme education, pas experience\n"85        "Pour chaque bloc formation ou expérience où une date est mentionnée (MM/YYYY ou mois/YYYY etc), remplir systématiquement annee_debut et annee_fin.\n"86        "Ne jamais laisser ces champs vides si la date est dans le texte.\n"87        "Si une seule date specifié pour une experience ou une formation met la même date pour start_date et end_date. Exemples : \n"88        "Exemple 1 : \n"89        "Texte : \"...01/2024 – En cours...\"\n"90        "Output : \"start_date\": \"01/2024\", \"end_date\": \"01/2024\"\n"91        "Exemple 2 : \n"92        "Texte : \"...mai 2025...\"\n"93        "Output : \"start_date\": \"05/2025\", \"end_date\": \"05/2025\"\n"94        "Exemple 3 : \n"95        "Texte : \"...mai 2025 – juin 2026...\"\n"96        "Output : \"start_date\": \"05/2025\", \"end_date\": \"06/2026\"\n"97        "Extraction des dates est *très* importante. Ne laisse jamais les dates vides\n"98        "Exemples types de formations :  CAP Boucherie, Licence Pro Métiers de l’Énergétique, Baccalauréat Général\n"99        "Exemples catégories de formations :  Transport, énergie, langues, esthétique\n"100        "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’inclure que les permis explicitement mentionnés dans le texte. S’il n’y a aucune mention de permis, renvoyer une liste vide.\n"101        "### Exemple 1 (avec permis) \n"102        "Texte : \"...j’ai obtenu mon permis B en 2015...\"\n"103        "Output : \"types_des_permis_de_conduire\": [\"permis B\"]\n"104        "### Exemple 2 (sans permis) \n"105        "Texte : \"...j’ai étudié à l’Université de Paris...\"\n"106        "Output : \"types_des_permis_de_conduire\": []\n"107        "Output *only* the completed JSON.\n"108        "### Template:\n"109        f"{template_str}\n"110        "### Text:\n"111        f"{text}\n\n"112        "<|output|>"113        for text in texts114    ]115    print("Prompts prepared.", flush=True)116    outputs = []117    with torch.no_grad():118        for i in range(0, len(prompts), batch_size):119            batch = prompts[i : i+batch_size]120            enc = tokenizer(121                batch,122                return_tensors="pt",123                truncation=True,124                padding=True,125                max_length=max_length126            ).to(device)127            print(f"Generating outputs with model for batch {i//batch_size+1}...", flush=True)128            ids = model.generate(**enc, max_new_tokens=max_new_tokens, num_beams=1, use_cache=False)129            outputs += tokenizer.batch_decode(ids, skip_special_tokens=True)130    print("Outputs generated.", flush=True)131    elapsed = time.perf_counter() - start_time132    print(f"NuExtract prediction completed in {elapsed:.2f} seconds.", flush=True)133    return [out.split("<|output|>")[1] for out in outputs]134 135template = """{136    "nom": "", "email": "", "telephone": "",137    "education": [{"type_de_formation": "", "categorie_de_formation": "", "annee_debut": "", "annee_fin": ""}],138    "experience": [{"position": "", "entreprise": "", "annee_debut": "", "annee_fin": ""}],139    "types_des_permis_de_conduire": [""]140}"""141 142data_model = {143        "experience": [{"start_date": "", "end_date": "", "job_category_id": ""}],144        "education": [{"training_type_id": "", "training_category_id": "", "start_date": "", "end_date": ""}],145        "email": "",146        "phone": "",147        "mobility": [{"id": "", "title": ""}]148}149 150@app.get("/health")151async def health():152    return {"status": "ok"}153 154@app.post("/extract")155async def extract(file: UploadFile = File(...)):156    suffix = Path(file.filename).suffix or ".pdf"157    try:158        # Create one global client; reused across calls159        # supabase = create_client(os.environ.get("DATABASE_URL"), os.environ.get("SUPABASE_SERVICE_ROLE_KEY"))160 161        # Use Supabase client to query tables, map names to IDs162        job_categories = supabase.table("Job_category").select("id, title").execute()163 164        print("Job Categories: ", job_categories)165 166        training_types = supabase.table("Training_type").select("id, title").execute()167 168        print("Training Types: ", training_types)169 170        training_categories = supabase.table("Training_category").select("id, title").execute()171 172        print("Training Categories: ", training_categories)173 174        mobility = supabase.table("Mobility").select("id, title").execute()175 176        print("Mobility: ", mobility)177 178        # … your LLM logic …179            180        with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:181            data = await file.read()182            tmp.write(data)183            tmp_path = tmp.name184        print(f"Upload saved to {tmp_path}", flush=True)185    except Exception as e:186        print(f"Cannot save upload: {e}", flush=True)187        raise HTTPException(400, f"Cannot save upload: {e}")188 189    try:190        converter = DocumentConverter()191        result    = converter.convert(tmp_path)192        raw_text  = result.document.export_to_text()193        print("Docling conversion complete.", flush=True)194    except Exception as e:195        print(f"Docling error: {e}", flush=True)196        raise HTTPException(500, f"Docling error: {e}")197    finally:198        try: os.remove(tmp_path)199        except OSError: pass200 201    try:202        extracted_json_str = predict_NuExtract([raw_text], template)[0]203        print("Extraction with NuExtract complete.", flush=True)204        print("⏺ RAW MODEL OUTPUT:\n", extracted_json_str)205        print("⏺ RAW MODEL OUTPUT (repr):\n", repr(extracted_json_str))206 207        print("Clearing Cache", flush=True)208        if device == "mps":209            torch.mps.empty_cache()210        elif device == "cuda":211            torch.cuda.empty_cache()212        elif device == "cpu":213            torch.cpu.empty_cache()214        return {"result": json.loads(extracted_json_str)}215    except Exception as e:216        print(f"Extraction error: {e}", flush=True)217        raise HTTPException(500, f"Extraction error: {e}")