""" CV Transformer - Hugging Face Space Automatyczna transformacja notatek do profesjonalnych wpisów CV. Model: speakleash/Bielik-1.5B-v3.0-Instruct """ import os import re from pathlib import Path import torch import gradio as gr import pymupdf from docx import Document from transformers import AutoTokenizer, AutoModelForCausalLM, set_seed from huggingface_hub import login set_seed(42) # --------------------------------------------------------------------------- # Autoryzacja # --------------------------------------------------------------------------- HF_TOKEN = os.environ.get("HF_TOKEN") if HF_TOKEN: login(token=HF_TOKEN) # --------------------------------------------------------------------------- # Ladowanie modelu # --------------------------------------------------------------------------- MODEL_ID = "speakleash/Bielik-1.5B-v3.0-Instruct" device = "cuda" if torch.cuda.is_available() else "cpu" print("DEVICE:", device) print("Ladowanie tokenizera...") tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) print("Ladowanie modelu (moze chwile potrwac)...") model = AutoModelForCausalLM.from_pretrained( MODEL_ID, dtype=torch.bfloat16 if device == "cuda" else torch.float32, device_map=None, ).to(device) model.eval() print("Model zaladowany.") # --------------------------------------------------------------------------- # Post-processing outputu # --------------------------------------------------------------------------- def _wyczysc_output(tekst: str) -> str: tekst = re.sub(r"\*{1,2}([^*]+)\*{1,2}", r"\1", tekst) linie = tekst.split("\n") linie_czyste = [] for linia in linie: linia = linia.strip() if not linia: continue if re.match(r"^[A-ZĄĆĘŁŃÓŚŹŻ][^.!?]{0,40}:$", linia): continue if re.match(r"^-\s+\w+:$", linia): continue linie_czyste.append(linia) if len(linie_czyste) == 0: return tekst.strip() elif len(linie_czyste) == 1: return linie_czyste[0] else: wynik = linie_czyste[0] pozostale = [l.lstrip("- ").strip() for l in linie_czyste[1:] if l.lstrip("- ").strip()] if pozostale: wynik += " - " + ", ".join(pozostale) return wynik # --------------------------------------------------------------------------- # Glowna funkcja transformujaca # --------------------------------------------------------------------------- def transform_to_cv(note: str, max_new_tokens: int = 150, temperature: float = 0.2, top_p: float = 0.9) -> str: messages = [ { "role": "system", "content": ( "Jestes ekspertem od pisania CV po polsku. " "Przeksztalcasz potoczne notatki w krotkie, profesjonalne wpisy do CV. " "Zasady: bez naglowkow, bez markdown, bez wymyslania informacji, " "maksymalnie 1-2 zdania, tylko gotowy wpis." ), }, { "role": "user", "content": "Notatka: robilam praktyki w hr, umawialam rozmowy rekrutacyjne i odpisywalam na maile\nWpis:", }, { "role": "assistant", "content": "Praktykant ds. rekrutacji - prowadzenie korespondencji z kandydatami, koordynacja harmonogramu rozmow kwalifikacyjnych.", }, { "role": "user", "content": f"Notatka: {note}\nWpis:", }, ] prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) if not isinstance(prompt, str): prompt = tokenizer.decode(prompt, skip_special_tokens=False) inputs = tokenizer(prompt, return_tensors="pt").to(model.device) input_len = inputs["input_ids"].shape[1] with torch.inference_mode(): outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, do_sample=True, pad_token_id=tokenizer.eos_token_id, ) response = tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True) return _wyczysc_output(response.strip()) # --------------------------------------------------------------------------- # Wczytywanie plikow CV (PDF / DOCX / TXT) # --------------------------------------------------------------------------- def wczytaj_cv(sciezka: str) -> str: path = Path(sciezka) if not path.exists(): raise FileNotFoundError(f"Nie znaleziono pliku: {sciezka}") ext = path.suffix.lower() if ext == ".pdf": tekst_strony = [] with pymupdf.open(path) as doc: for page in doc: tekst = page.get_text("text", sort=True) if tekst.strip(): tekst_strony.append(tekst.strip()) return "\n".join(tekst_strony) elif ext == ".docx": doc = Document(path) akapity = [p.text.strip() for p in doc.paragraphs if p.text.strip()] return " ".join(akapity) elif ext == ".txt": return path.read_text(encoding="utf-8") else: raise ValueError(f"Nieobslugiwany format pliku: {ext}. Uzyj .pdf, .docx lub .txt") SEKCJE_CV = [ "doswiadczenie", "experience", "wyksztalcenie", "edukacja", "education", "umiejetnosci", "skills", "projekty", "projects", "jezyki", "languages", "certyfikaty", "certificates", "wolontariat", "volunteer", "zainteresowania", "interests", "podsumowanie", "summary", "osiagniecia", "achievements", ] def podziel_na_sekcje(tekst: str) -> list: linie = tekst.split("\n") sekcje = [] aktualna_nazwa = "Ogólne" aktualne_wpisy = [] for linia in linie: linia = linia.strip() if not linia: continue if linia.isupper() and len(linia) < 40: if aktualne_wpisy: sekcje.append({"nazwa": aktualna_nazwa, "wpisy": aktualne_wpisy}) aktualna_nazwa = linia aktualne_wpisy = [] else: if len(linia.split()) >= 5: aktualne_wpisy.append(linia) if aktualne_wpisy: sekcje.append({"nazwa": aktualna_nazwa, "wpisy": aktualne_wpisy}) return sekcje def enhance_cv_from_file(sciezka: str) -> list: tekst = wczytaj_cv(sciezka) sekcje = podziel_na_sekcje(tekst) wyniki = [] for sekcja in sekcje: wpisy_poprawione = [] for wpis in sekcja["wpisy"][:3]: if len(wpis.split()) < 4: continue poprawiony = transform_to_cv(wpis) wpisy_poprawione.append({"oryginal": wpis, "poprawiony": poprawiony}) wyniki.append({"sekcja": sekcja["nazwa"], "wpisy": wpisy_poprawione}) return wyniki # --------------------------------------------------------------------------- # Wrappery dla Gradio (obsluga bledow) # --------------------------------------------------------------------------- def gradio_transform(notatka: str) -> str: if not notatka.strip(): return "Wpisz notatkę żeby zobaczyć wynik." try: return transform_to_cv(notatka) except Exception as e: return f"Błąd: {str(e)}" def gradio_enhance(plik) -> str: if plik is None: return "Wgraj plik CV (.pdf, .docx lub .txt)" try: wyniki = enhance_cv_from_file(plik.name) output = "" for sekcja in wyniki: if not sekcja["wpisy"]: continue output += f"### {sekcja['sekcja']}\n\n" for wpis in sekcja["wpisy"]: output += f"**Oryginal:**\n{wpis['oryginal']}\n\n" output += f"**Poprawiony:**\n{wpis['poprawiony']}\n\n" output += "---\n\n" return output if output else "Nie znaleziono wpisów do poprawy." except Exception as e: return f"Błąd: {str(e)}" # --------------------------------------------------------------------------- # Interfejs Gradio # --------------------------------------------------------------------------- with gr.Blocks(title="CV Transformer") as demo: gr.Markdown( """ # CV Transformer System automatycznej transformacji notatek do profesjonalnych wpisów CV. Oparty na modelu **Bielik-1.5B-v3.0-Instruct**. """ ) with gr.Tabs(): with gr.TabItem("Transformacja notatki"): gr.Markdown( "Wpisz potoczną notatkę o swoim doświadczeniu, " "a system przekształci ją w profesjonalny wpis do CV." ) with gr.Row(): with gr.Column(): input_notatka = gr.Textbox( label="Twoja notatka", placeholder="np. robilam praktyki w ksiegowosci, liczyłam faktury...", lines=4, ) btn_transform = gr.Button("Przekształć", variant="primary") with gr.Column(): output_wpis = gr.Textbox(label="Profesjonalny wpis CV", lines=4) btn_transform.click(fn=gradio_transform, inputs=input_notatka, outputs=output_wpis) gr.Examples( examples=[ ["robilam praktyki w ksiegowosci, liczyłam faktury i siedzilam w excelu"], ["znam pythona, troche sql, robilam cos w react, git tez uzywam na co dzien"], ["zrobilam aplikacje webowa do zarzadzania zadaniami, backend w django, baza postgres"], ["angielski na poziomie B2, troche niemca ze szkoly, prawo jazdy kat B"], ], inputs=input_notatka, ) with gr.TabItem("Poprawa CV z pliku"): gr.Markdown( "Wgraj swój plik CV w formacie PDF, DOCX lub TXT. " "System wczyta go, podzieli na sekcje i poprawi każdy wpis." ) with gr.Row(): with gr.Column(): input_plik = gr.File(label="Wgraj plik CV", file_types=[".pdf", ".docx", ".txt"]) btn_enhance = gr.Button("Popraw CV", variant="primary") with gr.Column(): output_cv = gr.Markdown(label="Poprawione wpisy") btn_enhance.click(fn=gradio_enhance, inputs=input_plik, outputs=output_cv) if __name__ == "__main__": demo.launch()