first commit

This commit is contained in:
2026-03-26 15:00:38 +01:00
parent 68b640eadb
commit 048220a6dc
13 changed files with 606 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(pip install:*)",
"Bash(pip3 install:*)",
"Bash(python3 -m pip install python-docx)",
"Bash(python3:*)"
]
}
}

View File

@@ -0,0 +1,54 @@
---
name: formation-docx-replacer
description: Use when the user asks to create a new formation dossier, replace text in docx files, or apply a YAML config to modify formation documents. Triggers on keywords like formation, dossier, remplacements, docx, yaml config, nouvelle formation.
---
# Formation DOCX Replacer
## Overview
Prepare un dossier de formation a partir du template et guide l'utilisateur pour configurer et lancer les remplacements dans les .docx.
## When to Use
- L'utilisateur veut creer une nouvelle formation
- L'utilisateur mentionne un nouveau dossier de formation
- L'utilisateur veut dupliquer le template pour un nouveau client/formation
## Workflow
1. **Demander le nom de la formation** a l'utilisateur (ex: "Excel Avance Dupont Avril 2026")
2. **Creer le dossier** dans `formations/` en slugifiant le nom (ex: `formations/excel_avance_dupont_avril_2026/`)
3. **Copier le template YAML** : `template/formation_template.yaml` -> `formations/<nom>/formation.yaml`
4. **Indiquer a l'utilisateur** :
- Le chemin du fichier YAML a editer
- Qu'il doit remplir les paires ancien/nouveau avec ses infos
- La commande exacte a lancer une fois le YAML pret :
```
python3 replace_docx.py formations/<nom>/formation.yaml
```
Ne PAS essayer de remplir le YAML a la place de l'utilisateur. Il connait ses infos.
## Format YAML
```yaml
dossier_source: template
remplacements:
- ancien: "texte a chercher"
nouveau: "texte de remplacement"
- ancien: "autre texte"
nouveau: "nouveau texte"
fichiers: ["seul_ce_fichier.docx"] # optionnel, par defaut tous
```
## Ce que le script gere
- Paragraphes, tableaux (imbriques), en-tetes, pieds de page
- Texte decoupe sur plusieurs runs Word
- Preservation du formatage (police, taille, couleur, gras)
## Common Mistakes
- **Texte introuvable** : Word fragmente parfois le texte en plusieurs runs. Verifier avec python-docx
- **Accents** : YAML en UTF-8

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

10
.idea/material_theme_project_new.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="userId" value="2bbcbe4d:19d1b20fd6b:-65c4" />
</MTProjectMetadataState>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/qualiopi.iml" filepath="$PROJECT_DIR$/.idea/qualiopi.iml" />
</modules>
</component>
</project>

19
.idea/php.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

8
.idea/qualiopi.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

53
CLAUDE.md Normal file
View File

