fiche-ppi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: fiche-ppi
3
+ Version: 0.1.0
4
+ Summary: Generate PPI sheets from Excel templates
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/fiche-ppi
8
+ Project-URL: Bug-tracker, https://github.com/yourusername/fiche-ppi/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pandas>=1.5.0
15
+ Requires-Dist: numpy>=1.24.0
16
+ Requires-Dist: requests>=2.28.0
17
+ Requires-Dist: beautifulsoup4>=4.12.0
18
+
19
+ # Générateur de Fiches PPI
20
+
21
+ Générateur de fiches PPI (Phrases Préfabriquées des Interactions) à partir de grilles d'analyse de PPI pré-remplies.
22
+
23
+ ## 📋 Description
24
+
25
+ Cet outil permet de générer automatiquement des fiches d'analyse PPI à partir de paires de grilles Excel (oral/écrit) pré-remplies. Les fiches produites suivent la grille d'analyse développée dans le cadre du projet ANR PREFAB et résument certaines informations pour faciliter la saisie des fiches finales.
26
+
27
+ Les fonctionnalités principales :
28
+
29
+ - **Mode simple** : génération d'une fiche à partir d'une paire de fichiers (oral + écrit)
30
+ - **Mode batch** : génération de plusieurs fiches à partir d'un dossier contenant des paires de fichiers `*_Or.xlsx` / `*_Ph.xlsx`
31
+ - **Interface graphique** intuitive (Tkinter)
32
+ - **Formatage automatique** des cellules (gras, couleurs, tags XML)
33
+
34
+ ## 🚀 Installation
35
+
36
+ ### Depuis PyPI (recommandé)
37
+
38
+ ```bash
39
+ pip install fiche-ppi
40
+ ```
41
+
42
+ ### Depuis les sources
43
+
44
+ ```bash
45
+ git clone https://github.com/yourusername/fiche-ppi.git
46
+ cd fiche-ppi
47
+ pip install -e .
48
+ ```
49
+
50
+ ### Dépendances
51
+
52
+ - Python ≥ 3.8
53
+ - pandas ≥ 1.5.0
54
+ - numpy ≥ 1.24.0
55
+ - requests ≥ 2.28.0
56
+ - beautifulsoup4 ≥ 4.12.0
57
+ - ppi_analyser (dépendance interne)
58
+
59
+ **Note** : Sur Linux, `tkinter` peut nécessiter l'installation d'un paquet système :
60
+
61
+ ```bash
62
+ sudo apt-get install python3-tk # Debian/Ubuntu
63
+ sudo dnf install python3-tkinter # Fedora
64
+ ```
65
+
66
+ ## 💻 Utilisation
67
+
68
+ ### Interface graphique
69
+
70
+ Lancez l'interface graphique :
71
+
72
+ ```bash
73
+ fiche-ppi-gui
74
+ ```
75
+
76
+ ### Mode graphique - Onglet Simple
77
+
78
+ 1. Sélectionnez le fichier oral (`*_Or.xlsx`)
79
+ 2. Sélectionnez le fichier écrit (`*_Ph.xlsx`)
80
+ 3. Le chemin de sortie est automatiquement généré (modifiable)
81
+ 4. Cliquez sur "⚡ Générer la fiche"
82
+
83
+ ### Mode graphique - Onglet Batch
84
+
85
+ 1. Sélectionnez un dossier contenant des paires de fichiers
86
+ 2. Les paires `*_Or.xlsx` / `*_Ph.xlsx` sont automatiquement détectées
87
+ 3. Cliquez sur "⚡ Générer toutes les fiches"
88
+
89
+ ### Ligne de commande
90
+
91
+ ```bash
92
+ python -m fiche_ppi.fiche_ppi fichier_oral.xlsx fichier_ecrit.xlsx -o fiche.xlsx
93
+ ```
94
+
95
+ ## 📁 Structure des fichiers d'entrée
96
+
97
+ Les fichiers Excel doivent contenir les colonnes suivantes (issues de l'export Lexicoscope) :
98
+
99
+ - `sentId`, `left`, `node`, `right`, `author`, `collection`
100
+ - `corpusId`, `pubdate`, `publisher`, `pubplace`, `puburl`
101
+ - `source_language`, `sourcefilename`, `sub_genre`, `title`
102
+ - `type`, `wordsnumber`, `year`
103
+
104
+ et les colonnes d'annotation PPI :
105
+
106
+ - `Lemme`, `Forme PPI`, `Acception`, `Type de phrase`
107
+ - `Modalité d'énonciation`, `Expansion`, `Modifieurs`
108
+ - `Cooccurrents`, `Fonction globale`, `Fonctions spécifiques`
109
+ - `milieu`, `secteur`, `Remarques`
110
+
111
+ ## Génération des colonnes de la fiche PPI
112
+
113
+ | Propriété | Comment c'est généré |
114
+ |-----------|---------------------|
115
+ | **Fe_1a PPI** | Prend la valeur de la colonne `Forme PPI` du premier enregistrement du DataFrame combiné (oral + écrit) |
116
+ | **Fe_1b Acception** | Joint les valeurs uniques non-vides de la colonne `Acception` (triées, séparées par ", ") |
117
+ | **Fe_1c Variantes formelles** | Calcule les variantes formelles pour l'oral et l'écrit séparément :<br>- Extrait les modifieurs de la colonne `Modifieurs`<br>- Nettoie les tokens de modifieurs<br>- Applique `remove_modifier()` pour retirer les modifieurs de la colonne `node`<br>- Formate : `\n- Oral : var1, var2\n- Écrit : var3, var4` |
118
+ | **Fe_1e Prononciation** | Pour le lemme (premier `Lemme` du DataFrame oral) :Scrape la page Wiktionary du lemme pour extraire les URLs des fichiers audio MP3 - Extrait les URLs des fichiers MP3<br>- Retourne une liste d'URLs séparées par des sauts de ligne |
119
+ | **Fe_2a Statut syntaxique phrase** | Joint les valeurs uniques de la colonne `Type de phrase` |
120
+ | **Fe_2c Modalité de phrase** | Joint les valeurs uniques de la colonne `Modalité d'énonciation` |
121
+ | **Fe_2e Expansion éventuelle** | Joint les valeurs uniques de la colonne `Expansion` |
122
+ | **Fe_3a Fonction globale** | Joint les valeurs uniques de la colonne `Fonction globale` |
123
+ | **Fe_3b Fonctions spécifiques** | Joint les valeurs uniques de la colonne `Fonctions spécifiques` |
124
+ | **Fe_3c Codes Fonction globale** | Joint les valeurs uniques de la colonne `Fonction globale` (identique à Fe_3a) |
125
+ | **Fe_3d Codes Fonctions spécifiques** | Joint les valeurs uniques de la colonne `Fonctions spécifiques` (identique à Fe_3b) |
126
+ | **Fe_3f Structure interactionnelle** | Pour l'oral, l'écrit et le combiné :<br>- Calcule les fréquences des colonnes `Déclenchement`, `Portée`, `Position`<br>- Formate : `Oral :\n\t- Déclenchement : val1 (n), val2 (n)\n\t- Portée : ...` |
127
+ | **Fe_3g Contexte spécifique** | Joint les valeurs uniques de la colonne `milieu` |
128
+ | **Fe_3h Modalité écrite et orale** | Joint les valeurs uniques de la colonne `secteur` |
129
+ | **Fe_4a Cooccurrents privilégiés** | Pour l'oral, l'écrit et le combiné :<br>- Parse la colonne `Cooccurrents`<br>- Sépare les éléments antéposés (a) et postposés (p)<br>- Compte les fréquences avec `Counter()`<br>- Calcule les pourcentages d'antéposés/postposés<br>- Formate : `Oral :\n\t- Cooccurrents antéposés (X%) : ...\n\t- Cooccurrents postposés (Y%) : ...` |
130
+ | **Fe_4b Modifieurs de la PPI** | Joint les valeurs uniques de la colonne `Modifieurs` |
131
+ | **Fe_9a Remarques** | Joint les valeurs uniques de la colonne `Remarques` |
132
+
133
+ ---
134
+
135
+ **LIDILEM · ANR PREFAB**
@@ -0,0 +1,117 @@
1
+ # Générateur de Fiches PPI
2
+
3
+ Générateur de fiches PPI (Phrases Préfabriquées des Interactions) à partir de grilles d'analyse de PPI pré-remplies.
4
+
5
+ ## 📋 Description
6
+
7
+ Cet outil permet de générer automatiquement des fiches d'analyse PPI à partir de paires de grilles Excel (oral/écrit) pré-remplies. Les fiches produites suivent la grille d'analyse développée dans le cadre du projet ANR PREFAB et résument certaines informations pour faciliter la saisie des fiches finales.
8
+
9
+ Les fonctionnalités principales :
10
+
11
+ - **Mode simple** : génération d'une fiche à partir d'une paire de fichiers (oral + écrit)
12
+ - **Mode batch** : génération de plusieurs fiches à partir d'un dossier contenant des paires de fichiers `*_Or.xlsx` / `*_Ph.xlsx`
13
+ - **Interface graphique** intuitive (Tkinter)
14
+ - **Formatage automatique** des cellules (gras, couleurs, tags XML)
15
+
16
+ ## 🚀 Installation
17
+
18
+ ### Depuis PyPI (recommandé)
19
+
20
+ ```bash
21
+ pip install fiche-ppi
22
+ ```
23
+
24
+ ### Depuis les sources
25
+
26
+ ```bash
27
+ git clone https://github.com/yourusername/fiche-ppi.git
28
+ cd fiche-ppi
29
+ pip install -e .
30
+ ```
31
+
32
+ ### Dépendances
33
+
34
+ - Python ≥ 3.8
35
+ - pandas ≥ 1.5.0
36
+ - numpy ≥ 1.24.0
37
+ - requests ≥ 2.28.0
38
+ - beautifulsoup4 ≥ 4.12.0
39
+ - ppi_analyser (dépendance interne)
40
+
41
+ **Note** : Sur Linux, `tkinter` peut nécessiter l'installation d'un paquet système :
42
+
43
+ ```bash
44
+ sudo apt-get install python3-tk # Debian/Ubuntu
45
+ sudo dnf install python3-tkinter # Fedora
46
+ ```
47
+
48
+ ## 💻 Utilisation
49
+
50
+ ### Interface graphique
51
+
52
+ Lancez l'interface graphique :
53
+
54
+ ```bash
55
+ fiche-ppi-gui
56
+ ```
57
+
58
+ ### Mode graphique - Onglet Simple
59
+
60
+ 1. Sélectionnez le fichier oral (`*_Or.xlsx`)
61
+ 2. Sélectionnez le fichier écrit (`*_Ph.xlsx`)
62
+ 3. Le chemin de sortie est automatiquement généré (modifiable)
63
+ 4. Cliquez sur "⚡ Générer la fiche"
64
+
65
+ ### Mode graphique - Onglet Batch
66
+
67
+ 1. Sélectionnez un dossier contenant des paires de fichiers
68
+ 2. Les paires `*_Or.xlsx` / `*_Ph.xlsx` sont automatiquement détectées
69
+ 3. Cliquez sur "⚡ Générer toutes les fiches"
70
+
71
+ ### Ligne de commande
72
+
73
+ ```bash
74
+ python -m fiche_ppi.fiche_ppi fichier_oral.xlsx fichier_ecrit.xlsx -o fiche.xlsx
75
+ ```
76
+
77
+ ## 📁 Structure des fichiers d'entrée
78
+
79
+ Les fichiers Excel doivent contenir les colonnes suivantes (issues de l'export Lexicoscope) :
80
+
81
+ - `sentId`, `left`, `node`, `right`, `author`, `collection`
82
+ - `corpusId`, `pubdate`, `publisher`, `pubplace`, `puburl`
83
+ - `source_language`, `sourcefilename`, `sub_genre`, `title`
84
+ - `type`, `wordsnumber`, `year`
85
+
86
+ et les colonnes d'annotation PPI :
87
+
88
+ - `Lemme`, `Forme PPI`, `Acception`, `Type de phrase`
89
+ - `Modalité d'énonciation`, `Expansion`, `Modifieurs`
90
+ - `Cooccurrents`, `Fonction globale`, `Fonctions spécifiques`
91
+ - `milieu`, `secteur`, `Remarques`
92
+
93
+ ## Génération des colonnes de la fiche PPI
94
+
95
+ | Propriété | Comment c'est généré |
96
+ |-----------|---------------------|
97
+ | **Fe_1a PPI** | Prend la valeur de la colonne `Forme PPI` du premier enregistrement du DataFrame combiné (oral + écrit) |
98
+ | **Fe_1b Acception** | Joint les valeurs uniques non-vides de la colonne `Acception` (triées, séparées par ", ") |
99
+ | **Fe_1c Variantes formelles** | Calcule les variantes formelles pour l'oral et l'écrit séparément :<br>- Extrait les modifieurs de la colonne `Modifieurs`<br>- Nettoie les tokens de modifieurs<br>- Applique `remove_modifier()` pour retirer les modifieurs de la colonne `node`<br>- Formate : `\n- Oral : var1, var2\n- Écrit : var3, var4` |
100
+ | **Fe_1e Prononciation** | Pour le lemme (premier `Lemme` du DataFrame oral) :Scrape la page Wiktionary du lemme pour extraire les URLs des fichiers audio MP3 - Extrait les URLs des fichiers MP3<br>- Retourne une liste d'URLs séparées par des sauts de ligne |
101
+ | **Fe_2a Statut syntaxique phrase** | Joint les valeurs uniques de la colonne `Type de phrase` |
102
+ | **Fe_2c Modalité de phrase** | Joint les valeurs uniques de la colonne `Modalité d'énonciation` |
103
+ | **Fe_2e Expansion éventuelle** | Joint les valeurs uniques de la colonne `Expansion` |
104
+ | **Fe_3a Fonction globale** | Joint les valeurs uniques de la colonne `Fonction globale` |
105
+ | **Fe_3b Fonctions spécifiques** | Joint les valeurs uniques de la colonne `Fonctions spécifiques` |
106
+ | **Fe_3c Codes Fonction globale** | Joint les valeurs uniques de la colonne `Fonction globale` (identique à Fe_3a) |
107
+ | **Fe_3d Codes Fonctions spécifiques** | Joint les valeurs uniques de la colonne `Fonctions spécifiques` (identique à Fe_3b) |
108
+ | **Fe_3f Structure interactionnelle** | Pour l'oral, l'écrit et le combiné :<br>- Calcule les fréquences des colonnes `Déclenchement`, `Portée`, `Position`<br>- Formate : `Oral :\n\t- Déclenchement : val1 (n), val2 (n)\n\t- Portée : ...` |
109
+ | **Fe_3g Contexte spécifique** | Joint les valeurs uniques de la colonne `milieu` |
110
+ | **Fe_3h Modalité écrite et orale** | Joint les valeurs uniques de la colonne `secteur` |
111
+ | **Fe_4a Cooccurrents privilégiés** | Pour l'oral, l'écrit et le combiné :<br>- Parse la colonne `Cooccurrents`<br>- Sépare les éléments antéposés (a) et postposés (p)<br>- Compte les fréquences avec `Counter()`<br>- Calcule les pourcentages d'antéposés/postposés<br>- Formate : `Oral :\n\t- Cooccurrents antéposés (X%) : ...\n\t- Cooccurrents postposés (Y%) : ...` |
112
+ | **Fe_4b Modifieurs de la PPI** | Joint les valeurs uniques de la colonne `Modifieurs` |
113
+ | **Fe_9a Remarques** | Joint les valeurs uniques de la colonne `Remarques` |
114
+
115
+ ---
116
+
117
+ **LIDILEM · ANR PREFAB**
File without changes
@@ -0,0 +1,325 @@
1
+ """
2
+ fiche_ppi.py
3
+ Génère une fiche PPI consolidée à partir de deux fichiers Excel (oral / écrit).
4
+
5
+ Usage:
6
+ python fiche_ppi.py <file_oral.xlsx> <file_ecrit.xlsx>
7
+ """
8
+
9
+ # ── Imports ────────────────────────────────────────────────────────────────────
10
+
11
+ import argparse
12
+ import re
13
+ import sys
14
+ from collections import Counter
15
+ from urllib.parse import quote
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+ import requests
20
+ from bs4 import BeautifulSoup
21
+
22
+ from .format_excel import format_ppi_bold
23
+
24
+
25
+ # ── CLI ────────────────────────────────────────────────────────────────────────
26
+
27
+ def parse_args() -> argparse.Namespace:
28
+ parser = argparse.ArgumentParser(
29
+ description="Génère une fiche PPI consolidée à partir de deux fichiers Excel."
30
+ )
31
+ parser.add_argument("file_oral", help="Fichier Excel oral (.xlsx)")
32
+ parser.add_argument("file_ecrit", help="Fichier Excel écrit (.xlsx)")
33
+ parser.add_argument(
34
+ "-o", "--output",
35
+ default="fiche.xlsx",
36
+ help="Fichier de sortie (défaut: fiche.xlsx)"
37
+ )
38
+ return parser.parse_args()
39
+
40
+
41
+ # ── Wiktionary ─────────────────────────────────────────────────────────────────
42
+
43
+ def get_wiktionary_pronunciation(expression: str, lang: str = "fr") -> list[str]:
44
+ """
45
+ Récupère les URLs audio MP3 pour une expression depuis Wiktionary.
46
+ Retourne une liste d'URLs ou une liste vide en cas d'échec.
47
+ """
48
+ url = f"https://{lang}.wiktionary.org/wiki/{quote(expression)}"
49
+ headers = {"User-Agent": "Mozilla/5.0 (pronunciation-fetcher/1.0)"}
50
+
51
+ try:
52
+ response = requests.get(url, headers=headers, timeout=10)
53
+ response.raise_for_status()
54
+
55
+ soup = BeautifulSoup(response.text, "html.parser")
56
+ audio_urls = []
57
+
58
+ for source in soup.find_all("source"):
59
+ src = source.get("src", "")
60
+ if src and src.startswith("//") and src.endswith(".mp3"):
61
+ audio_urls.append("https:" + src)
62
+
63
+ if not audio_urls:
64
+ print(f"[warn] Aucun audio trouvé pour '{expression}'.", file=sys.stderr)
65
+
66
+ return list(set(audio_urls))
67
+
68
+ except requests.exceptions.ConnectionError:
69
+ print("[warn] Impossible de joindre Wiktionary.", file=sys.stderr)
70
+ except requests.exceptions.Timeout:
71
+ print(f"[warn] Timeout pour '{expression}'.", file=sys.stderr)
72
+ except requests.exceptions.HTTPError as e:
73
+ print(f"[warn] HTTP {e.response.status_code} pour '{expression}'.", file=sys.stderr)
74
+ except Exception as e:
75
+ print(f"[warn] Erreur inattendue : {e}", file=sys.stderr)
76
+
77
+ return []
78
+
79
+
80
+ # ── Nettoyage des données ──────────────────────────────────────────────────────
81
+
82
+ def stringify(df: pd.DataFrame) -> pd.DataFrame:
83
+ """
84
+ Remplace les NaN / None / chaînes vides par "" et convertit toutes
85
+ les colonnes en str. Ne modifie pas le DataFrame en place — retourne
86
+ une copie.
87
+ """
88
+ df = df.copy()
89
+ for col in df.columns:
90
+ df[col] = df[col].apply(
91
+ lambda x: ""
92
+ if (x is None or (isinstance(x, float) and np.isnan(x)))
93
+ else x
94
+ )
95
+ df[col] = df[col].astype(str).str.strip()
96
+ # Remplace les chaînes vides ou purement blanches par ""
97
+ df[col] = df[col].apply(lambda x: "" if not x.split() else x)
98
+ return df
99
+
100
+
101
+ def clean_list(series) -> list[str]:
102
+ """Retourne les valeurs non-vides d'une Series ou d'une liste."""
103
+ return [str(v) for v in series if isinstance(v, str) and v.strip()]
104
+
105
+
106
+ def join_unique(series) -> str:
107
+ """Joint les valeurs uniques non-vides triées d'une Series."""
108
+ values = sorted(
109
+ set(series.dropna().astype(str).str.strip()) - {"", "nan"}
110
+ )
111
+ return ", ".join(values)
112
+
113
+
114
+ def clean_modifieurs(modifieurs: list[str]) -> list[str]:
115
+ return [re.sub(r"[.,»]", "", m).strip().lower() for m in modifieurs]
116
+
117
+
118
+ # ── Variantes formelles ────────────────────────────────────────────────────────
119
+
120
+ def remove_modifier(modifier_tokens: list[str], forme: str) -> str:
121
+ """Retire les tokens de modifieur présents dans la forme."""
122
+ modifier_lower = {t.lower() for t in modifier_tokens}
123
+ tokens = forme.split()
124
+ filtered = [t for t in tokens if t.lower() not in modifier_lower]
125
+ result = " ".join(filtered).replace(" -", "-")
126
+ return result.strip()
127
+
128
+
129
+ def compute_variantes_formelles(df: pd.DataFrame) -> list[str]:
130
+ modifieurs = [m for m in df["Modifieurs"] if isinstance(m, str) and m.strip()]
131
+ modifier_tokens = " ".join(modifieurs).lower().split()
132
+
133
+ nodes = df["node"].str.replace(" -", "-", regex=False)
134
+ variantes = nodes.apply(lambda x: remove_modifier(modifier_tokens, x))
135
+ variantes = clean_modifieurs(variantes.tolist())
136
+ return list(set(variantes))
137
+
138
+
139
+ # ── Cooccurrents ───────────────────────────────────────────────────────────────
140
+
141
+ def count_cooccurrents(df: pd.DataFrame) -> dict:
142
+ """
143
+ Parse la colonne Cooccurrents et calcule les stats
144
+ antéposés (a) / postposés (p).
145
+ """
146
+ raw = ", ".join(clean_list(df["Cooccurrents"]))
147
+ items = [c.strip() for c in raw.replace(";", ",").split(",") if c.strip()]
148
+
149
+ cooc_a = [c.lower() for c in items if "(a)" in c]
150
+ cooc_p = [c.lower() for c in items if "(p)" in c]
151
+ all_coocs = [
152
+ re.sub(r"\((a|p)\)", "", c).strip().lower()
153
+ for c in items
154
+ ]
155
+
156
+ total = len(cooc_a) + len(cooc_p)
157
+ pct_a = round(len(cooc_a) / total * 100, 2) if total > 0 else 0.0
158
+ pct_p = round(len(cooc_p) / total * 100, 2) if total > 0 else 0.0
159
+
160
+ def fmt(counter_items, tag):
161
+ pattern = f"({tag})"
162
+ return ", ".join(
163
+ f"{cooc.replace(pattern, '').strip()} ({n})"
164
+ for cooc, n in counter_items
165
+ )
166
+
167
+ return {
168
+ "all": ", ".join(f"{c} ({n})" for c, n in Counter(all_coocs).most_common()),
169
+ "anteposés": fmt(Counter(cooc_a).most_common(), "a"),
170
+ "postposés": fmt(Counter(cooc_p).most_common(), "p"),
171
+ "pct_a": pct_a,
172
+ "pct_p": pct_p,
173
+ }
174
+
175
+
176
+ def format_cooc_stats(stats: dict, label: str) -> str:
177
+ return (
178
+ f"<bold>{label}</bold>:\n"
179
+ f"\t- <bold>Cooccurrents antéposés</bold> ({stats['pct_a']}%) : {stats['anteposés']}\n"
180
+ f"\t- <bold>Cooccurrents postposés</bold> ({stats['pct_p']}%) : {stats['postposés']}\n"
181
+ f"\t- <bold>Total</bold> : {stats['all']}\n"
182
+ )
183
+
184
+
185
+ # ── Structure interactionnelle ─────────────────────────────────────────────────
186
+
187
+ def get_interaction_stats(df: pd.DataFrame) -> str:
188
+ """Comptages sur Déclenchement, Portée, Position."""
189
+
190
+ def fmt_counts(series):
191
+ counts = Counter(clean_list(series.str.lower().str.strip()))
192
+ return ", ".join(f"{val} ({n})" for val, n in counts.most_common())
193
+
194
+ decl = fmt_counts(df["Déclenchement"])
195
+ port = fmt_counts(df["Portée"])
196
+ pos = fmt_counts(df["Position"])
197
+
198
+ return (
199
+ f"\t- <bold>Déclenchement</bold> : {decl}\n"
200
+ f"\t- <bold>Portée</bold> : {port}\n"
201
+ f"\t- <bold>Position</bold> : {pos}\n"
202
+ )
203
+
204
+
205
+ # ── Construction de la fiche ───────────────────────────────────────────────────
206
+
207
+ FICHE_COLS = [
208
+ "Fe_1a PPI",
209
+ "Fe_1b Acception",
210
+ "Fe_1c Variantes formelles",
211
+ "Fe_1e Prononciation",
212
+ "Fe_2a Statut syntaxique phrase",
213
+ "Fe_2b Type phrase",
214
+ "Fe_2c Modalité de phrase",
215
+ "Fe_2d Structure syntaxique globale",
216
+ "Fe_2e Expansion éventuelle",
217
+ "Fe_2f Construction syntaxique détaillée",
218
+ "Fe_2g Alternances syntaxiques",
219
+ "Fe_3a Fonction globale",
220
+ "Fe_3b Fonctions spécifiques",
221
+ "Fe_3c Codes Fonction globale",
222
+ "Fe_3d Codes Fonctions spécifiques",
223
+ "Fe_3e Fonctionnement pragma-sémantique",
224
+ "Fe_3f Structure interactionnelle",
225
+ "Fe_3g Contexte spécifique",
226
+ "Fe_3h Modalité écrite et orale",
227
+ "Fe_4a Cooccurrents privilégiés communs à la PPI",
228
+ "Fe_4b Modifieurs de la PPI",
229
+ "Fe_4c Renvois synonymiques",
230
+ "Fe_5a Marques d'usage de la PPI",
231
+ "Fe_6a Définitions et sources",
232
+ "Fe_7a Gestes/comportements associés",
233
+ "Fe_9a Remarques",
234
+ "Fe_9b Références",
235
+ "Fe_10a Noms des rédacteurs",
236
+ "Fe_10b Date de mise à jour",
237
+ ]
238
+
239
+
240
+ def build_fiche(
241
+ df_oral: pd.DataFrame,
242
+ df_ecrit: pd.DataFrame,
243
+ df_combined: pd.DataFrame,
244
+ ) -> pd.DataFrame:
245
+
246
+ # Variantes formelles
247
+ var_oral = compute_variantes_formelles(df_oral)
248
+ var_ecrit = compute_variantes_formelles(df_ecrit)
249
+ variantes_str = (
250
+ f"\n- <bold>Oral</bold> : {', '.join(var_oral)}"
251
+ f"\n- <bold>Écrit</bold> : {', '.join(var_ecrit)}\n"
252
+ )
253
+
254
+ # Prononciation
255
+ lemme = df_oral["Lemme"].iloc[0]
256
+ prononciation = "\n".join(get_wiktionary_pronunciation(lemme))
257
+
258
+ # Cooccurrents
259
+ cooc_oral = count_cooccurrents(df_oral)
260
+ cooc_ecrit = count_cooccurrents(df_ecrit)
261
+ cooc_combined = count_cooccurrents(df_combined)
262
+
263
+ cooc_str = (
264
+ format_cooc_stats(cooc_oral, "Oral")
265
+ + format_cooc_stats(cooc_ecrit, "Écrit")
266
+ + format_cooc_stats(cooc_combined, "Combiné")
267
+ )
268
+
269
+ # Structure interactionnelle
270
+ stats_oral = get_interaction_stats(df_oral)
271
+ stats_ecrit = get_interaction_stats(df_ecrit)
272
+ stats_combined = get_interaction_stats(df_combined)
273
+
274
+ stats_str = (
275
+ "<bold>Oral</bold> :\n" + stats_oral
276
+ + "<bold>Écrit</bold> :\n" + stats_ecrit
277
+ + "<bold>Combiné</bold> :\n" + stats_combined
278
+ )
279
+
280
+ # Remplissage
281
+ df_fiche = pd.DataFrame(columns=FICHE_COLS, index=[0])
282
+
283
+ df_fiche["Fe_1a PPI"] = df_combined["Forme PPI"].values[0]
284
+ df_fiche["Fe_1b Acception"] = join_unique(df_combined["Acception"])
285
+ df_fiche["Fe_1c Variantes formelles"] = variantes_str
286
+ df_fiche["Fe_1e Prononciation"] = prononciation
287
+ df_fiche["Fe_2a Statut syntaxique phrase"] = join_unique(df_combined["Type de phrase"])
288
+ df_fiche["Fe_2c Modalité de phrase"] = join_unique(df_combined["Modalité d'énonciation"])
289
+ df_fiche["Fe_2e Expansion éventuelle"] = join_unique(df_combined["Expansion"])
290
+ df_fiche["Fe_3a Fonction globale"] = join_unique(df_combined["Fonction globale"])
291
+ df_fiche["Fe_3b Fonctions spécifiques"] = join_unique(df_combined["Fonctions spécifiques"])
292
+ df_fiche["Fe_3c Codes Fonction globale"] = join_unique(df_combined["Fonction globale"])
293
+ df_fiche["Fe_3d Codes Fonctions spécifiques"] = join_unique(df_combined["Fonctions spécifiques"])
294
+ df_fiche["Fe_3f Structure interactionnelle"] = stats_str
295
+ df_fiche["Fe_3g Contexte spécifique"] = join_unique(df_combined["milieu"])
296
+ df_fiche["Fe_3h Modalité écrite et orale"] = join_unique(df_combined["secteur"])
297
+ df_fiche["Fe_4a Cooccurrents privilégiés communs à la PPI"] = cooc_str
298
+ df_fiche["Fe_4b Modifieurs de la PPI"] = join_unique(df_combined["Modifieurs"])
299
+ df_fiche["Fe_9a Remarques"] = join_unique(df_combined["Remarques"])
300
+
301
+ # Pivot
302
+ df_fiche = df_fiche.T.reset_index()
303
+ df_fiche.rename(columns={"index": "Propriétés", 0: "Valeurs"}, inplace=True)
304
+
305
+ return df_fiche
306
+
307
+
308
+ # ── Main ───────────────────────────────────────────────────────────────────────
309
+
310
+ def main():
311
+ args = parse_args()
312
+
313
+ df_oral = stringify(pd.read_excel(args.file_oral))
314
+ df_ecrit = stringify(pd.read_excel(args.file_ecrit))
315
+ df_combined = pd.concat([df_oral, df_ecrit], ignore_index=True)
316
+
317
+ df_fiche = build_fiche(df_oral, df_ecrit, df_combined)
318
+
319
+ print(df_fiche.to_string(index=False))
320
+ format_ppi_bold(df_fiche, args.output)
321
+ print(f"\n[ok] Fiche exportée → {args.output}")
322
+
323
+
324
+ if __name__ == "__main__":
325
+ main()