Ce qui a changé, en direct du code
Le contenu ci-dessous n'est pas une page marketing : c'est le fichier CHANGELOG.md réellement livré dans le zip téléchargeable, lu automatiquement à chaque affichage — jamais ressaisi à la main, donc jamais désynchronisé.
Version actuelle : v1.4.0 Télécharger cette version
[1.0.4] - 2026-07-25
Corrigé (2026-07-25, génération : troncature JSON sur les thèmes riches + LA vraie cause du bandeau d'en-tête manquant)
- Retour utilisateur : « Encore une erreur lors de la génération... La réponse de l'IA semble avoir été coupée avant la fin (JSON incomplet) » sur un type de slide riche ("Fiabilité / Complétude", 53 éléments), et « toujours un problème sur les entêtes de layout de type Contenu » malgré plusieurs vagues de correctifs précédentes.
- Troncature JSON : le pipeline de génération (
generate/prompts.py::system_generate_slide) exigeait de reproduire un modèle de type de slide À L'IDENTIQUE, sans AUCUNE échappatoire de simplification — contrairement au pipeline d'ÉDITION, qui avait déjà cette permission depuis une vague précédente pour éviter le même problème. Un modèle riche en blocs répétitifs (plusieurs jauges de progression, un tableau à de nombreuses lignes) pouvait donc facilement dépasser le budget de tokens. Fix : même échappatoire ajoutée côté génération — le LLM peut désormais simplifier le nombre de blocs PUREMENT DÉCORATIFS/RÉPÉTITIFS pour rester concis, en conservant toujours titre/KPI/texte principal intacts. - Bug réel enfin résolu — root cause trouvée par accès direct aux données réelles de l'utilisateur (pas seulement des captures d'écran) :
core/deck.py::apply_slide_type_imagescomptait TOUTES les images d'un modèle de slide ensemble, sans distinguer leur rôle, avant de compléter les manquantes par simple comptage. Si le LLM gardait l'image de contenu (ex une icône) mais omettait l'image décorative du bandeau d'en-tête, ce comptage global créditait à tort le déficit à la MAUVAISE image : le bandeau n'était jamais réinséré, une icône dupliquée apparaissait à la place — exactement le symptôme photographié à plusieurs reprises. Fix : les images décoratives et les images de contenu sont désormais comptées et complétées INDÉPENDAMMENT (par le rôle), comme c'était déjà le cas pour les formes décoratives. - 2e bug distinct trouvé et corrigé au passage : un surplus de formes décoratives (le LLM en invente parfois une en trop, ex un filet dupliqué) n'était jusqu'ici jamais retiré — seul un déficit était comblé. Une forme en trop restait alors affichée superposée à l'en-tête. Fix : un surplus est désormais retiré, symétriquement au comblement d'un déficit.
- Vérifié directement contre les données réelles de l'utilisateur (log complet fourni + inspection du deck sauvegardé) : le fix corrige bien la slide réellement cassée, sans dupliquer l'icône de contenu.
- +4 tests dédiés (
tests/test_slide_type_image_fidelity.py,tests/test_slide_types.py).
Corrigé/Ajouté (2026-07-25, dictée vocale : sensibilité, phrase coupée à l'arrêt, retour visuel d'activité)
- Plantage réel corrigé : une 1re tentative d'ajout d'une barre de niveau du micro ouvrait un 2e flux PyAudio séparé de celui déjà utilisé pour la reconnaissance — plantage NATIF reproductible en usage réel (accès concurrent au même périphérique audio, aucune exception Python attrapable). Fix définitif : un seul flux PyAudio est désormais utilisé, enveloppé pour calculer le niveau au passage.
- Retour utilisateur (sensibilité) : « je dois parler très fort et très près du micro ». Root cause en 2 parties trouvée dans le comportement documenté de
speech_recognition: une calibration du bruit ambiant trop courte (sous le minimum recommandé par la bibliothèque) et un seuil de détection qui dérivait de façon imprévisible sur une session longue. Fix : calibration allongée, seuil gelé après calibration, plafonné, et amplification numérique (audioop) du signal capté avant envoi à la reconnaissance quand il est trop faible. - Bug réel corrigé — fin de phrase manquante au clic « Arrêter » : l'audio déjà capturé (une phrase complète) était purement et simplement jeté si l'utilisateur cliquait Arrêter pendant la capture, au lieu d'être envoyé à la reconnaissance. Fix d'une ligne, avec test de régression dédié reproduisant exactement ce scénario.
- Retour utilisateur (retour visuel) : le texte en direct n'étant pas réalisable avec le service de reconnaissance gratuit (une phrase entière à la fois, pas de streaming), 2 retours visuels ajoutés à la place : le bouton de dictée s'éclaircit en direct avec le niveau de son capté, et un bandeau jaune apparaît après une faiblesse soutenue du son pour inviter à se rapprocher du micro (disparaît dès que le son redevient suffisant ou que la dictée s'arrête). Un état « Récupération du texte… » s'affiche aussi le temps que la dernière phrase soit reconnue après un clic Arrêter.
- Retour utilisateur (paramètres micro) : la liste des micros dans Paramètres > IA affichait jusqu'à 3-4 fois le même périphérique physique (Windows l'expose une fois par API audio) — filtrée sur l'API par défaut. Une barre de niveau en direct a aussi été ajoutée au bouton « Tester le micro… », et le libellé qui restait figé sur « En écoute… » après avoir cliqué Arrêter affiche désormais un état explicite.
- +25 tests dédiés (
tests/test_dictation.py,tests/test_settings_dialog_mic.py).
Corrigé (2026-07-25, glisser une slide dans la bande de vignettes remontait toujours sur la première)
- Retour utilisateur : « Lorsque je déplace l'ordre des slides, je remonte systématiquement sur le premier slide après. » Root cause :
ui/main_window.py::_on_slides_reorderedforçaitcurrent_index = 0après tout réordonnancement, au lieu de suivre la slide réellement déplacée. - Fix : la position réelle de la slide glissée (suivie correctement par Qt pendant le déplacement) est désormais transmise et utilisée pour garder la sélection sur la bonne slide.
- +3 tests dédiés (
tests/test_slide_reorder_selection.py).
Corrigé (2026-07-25, échec de génération sur data_uri vide + projet ouvert supprimé + placeholder de brief trop spécifique)
- Retour utilisateur (placeholder) : l'exemple affiché en filigrane dans le champ de brief (« Nouveau depuis un brief ») était très spécifique à un contexte métier (COSYNC/SIM 3/TANIT) — remplacé par un exemple générique (comité de direction, projet X, KPI/avancement/risques) dans ui/generate_dialog.py.
- Retour utilisateur (échec de génération) : génération depuis un brief + fichier .pptx joint + charte fraîchement sélectionnée, échec sur la slide « Deux méthodes, un seul document » : Deck non conforme au schéma : slides/0/elements/0/data_uri : '' does not match '^data:image/'.
- Root cause : generate/slide_gen.py::_resolve_asset_refs() — quand le LLM émet un élément image avec un asset_ref INCONNU (absent du catalogue de la charte) ET pose en plus, par prudence, un data_uri vide (""), le test elif "data_uri" not in el: ne se déclenchait PAS (la clé EST présente, juste invalide) : l'erreur informative « Référence d'image inconnue » (avec la liste des noms valides, qui aurait permis à la boucle de relance de generate_slide() de corriger le tir) était court-circuitée, et l'élément atteignait la validation de schéma avec un data_uri vide — échec cryptique, sans seconde chance.
- Fix : elif not el.get("data_uri") — un data_uri vide est désormais traité comme absent, déclenchant la relance informative comme prévu.
- Nouveaux tests dans tests/test_asset_ref.py (+2) : la combinaison asset_ref inconnu + data_uri vide déclenche bien la relance (et se résout si la 2e tentative référence un nom valide), et lève bien l'erreur claire si elle persiste au-delà de la relance.
- Bug latent détecté en creusant le log fourni par l'utilisateur (secondaire, pas la cause du blocage ci-dessus) : le log montrait aussi sqlite3.IntegrityError: FOREIGN KEY constraint failed lors de la persistance du message d'erreur en historique de chat (storage/chat_history.py::add_message) — déjà catché et journalisé (non fatal), mais révélait une INCOHÉRENCE réelle : ui/project_manager_dialog.py::ProjectManagerDialog permet de supprimer N'IMPORTE QUEL projet, y compris celui actuellement ouvert dans la fenêtre principale, sans rien savoir de cet état (aucun couplage avec MainWindow.project_id). Fermer ce dialogue ensuite sans ouvrir un autre projet laissait self.project_id pointer vers une ligne supprimée : toute action suivante qui persiste (historique de chat, autosave) échouait alors silencieusement en arrière-plan jusqu'à la création/l'ouverture d'un nouveau projet.
- Fix : MainWindow._open_project() détecte désormais ce cas (projects.get_title(self.project_id) is None après la fermeture du dialogue) et détache la fenêtre de cet id inexistant — le deck affiché en mémoire n'est pas perdu, il redeviendra un nouveau projet dès la prochaine action qui en crée un (_on_prompt), avec un message clair dans le chat plutôt qu'un échec silencieux.
- Nouveau fichier tests/test_open_project_detaches_deleted_current.py (4 tests) : détachement après suppression du projet courant + fermeture, persistance de chat qui ne lève plus après coup, bascule propre si un AUTRE projet est ouvert dans le même passage du dialogue, et non-régression (un simple renommage du projet courant ne détache rien).
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après cette section : 1047 passed, 6 deselected à chaque
fois, aucune régression.
Changé (2026-07-25, dictée vocale : abandon de SAPI natif au profit d'une reconnaissance cloud gratuite)
- Retour utilisateur : « pour la reconnaissance vocale windows il me dit que le paramétrage n'est pas fait... On ne peut pas utiliser une solution embarquée avec un bon niveau de compréhension sans passer par là (mais sans abonnement), comme ça existe sur un navigateur ou même dans Claude app ? » Choix confirmé après clarification : « faire comme avec un navigateur web ».
- Décision : abandon de
SAPI.SpInprocRecognizer(Windows natif, hors-ligne — cf. 27e/28e vagues) au profit despeech_recognition+recognize_google(): le même service de reconnaissance vocale gratuit et sans clé qui alimente le Web Speech API des navigateurs. Fonctionne immédiatement, sans configuration Windows préalable — compromis assumé : nécessite désormais une connexion Internet à chaque dictée (SAPI était hors-ligne). ui/dictation_worker.pyréécrit : capture audio via PyAudio (speech_recognition.Microphone), boucle d'écoute bloquante avec vérification périodique de l'arrêt coopératif (timeout=0.5s), phrases bornées à 10s (phrase_time_limit). Toute la mécanique COM/SAPI (apartments,DispatchWithEvents, tokens de moteur/périphérique) disparaît — plus de risque de segfault natif lié au cycle de vie COM (cf. 28e vague).list_audio_inputs()énumère désormais les périphériques PyAudio filtrés surmaxInputChannels > 0(l'énumération brute mélange entrées ET sorties — vérifié empiriquement sur ce poste : haut-parleurs, mappeur de sons apparaissaient aussi) ; les noms sont normalisés (certains périphériques Bluetooth HFP renvoient des noms avec des retours à la ligne intégrés côté pilote, vérifié empiriquement — auraient cassé l'affichage d'une ligne du combo).has_recognition_engine()supprimée (n'a plus de sens : plus de "moteur à installer") —ui/settings_dialog.pyaffiche désormais un avertissement uniquement si les dépendances Python (speech_recognition/PyAudio) sont absentes, et le groupe est renommé « Dictée vocale (reconnaissance en ligne — nécessite Internet) ».- Piège de sûreté QThread détecté et corrigé en cours de route : l'ancien
MicButton.shutdown()faisaitworker.wait(2000)puis abandonnait INCONDITIONNELLEMENT la référence Python (self._worker = None) — sûr avec SAPI (arrêt réel en ~50ms, poll actif) mais plus avec le nouvel appel micro BLOQUANT (recognizer.listen(), jusqu'à ~10s pour se dénouer si Arrêter/fermeture arrive en pleine phrase) : un timeout de 2s aurait pu réellement se produire, abandonnant la seule référence Python à un QThread encore actif — même crash "QThread: Destroyed while thread is still running" déjà rencontré et corrigé ailleurs dans cette appli. Fix : nouvelle liste_retired_workers, le worker y reste référencé jusqu'à sonfinishedréel siwait(2000)échoue (même schéma queSettingsDialog._device_workers, qui s'est avéré DÉJÀ sûr sans changement — vérifié en le relisant). requirements.txt:pywin32(devenu inutile, plus aucune référence dans le code de prod) remplacé parSpeechRecognition/PyAudio— vérifié : wheels précompilés disponibles pour cette version de Python sur Windows (aucun compilateur requis), installation + énumération réelle des périphériques testées contre ce poste (14 entrées détectées, dont les 2 mêmes micros USB/Realtek déjà identifiés en 28e vague).tests/test_dictation.pyettests/test_settings_dialog_mic.pyréécrits pour le nouveau moteur (35 tests) : phrases reconnues en séquence,UnknownValueErrorignorée silencieusement (jamais une erreur utilisateur),RequestError/micro indisponible surfacés clairement, sélection de périphérique appliquée, filtrage entrées/sorties, normalisation des noms, ET un nouveau test dédié couvrant le piège de sûreté QThread ci-dessus.- Limite assumée, non vérifiable en boîte à outils de test : la qualité RÉELLE de reconnaissance (dépend du service cloud + de la connexion Internet du poste utilisateur) — seule l'énumération des périphériques a pu être testée en conditions réelles sur ce poste de développement.
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après cette section : 1041 passed, 6 deselected à chaque
fois, aucune régression.
Corrigé (2026-07-25, bouton « Tester la connexion » mal placé + lenteur d'édition due aux vignettes)
- Retour utilisateur (paramètres) : « le bouton Tester la connexion n'est pas au bon endroit car très loin de la zone de paramétrage du LLM. » Remonté juste sous les pages de backend (GHE/GitHub Models/OpenAI-compatible) dans ui/settings_dialog.py, avant max_tokens/Images/Dictée — reste visuellement associé à ce qu'il teste réellement.
- Retour utilisateur (lenteur d'édition) : « je pense que ça vient du fait de redessiner la miniature à chaque changement ou la gestion des Annuler/Rétablir. » Hypothèse confirmée en lisant le code : ui/slide_strip.py::SlideStrip.refresh() reconstruisait ET rasterisait la scène complète de toutes les slides à chaque appel (débounce 400ms déjà en place côté MainWindow._strip_refresh_timer, mais le coût du rendu lui-même n'était jamais réduit), alors qu'une seule slide change généralement.
- Fix : nouveau cache de vignettes PAR SLIDE dans SlideStrip (_icon_for), clé = id de la slide :
- cas rapide (frappe/glisser) : la slide inchangée reste le MÊME objet Python d'un refresh à l'autre (grâce à core/deck.py::clone_for_slide_update, 27e vague) — réutilisation par simple comparaison d'identité, coût quasi nul ;
- cas Annuler/Rétablir : CommandStack.undo()/redo() restaure des copies INDÉPENDANTES (copy.deepcopy), donc de nouveaux objets même à contenu identique à un état déjà affiché — repli sur une empreinte de contenu (JSON trié + md5) qui, elle, matche ;
- invalidation correcte sur changement de charte (hash de contenu, mémoïsé par identité d'objet — coût nul si la charte ne change pas) ou de format, et sur changement de POSITION/total (le rendu incruste un numéro de page dérivé de slide_index/slide_total, cf. render/scene_builder.py::_add_page_number — une réorganisation par glisser-déposer doit re-rendre même une slide dont le contenu propre n'a pas changé, sinon son numéro de page resterait figé) ;
- purge des entrées de slides supprimées à chaque refresh (pas de croissance illimitée du cache sur une session longue).
- Piège détecté en cours de route par un test EXISTANT (tests/test_page_number_wiring.py) : une 1re version du cache ne comparait que charte+format, oubliant que la position/le total font aussi partie de ce qui est rendu — corrigé avant de casser le numéro de page après un glisser-déposer dans la bande.
- Nouveau fichier tests/test_slide_strip_thumbnail_cache.py (8 tests) : slide inchangée non re-rendue, seule la slide modifiée l'est, contenu identique via un nouvel objet réutilise l'icône (Annuler/Rétablir), changement de charte/format force le re-rendu, réorganisation force le re-rendu (numéro de page), slide supprimée purgée du cache, hash de charte mémoïsé par identité.
- Aucun changement de comportement visible : les vignettes restent toujours exactes, seul le travail de RENDU redondant est éliminé.
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après cette section : 1039 passed, 6 deselected à chaque
fois, aucune régression.
Ajouté (2026-07-25, journalisation diagnostique pour le problème d'en-têtes persistant)
- Retour utilisateur : « Encore des problèmes sur les entêtes, Sur les 5
slides avec layout 'Contenu' : capture 1 : entête OK, mais problème avec le
texte juste au dessus du titre / capture 2 : pas le fond de l'entête /
capture 3 : c'est parfait / capture 4 : pas le fond de l'entête / capture 5 :
pas le fond de l'entête. » Après 3 vagues de corrections ciblées consécutives
(26e/27e/28e — backfill d'images, resynchronisation des formes décoratives,
restauration du slide_type) qui ont chacune corrigé une vraie cause
racine différente sans éliminer complètement le symptôme, pas de 4e
correction devinée à partir de simples captures d'écran : décision
délibérée d'ajouter de la journalisation aux 3 points de décision qui
déterminent le fond/les images d'en-tête d'une slide, pour qu'une
prochaine reproduction produise un vrai extrait de log exploitable plutôt
que d'autres captures. Précédent direct dans ce même projet (bug de
troncature de patch, résolu uniquement grâce à un log brut après plusieurs
correctifs à l'aveugle infructueux).
- core/deck.py::apply_slide_type_background() : log info quand le fond
est appliqué avec succès (id slide + slide_type), warning quand
slide_type est renseigné mais qu'aucune entrée de charte/modèle
correspondante n'a de fond.
- core/deck.py::apply_slide_type_images() : warning si slide_type est
renseigné sans modèle correspondant ; info récapitulatif (nombre
d'images avant/après, nombre attendu par le modèle, formes décoratives
avant/après) dès que quelque chose change réellement.
- services/edit_service.py::_preserve_or_enforce_slide_type() : info
quand le slide_type est FORCÉ (référencé par nom dans le prompt
d'édition) ou RESTAURÉ (un patch l'avait supprimé/modifié sans que le
prompt ne référence aucun type par son nom) — ce mécanisme corrige déjà le
cas où visual_review fait disparaître le champ, mais on ne savait pas
s'il se déclenchait effectivement sur les repros récents.
- generate/slide_gen.py::_normalize() : warning quand un slide_type
renvoyé par le LLM (ou hérité du plan) est rejeté comme hors catalogue
(hallucination) — 3e point de décision distinct, spécifique à la
GÉNÉRATION initiale (pas à l'édition), qui peut expliquer un en-tête
blanc dès la première génération d'un deck plutôt qu'après une édition.
- Aucun correctif fonctionnel n'a été tenté sur le rendu des en-têtes
lui-même dans cette vague — uniquement de la visibilité. Si le problème
persiste, la prochaine étape utile est de relancer une génération/édition
qui le reproduit puis de renvoyer le contenu du fichier log (dossier de
logs de l'application, purge quotidienne à 30 jours — voir vague 6) autour
du moment de la reproduction, ce qui permettra d'identifier lequel des 3
points de décision ci-dessus est en cause pour de bon.
- Le nouveau point de contraste signalé (« capture 1 : entête OK, mais
problème avec le texte juste au dessus du titre ») n'est PAS encore
investigué — symptôme distinct du fond manquant, à traiter séparément une
fois le fond stabilisé.
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après cette section : 1031 passed, 6 deselected à chaque
fois, aucune régression (changements additifs, uniquement de la
journalisation).
Corrigé/Ajouté (2026-07-24, la dictée vocale ne fonctionnait pas + sélecteur de micro)
- Retour utilisateur : « pour le micro ça ne semble pas fonctionner. Est-ce
qu'il ne faudrait pas un paramétrage du micro car par exemple sur mon pc
j'en ai plusieurs (sauvegarder le paramétrage dans Roaming pour que ce soit
par utilisateur). Il faudrait pouvoir tester le fonctionnement du micro
depuis le paramétrage. »
- Root cause n°1 (pourquoi ça ne marchait pas du tout) : SAPI. (moteur PARTAGÉ utilisé jusqu'ici) suit TOUJOURS le
SpSharedRecognizer
périphérique d'entrée PAR DÉFAUT du Panneau de configuration Windows —
une application ne peut PAS lui faire écouter un autre micro
(recognizer.AudioInput = token y échoue). Sur un poste à plusieurs
micros où le mauvais est configuré par défaut, la dictée n'entend jamais
rien — SANS AUCUNE ERREUR (le pipeline SAPI se met en place normalement
même sans bon micro sélectionné, ni même sans AUCUN moteur de
reconnaissance installé). Fix : bascule sur SAPI.SpInprocRecognizer
(moteur EN PROCESSUS), qui accepte recognizer.AudioInput = <token>.
- Nouveau réglage « Dictée vocale » dans Paramètres > IA : sélecteur de
micro (liste réellement énumérée par SAPI, SAPI.SpObjectTokenCategory
sur la catégorie AudioInput), persisté par prefs.py (%APPDATA%\, Roaming, PAR UTILISATEUR — exactement la demande), et bouton
SlydeForge
« Tester le micro… » qui démarre une VRAIE session de dictée et affiche
chaque phrase reconnue en direct, pour vérifier le pipeline complet avant
de compter dessus dans le chat. Avertissement explicite si AUCUN moteur de
reconnaissance n'est installé (détecté via la catégorie Recognizers,
vide sur ce cas) — avec l'endroit exact où l'activer (Paramètres Windows
> Heure et langue > Voix).
- Crash natif RÉEL trouvé et corrigé en cours de route : list_audio_/
inputs()has_recognition_engine() provoquaient un SEGFAULT (pas juste une
exception Python) en retournant des objets COM bruts APRÈS avoir appelé
pythoncom.CoUninitialize() — l'apartment COM peut être démonté avant que
l'appelant n'utilise l'objet retourné, produisant un accès mémoire
invalide. Fix : ces fonctions extraient désormais les données (id,
description) PENDANT que l'apartment est encore active et ne retournent
que des types Python simples ; le micro effectivement SÉLECTIONNÉ (objet
COM brut, nécessaire pour recognizer.AudioInput = token) n'est manipulé
QUE depuis un contexte où l'apartment reste active tout du long (Dictation, jamais depuis une fonction "one-shot" comme les 2
Worker.run()
précédentes).
- ui/mic_button.py::MicButton lit désormais ce réglage (prefs.get(...))
au démarrage d'une écoute — le micro choisi dans Paramètres est
RÉELLEMENT utilisé dans le chat/le brief, pas seulement affiché dans le
dialogue de test.
- Nouveaux tests : tests/test_dictation.py (+7 : énumération micros/moteur,
sélection de micro appliquée au recognizer, repli silencieux si le micro
configuré a disparu), tests/test_settings_dialog_mic.py (12 : combo
peuplé/vide, avertissement moteur absent, persistance Roaming, test micro
bout-en-bout avec un faux worker "en écoute", fermeture qui attend la fin
réelle du thread de test). Vérifié contre l'environnement RÉEL de ce poste
(2 micros SAPI détectés — USB Audio, Realtek — confirmant que le code
fonctionne avec du matériel réel, même si ce poste précis n'a aucun moteur
de reconnaissance installé pour aller plus loin).
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après cette section : 1031 passed, 6 deselected à chaque
fois, aucune régression.
Ajouté (2026-07-24, dictée vocale dans les zones de prompt IA)
- Retour utilisateur : « Ajouter dans les zones de prompts IA la
possibilité de dicter par la voix les modifs. » Nouveau bouton
« 🎤 Dicter » dans le champ de prompt principal (ui/chat_panel.py) et
dans le brief de « Nouveau depuis un brief » (ui/generate_dialog.py) —
bascule démarrer/arrêter l'écoute, insère chaque phrase reconnue au
curseur (espacement automatique, jamais de texte collé).
- Choix technique retenu avec l'utilisateur (3 options présentées,
tranchées avant implémentation vu l'impact réel coût/qualité/dépendance) :
reconnaissance vocale NATIVE Windows (SAPI.SpSharedRecognizer via
pywin32, même moteur que « Reconnaissance vocale » dans les Paramètres
Windows) — gratuite, hors-ligne, aucune clé/API, mais qualité et
disponibilité dépendent ENTIÈREMENT de la configuration Windows du poste
(langue de dictée installée). Nouvelle dépendance pywin32 (requirements.txt),
déjà couverte par le mécanisme d'installation auto existant (.bat).
- ui/dictation_worker.py::DictationWorker (QThread, même schéma
d'annulation coopérative — threading.Event — que le bouton Stop des
appels IA ailleurs dans l'appli) et ui/mic_button.py::MicButton (bouton
réutilisable, agnostique du champ cible) + insert_dictated_text().
MicButton.shutdown() arrête et ATTEND la fin réelle du thread — câblé
dans MainWindow._shutdown_workers() et GenerateDialog.done() — même
précaution que le crash QThread déjà corrigé cette même session pour un
autre widget (SpellcheckTextEdit).
- Limite assumée, à vérifier par l'utilisateur : ni la disponibilité
réelle d'un moteur de dictée FRANÇAIS ni la qualité de reconnaissance
n'ont pu être testées dans cet environnement de développement (aucun
moteur SAPI enregistré sur ce poste, aucun micro) — seule l'INTÉGRATION
(démarrage/arrêt du thread, insertion du texte, gestion d'erreur claire si
le moteur est indisponible) est couverte par les tests automatisés, via
des objets COM simulés.
- Nouveau fichier tests/test_dictation.py (15 tests). Vérifié visuellement
(captures d'écran des 2 zones de prompt, état repos et état « en écoute »).
Corrigé (2026-07-24, lenteur perçue en éditant manuellement une slide)
- Retour utilisateur : « les modifs manuelles manquent de fluidité, il
doit y avoir des lenteurs dans le code ». Trois root causes trouvées et
corrigées :
- 1. Double deepcopy du deck ENTIER à chaque frappe/geste —
ui/properties_panel.py::_auto_apply (champ texte) émet
element_changed à CHAQUE frappe (non débouncé) ; MainWindow::/
_on_element_changed_on_geometry_changed faisaient deck_helpers. (deepcopy intégral) PUIS
clone(self.deck)core/commands.py:: recopiait ENCORE l'argument en interne — deux
CommandStack.push
deepcopy complets du deck, y compris CHAQUE image encodée en base64 de
CHAQUE AUTRE slide, par frappe. Fix : CommandStack.push ne recopie
plus son argument (l'appelant lui cède la propriété exclusive d'un objet
déjà frais — undo()/redo() re-matérialisent de toute façon une copie
indépendante à la restauration, donc le stockage n'a jamais eu besoin
d'être défensif) ; nouveau core/deck.py::clone_for_slide_update (copie
structurelle : seule la slide ciblée est dupliquée, toutes les autres
restent des références partagées) remplace clone() dans
_on_element_changed/_on_geometry_changed.
- 2. Recalcul complet des cibles d'accroche à CHAQUE pixel de
déplacement — render/items.py::ElementItem.itemChange (guides
d'alignement) se déclenche à chaque frame d'un glisser souris, et
parcourait TOUS les items de la scène à chaque appel pour reconstruire
les cibles de snap. Les autres éléments ne bougent jamais pendant le
glisser en cours : les cibles sont désormais calculées UNE SEULE FOIS par
geste (mise en cache entre mousePressEvent et mouseReleaseEvent).
- Nouveaux fichiers/tests : tests/test_manual_edit_performance.py (4,
vérifie qu'éditer une slide ne recopie/touche jamais les AUTRES),
tests/test_commands.py (+3), tests/test_canvas_editing.py (+1, cache
des cibles de snap sur 1 seul geste).
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après ce fix : 998 passed, 6 deselected à chaque fois,
aucune régression.
Corrigé (2026-07-24, en-tête totalement blanc après le contrôle de lisibilité)
- Retour utilisateur : après le fix précédent, 3 des 5 slides "Contenu"
montrent bien l'en-tête avec l'image ; les 2 restantes n'ont AUCUNE
couleur/image d'en-tête (fond totalement blanc, pas juste "bleu sans
image" ou "gris" comme avant).
- Root cause : le contrôle de lisibilité automatique (services/) édite une slide via le même pipeline
visual_review.py::review_and_fix
que le chat pour corriger un contraste — son patch, en retouchant les
éléments concernés, faisait parfois disparaître le champ slide_type
lui-même (bien qu'instruit de ne rien ajouter/retirer). Sans ce champ,
apply_slide_type_background/apply_slide_type_images n'ont plus AUCUN
moyen de savoir quel modèle appliquer — toute l'enforcement fond/images se
désactive silencieusement, d'où un en-tête totalement blanc.
- Fix : services/edit_service.py::_preserve_or_enforce_slide_type
(remplace _enforce_referenced_slide_type) — en plus de FORCER le type
quand le prompt le référence explicitement par son nom (comportement
existant), RESTAURE désormais la valeur de slide_type d'AVANT le patch
si elle a disparu/changé alors que l'instruction ne parle d'AUCUN type par
son nom — une édition qui ne parle pas de layout (ex une correction de
contraste) n'a aucune raison légitime de faire perdre le type assigné à
la slide.
- Nouveau test tests/test_edit_service.py::
test_edit_by_prompt_restores_slide_type_dropped_by_an_unrelated_patch
(reproduit le patch exact : une couleur changée + slide_type supprimé
dans la même réponse).
- Note pour la suite : le même utilisateur signale aussi que certains
éléments SUPERPOSÉS à un en-tête (image réelle, jamais montrée au LLM
générateur — cf. core/deck.py::strip_image_data) manquent parfois de
contraste — c'est exactement le rôle du contrôle de lisibilité
automatique ci-dessus (seul point du pipeline à voir le rendu RÉEL, via
vision). Ce fix devrait le rendre plus efficace (ses corrections ne
cassent plus la structure du modèle en même temps qu'elles ajustent une
couleur de texte) ; à revérifier si le problème persiste avec un exemple
concret.
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après ce fix : 990 passed, 6 deselected à chaque fois,
aucune régression.
Corrigé (2026-07-24, teinte de fond incohérente entre slides d'un même type)
- Retour utilisateur : sur une génération neuve, 4 des 5 slides du même
type "Contenu" montraient chacune une teinte de fond DIFFÉRENTE (bleu
foncé, bleu, gris) — une seule affichait le rendu correct, identique au
modèle de la charte.
- Root cause trouvée : services/visual_review.py::review_and_fix
(contrôle de lisibilité automatique en fin de génération) peut, pour
corriger un contraste jugé insuffisant, modifier la couleur (fill) d'une
forme DÉCORATIVE existante (ex la bande d'en-tête) — via le même pipeline
d'édition que le chat (core/patch.py::apply_patch), donc légitimement
autorisé à changer des couleurs. MAX_FIXES = 4 explique exactement le
nombre de slides touchées observé. Jusqu'ici, apply_slide_type_images
(vague précédente) ne faisait que COMBLER un déficit de nombre — une forme
décorative déjà présente, même altérée, n'était jamais restaurée.
- Fix : les formes décoratives (role:"decoration") déjà présentes sont
désormais RESYNCHRONISÉES avec le modèle à chaque passage (id conservé,
jamais de duplication) — même philosophie que apply_slide_type_background
pour le fond : une forme décorative porte l'identité visuelle de la
charte, jamais un contenu que l'IA (génération OU correction ultérieure)
doit pouvoir adapter. Comportement des IMAGES inchangé (backfill de compte
uniquement, jamais de resynchronisation — une image de contenu insérée par
ailleurs ne doit jamais être confondue avec l'image décorative).
- Nouveaux tests dans tests/test_slide_type_image_fidelity.py (+1,
reproduisant le scénario exact d'une correction de lisibilité qui altère
une forme décorative) ; mis à jour le test existant qui encodait l'ancien
comportement "jamais de resynchronisation" (devenu intentionnellement
faux pour les formes décoratives).
Corrigé (2026-07-24, filet d'accent décoratif manquant sous un bandeau d'en-tête)
- Retour utilisateur : sur le type de slide "Contenu", l'en-tête restait
bleu uni (sans image) ou gris, ET il manquait le trait jaune sous l'en-tête
pourtant présent dans l'aperçu du modèle de charte. core/deck.py:: ne réinsérait QUE les éléments-image manquants —
apply_slide_type_images
jamais les formes décoratives simples (ex un filet d'accent
type:"shape", role:"decoration"). Contrairement à une image (base64,
peu fiable à reproduire), une forme n'a AUCUNE raison technique d'être
omise par le LLM — mais la consigne de prompt qui autorise explicitement à
"simplifier les éléments purement décoratifs/répétitifs" (ajoutée pour
éviter une troncature sur des blocs répétés) fait parfois sauter, par
erreur, un filet UNIQUE et non répétitif.
- Fix : apply_slide_type_images réinsère désormais aussi les formes
décoratives manquantes du modèle (role == "decoration"), avec la MÊME
heuristique prudente que pour les images (comptage par groupe, jamais de
retrait/remplacement d'une forme déjà présente) — mais en groupe
INDÉPENDANT des images, pour ne jamais dupliquer le mauvais élément en cas
de déficit croisé (slide qui a l'image mais pas le filet, ou l'inverse).
Câblé automatiquement aux 3 mêmes points que la fonction existante
(génération, les 2 pipelines d'édition, et « Réappliquer une charte » /
« Appliquer au deck » depuis la vague précédente) — aucun nouveau bouton
nécessaire, relancer une de ces actions existantes suffit à corriger les
slides déjà générées.
- Nouveaux tests dans tests/test_slide_type_image_fidelity.py (+3 :
réinsertion du filet manquant / non-duplication s'il est déjà présent /
les formes non-décoratives d'une slide normale — cartes, bordures — ne
masquent jamais un déficit réel).
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après ces 2 sections (en plus de la section
« Fond de la slide… » plus bas, déjà en place) : 989 passed, 6 deselected à
chaque fois, aucune régression.
Ajouté (2026-07-24, entrée « Fond de la slide… » dans le menu contextuel)
- Retour utilisateur : « Pour changer l'image de fond d'une slide le
double clic fonctionne, mais ce n'est pas vraiment naturel [...] je
pensais au clic droit [...] ne pas retirer le double clic car c'est pas
mal pour aller vite. » Nouvelle entrée « Fond de la slide… » dans le menu
contextuel partagé (ui/element_context_menu.py), TOUJOURS présente (que
le clic droit ait eu lieu sur un élément ou sur le fond — même convention
que « Format de l'arrière-plan… » dans PowerPoint) : plus simple/prévisible
qu'une entrée qui apparaît/disparaît selon la zone cliquée, sans retirer le
raccourci rapide du double-clic ajouté la vague précédente.
- Câblé dans les 2 éditeurs (MainWindow, SlideTemplateEditorDialog) vers
leur _edit_background_image() respectif (déjà existant, réutilisé tel
quel — même dialogue que le double-clic et le menu Insérer).
- Enrichi tests/test_context_menu_and_delete.py (+5 : entrée absente par
défaut / présente et déclenchant le callback quand fournie / transmise par
les 2 éditeurs). Vérifié visuellement (capture d'écran du menu réellement
rendu).
Corrigé (2026-07-24, éditer 1 slide en modifiait plusieurs sans rapport)
- Retour utilisateur : « J'ai voulu changer la slide 1 et il me propose
des modifs sur 5 slides. Pourquoi ? ». Root cause : core/patch.py:: réappliquait fond/images de type de slide
apply_patch
(apply_slide_type_background/apply_slide_type_images) sur TOUTES les
slides du deck à CHAQUE patch, quel que soit son périmètre réel — pas
seulement celles visées par le patch en cours. Toute slide portant un
slide_type correspondant à un type de charte pouvait donc être
silencieusement retouchée par un edit sans rapport, gonflant le diff
affiché à l'utilisateur (compté par vraie comparaison de valeur, pas un
artefact d'affichage) et écrasant au passage toute personnalisation propre
à cette slide (ex un image_rect de recadrage).
- Fix : la réapplication est désormais limitée aux slides RÉELLEMENT
touchées par le patch (comparaison de contenu avant/après par id, robuste
à un réordonnancement/une insertion/une suppression — plus fiable
qu'analyser les chemins RFC 6902 bruts). Une slide dont le contenu est
strictement identique à avant ce patch n'a de toute façon rien à corriger.
- Contrepartie assumée et compensée : ce mécanisme servait aussi,
accidentellement, à "auto-guérir" au fil du temps une slide dont le modèle
de type avait évolué APRÈS sa génération (ex une image d'en-tête ajoutée
à la charte a posteriori) — dès qu'une AUTRE slide du même deck était
éditée. Avec ce fix, ce filet accidentel disparaît. core/theme.py:: (utilisé par les 2 actions déjà existantes « Réappliquer une
apply_theme
charte enregistrée… » et « Appliquer au deck » depuis l'éditeur de charte)
fait maintenant EXPLICITEMENT ce que le hasard faisait avant : resynchronise
le fond/les images de CHAQUE slide avec le modèle de son slide_type. Ici,
toucher toutes les slides concernées est le comportement ATTENDU d'une
action délibérée de l'utilisateur, pas un effet de bord surprenant d'une
simple retouche de texte.
- Nouveaux tests : tests/test_slide_type_background_fidelity.py (+2 :
slide hors périmètre non touchée malgré un fond périmé / nouvelle slide
ajoutée par le même patch bien couverte), tests/test_theme.py (+2 :
apply_theme resynchronise/laisse les slides sans slide_type intactes).
Ajouté (2026-07-24, sélectionner le fond de la slide directement sur le canevas)
- Retour utilisateur : « depuis l'édition d'une slide je ne peux pas
sélectionner le fond d'écran (exemple pour le supprimer ou le changer
directement sans passer par le menu) ». Un double-clic qui ne touche AUCUN
élément (donc le fond, ou une zone vide) ouvre désormais directement le
réglage du fond (BackgroundImageDialog, remplacer/retirer/position/zoom
— « Retirer l'image de fond » y couvre déjà la suppression), jusque-là
accessible uniquement via le menu Insérer.
- ui/canvas_view.py::CanvasView gagne un nouveau signal
background_double_clicked, câblé côté MainWindow ET
SlideTemplateEditorDialog vers leur _edit_background_image() respectif
(déjà existants tous les deux, réutilisés tels quels).
- Nouveau fichier tests/test_canvas_background_selection.py (4 tests).
Piège de test découvert : faire transiter un QMouseEvent synthétique
jusqu'au mécanisme d'interaction interne RÉEL d'un ElementItem fait
planter ce bac à sable offscreen de façon déterministe (accès mémoire
invalide) — contourné en ne testant QUE la logique de routage ajoutée
(fond vs élément suivi), sans dépendre du dispatch Qt complet vers un
élément réel pour cette assertion précise.
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après ces 2 sections : 982 passed, 6 deselected à chaque
fois, aucune régression.
Corrigé (2026-07-24, échec HTTP 400 en appliquant un layout de charte nommé)
- Retour utilisateur : « utiliser le layout Couverture (variante photo) »
sur une slide échouait avec « Erreur HTTP 400 : Bad Request », sans détail
exploitable. Root cause trouvée : generate/prompts.py:: (édition) et le
_slide_type_templates_blocktemplate_block de génération
embarquaient le JSON COMPLET de CHAQUE type de slide de la charte
(json.dumps(t["template"], ...)) — y compris leurs background/éléments
type:"image" en base64 BRUT — à CHAQUE prompt, alors même que le LLM est
déjà instruit de ne jamais les reproduire (ils sont réappliqués
automatiquement, cf. core/deck.py::apply_slide_type_background/). Sur une charte à plusieurs types personnalisés
apply_slide_type_images
avec de grosses images (ex Design System Socomec), le payload cumulé
pouvait devenir énorme, jusqu'à dépasser une limite de taille/validation du
backend — un cas jamais rencontré avec les chartes de test plus modestes
utilisées jusqu'ici. Fix : core/deck.py::strip_image_data (déjà
existant, déjà appliqué à l'embarquement du deck/de la slide courante,
cf. bug similaire résolu antérieurement — token overflow 1 763 556 vs
936 000) est maintenant AUSSI appliqué à ces 2 sites d'embarquement de
modèle, qui l'avaient manqué lors de leur ajout initial.
- Diagnostics ajoutés pour la prochaine fois (même si ce fix réduit
fortement le risque, un échec HTTP peut toujours survenir pour d'autres
raisons) : ai/client.py::log_http_failure trace désormais, à CHAQUE échec
HTTP non-200 (401/403/429/autre), le détail extrait de la réponse ET la
taille du corps de la REQUÊTE envoyée (resp.request.body, conservé par
requests) — un indice utile même quand le backend ne renvoie RIEN de plus
qu'« Bad Request » dans son corps de réponse. Câblé dans ai/client.py:: et
_raise_for_statusai/streaming.py::_raise_stream_error (chemin
streamé, utilisé par l'édition via chat).
- Second filet de sécurité, complémentaire (retour utilisateur : « il
faut vraiment respecter les propriétés définies par le layout ») :
services/edit_service.py::_enforce_referenced_slide_type force
slide.slide_type (et donc fond+images du modèle) quand le PROMPT
UTILISATEUR référence sans ambiguïté le nom EXACT d'un type existant de la
charte — même si le LLM omet mécaniquement ce champ dans son patch (cas
réel observé, indépendant du bug de payload ci-dessus). Le popup
"/layout" insère toujours ce nom exact, rendant cette détection fiable
sans interpréter de langage naturel. Même philosophie que partout ailleurs
dans ce projet (ids, rôles, couleurs, fond, images) : ne jamais dépendre
uniquement de la fidélité mécanique du LLM pour un champ déterminable côté
code.
- Nouveaux tests : tests/test_error_detail_extraction.py (+3, journalisation
y compris avec un faux resp incomplet), tests/test_edit_service.py (+3,
forçage de slide_type référencé / non forcé si non référencé / patch LLM
respecté si le nom n'apparaît pas dans le prompt).
Ajouté (2026-07-24, nom du layout affiché sous le titre dans la bande de vignettes)
- Retour utilisateur : « pour mieux comprendre le layout utilisé... voir
en dessous du titre en italique le nom du layout utilisé ou indiquer si
c'est généré par l'IA ». ui/slide_strip.py affiche maintenant une 2e
ligne en italique sous le titre de chaque vignette : "Type : <nom>" si
slide.slide_type est défini, sinon "Généré par l'IA" — toujours un des
deux, jamais rien, pour lever l'ambiguïté dans tous les cas.
- Nouveau _SlideItemDelegate (QStyledItemDelegate) : QListWidgetItem
ne permet pas de mélanger 2 styles de police dans le même texte, d'où ce
delegate dédié plutôt qu'un simple \n dans le texte de l'item. 2 pièges
trouvés et corrigés à la vérification par capture d'écran (pas seulement
par les tests automatisés) : (1) SE_ItemViewItemText doit être calculé
AVANT de vider opt.text — le style Fusion utilisé par cette appli
dimensionne ce sous-rect d'après le texte réel de l'option, un texte vidé
trop tôt produisait un rect dégénéré et un chevauchement visuel avec la
vignette ; (2) la couleur du texte sélectionné ne doit PAS venir de
opt.palette.highlightedText() (quasi invisible ici) mais des constantes
ui/style.py::ACCENT/TEXT/TEXT_MUTED — cette appli force la couleur du
texte sélectionné via QSS (QListWidget::item:selected { color: {ACCENT} })
plutôt que via le rôle de palette Qt standard.
- Nouveau fichier tests/test_slide_strip_layout_subtitle.py (5 tests).
Vérifié visuellement (captures d'écran de SlideStrip réellement rendu :
sous-titre italique lisible aussi bien sur un item normal que sur l'item
actuellement sélectionné).
Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après ces 2 sections : 974 passed, 6 deselected à chaque
fois, aucune régression (inclut aussi la correction ci-dessous, déjà en
place au moment de ce run).
Corrigé (2026-07-24, même risque de crash QThread dans la connexion GitHub des Paramètres)
- Suite de la correction précédente (section juste en dessous) : le
risque de même nature qui y avait été repéré au passage sans être corrigé
(ui/settings_dialog.py::SettingsDialog._start_device_flow) est maintenant
traité.
- Root cause : identique au bug spellcheck — le worker de poll du Device
Flow GitHub (attend la validation de l'utilisateur sur github.com) était
gardé dans un slot UNIQUE self._device_worker, écrasé sans garde à chaque
appel de _start_device_flow. Un double-clic sur « Se connecter avec
GitHub » (ou une réouverture du flux avant la fin du 1er poll) supprimait
la SEULE référence Python vers un QThread encore en cours d'exécution,
l'exposant au même crash QThread: Destroyed while thread '' is still.
running
- Fix : self._device_worker remplacé par self._device_workers:, même convention que
list[Worker]MainWindow._run/self._workers et
SpellcheckTextEdit._run_check/self._workers — un worker est ajouté à la
liste avant start() et retiré uniquement sur son signal finished. En
complément (filet de sécurité en pratique, pas seulement côté code), le
bouton « Se connecter avec GitHub » est désormais désactivé pendant
l'attente de validation et réactivé à la fin (succès ou échec) — même
pattern que _test()/self._test_btn juste en dessous dans le même
fichier.
- Nouveau fichier tests/test_settings_dialog.py (4 tests) : reproduit le
chevauchement de deux appels à _start_device_flow avant la fin du 1er
(même principe que
test_spellcheck_text_edit.py::test_overlapping_checks_do_not_drop_reference_to_still_running_worker,
via un faux _PendingWorker qui ne se termine jamais tout seul), et
vérifie la désactivation/réactivation du bouton de connexion.
- Suite complète (pytest -q --deselect tests/test_spinner_animation.py)
relancée deux fois après le fix : 963 passed, 6 deselected à chaque fois,
aucune régression.
Corrigé (2026-07-24, crash QThread pendant la frappe d'un prompt)
- Retour utilisateur : crash reproduit avec un log complet montrant deux
lignes « Worker démarré » à ~1 seconde d'écart sans aucun « Worker terminé »
entre les deux, suivies de [Qt] QThread: Destroyed while thread '' is. Réapparu une fois sur deux ("j'ai refait une 2e fois la
still running
manip et ça a très bien fonctionné").
- Root cause localisée : ui/spellcheck_text_edit.py::SpellcheckTextEdit
(correcteur orthographique du champ de prompt principal, appel LLM
débouncé 1200 ms à chaque pause de frappe) gardait chaque worker de
vérification dans un slot UNIQUE self._worker, écrasé sans garde par
l'appel débouncé suivant. Sur un prompt un peu long tapé en plusieurs temps
(typiquement 2 phrases avec une pause entre les deux, comme le prompt
rapporté), rien n'empêchait un 2e passage de démarrer avant que la réponse
LLM du 1er ne soit revenue (latence réseau) — l'écrasement de
self._worker supprimait alors la SEULE référence Python vers un QThread
encore en train d'exécuter son run(), le rendant éligible au garbage
collector en plein vol. Le déplacement de fenêtre mentionné par
l'utilisateur est vraisemblablement une coïncidence de timing (la boucle de
messages Windows imbriquée d'un redimensionnement/déplacement peut
influencer QUAND le GC se déclenche) plutôt qu'une cause distincte.
- Fix : self._worker remplacé par self._workers: list[Worker], même
convention déjà en place dans MainWindow._run/self._workers — un
worker est ajouté à la liste avant start() et retiré uniquement sur son
signal finished (jamais avant qu'il n'ait réellement terminé), donc 2
vérifications qui se chevauchent restent toutes deux référencées jusqu'à
leur fin réelle.
- Risque de même nature repéré au passage, non corrigé (hors scope, pas de
preuve d'incident réel) : ui/settings_dialog.py::_start_device_flow
utilise le même schéma à slot unique (self._device_worker) SANS
désactivation du bouton de connexion pendant l'attente — un double-clic y
exposerait potentiellement au même bug. Signalé pour une vague ultérieure.
- Nouveau test tests/test_spellcheck_text_edit.py:: (+1,
test_overlapping_checks_do_not_drop_reference_to_still_running_worker
11 tests au total dans ce fichier) : reproduit précisément le scénario de
chevauchement (2 appels de vérification, le 1er encore « en vol » quand le
2e démarre) et vérifie que les deux restent référencés jusqu'à leur
finished respectif.
Corrigé (2026-07-24, image d'en-tête d'un type de slide toujours absente)
- Retour utilisateur (captures d'écran) : après la correction du fond de
slide (v1.0.3), les slides générées avec un type personnalisé affichaient
toujours une bande unie bleue/grise à la place du motif/photo d'en-tête
attendu. Root cause DIFFÉRENTE du bug précédent : ce motif n'est pas porté
par background mais par un ÉLÉMENT type: "image" séparé, superposé à une
forme de couleur — donc non couvert par le correctif précédent. Même
limite structurelle en jeu : une longue chaîne base64 est peu fiable à faire
recopier fidèlement par une réponse LLM.
- core/deck.py::apply_slide_type_images réinsère PROGRAMMATIQUEMENT les
éléments-image du modèle que le LLM n'a pas reproduits, avec une heuristique
volontairement prudente (uniquement si la slide compte MOINS d'images que le
modèle n'en définit ; jamais d'appariement id/position, trop fragile ;
jamais de retrait/remplacement d'une image déjà présente). Câblé aux mêmes
points que le correctif de fond : génération (generate/slide_gen.py::) et édition (
_normalizecore/patch.py::apply_patch). Prompts (generate/) mis à jour pour dire explicitement au LLM de ne PAS recopier
prompts.py
ces éléments-image de modèle.
- Nouveau fichier tests/test_slide_type_image_fidelity.py (8 tests).
Suite complète (958 tests, 2 runs consécutifs) verte.
Ajouté (2026-07-24, annuler/rétablir dans l'éditeur de charte)
- Retour utilisateur : "lorsque je change des propriétés, je souhaite
avoir de bouton Annuler et rétablir (+ icônes) afin de pouvoir revenir en
arrière si je ne suis pas satisfait. Conserver la pile des modifs." Boutons
« Annuler »/« Rétablir » (icônes + raccourcis Ctrl+Z/Ctrl+Y natifs) ajoutés
à ui/theme_editor.py, réutilisant core/commands.py::CommandStack (déjà
utilisée par le dialogue de modèle de slide) sur l'ensemble du dict de
charte.
- Les frappes successives dans un même champ (couleur, police, taille, texte,
case à cocher…) fusionnent en UNE seule entrée d'annulation ; les actions
discrètes (Ajouter/Renommer/Supprimer une image, une icône, un type de
slide) restent chacune une entrée séparée, même en cliquant plusieurs fois
de suite sur le même bouton.
- Bug découvert et corrigé pendant les tests : plusieurs contrôles étant
câblés DIRECTEMENT sur le slot commun (textChanged.connect(self._on_change)
etc.), Qt passait la valeur émise (texte, police, état coché…) dans le 1er
argument positionnel — qui était force_new_entry — le rendant quasi
toujours vrai et désactivant silencieusement la fusion des frappes. Corrigé
en rendant force_new_entry keyword-only et en absorbant les arguments de
signal dans *_signal_args.
- Nouveau fichier tests/test_theme_editor_undo_redo.py (8 tests, couvre
notamment ce bug de fusion). Suite complète (958 tests, 2 runs consécutifs)
verte. Vérifié visuellement (capture d'écran, boutons actifs/inactifs
cohérents avec l'état de la pile).
Ajouté (2026-07-24, aperçu de charte modernisé + aperçu par type de slide)
- Aperçu par défaut de l'éditeur de charte redessiné (retour utilisateur :
"ça fait vraiment basique aujourd'hui") : ui/theme_editor.py::_sample_slide
reprend le vocabulaire visuel premium déjà établi ailleurs dans l'app
(bandeau d'en-tête avec sur-titre, filet d'accent latéral signature, carte
"En bref" bordée + ombre portée avec badge et chiffre clé — cf. generate/ et le Design System Socomec réel) au lieu d'un simple titre +
layouts.py
liste à puces. Exerce désormais les 6 rôles principaux de la palette
(primary/secondary/accent/muted/background/text), bordures et ombre.
- Sélectionner un type de slide personnalisé dans la liste prévisualise
désormais SA PROPRE mise en forme (fond + éléments réels du modèle) au
lieu de rester sur l'aperçu générique — avec un bouton dédié « ↺ Aperçu de
la charte » pour revenir à l'aperçu par défaut sans fermer/rouvrir
l'éditeur. Toute édition live (couleurs, polices…) continue de s'appliquer
à l'aperçu actuellement affiché, qu'il s'agisse du défaut ou d'un type
sélectionné.
- 942 tests pytest passent (2 runs complets consécutifs, ~116s chacun, hors
les 2 tests spinner). Nouveau fichier tests/test_theme_editor_preview.py
(6 tests). Vérifié visuellement (captures d'écran de l'éditeur réellement
rendu, aperçu par défaut ET aperçu d'un type sélectionné).