Consolider des CV PowerPoint dans Word
Créez un pipeline local qui consolide des CV PPTX en DOCX avec audit.
Introduction
30 Second Summary
Plusieurs versions d'un même CV finissent souvent par répéter les mêmes expériences sous des formulations différentes. Retrouver l'origine de chaque date devient alors un travail minutieux.
Dans ce projet, vous allez créer un consolidateur local qui transforme un dossier de CV PowerPoint en un CV Microsoft Word sans inventer de contenu. Un audit JSON reliera chaque ligne retenue à sa source.
What You'll Build
Vous ouvrirez un CV Word consolidé où chaque ligne retenue remonte jusqu'à son paragraphe d'origine dans une présentation précise.
À la fin de ce projet, vous aurez :
- Une extraction par lot capable de traiter jusqu'à une trentaine de fichiers .pptx tout en affichant le nombre de blocs trouvés dans chaque source.
- Une traçabilité complète qui conserve le fichier, la diapositive, la forme et le paragraphe d'origine de chaque bloc.
- Un CV Word consolidé qui regroupe prudemment les doublons grâce à la similarité sémantique sans reformuler le texte source.
- Secret Mission: calibrer le seuil sémantique avec vos propres exemples de vrais et faux doublons.
Y a-t-il des prérequis ?
Vous devez déjà disposer de Microsoft Word desktop pour préparer le modèle.
Le premier téléchargement des modèles nécessite une connexion internet.
Before We Start
Ce point de départ vous permet de définir les CV à consolider et de vous engager à conserver uniquement les faits présents dans les sources. Chaque bloc extrait devra rester traçable avec un statut final explicite.
Set Up the Environment and Word Template
Your source CVs contain personal data that must remain traceable throughout the consolidation. A reproducible local workspace gives every source file a fixed place before processing begins.
In this step, you will prepare an isolated Python environment. You will also turn the supplied Microsoft Word document into an explicit template for every CV category.
In this step, get ready to:
- Create the local folder structure for the consolidation project.
- Install the pinned Python packages inside an isolated environment.
- Prepare the tagged Word template for the JupyterLab workflow.
Create the isolated workspace
A virtual environment keeps this project's packages separate from other Python projects on your computer. The pinned dependencies require Python 3.12 or later for this project.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter to open the terminal.
The first check identifies the Python version available through your terminal. Use the command for your operating system.
macOS
- Display your installed Python version by running this command:
python3 --version
What does this command do?
The --version option prints the version connected to the python3 command. This result determines which setup path applies to your computer.
Windows
- Display your installed Python version by running this command:
python --version
What does this command do?
The --version option prints the version connected to the python command. This result determines which setup path applies to your computer.
✔️ I see version 3.12 or higher
Your Python installation is ready to create the isolated environment for this project.
ⓧ I see an older version
The installed version cannot support the complete project environment. Upgrade Python before creating .venv.
- Visit the official Python downloads page.
- Download Python 3.12 or later for your operating system.
- Run the downloaded installer.
- Complete the guided installation prompts.
- Close the current terminal after installation finishes.
- Reopen the terminal through system search.
- Repeat the version check above.
ⓧ Command not found
Your terminal cannot find a Python installation. Install a supported version before continuing.
- Visit the official Python downloads page.
- Download Python 3.12 or later for your operating system.
- Run the downloaded installer.
- Complete the guided installation prompts.
- Close the current terminal after installation finishes.
- Reopen the terminal through system search.
- Repeat the version check above.
Python version still looks wrong?
A terminal opened before installation can retain an outdated command path. Close every terminal window before checking the version again.
If the older version remains active, help me find the Python executable used by my terminal.
The project structure separates source CVs from templates and generated output. The cv-reconciler folder also contains the isolated environment used by the notebook.
macOS
- Create the project workspace on your Desktop by running these commands:
cd ~/Desktop
mkdir cv-reconciler
cd cv-reconciler
mkdir input_pptx template output
python3 -m venv .venv
source .venv/bin/activate
ls
What do these commands create?
- The cv-reconciler directory holds the complete local project.
- The three subdirectories separate source CVs from Word templates and generated output.
- The .venv directory contains the isolated Python environment.
- The final ls command displays the workspace contents.
Windows
- Create the project workspace on your Desktop by running these commands:
Set-Location ~/Desktop
New-Item -ItemType Directory -Path cv-reconciler
Set-Location cv-reconciler
New-Item -ItemType Directory -Path input_pptx, template, output
python -m venv .venv
.\.venv\Scripts\Activate.ps1
Get-ChildItem
What do these commands create?
- The cv-reconciler directory holds the complete local project.
- The three subdirectories separate source CVs from Word templates and generated output.
- The .venv directory contains the isolated Python environment.
- The final directory listing displays the workspace contents.
You should see input_pptx, template, and output in the listing. Your terminal prompt should also show that .venv is active.
Environment activation failed?
Confirm that your terminal is inside the cv-reconciler folder. The activation script lives inside its .venv directory.
If PowerShell blocks the activation script, help me understand the policy message safely.
Why use a local environment?
The CVs remain on your computer throughout this pipeline. The isolated environment also preserves the package versions used to make each processing decision.
This combination supports repeatable runs without sending the source documents to a hosted processing service.
The dependency file records the exact releases used by the pipeline. Future installations can reconstruct the same software environment from this one file.
- Create a plain-text file named requirements.txt inside the cv-reconciler folder using your text editor.
- Paste this dependency list into requirements.txt:
jupyterlab==4.6.4
python-pptx==1.0.2
docxtpl==0.20.2
sentence-transformers==6.1.0
transformers==5.19.0
Why pin every package?
Each == entry instructs pip to install one exact release. This prevents later package updates from changing the pipeline unexpectedly.
- Save requirements.txt.
- Confirm that requirements.txt now appears beside the three project directories.
This installation can take a few minutes because the machine learning packages bring several supporting libraries. Keep the terminal open while progress messages continue.
macOS
- Install the pinned dependencies inside the active environment by running:
python -m pip install -r requirements.txt
What does this command install?
The command asks pip to read every package pin from requirements.txt. The active environment keeps those packages inside .venv.
Windows
- Install the pinned dependencies inside the active environment by running:
python -m pip install -r requirements.txt
What does this command install?
The command asks pip to read every package pin from requirements.txt. The active environment keeps those packages inside .venv.
The installation succeeds when the terminal returns to its prompt without a package installation error. Your environment now contains the exact releases listed in requirements.txt.
Package installation failed?
Check that the terminal prompt shows the active .venv environment. Confirm that requirements.txt is saved inside cv-reconciler.
If a package reports an incompatible Python version, help me diagnose the failed installation.
Prepare the Word template
The source filenames become part of each block's provenance later. Keeping the original files unchanged protects that link back to the source.
- Copy your source .pptx CV files into the input_pptx folder using Finder on macOS or File Explorer on Windows.
- Copy the supplied Word template into the template folder as modele_cv.docx.
You should now see at least one source CV inside input_pptx. The original Word model should appear at template/modele_cv.docx.
Why keep the original template?
The untouched modele_cv.docx file gives you a clean recovery point. The pipeline renders the final CV from the tagged copy.
A Word run is a continuous stretch of text with one formatting definition. Each Jinja tag must remain in one run so the template engine can detect it.
Keep each tag together
This part is fiddly because Word can split a tag when its formatting changes. Type each complete tag in one uninterrupted action.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Microsoft Word.
- Press Enter to open Microsoft Word desktop.
- Open template/modele_cv.docx from the cv-reconciler folder.
- Save a copy as template/modele_cv_balisé.docx.
The tagged copy preserves the supplied layout while exposing one destination for every category. Replace the corresponding content areas with these exact tags.
- Type each tag below into its matching area of template/modele_cv_balisé.docx:
{{ nom_prenom }}
{{ coordonnees }}
{{ formation_academique }}
{{ formation_professionnelle }}
{{ certifications }}
{{ langues }}
{{ experience_professionnelle }}
{{ competences_cles }}
{{ competences_techniques }}
{{ autres }}
What do these tags control?
Each tag names one destination in the consolidated CV. The renderer later replaces it with selected source text from the matching category.
The surrounding Word style remains part of the template. This preserves the supplied document's visual structure.
- Save template/modele_cv_balisé.docx.
- Open document search with Cmd+F on macOS or Ctrl+F on Windows.
- Search for {{.
You should find exactly ten complete tags. Each tag should appear in its intended section of the copied Word template.
A tag is missing or split?
Delete the affected placeholder completely. Retype the full tag without changing its formatting midway through the text.
Confirm that the tagged copy uses the exact filename modele_cv_balisé.docx. You can also ask for help checking all ten template tags.
Launch JupyterLab and open the notebook
The supplied notebook becomes the control panel for extraction and classification. It also holds the later deduplication and document generation cells.
- Copy the supplied notebook into the cv-reconciler folder as cv_reconciler.ipynb.
- Return to the terminal with the active .venv environment.
You should now see cv_reconciler.ipynb beside requirements.txt when you view the cv-reconciler folder.
The first JupyterLab launch may take a short moment while the local interface starts. Keep the terminal running because it hosts the notebook process.
Before you launch it, which project folders do you expect to see in the JupyterLab file browser?
- Launch JupyterLab from the project folder by running this command:
jupyter lab
What happens when JupyterLab starts?
JupyterLab starts a local process. Its file browser uses the terminal's current directory as the starting location.
The interface normally opens in your browser. The terminal remains occupied while the process runs.
- Confirm that the JupyterLab file browser lists input_pptx, template, and output.
- Confirm that the output directory is empty.
- Select cv_reconciler.ipynb in the JupyterLab file browser.
You should see cv_reconciler.ipynb open in a notebook tab. The file browser should still show the three project directories.
JupyterLab did not open?
Confirm that the terminal prompt shows the active .venv environment. Check that the terminal is inside the cv-reconciler folder.
If the command remains unavailable, help me troubleshoot JupyterLab startup.
✔️ Awesome, I've got everything!
Your isolated environment is active. Your source folders and tagged Word template are ready for extraction.
ⓧ I'd like to double check the full code
Compare your project against this complete setup state.
- Confirm that input_pptx contains your source .pptx CV files.
- Confirm that output exists and remains empty.
- Confirm that the original template exists at template/modele_cv.docx.
- Confirm that the tagged copy exists at template/modele_cv_balisé.docx with all ten single-run tags.
- Confirm that cv_reconciler.ipynb exists inside cv-reconciler.
- Compare requirements.txt with the complete reference below.
jupyterlab==4.6.4
python-pptx==1.0.2
docxtpl==0.20.2
sentence-transformers==6.1.0
transformers==5.19.0
What should match exactly?
The package names and version pins should match this reference exactly. These entries define the reproducible environment used by the remaining steps.
Your local workspace now separates source CVs from templates and generated output. Next, you will extract every readable block while preserving its original location.
Extract Traceable PowerPoint Blocks
Your isolated environment is ready. Your tagged Word template now has explicit destinations for the source content.
The next risk is silent loss. PowerPoint CVs can hide text inside groups, tables, notes, diagrams, or charts.
This step uses python-pptx to create one provenance record per visible paragraph. An OOXML fallback catches strings that the structured pass does not represent.
In this step, get ready to:
- Load the supplied pipeline files into the existing project.
- Run the notebook configuration for the local paths.
- Extract every CV text block with its provenance.
Load the supplied pipeline files
The supplied files keep the extraction logic consistent with the later pipeline stages. JupyterLab may ask you to confirm that the existing notebook should be replaced.
- Use the upload icon above the project files in the JupyterLab file browser from earlier.
- Select the supplied cv_pipeline.py file.
- Confirm that cv_pipeline.py appears beside cv_reconciler.ipynb.
- Upload the supplied cv_reconciler.ipynb file.
- Approve the prompt that replaces the existing notebook file.
- Return to the notebook tab from earlier.
- Reload the notebook from disk if JupyterLab reports that the file changed.
Good progress. Your project now has the exact extraction source that preserves every downstream audit decision.
Configure the notebook paths
The configuration cell gives the notebook one shared set of paths. This prevents extraction from reading a different folder than the later rendering stages.
- Run the first code cell that begins with from pathlib import Path.
- Run the configuration cell that begins with INPUT_DIR = Path('input_pptx').
You should see input_pptx, template/modele_cv_balisé.docx, and output/cv_consolide.docx on separate output lines.
- Switch to cv_pipeline.py in the JupyterLab file browser.
- Find walk_shape() with the editor search control.
- Find xml_fallback_records() with the same search control.
How does the extraction stay traceable?
- The walk_shape() function follows nested shape groups recursively.
- Table extraction skips cells hidden by a merge through is_spanned.
- The make_record() function records the source file, slide, shape, paragraph, and extraction method.
- The xml_fallback_records() function captures uncovered strings from slides, notes, diagrams, and charts.
Run and inspect the extraction
The extract_all() function processes every .pptx file in input_pptx. It adds source_order after combining the records.
Before you run the extraction, which provenance fields do you expect to appear beside the source text?
- Switch back to the open cv_reconciler.ipynb tab.
- Run the extraction cell that begins with records = extract_all(INPUT_DIR).
You should see a file count above zero. The block count should also be above zero.
The printed example should include source_id, file, slide, and text. That is the crucial proof that extracted text can be traced back to its origin.
The first printed dictionary shows one record. A temporary notebook expression lets you inspect the complete in-memory list for xml_fallback entries.
- Add this temporary expression as the final line of the extraction cell:
records
What does this expression do?
A notebook displays the value of its final expression. This exposes the record list for a temporary audit without changing the extraction pipeline.
- Run the extraction cell again.
- Use your browser's page search on the expanded output.
- Search for xml_fallback.
- Inspect a matching dictionary if your source files contain uncovered OOXML text.
- Delete the temporary final records expression.
A matching record identifies its origin through the shape field. If no match appears, the structured extraction already covered the visible strings in this particular batch.
Before you restore the final output, do you expect the same nonzero counts and provenance example to appear?
- Run the restored extraction cell once more.
You should see the nonzero file count, the nonzero block count, and one provenance dictionary again. Your notebook is now back to the supplied final structure.
Extraction did not return records?
Confirm that the source files are directly inside input_pptx. Each source filename must end with .pptx.
If the notebook reports Aucun fichier .pptx dans, confirm that JupyterLab is running from the project folder.
Ask for help with debugging an empty PowerPoint extraction.
✔️ Awesome, I've got everything!
Your supplied pipeline files are in place. Double-check that the restored extraction cell is saved before moving on.
ⓧ I'd like to double check the full code
Compare your complete cv_pipeline.py file with this supplied reference.
import json
import re
import unicodedata
import xml.etree.ElementTree as ET
from collections import Counter, defaultdict
from pathlib import Path
from zipfile import ZipFile
from docxtpl import DocxTemplate, Listing
from pptx import Presentation
from sentence_transformers import SentenceTransformer
from transformers import pipeline
CLASSIFICATION_MODEL = "MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli"
EMBEDDING_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
CATEGORY_KEYS = {
"Nom et prénom": "nom_prenom",
"Coordonnées": "coordonnees",
"Formation académique": "formation_academique",
"Formation professionnelle": "formation_professionnelle",
"Certifications": "certifications",
"Langues": "langues",
"Expérience professionnelle": "experience_professionnelle",
"Compétences clés": "competences_cles",
"Compétences techniques": "competences_techniques",
"Autres": "autres",
}
MONTHS = (
"janvier|fevrier|mars|avril|mai|juin|juillet|aout|septembre|"
"octobre|novembre|decembre"
)
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
FULL_DATE_RE = re.compile(rf"\b\d{{1,2}}\s+(?:{MONTHS})\s+(?:19|20)\d{{2}}\b")
MONTH_YEAR_RE = re.compile(rf"\b(?:{MONTHS})\s+(?:19|20)\d{{2}}\b")
DATE_FRAGMENT_RE = re.compile(
rf"\b(?:\d{{1,2}}\s+)?(?:{MONTHS})\s+(?:19|20)\d{{2}}\b|"
r"\b(?:19|20)\d{2}\b"
)
def clean_text(text):
return " ".join(str(text).replace("\v", "\n").split())
def normalize(text):
text = unicodedata.normalize("NFKD", clean_text(text))
text = "".join(char for char in text if not unicodedata.combining(char))
text = text.lower()
return re.sub(r"[^a-z0-9@+]+", " ", text).strip()
def strip_dates(text):
return clean_text(DATE_FRAGMENT_RE.sub(" ", normalize(text)))
def make_record(path, slide, shape, paragraph, text, extraction):
source_id = f"{path.name}::s{slide}::sh{shape}::p{paragraph}::{extraction}"
return {
"source_id": source_id,
"file": path.name,
"slide": slide,
"shape": shape,
"paragraph": paragraph,
"extraction": extraction,
"raw_text": str(text),
"text": clean_text(text),
"category": None,
"confidence": None,
"status": "extracted",
"duplicate_of": None,
}
def append_text_frame(records, path, slide_number, shape_id, text_frame, extraction):
for paragraph_number, paragraph in enumerate(text_frame.paragraphs, start=1):
text = clean_text(paragraph.text)
if text:
records.append(
make_record(path, slide_number, shape_id, paragraph_number, text, extraction)
)
def walk_shape(records, path, slide_number, shape):
children = getattr(shape, "shapes", None)
if children is not None:
for child in children:
walk_shape(records, path, slide_number, child)
return
if getattr(shape, "has_table", False):
for cell_number, cell in enumerate(shape.table.iter_cells(), start=1):
if cell.is_spanned:
continue
append_text_frame(
records, path, slide_number,
f"{shape.shape_id}-cell{cell_number}", cell.text_frame, "table"
)
return
if getattr(shape, "has_text_frame", False):
append_text_frame(
records, path, slide_number, shape.shape_id, shape.text_frame, "shape"
)
def xml_fallback_records(path, structured_records):
relevant_prefixes = (
"ppt/slides/slide", "ppt/notesSlides/notesSlide",
"ppt/diagrams/data", "ppt/charts/chart",
)
covered = "\n".join(normalize(record["text"]) for record in structured_records)
seen = set()
fallback = []
with ZipFile(path) as archive:
xml_names = [
name for name in archive.namelist()
if name.endswith(".xml") and name.startswith(relevant_prefixes)
]
for xml_name in xml_names:
root = ET.fromstring(archive.read(xml_name))
for element in root.iter():
if not element.tag.endswith("}t") or not element.text:
continue
text = clean_text(element.text)
normalized = normalize(text)
if not normalized or normalized in covered or normalized in seen:
continue
seen.add(normalized)
fallback.append(
make_record(
path, 0, xml_name, len(fallback) + 1, text, "xml_fallback"
)
)
return fallback
def extract_presentation(path):
presentation = Presentation(str(path))
records = []
for slide_number, slide in enumerate(presentation.slides, start=1):
for shape in slide.shapes:
walk_shape(records, path, slide_number, shape)
records.extend(xml_fallback_records(path, records))
return records
def extract_all(input_dir):
paths = sorted(Path(input_dir).glob("*.pptx"))
if not paths:
raise FileNotFoundError(f"Aucun fichier .pptx dans {input_dir}")
records = []
for path in paths:
records.extend(extract_presentation(path))
for index, record in enumerate(records):
record["source_order"] = index
return records
def structural_category(text):
value = normalize(text)
aliases = {normalize(label): label for label in CATEGORY_KEYS if label != "Autres"}
aliases.update({
"formation": "Formation académique",
"experiences professionnelles": "Expérience professionnelle",
"competences": "Compétences clés",
"competences informatiques": "Compétences techniques",
})
return aliases.get(value)
def rule_category(text):
value = normalize(text)
if re.search(r"\b[^\s@]+@[^\s@]+\.[^\s@]+\b", text) or re.search(
r"\b(?:\+33|0)\s*[1-9](?:[\s.\-]*\d{2}){4}\b", text
):
return "Coordonnées"
if re.search(r"\b(certificat|certification|certifie|badge)\b", value):
return "Certifications"
if re.search(r"\b(langue|francais|anglais|espagnol|allemand|italien)\b", value):
return "Langues"
if re.search(r"\b(universite|ecole|master|licence|baccalaureat|diplome)\b", value):
return "Formation académique"
if re.search(r"\b(stage de formation|formation continue|atelier professionnel)\b", value):
return "Formation professionnelle"
if re.search(r"\b(python|java|sql|aws|azure|linux|docker|kubernetes|excel)\b", value):
return "Compétences techniques"
return None
def load_classifier():
return pipeline(
"zero-shot-classification", model=CLASSIFICATION_MODEL, device="cpu"
)
def classify_records(records, classifier):
labels = list(CATEGORY_KEYS)
for record in records:
structural = structural_category(record["text"])
if structural:
record["category"] = structural
record["confidence"] = 1.0
record["status"] = "structural"
continue
ruled = rule_category(record["text"])
if ruled:
record["category"] = ruled
record["confidence"] = 1.0
record["status"] = "classified"
continue
result = classifier(
record["text"], candidate_labels=labels,
hypothesis_template="Ce texte appartient à la rubrique {}.",
multi_label=False,
)
record["category"] = result["labels"][0]
record["confidence"] = round(float(result["scores"][0]), 4)
record["status"] = "classified"
return records
def find_exact_duplicate_groups(records):
groups = defaultdict(list)
for record in records:
if record["status"] == "classified":
groups[(record["category"], normalize(record["text"]))].append(
record["source_id"]
)
return [group for group in groups.values() if len(group) > 1]
def load_embedder():
return SentenceTransformer(EMBEDDING_MODEL, device="cpu")
def non_date_numbers(text):
return set(re.findall(r"\b\d+(?:[.,]\d+)?\b", strip_dates(text)))
def safe_to_merge(text_a, text_b):
core_a = strip_dates(text_a)
core_b = strip_dates(text_b)
if not core_a or not core_b:
return False
if non_date_numbers(text_a) != non_date_numbers(text_b):
return False
if core_a in core_b or core_b in core_a:
return True
tokens_a = {token for token in core_a.split() if len(token) > 2}
tokens_b = {token for token in core_b.split() if len(token) > 2}
if not tokens_a or not tokens_b:
return False
coverage = len(tokens_a & tokens_b) / min(len(tokens_a), len(tokens_b))
return coverage >= 0.78
def date_signature(text):
matches = DATE_FRAGMENT_RE.findall(normalize(text))
return "|".join(matches) if matches else "none"
def date_precision(text):
value = normalize(text)
if FULL_DATE_RE.search(value):
return 3
if MONTH_YEAR_RE.search(value):
return 2
if YEAR_RE.search(value):
return 1
return 0
def date_is_coherent(text):
years = [int(year) for year in YEAR_RE.findall(normalize(text))]
return int(len(years) < 2 or years[0] <= years[-1])
def deduplicate_records(records, embedder, threshold=0.86):
classified_indices = [
index for index, record in enumerate(records) if record["status"] == "classified"
]
parent = {index: index for index in classified_indices}
def find(index):
while parent[index] != index:
parent[index] = parent[parent[index]]
index = parent[index]
return index
def union(left, right):
root_left = find(left)
root_right = find(right)
if root_left != root_right:
parent[root_right] = root_left
by_category = defaultdict(list)
for index in classified_indices:
by_category[records[index]["category"]].append(index)
for indices in by_category.values():
if len(indices) < 2:
continue
texts = [strip_dates(records[index]["text"]) for index in indices]
embeddings = embedder.encode(texts, normalize_embeddings=True)
similarities = embedder.similarity(embeddings, embeddings)
for left_position in range(len(indices)):
for right_position in range(left_position + 1, len(indices)):
score = float(similarities[left_position][right_position])
left_index = indices[left_position]
right_index = indices[right_position]
if score >= threshold and safe_to_merge(
records[left_index]["text"], records[right_index]["text"]
):
union(left_index, right_index)
components = defaultdict(list)
for index in classified_indices:
components[find(index)].append(index)
for members in components.values():
signatures = Counter(date_signature(records[index]["text"]) for index in members)
def rank(index):
record = records[index]
signature = date_signature(record["text"])
return (
date_is_coherent(record["text"]), date_precision(record["text"]),
signatures[signature], len(normalize(record["text"]).split()),
-record["source_order"],
)
winner = max(members, key=rank)
records[winner]["status"] = "selected"
records[winner]["duplicate_sources"] = [
records[index]["source_id"] for index in members if index != winner
]
for index in members:
if index == winner:
continue
records[index]["status"] = "duplicate"
records[index]["duplicate_of"] = records[winner]["source_id"]
return records
def selected_records(records, category):
selected = [
record for record in records
if record["status"] == "selected" and record["category"] == category
]
if category == "Expérience professionnelle":
selected.sort(
key=lambda record: max(
[int(year) for year in YEAR_RE.findall(normalize(record["text"]))] or [-1]
), reverse=True,
)
else:
selected.sort(key=lambda record: record["source_order"])
return selected
def assert_integrity(records):
allowed = {"selected", "duplicate", "structural"}
invalid = [record["source_id"] for record in records if record["status"] not in allowed]
if invalid:
raise AssertionError(f"Blocs sans statut final: {invalid[:10]}")
for record in records:
if record["status"] == "duplicate" and not record["duplicate_of"]:
raise AssertionError(f"Doublon sans cible: {record['source_id']}")
return True
def build_context(records):
context = {}
for category, key in CATEGORY_KEYS.items():
items = [record["text"] for record in selected_records(records, category)]
context[key] = Listing("\n".join(items))
return context
def render_word(records, template_path, output_path):
template = DocxTemplate(str(template_path))
declared = set(template.get_undeclared_template_variables())
required = set(CATEGORY_KEYS.values())
missing = sorted(required - declared)
unexpected = sorted(declared - required)
if missing or unexpected:
raise ValueError(f"Balises manquantes: {missing}; balises inattendues: {unexpected}")
context = build_context(records)
template.render(context, autoescape=True)
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
template.save(str(output_path))
return Path(output_path)
def summarize(records):
counts = Counter(record["status"] for record in records)
return {
"total": len(records), "selected": counts["selected"],
"duplicates": counts["duplicate"], "structural": counts["structural"],
"without_final_status": len(records) - counts["selected"]
- counts["duplicate"] - counts["structural"],
}
def write_audit(records, audit_path, threshold=0.86):
payload = {
"classification_model": CLASSIFICATION_MODEL,
"embedding_model": EMBEDDING_MODEL,
"semantic_threshold": threshold,
"summary": summarize(records),
"records": records,
}
Path(audit_path).parent.mkdir(parents=True, exist_ok=True)
Path(audit_path).write_text(
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
)
return Path(audit_path)
This full-file reference includes the extraction functions used in this step. It also preserves the supplied definitions required by the remaining notebook cells.
Compare your complete cv_reconciler.ipynb file with this supplied reference.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Consolidation locale de CV PowerPoint\n",
"Ce notebook conserve le texte source et écrit toutes les décisions dans un audit JSON."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"from cv_pipeline import (\n",
" assert_integrity, classify_records, deduplicate_records,\n",
" extract_all, find_exact_duplicate_groups, load_classifier,\n",
" load_embedder, render_word, summarize, write_audit,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"INPUT_DIR = Path('input_pptx')\n",
"TEMPLATE_PATH = Path('template/modele_cv_balisé.docx')\n",
"OUTPUT_DOCX = Path('output/cv_consolide.docx')\n",
"AUDIT_PATH = Path('output/audit.json')\n",
"SEMANTIC_THRESHOLD = 0.86\n",
"for folder in (INPUT_DIR, TEMPLATE_PATH.parent, OUTPUT_DOCX.parent):\n",
" folder.mkdir(parents=True, exist_ok=True)\n",
"print(INPUT_DIR, TEMPLATE_PATH, OUTPUT_DOCX, sep='\\n')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"records = extract_all(INPUT_DIR)\n",
"files = sorted({record['file'] for record in records})\n",
"print({'files': len(files), 'blocks': len(records)})\n",
"print(records[0])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"classifier = load_classifier()\n",
"classify_records(records, classifier)\n",
"exact_groups = find_exact_duplicate_groups(records)\n",
"low_confidence = [\n",
" record for record in records\n",
" if record['status'] == 'classified' and record['confidence'] < 0.45\n",
"]\n",
"print({'exact_duplicate_groups': len(exact_groups), 'low_confidence': len(low_confidence)})\n",
"print(low_confidence[:3])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"embedder = load_embedder()\n",
"deduplicate_records(records, embedder, threshold=SEMANTIC_THRESHOLD)\n",
"print(summarize(records))\n",
"print([record for record in records if record['status'] == 'duplicate'][:3])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert_integrity(records)\n",
"render_word(records, TEMPLATE_PATH, OUTPUT_DOCX)\n",
"write_audit(records, AUDIT_PATH, threshold=SEMANTIC_THRESHOLD)\n",
"result = summarize(records)\n",
"print(result)\n",
"print(OUTPUT_DOCX.resolve())\n",
"print(AUDIT_PATH.resolve())"
]
}
],
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.12"}
},
"nbformat": 4,
"nbformat_minor": 5
}
This notebook reference contains the configured paths and the extraction cell you executed. Later cells remain unexecuted at this point.
Every extracted block now carries a route back to its source. Next, you will assign each non-structural block to a required CV category.
Classify the Extracted Blocks
The extraction pass has turned each PowerPoint paragraph into a record with a provenance trail. Your open JupyterLab session still holds those records in memory.
A closed taxonomy gives every block a destination without rewriting its source text. Different CVs can describe the same fact with different words, so this step also tests what an exact comparison can recognize.
In this step, get ready to:
- Assign every extracted block to one of the ten Word template categories.
- Test exact duplicate detection against differently worded source blocks.
- Review low-confidence classifications without changing their source wording.
Classify every extracted block
The pipeline combines deterministic rules with zero-shot classification. Rules handle recognizable content first, while the local classifier chooses a best-fit category for everything else.
How does the classification stay controlled?
- The structural_category() function recognizes section headings already represented by the Word template.
- The rule_category() function handles recognizable evidence such as contact details or certification terms.
- The load_classifier() function loads MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli for the remaining records.
- The classify_records() function assigns a category without generating replacement text.
Every record lands in one of ten destinations: Nom et prénom, Coordonnées, Formation académique, Formation professionnelle, Certifications, Langues, Expérience professionnelle, Compétences clés, Compétences techniques, or Autres.
- Switch back to the open cv_reconciler.ipynb notebook.
- Locate the classification cell that begins with classifier = load_classifier().
The first model load can take several minutes while Hugging Face downloads the classifier files. Later runs use the local cache.
- Run the classification cell with the triangular control beside it.
The output reports counts named exact_duplicate_groups and low_confidence. It also displays a preview containing up to three low-confidence records.
That completes the classification pass. Every extracted block now has an explicit route through the pipeline.
Classifier does not load?
- Confirm that the notebook is still using the active project environment.
- Restore your internet connection before retrying the first model download.
- Rerun the cell after the download completes.
Help me diagnose why the local zero-shot classifier does not load in my Jupyter notebook.
Expose the exact-match blind spot
Exact duplicate detection groups records only when their normalized category and text match. This gives you a deliberately strict baseline to test against source blocks that express the same fact differently.
Before you run this check, do you think exact comparison will place two equivalent sentences with different wording in the same group? Keep your prediction in mind.
- Insert a temporary code cell directly below the classification output with the notebook toolbar.
- Display the exact duplicate groups by entering the expression below:
find_exact_duplicate_groups(records)
What does this check compare?
The helper groups records by their assigned category and normalized text. It returns only groups containing more than one source identifier.
Normalization removes differences such as capitalization or punctuation. Different wording still produces a different comparison value.
- Run the temporary cell with the triangular control beside it.
- Choose two source blocks from the same category that communicate the same fact with different wording.
- Compare their source_id values with the returned groups.
You will see a list containing only exact normalized matches. The differently worded pair remains separate, which confirms the intended limitation of this baseline.
No exact groups returned?
An empty list means your source files contain no repeated blocks with identical normalized text. The check still succeeds because it confirms how strict the comparison is.
If the cell reports that a name is unavailable, rerun the supplied classification cell first. This restores exact_groups and the classified records in notebook memory.
Help me understand my exact duplicate results.
- Delete the temporary cell after completing the comparison.
Review low-confidence classifications
A low score records uncertainty about the best category. The record still receives a category because the closed taxonomy requires a destination for every non-structural block.
What does low confidence preserve?
The notebook selects classified records with a confidence below 0.45 for review. This threshold changes which records you inspect, not their source wording.
The raw_text field retains the extracted source string. The text field retains the same wording with whitespace cleaned for processing.
- Return to the preview printed beneath the classification cell.
- Read the category value in each displayed record.
- Check that each displayed status value is classified.
- Compare each raw_text value with its corresponding text value.
- Confirm that the wording has not been rewritten by the classifier.
Before the final rerun, do you expect a low-confidence record to have a blank category or a best-fit category? Hold that prediction for the output.
- Return to the classification cell that begins with classifier = load_classifier().
- Rerun the cell with the triangular control beside it.
Every non-structural record keeps a best-fit category. The summary reports the exact duplicate group count, while the preview shows low-confidence records with their source-derived text intact.
✔️ My classification output is ready
Your notebook now retains classified records, exact duplicate groups, and a low-confidence review list. The source wording remains available for every decision.
ⓧ I'd like to double check the full code
- Compare the existing classification cell in cv_reconciler.ipynb with this reference.
classifier = load_classifier()
classify_records(records, classifier)
exact_groups = find_exact_duplicate_groups(records)
low_confidence = [
record for record in records
if record['status'] == 'classified' and record['confidence'] < 0.45
]
print({'exact_duplicate_groups': len(exact_groups), 'low_confidence': len(low_confidence)})
print(low_confidence[:3])
What does this cell do?
- The first line loads the local classifier on the configured processor.
- The second line assigns categories and confidence values to records.
- The third line stores exact duplicate groups in exact_groups.
- The list comprehension stores classified records below the review threshold in low_confidence.
- The final lines print the summary counts and a short review sample.
- Capture only the classification summary line in your screenshot.
- Crop out any record preview that contains personal CV text.
Your extracted blocks now have controlled categories without generated wording. Next, you will use semantic similarity and explicit safety barriers to evaluate the reformulations that exact matching left separate.
Merge Safely and Resolve Dates
Your previous pass gave every non-structural block a required category. Exact matching still leaves equivalent formulations in separate groups.
This step uses Sentence Transformers to create multilingual embeddings that propose duplicate groups. Deterministic gates protect unique facts before any merge.
Explicit date rules then choose one existing source version for each accepted group.
In this step, get ready to:
- Propose semantic duplicate groups within each category.
- Apply lexical and numerical safety gates before each merge.
- Choose one source version with explicit date-ranking rules.
Propose semantic duplicate groups
An embedding represents the meaning of a text as numbers that can be compared. The pipeline limits each comparison to records that share the same category.
Why combine embeddings with rules?
Embeddings can find reformulations that exact matching misses. Rules decide whether those reformulations are safe to merge.
This hybrid approach limits the model to comparison. Every retained sentence still comes from a source record.
- Switch back to cv_reconciler.ipynb in the active JupyterLab session.
- Locate the cell that begins with embedder = load_embedder().
- Confirm that the configuration cell keeps SEMANTIC_THRESHOLD set to 0.86.
- Confirm that deduplicate_records() receives threshold=SEMANTIC_THRESHOLD.
What Does the Candidate Stage Do?
- The local model sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 converts each date-stripped text into a vector.
- The pipeline compares vectors only inside their assigned category.
- The threshold 0.86 identifies possible duplicate connections.
- A possible connection still needs approval from the safety gates.
Inspect the merge barriers and date ranking
Semantic similarity can connect related experiences that contain different facts. The function safe_to_merge() blocks unsafe connections before components are formed.
- Select cv_pipeline.py in the JupyterLab file browser to display its source in the editor.
- Search for def safe_to_merge(text_a, text_b): to inspect the merge conditions.
- Check the comparison between the values returned by non_date_numbers().
- Check the containment test involving core_a and core_b.
- Check the final lexical coverage comparison against 0.78.
How Are Merges Authorized?
- Date stripping prevents date formats from controlling the wording comparison.
- Non-date number sets must match exactly.
- Containment accepts a pair when one date-stripped text contains the other.
- Lexical coverage must reach 0.78 when containment does not apply.
Accepted pairs become connections inside a duplicate component. Each component needs one retained source record.
- Search for def date_signature(text): to see how repeated date variants are counted.
- Locate the rank() function inside deduplicate_records() to verify the priority order.
How Is the Retained Version Chosen?
- A coherent year sequence receives the highest priority.
- A full date outranks a month with a year. A month with a year outranks a year alone.
- A date signature found in more source variants receives the next priority.
- Richer source wording breaks later ties.
- The earliest source_order provides the final deterministic choice.
The winner receives the status selected plus its duplicate_sources list.
Each remaining member receives the status duplicate. Its duplicate_of value points to the winning source_id.
Run the semantic deduplication
The first model load may take longer because the embedding model files need to download.
Before you run the cell, decide whether you expect every semantically similar pair to survive the safety gates.
- Return to the embedding cell in cv_reconciler.ipynb.
- Run the selected cell with the notebook run control.
You should see a summary with total, selected, duplicates, structural, and without_final_status counts. The next output shows up to three duplicate records.
- Confirm that without_final_status equals 0.
- Confirm that the selected count is lower than the classified record total from the previous step.
- Inspect each displayed duplicate for a populated duplicate_of value.
- Check that each displayed duplicate preserves its original text value.
That safeguard has done its job. Genuine repetition is reduced while every source record remains traceable.
Deduplication Cell Not Finishing?
- Keep the cell running while JupyterLab shows that the kernel is busy.
- Check your internet connection if the initial model download stops progressing.
- Rerun the import cell if the notebook kernel restarted.
- Rerun the extraction cell before repeating the classification cell.
Ask for help by selecting help me troubleshoot the embedding and deduplication cell.
✔️ Awesome, I've got everything!
Your active notebook session now contains selected, duplicate, and structural records. The duplicate links preserve the provenance needed for the final audit.
ⓧ I'd like to double check the full code
- Compare the existing deduplication cell with this exact reference:
embedder = load_embedder()
deduplicate_records(records, embedder, threshold=SEMANTIC_THRESHOLD)
print(summarize(records))
print([record for record in records if record['status'] == 'duplicate'][:3])
What Does This Cell Do?
- The first line loads the multilingual embedding model on the CPU.
- The second line applies semantic grouping with the configured threshold.
- The first print statement reports the final status counts.
- The final print statement displays sample duplicate records with their provenance links.
Your records now have explicit final statuses plus traceable duplicate links. Next, you will render the selected source text into the Word template and create the machine-readable audit.
Render the Word CV and Audit
Your active JupyterLab session now holds one selected record from each safe duplicate group. Every other extracted block remains traceable through its final status.
The final package uses docxtpl to place selected source text into Microsoft Word. A JSON audit preserves the evidence behind every decision.
In this step, get ready to:
- Validate the final status of every extracted block.
- Generate the consolidated CV package from the final records.
- Review the package for audit coverage plus readable pagination.
Generate the consolidated files
The assert_integrity() function acts as a publishing gate. It stops the process if a record lacks a final status.
The render fills all ten template categories with Listing values. The audit records the same final data in a machine-readable form.
- Switch back to the cv_reconciler.ipynb tab in your active JupyterLab session.
- Select the final notebook cell that starts with assert_integrity(records).
Before you run the final cell, do you expect every extracted block to pass the integrity gate?
- Generate the auditable output package by running this final cell:
assert_integrity(records)
render_word(records, TEMPLATE_PATH, OUTPUT_DOCX)
write_audit(records, AUDIT_PATH, threshold=SEMANTIC_THRESHOLD)
result = summarize(records)
print(result)
print(OUTPUT_DOCX.resolve())
print(AUDIT_PATH.resolve())
What does this cell do?
- The assert_integrity() call verifies that every record is selected, duplicate, or structural.
- The integrity check also requires every duplicate to have a duplicate_of target.
- The render_word() call validates the ten template tags. It fills those tags with selected source text.
- The write_audit() call records the models, threshold, summary, decisions, plus provenance.
- The summarize() call counts every final status.
- Check the printed summary beneath the final cell.
- Check the final two printed paths.
You should see without_final_status with the value 0. The printed paths should end with output/cv_consolide.docx plus output/audit.json.
That publishing gate is cleared. Your Word CV plus its audit now come from the same final record set.
Seeing an integrity or template error?
- Rerun the previous deduplication cell if the integrity check reports unfinished records.
- Inspect any duplicate record that lacks a duplicate_of value.
- Compare the tags in template/modele_cv_balisé.docx with the ten tags from the setup step if template validation fails.
Help me diagnose the failed integrity or template check.
✔️ Awesome, I've got everything!
Your notebook has completed the full pipeline. Double check that the final cell finished without an exception.
ⓧ I'd like to double check the full code
Compare your cumulative cv_reconciler.ipynb notebook with this full reference.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Consolidation locale de CV PowerPoint\n",
"Ce notebook conserve le texte source et écrit toutes les décisions dans un audit JSON."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"from cv_pipeline import (\n",
" assert_integrity, classify_records, deduplicate_records,\n",
" extract_all, find_exact_duplicate_groups, load_classifier,\n",
" load_embedder, render_word, summarize, write_audit,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"INPUT_DIR = Path('input_pptx')\n",
"TEMPLATE_PATH = Path('template/modele_cv_balisé.docx')\n",
"OUTPUT_DOCX = Path('output/cv_consolide.docx')\n",
"AUDIT_PATH = Path('output/audit.json')\n",
"SEMANTIC_THRESHOLD = 0.86\n",
"for folder in (INPUT_DIR, TEMPLATE_PATH.parent, OUTPUT_DOCX.parent):\n",
" folder.mkdir(parents=True, exist_ok=True)\n",
"print(INPUT_DIR, TEMPLATE_PATH, OUTPUT_DOCX, sep='\\n')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"records = extract_all(INPUT_DIR)\n",
"files = sorted({record['file'] for record in records})\n",
"print({'files': len(files), 'blocks': len(records)})\n",
"print(records[0])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"classifier = load_classifier()\n",
"classify_records(records, classifier)\n",
"exact_groups = find_exact_duplicate_groups(records)\n",
"low_confidence = [\n",
" record for record in records\n",
" if record['status'] == 'classified' and record['confidence'] < 0.45\n",
"]\n",
"print({'exact_duplicate_groups': len(exact_groups), 'low_confidence': len(low_confidence)})\n",
"print(low_confidence[:3])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"embedder = load_embedder()\n",
"deduplicate_records(records, embedder, threshold=SEMANTIC_THRESHOLD)\n",
"print(summarize(records))\n",
"print([record for record in records if record['status'] == 'duplicate'][:3])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert_integrity(records)\n",
"render_word(records, TEMPLATE_PATH, OUTPUT_DOCX)\n",
"write_audit(records, AUDIT_PATH, threshold=SEMANTIC_THRESHOLD)\n",
"result = summarize(records)\n",
"print(result)\n",
"print(OUTPUT_DOCX.resolve())\n",
"print(AUDIT_PATH.resolve())"
]
}
],
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.12"}
},
"nbformat": 4,
"nbformat_minor": 5
}
- Select the output folder in the JupyterLab sidebar.
You should see cv_consolide.docx plus audit.json inside the folder.
Inspect the audit trail
The audit makes each consolidation decision reviewable. Every extracted record keeps its category, confidence, final status, duplicate links, plus original provenance.
- Open audit.json from the output folder selected earlier.
- Confirm that semantic_threshold has the value 0.86.
- Confirm that the summary object reports without_final_status as 0.
- Locate a selected record with a non-empty duplicate_sources list.
- Trace one duplicate record through its duplicate_of value.
- Match that value to the retained record's source_id.
The selected record now shows which source variants it represents. The duplicate remains visible as evidence instead of disappearing from the pipeline.
Review the Word document
The Listing values preserve source line breaks plus the current character style at each tagged placeholder. A larger consolidated text volume can still change page breaks.
Before you inspect the document, where do you expect the longest source sections to affect pagination?
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Microsoft Word into the search bar.
- Press Enter to open Microsoft Word.
- Choose output/cv_consolide.docx in Word's document picker.
- Inspect every template section for its original heading.
- Compare the rendered text style with the surrounding template style.
- Check every page boundary for clipped or overlapping text.
You should see selected source text grouped under the ten template categories. The template structure remains present throughout the document.
Your auditable CV package is complete. Every rendered line comes from a selected source record while every extracted block remains accounted for.
Secret mission
Calibrer le seuil de fusion
Mesurez le seuil de fusion sur des reformulations issues de vos propres CV. Vous privilégierez un seuil sans faux positif avant de régénérer le CV consolidé et son audit.
Clean Up Your Resources
Clean Up Your Resources
Le pipeline fonctionne entièrement en local. Aucun service payant ne continue de tourner après le projet.
Microsoft Word desktop peut nécessiter une licence existante. Décidez si vous souhaitez conserver vos ressources locales, mettre le pipeline en pause ou supprimer les données sensibles.
Resources you used:
- La session JupyterLab active qui conserve les enregistrements finaux en mémoire.
- L'environnement Python isolé dans .venv.
- Le dossier input_pptx qui contient les CV PowerPoint sources.
- Le dossier template qui contient les modèles Word.
- Le dossier output qui contient le CV consolidé.
- Le fichier output/audit.json qui conserve les décisions du pipeline.
- Le fichier cv_pipeline.py qui contient le pipeline de consolidation.
- Le notebook cv_reconciler.ipynb qui exécute le traitement.
- Le script threshold_calibration.py qui calibre le seuil de fusion.
- Le jeu de validation labeled_pairs.csv qui contient vos paires annotées.
- Le fichier requirements.txt qui épingle les dépendances.
- Le cache local Hugging Face du modèle MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli.
- Le cache local du modèle sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2.
Keep everything running
Aucune action immédiate n'est nécessaire. Cette option convient si vous comptez consolider de nouvelles versions de vos CV.
- Conservez la session JupyterLab active pour garder les enregistrements finaux en mémoire.
- Conservez .venv pour réutiliser les dépendances épinglées.
- Gardez input_pptx pour retraiter les CV sources.
- Gardez template pour réutiliser les modèles Word.
- Gardez output pour conserver le document généré.
- Conservez output/audit.json avec le document auquel il correspond.
- Conservez les fichiers du pipeline pour reproduire la consolidation.
- Gardez le cache local des modèles pour éviter un nouveau téléchargement.
- Protégez le dossier du projet car il contient des données personnelles.
Conservez le document avec son audit
Le fichier output/cv_consolide.docx reflète les décisions enregistrées dans output/audit.json. Garder les deux fichiers ensemble préserve la traçabilité du résultat.
Pause - I'll come back to this later
Cette option libère la mémoire utilisée par JupyterLab. Tous les fichiers locaux restent disponibles pour une prochaine exécution.
- Revenez au terminal utilisé pour lancer JupyterLab.
- Appuyez sur Ctrl+C pour arrêter le processus actif.
L'invite du terminal revient lorsque JupyterLab est arrêté. Aucun processus du projet ne continue alors de tourner.
- Fermez les documents du projet ouverts dans Microsoft Word.
- Conservez le dossier du projet à son emplacement actuel.
- Gardez le cache local des modèles pour reprendre sans nouveau téléchargement.
JupyterLab reste-t-il actif ?
Vérifiez que le terminal utilisé est bien celui qui exécute JupyterLab. Répétez ensuite le raccourci d'interruption dans ce terminal.
Aidez-moi à arrêter ma session JupyterLab locale.
Delete - I don't want to use this again
Cette option retire les documents sensibles du projet. Elle libère aussi l'espace utilisé par l'environnement Python.
La suppression est définitive
Les CV sources, les modèles Word, le CV consolidé et son audit seront supprimés. Gardez ailleurs toute copie que vous êtes autorisé à conserver.
- Revenez au terminal utilisé pour lancer JupyterLab.
- Appuyez sur Ctrl+C pour arrêter le processus actif.
- Fermez chaque document du projet encore ouvert dans Microsoft Word.
- Confirmez que le terminal se trouve toujours dans le dossier qui contient cv_pipeline.py.
macOS
- Supprimez les dossiers locaux du projet en exécutant ces commandes:
rm -rf .venv input_pptx template output
rm -f cv_pipeline.py cv_reconciler.ipynb threshold_calibration.py labeled_pairs.csv requirements.txt
Ce que ces commandes suppriment
- La première commande retire l'environnement Python avec les dossiers de documents.
- La seconde commande retire le pipeline avec le notebook et le jeu de calibration.
L'invite du terminal revient sans message lorsque la suppression aboutit. Les fichiers du projet ne sont alors plus présents dans ce dossier.
La suppression est-elle bloquée ?
Vérifiez que JupyterLab est complètement arrêté. Fermez aussi chaque fichier du projet encore utilisé par une application.
Aidez-moi à débloquer la suppression des fichiers locaux.
Windows
- Supprimez les dossiers locaux du projet en exécutant ces commandes:
Remove-Item -Recurse -Force .venv, input_pptx, template, output
Remove-Item -Force cv_pipeline.py, cv_reconciler.ipynb, threshold_calibration.py, labeled_pairs.csv, requirements.txt
Ce que ces commandes suppriment
- La première commande retire l'environnement Python avec les dossiers de documents.
- La seconde commande retire le pipeline avec le notebook et le jeu de calibration.
L'invite du terminal revient sans message lorsque la suppression aboutit. Les fichiers du projet ne sont alors plus présents dans ce dossier.
La suppression est-elle bloquée ?
Vérifiez que JupyterLab est complètement arrêté. Fermez aussi chaque fichier du projet encore utilisé par une application.
Aidez-moi à débloquer la suppression des fichiers locaux.
Le cache des modèles se trouve séparément dans votre profil utilisateur. Sa suppression est utile si vous souhaitez récupérer tout l'espace disque occupé par ce projet.
- Recherchez multilingual-MiniLMv2-L6-mnli-xnli dans le cache local des modèles de votre profil utilisateur.
- Supprimez uniquement le dossier de cache correspondant au modèle de classification.
- Recherchez paraphrase-multilingual-MiniLM-L12-v2 dans le même cache local.
- Supprimez uniquement le dossier de cache correspondant au modèle d'embeddings.
- Relancez chaque recherche pour confirmer qu'aucun dossier correspondant ne reste.
Impossible d'identifier le bon cache ?
Ne supprimez pas un dossier de cache si vous ne pouvez pas le relier clairement à l'un des deux modèles. Cette précaution évite de retirer des modèles utilisés par d'autres projets.
Aidez-moi à distinguer les deux modèles de ce projet dans mon cache local.
Nice Work!
Nice Work!
Mission accomplie. Votre pipeline local dans JupyterLab transforme vos CV Microsoft PowerPoint en un CV Microsoft Word consolidé sans inventer de contenu. Chaque décision reste vérifiable dans l'audit.
Vous avez appris à :
- Extraire les blocs textuels de chaque CV avec une provenance complète. L'audit OOXML récupère aussi les chaînes moins visibles.
- Classer chaque bloc dans une taxonomie fermée avec la classification zero-shot. La similarité sémantique regroupe prudemment les reformulations. Des règles temporelles explicites départagent les variantes de dates.
- Générer un CV Word consolidé à partir du modèle balisé. Un audit JSON prouve que chaque bloc source possède un statut final.
- Secret Mission : Calibrer le seuil de fusion sur au moins douze paires difficiles. Le seuil recommandé privilégie l'absence de faux positifs.
Prêt à tester vos connaissances ?