cv-tool — Docs

Architecture

CV.pdf
  │
  ├─ PDF natif ──► pdfplumber ──► texte brut
  └─ PDF scanné ──► pdf2image + pytesseract ──► texte brut
                        │
                        ▼
                 texte nettoyé
                        │
                        ▼
         ┌─────────────────────────────┐
         │  Schéma JSON utilisateur    │
         │  + LLM local (LM Studio)    │
         └─────────────────────────────┘
                        │
                        ▼
                  JSON structuré

Structure du code

cv_tool/
  __init__.py — package init
  cli.py — interface CLI (click)
  extractor.py — PDF extraction + OCR
  llm.py — connexion LLM local
  schema.py — chargement schéma JSON
  template.py — export docx (bonus)
schemas/
  default.yaml — schéma par défaut

Extraction PDF

Stratégie en deux niveaux : pdfplumber d'abord (rapide, préserve la structure), fallback OCR si le PDF est un scan.

def extract_pdf_text(pdf_path: str | Path, min_chars_threshold: int = 50) -> str:
    text = _extract_native(pdf_path)
    if text and len(text.strip()) >= min_chars_threshold:
        return text
    return _ocr_pdf(pdf_path)

Comment il détecte un scan : si le texte natif extrait fait moins de 50 caractères au total, on considère que c'est un scan et on passe en OCR.

Schéma JSON dynamique

Le schéma n'est jamais hardcodé. L'utilisateur fournit le sien (YAML, JSON ou inline). Le YAML est converti en valeurs par défaut JSON pour guider le LLM.

# YAML (schemas/default.yaml)
personal:
  full_name: string
  email: string
experience:
  - company: string
    title: string

# CLI
cv-tool cv.pdf --schema mon-schema.yaml

Conversion YAML → JSON par défaut :

# YAML input
personal:
  full_name: string
  email: string

# → JSON converted
{
  "personal": {
    "full_name": "",
    "email": ""
  }
}

Prompt système généré

Le prompt est construit dynamiquement à partir du schéma :

Tu es un expert en analyse de CV et structuration de données.
Ton objectif est de transformer le texte brut d'un CV en JSON conforme AU SCHÉMA CI-DESSOUS.

RÈGLES IMPORTANTES :
- EXTRAIS TOUTE l'information pertinente du CV
- Si une section n'existe PAS : retourne [] ou ""
- NE PAS inventer d'informations
- Le JSON doit être VALIDE et respecter EXACTEMENT la structure

SCHÉMA JSON ATTENDU :
{schema_json}

LLM local (LM Studio)

# .env
LM_STUDIO_URL=http://localhost:1234/v1
LM_STUDIO_API_KEY=your-key-here

# Python
client = openai.OpenAI(
    base_url=os.getenv("LM_STUDIO_URL"),
    api_key=os.getenv("LM_STUDIO_API_KEY"),
)

Compatible : LM Studio, Ollama, ou tout endpoint compatible OpenAI API. Le modèle actif dans LM Studio est utilisé par défaut.

CLI

# JSON avec schéma par défaut
cv-tool cv.pdf

# JSON avec schéma personnalisé
cv-tool cv.pdf --schema mon-schema.yaml

# Texte brut uniquement
cv-tool cv.pdf --extract

# Export Word (bonus)
cv-tool cv.pdf --format docx

# Sauvegarde dans un fichier
cv-tool cv.pdf -o output.json

# Verbose
cv-tool cv.pdf -v

Export Word (template.py)

Le module template.py génère un CV Word professionnel à partir du JSON structuré :

from cv_tool.template import generate_docx

# Utilisation interne (via --format docx)
generate_docx(data, "cv_output.docx")

Le template applique :

Configuration (config/default.yaml)

llm:
  url: "${LM_STUDIO_URL}"
  api_key: "${LM_STUDIO_API_KEY}"
  model: ""
  temperature: 0.1

ocr:
  engine: "tesseract"
  dpi: 300
  lang: "fra+eng"

output:
  format: "json"
  indent: 2