@@ -0,0 +1,53 @@
# CLAUDE.md - Qualiopi
## Contexte projet
Ce depot contient les documents de formation de la SAS MALIO (organisme de formation certifie Qualiopi). L'objectif principal est d'automatiser la creation de nouveaux dossiers de formation a partir de templates existants.
## Structure
```
qualiopi/
├── replace_docx.py # Script principal
├── template/ # Documents modeles (ne jamais modifier directement)
│ ├── *.docx # Documents template
│ └── formation_template.yaml # Modele de config
└── formations/ # Un sous-dossier par formation generee
└── <nom_formation>/
├── formation.yaml # Config avec les remplacements
├── rapport.md # Rapport des modifications
└── *.docx # Documents generes
```
## Outils disponibles
### replace_docx.py
Script Python de remplacement de texte dans des fichiers `.docx`.
- **Commande** : `python3 replace_docx.py <chemin/vers/formation.yaml>`
- **Dependances** : python-docx, pyyaml
- **Comportement** : copie les .docx du template dans le dossier du YAML, applique les remplacements et genere un `rapport.md`
## Workflow pour creer une formation
Via le skill : `/formation-docx-replacer <nom_formation>` (cree le dossier et copie le YAML)
Ou manuellement :
1. `mkdir formations/<nom_formation>`
2. `cp template/formation_template.yaml formations/<nom_formation>/formation.yaml`
3. Editer le YAML avec les paires ancien/nouveau
4. `python3 replace_docx.py formations/<nom_formation>/formation.yaml`
5. Verifier `rapport.md` dans le dossier (signaler les remplacements non trouves)
## Regles importantes
- **Ne jamais modifier le dossier template/** : le script copie les .docx dans le dossier de la formation
- **Chemins relatifs** : dans le YAML, les chemins sont relatifs a la racine du projet
- **Encodage** : fichiers YAML en UTF-8
- **Verifier le rapport** : apres chaque execution, signaler les remplacements non trouves a l'utilisateur
- **Formatage** : le script preserve les polices, tailles, couleurs et styles
## Skill associe
Le skill `formation-docx-replacer` dans `.claude/skills/` gere l'utilisation automatique du script.

113
README.md Normal file
View File

@@ -0,0 +1,113 @@
# Qualiopi - Generateur de dossiers de formation
Outil d'automatisation pour creer des dossiers de formation a partir d'un template existant. Remplace en masse les informations (client, dates, formateur, etc.) dans tous les fichiers `.docx` d'un dossier, en preservant le formatage d'origine.
## Prerequis
- Python 3.10+
- python-docx (`pip3 install python-docx`)
- PyYAML (`pip3 install pyyaml`)
## Structure du projet
```
qualiopi/
├── replace_docx.py # Script de remplacement
├── template/ # Dossier template (ne pas modifier)
│ ├── CONDITIONS_GENERALES_DE_VENTE.docx
│ ├── formation_template.yaml # Modele de config a copier
│ └── ... # Autres documents template
└── formations/ # Dossiers generes par formation
└── excel_dupont_avril2026/
├── formation.yaml # Config de la formation
├── rapport.md # Rapport des modifications
├── CONDITIONS_GENERALES_DE_VENTE.docx # Copie modifiee
└── ...
```
## Utilisation
### 1. Creer le dossier de la formation
```bash
mkdir -p formations/excel_dupont_avril2026
```
### 2. Copier et configurer le YAML
```bash
cp template/formation_template.yaml formations/excel_dupont_avril2026/formation.yaml
```
Editer `formation.yaml` avec les infos de la formation :
```yaml
dossier_source: template
remplacements:
- ancien: "Word - Initiation bureautique"
nouveau: "Excel Avance - TCD"
- ancien: "Societe Martin SARL"
nouveau: "Societe Dupont SAS"
- ancien: "10 et 11 mars 2026"
nouveau: "15 et 16 avril 2026"
```
### 3. Lancer le script
```bash
python3 replace_docx.py formations/excel_dupont_avril2026/formation.yaml
```
Le script :
1. Copie les `.docx` du template dans le dossier de la formation
2. Applique tous les remplacements
3. Genere un fichier `rapport.md` dans le dossier de la formation
### 4. Verifier le rapport
Ouvrir `formations/excel_dupont_avril2026/rapport.md`. Il contient :
- La date d'execution
- Un tableau par fichier avec les remplacements effectues et le nombre d'occurrences
- La liste des remplacements non trouves (s'il y en a)
## Options
### Cibler un fichier specifique
Par defaut, chaque remplacement s'applique a tous les `.docx`. Pour cibler un fichier :
```yaml
remplacements:
- ancien: "Texte specifique"
nouveau: "Nouveau texte"
fichiers: ["convention.docx"]
```
### Chemins
Tous les chemins dans le YAML sont relatifs a la racine du projet (dossier contenant `replace_docx.py`).
## Ce qui est pris en charge
| Zone du document | Supporte |
|---|---|
| Paragraphes du corps | Oui |
| Cellules de tableaux | Oui |
| Tableaux imbriques | Oui |
| En-tetes de page | Oui |
| Pieds de page | Oui |
| Formatage (police, taille, couleur, gras) | Preserve |
| Texte decoupe sur plusieurs runs | Gere automatiquement |
## Utilisation avec Claude
Le skill `/formation-docx-replacer` permet de preparer un dossier automatiquement :
```
/formation-docx-replacer ma_nouvelle_formation
```
Claude cree le dossier dans `formations/`, copie le template YAML et indique la marche a suivre.

272
replace_docx.py Normal file
View File

@@ -0,0 +1,272 @@
#!/usr/bin/env python3
"""
Script de remplacement de texte dans des fichiers .docx
Lit un fichier YAML de configuration et applique les remplacements
dans tous les .docx d'un dossier en préservant le formatage.
"""
import sys
import os
import shutil
import yaml
from docx import Document
from copy import deepcopy
def get_full_text_from_runs(runs):
"""Reconstitue le texte complet à partir des runs."""
return "".join(r.text for r in runs)
def replace_in_runs(runs, ancien, nouveau):
"""
Remplace du texte dans une liste de runs en préservant le formatage.
Gère le cas où le texte est découpé sur plusieurs runs.
Retourne le nombre de remplacements effectués.
"""
count = 0
full_text = get_full_text_from_runs(runs)
if ancien not in full_text:
return 0
while ancien in full_text:
count += 1
start_idx = full_text.index(ancien)
end_idx = start_idx + len(ancien)
# Trouver quels runs sont concernés
char_pos = 0
start_run = None
end_run = None
for i, run in enumerate(runs):
run_start = char_pos
run_end = char_pos + len(run.text)
if start_run is None and run_end > start_idx:
start_run = i
start_offset = start_idx - run_start
if run_end >= end_idx:
end_run = i
end_offset = end_idx - run_start
break
char_pos = run_end
if start_run is None or end_run is None:
break
# Cas simple : tout dans un seul run
if start_run == end_run:
run = runs[start_run]
run.text = run.text[:start_offset] + nouveau + run.text[end_offset:]
else:
# Multi-run : mettre le nouveau texte dans le premier run,
# vider les runs intermédiaires, ajuster le dernier
runs[start_run].text = runs[start_run].text[:start_offset] + nouveau
for i in range(start_run + 1, end_run):
runs[i].text = ""
runs[end_run].text = runs[end_run].text[end_offset:]
full_text = get_full_text_from_runs(runs)
return count
def replace_in_paragraph(paragraph, ancien, nouveau):
"""Remplace du texte dans un paragraphe."""
if not paragraph.runs:
return 0
return replace_in_runs(paragraph.runs, ancien, nouveau)
def replace_in_table(table, ancien, nouveau):
"""Remplace du texte dans toutes les cellules d'un tableau."""
count = 0
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
count += replace_in_paragraph(paragraph, ancien, nouveau)
# Tables imbriquées
for nested_table in cell.tables:
count += replace_in_table(nested_table, ancien, nouveau)
return count
def replace_in_headers_footers(doc, ancien, nouveau):
"""Remplace du texte dans les en-têtes et pieds de page."""
count = 0
for section in doc.sections:
for header in [section.header, section.first_page_header, section.even_page_header]:
if header and header.is_linked_to_previous is False:
for paragraph in header.paragraphs:
count += replace_in_paragraph(paragraph, ancien, nouveau)
for table in header.tables:
count += replace_in_table(table, ancien, nouveau)
for footer in [section.footer, section.first_page_footer, section.even_page_footer]:
if footer and footer.is_linked_to_previous is False:
for paragraph in footer.paragraphs:
count += replace_in_paragraph(paragraph, ancien, nouveau)
for table in footer.tables:
count += replace_in_table(table, ancien, nouveau)
return count
def process_docx(filepath, remplacements):
"""Applique tous les remplacements sur un fichier .docx."""
doc = Document(filepath)
rapport = {}
for remp in remplacements:
ancien = remp["ancien"]
nouveau = remp["nouveau"]
fichiers_cibles = remp.get("fichiers", None)
# Si des fichiers cibles sont spécifiés, vérifier que ce fichier en fait partie
if fichiers_cibles:
basename = os.path.basename(filepath)
if basename not in fichiers_cibles:
continue
count = 0
# Paragraphes du corps
for paragraph in doc.paragraphs:
count += replace_in_paragraph(paragraph, ancien, nouveau)
# Tableaux
for table in doc.tables:
count += replace_in_table(table, ancien, nouveau)
# En-têtes et pieds de page
count += replace_in_headers_footers(doc, ancien, nouveau)
if count > 0:
rapport[f"'{ancien}' -> '{nouveau}'"] = count
doc.save(filepath)
return rapport
def main():
if len(sys.argv) < 2:
print("Usage: python3 replace_docx.py <fichier_config.yaml>")
sys.exit(1)
config_path = os.path.abspath(sys.argv[1])
config_dir = os.path.dirname(config_path)
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
# Les chemins sont relatifs à la racine du projet (dossier contenant replace_docx.py)
project_dir = os.path.dirname(os.path.abspath(__file__))
dossier_source = config["dossier_source"]
if not os.path.isabs(dossier_source):
dossier_source = os.path.join(project_dir, dossier_source)
remplacements = config["remplacements"]
# Le dossier de travail est celui qui contient le fichier YAML
dossier_travail = config_dir
# Copier les .docx du template dans le dossier de travail
docx_copies = 0
for f in os.listdir(dossier_source):
if f.endswith(".docx") and not f.startswith("~$"):
src = os.path.join(dossier_source, f)
dst = os.path.join(dossier_travail, f)
if not os.path.exists(dst):
shutil.copy2(src, dst)
docx_copies += 1
if docx_copies > 0:
print(f"{docx_copies} fichier(s) .docx copie(s) depuis {dossier_source}")
else:
print(f"Fichiers .docx deja presents dans {dossier_travail}")
# Trouver tous les .docx du dossier de travail
docx_files = []
for f in os.listdir(dossier_travail):
if f.endswith(".docx") and not f.startswith("~$"):
docx_files.append(os.path.join(dossier_travail, f))
if not docx_files:
print("Aucun fichier .docx trouve.")
sys.exit(1)
print(f"\n{len(docx_files)} fichier(s) .docx trouve(s)\n")
# Appliquer les remplacements
rapport_global = {}
for filepath in sorted(docx_files):
rapport = process_docx(filepath, remplacements)
rel_path = os.path.relpath(filepath, dossier_travail)
rapport_global[rel_path] = rapport
# Identifier les remplacements non trouves
remplacements_non_trouves = set()
for remp in remplacements:
cle = f"'{remp['ancien']}' -> '{remp['nouveau']}'"
trouve = False
for fichier, rapport in rapport_global.items():
if cle in rapport:
trouve = True
break
if not trouve:
remplacements_non_trouves.add(cle)
# Generer le rapport markdown
from datetime import datetime
lignes = []
lignes.append(f"# Rapport de modifications")
lignes.append(f"")
lignes.append(f"**Date** : {datetime.now().strftime('%d/%m/%Y %H:%M')}")
lignes.append(f"**Template** : `{os.path.relpath(dossier_source, project_dir)}`")
lignes.append(f"**Fichiers traites** : {len(docx_files)}")
lignes.append(f"")
lignes.append(f"## Modifications par fichier")
lignes.append(f"")
for fichier, rapport in sorted(rapport_global.items()):
lignes.append(f"### {fichier}")
lignes.append(f"")
if rapport:
lignes.append(f"| Ancien | Nouveau | Occurrences |")
lignes.append(f"|---|---|---|")
for remp, count in rapport.items():
# remp est au format "'ancien' -> 'nouveau'"
parties = remp.split("' -> '")
ancien_txt = parties[0][1:] # enlever le ' du debut
nouveau_txt = parties[1][:-1] # enlever le ' de la fin
lignes.append(f"| {ancien_txt} | {nouveau_txt} | {count} |")
else:
lignes.append(f"Aucun remplacement effectue.")
lignes.append(f"")
if remplacements_non_trouves:
lignes.append(f"## Remplacements non trouves")
lignes.append(f"")
lignes.append(f"Les remplacements suivants n'ont ete trouves dans aucun fichier :")
lignes.append(f"")
for r in remplacements_non_trouves:
lignes.append(f"- {r}")
lignes.append(f"")
# Ecrire le rapport
rapport_path = os.path.join(dossier_travail, "rapport.md")
with open(rapport_path, "w", encoding="utf-8") as f:
f.write("\n".join(lignes))
print(f"Rapport genere : {rapport_path}")
print("Termine!")
if __name__ == "__main__":
main()

Binary file not shown.

View File

@@ -0,0 +1,43 @@
# Configuration de remplacement pour dossier de formation
# Usage:
# 1. Creer un dossier pour la formation : mkdir formations/ma_formation
# 2. Copier ce fichier dedans : cp template/formation_template.yaml formations/ma_formation/formation.yaml
# 3. Editer le fichier copie avec les bonnes infos
# 4. Lancer : python3 replace_docx.py formations/ma_formation/formation.yaml
#
# Le script copie automatiquement les .docx du template dans le dossier de la formation.
# Les chemins sont relatifs a la racine du projet.
# Dossier contenant les .docx template
dossier_source: template
# Liste des remplacements (appliques a TOUS les .docx copies)
remplacements:
# Infos formation
- ancien: "Word - Initiation bureautique"
nouveau: "Excel Avance - Tableaux croises dynamiques"
# Infos client
- ancien: "Societe Martin SARL"
nouveau: "Societe Dupont SAS"
# Dates
- ancien: "10 et 11 mars 2026"
nouveau: "15 et 16 avril 2026"
# Formateur
- ancien: "Marie Martin"
nouveau: "Jean Dupont"
# Lieu
- ancien: "14 allee d'Argenson, Chatellerault"
nouveau: "12 rue des Lilas, Poitiers"
# Duree
- ancien: "7 heures (1 jour)"
nouveau: "14 heures (2 jours)"
# Remplacement cible sur un seul fichier (optionnel)
# - ancien: "Texte specifique a la convention"
# nouveau: "Nouveau texte convention"
# fichiers: ["convention.docx"]