What changed, straight from the code
This isn't a marketing page: it's the actual CHANGELOG.md shipped inside the downloadable zip, read automatically on every visit — never retyped by hand, so it never drifts out of sync.
Current version : v1.4.0 Download this version
[1.3.2] - 2026-09-14
Corrigé — packaging/build_launcher_exe.py produisait un SlydeForge.exe corrompu
- Root cause : PyInstaller construisait directement dans dist_launcher\ du dépôt
— un lecteur Google Drive synchronisé (G:\Mon Drive\...). L'assemblage final
--onefile ré-ouvre l'exe déjà écrit pour lui ajouter le PKG archive puis réécrire
ses en-têtes (« Appending PKG archive to EXE » / « Fixing EXE headers ») — une
écriture binaire EN PLACE que ce lecteur n'aime pas. Le build se terminait sans
aucune erreur PyInstaller, mais l'exe produit refusait ensuite de démarrer :
[PYI-xxxxx:ERROR] Could not load PyInstaller's embedded PKG archive from the, quel que soit l'endroit où il était ensuite copié. Repéré en testant
executable
en conditions réelles CETTE MÊME release sur une installation locale.
- Correctif : le build PyInstaller tourne désormais entièrement dans un dossier
temporaire LOCAL (tempfile.TemporaryDirectory, jamais sur le lecteur synchronisé)
— seule la copie du .exe déjà entièrement assemblé (lecture/écriture simple d'un
fichier fini, jamais de ré-ouverture en écriture par blocs) est faite vers le dépôt.
Corrigé — slydeforge.ini::PYTHON_DIR cassait silencieusement l'exécution des sources de dev
- Root cause : un Python « embeddable » (celui embarqué dans une installation,
cf. packaging/build_portable_python.py) restreint sys.path via son fichier
pythonXY._pth à des chemins relatifs à SON PROPRE dossier — jamais au script
qu'on lui demande d'exécuter. En pointant PYTHON_DIR vers un Python embarqué
installé AILLEURS que ce dossier de sources (cas d'usage prévu de ce réglage :
garder un moteur rapide/compatible addons sans le dossier python\ local, lent
sur un lecteur réseau/cloud), le dossier .. de ce _pth pointait vers CET AUTRE
dossier d'installation — import version/prefs/ai/core/... résolvait alors
silencieusement vers les fichiers de cette autre installation, jamais vers les
sources de dev réellement en cours d'édition, sans le moindre message d'erreur
(juste un numéro de version affiché qui ne correspondait à aucun changement fait
localement). PYTHONPATH ne contourne pas cette restriction (vérifié) : ignoré
par ce mode.
- Correctif : app.py insère désormais explicitement son propre dossier en
tête de sys.path dès sa toute première ligne, avant tout import de sibling —
fonctionne quel que soit l'interpréteur utilisé (système, python\ embarqué
imbriqué, ou PYTHON_DIR pointant ailleurs).
Corrigé — mise à jour automatique pouvait planter l'installation
- Root cause : apply_update_from_zip (ancien comportement) écrasait directement le
dossier d'installation EN DIRECT depuis le process SlydeForge en cours — or ce
dossier embarque son propre interpréteur Python (python\), dont les
.pyd/.dll sont chargés en mémoire par ce même process : Windows refuse alors
leur écriture (Permission denied), un verrou qui ne se lève JAMAIS tant que le
process n'a pas quitté (contrairement à SlydeForge.exe, verrouillé seulement de
façon transitoire par un antivirus). Une mise à jour pouvait donc échouer en
cours de route et laisser l'installation à moitié mise à jour.
- Correctif : mise à jour désormais STAGÉE — stage_update_from_zip extrait
la nouvelle version dans un dossier neuf sous %APPDATA%\SlydeForge (rien n'y est
jamais verrouillé), puis finish_update.ps1 (lancé en tâche détachée par
launch_finish_update, sous PowerShell — jamais l'interpréteur embarqué) bascule
ce dossier par-dessus l'installation une fois SlydeForge complètement fermé, avec
plusieurs tentatives (robocopy /MIR) si un fichier reste transitoirement occupé.
ui/main_window.py mis à jour en conséquence (confirmation, notes de changelog
depuis la version installée, proposition de redémarrer immédiatement ou à la
prochaine fermeture).
- SlydeForge.exe est désormais toujours écrit EN DERNIER lors de l'extraction (et
retenté quelques fois en cas de verrou transitoire) : un échec sur ce seul fichier
n'interrompt plus la mise à jour du reste du code applicatif.
Corrigé — finish_update.ps1 (mise à jour stagée ci-dessus) ne s'exécutait pas du tout
- Root cause : le script contient des caractères accentués et était enregistré en
UTF-8 sans BOM. Windows PowerShell 5.1 (powershell.exe, celui utilisé partout
dans ce projet, y compris par launch_finish_update) interprète un .ps1 sans BOM
avec l'encodage ANSI système, jamais UTF-8 — ce qui corrompait le fichier au point
de le rendre injouable (ParserError: TerminatorExpectedAtEndOfString), quel que
soit le poste. Découvert en testant en conditions réelles l'application de CETTE
MÊME release sur une installation locale : la finalisation échouait silencieusement
(tâche détachée, jamais au premier plan), la mise à jour restait indéfiniment
« stagée » sans jamais être appliquée — regression fraîchement introduite par le
correctif ci-dessus, jamais détectée avant un test bout-en-bout réel (les tests
automatisés mockent l'appel PowerShell, cf. tests/test_update_service_staging.py).
- Correctif : fichier réenregistré en UTF-8 avec BOM (utf-8-sig) — seul
script .ps1 du projet à contenir des caractères non-ASCII, les deux autres
(make_shortcut.ps1, wait_dependencies.ps1) n'étaient pas affectés.
Corrigé — impossible de choisir un autre Python que le système en dépannage
- slydeforge.bat : nouveau fichier optionnel slydeforge.ini (jamais livré en
release) permettant de forcer manuellement le dossier Python à utiliser
(PYTHON_DIR=...), prioritaire sur le Python portable embarqué (python\) et sur
le Python système — utile quand ce dossier embarqué est absent localement (lenteur
constatée sur un lecteur réseau/Google Drive) mais qu'on veut quand même pointer
vers un interpréteur déjà compatible avec les addons installés plutôt que de
retomber sur un Python système potentiellement incompatible (bytecode .pyc lié à
la version mineure exacte de l'interpréteur qui l'a produit).
Ajouté
- Popup dédiée de sélection de langue (ui/language_dialog.py, menu
Paramètres > Langue…) : drapeau + nom natif, redémarrage proposé
immédiatement après le changement — sortie de l'ancien menu déroulant noyé dans
la page IA de ui/settings_dialog.py.
- Variante anglaise du texte de divulgation d'usage réseau des composants du
marketplace interactif de slide (ADDON_NETWORK_USAGE_EN, même principe que
ADDON_LABEL_EN/ADDON_CATEGORY_EN/ADDON_DESCRIPTION_EN déjà existants) :
un utilisateur anglophone voyait jusqu'ici ce texte (affiché avant toute
installation d'un composant — sondage, post-its, webcam, application
externe) rester en français faute de variante dédiée.
[1.3.0] - 2026-09-13
Corrigé — audit qualité avant release : flèches de spin-box illisibles et traductions anglaises manquantes
- QSpinBox brut dans
ui/timeline_edit_dialog.py/ui/agenda_edit_dialog.py(champ « Jalons »/« Items » de la génération IA de frise/agenda) : même défaut que les correctifs QSpinBox déjà documentés dans ce fichier (flèches +/- illisibles, style natif Windows ne respectant pas le QSS) — remplacés parui/spin_widgets.py::IntSpinBox, comme partout ailleurs dans l'app. Repéré par un garde-fou déjà existant (tests/test_presenter_bugfixes.py::test_no_raw_spinbox_remains_in_the_ui_package) mais jamais mis en échec jusqu'ici faute d'exécution récente de la suite complète. - 32 chaînes anglaises manquantes (marketplaces de composants/workflows : filtres de tri, aperçu, bandeau « composant payant » — fonctionnalités ajoutées progressivement sans régénération de
i18n/translations_en.py) + 1 entrée obsolète dupliquée sous le mauvais contexte —i18n/slydeforge_en.ts/.qmrégénérés.
Corrigé — suite de tests instable (crashs natifs intermittents empêchant toute exécution complète, parfois plusieurs heures)
- Constat : la suite (4551 tests) ne parvenait plus à s'exécuter jusqu'au bout — plusieurs causes distinctes de blocage/crash natif Qt en environnement headless, dont un vrai dialogue modal (
QDialog.exec()) resté sans mock dans un test de lancement de présentation, et un signal de worker en file d'attente livré très tard (teardown d'un test sans rapport) vers unQMessageBox.warning()réel jamais mocké à ce stade. Un filet de sécurité global (tests/conftest.py) neutralise désormais ce 2e cas par construction. La suite tourne maintenant en ~5 min viapytest-xdist(nouvelle dépendance dev) au lieu d'un run séquentiel unique fragile sur l'accumulation de widgets Qt de toute la session. - Aucun changement de comportement applicatif — uniquement
tests/,pyproject.toml(deps dev) ettests/conftest.py.
Corrigé — impossible de définir une couleur de fond de slide, seulement une image
- Retour utilisateur (capture d'écran) : « Sur le fond d'une slide je peux définir une image mais pas une couleur ».
- Root cause : le schéma (
background.color) et le rendu (core/theme.py::background_color, déjà lu parrender/scene_builder.py/render/pptx_renderer.py) supportaient déjà une couleur de fond de slide — seului/background_image_dialog.py(menu Insérer > Image de fond de la slide…) n'exposait aucun contrôle pour la régler : uniquement remplacer/retirer/repositionner une image. - Fix : ajout d'un
ColorPickerCombo(même widget réutilisable que le panneau de propriétés/l'éditeur de graphique — rôle de charte avec pastille de prévisualisation, ou couleur personnalisée via le sélecteur système) en tête du dialogue, indépendant de l'image. Le dialogue est renommé « Fond de la slide » (titre uniquement — l'entrée de menu et les libellés déjà testés restent inchangés) puisqu'il ne se limite plus à l'image.ui/slide_template_editor_dialog.py(modèles de type de charte) réutilise le même dialogue et bénéficie donc du même contrôle sans modification. - +4 tests (
tests/test_background_image_dialog.py) ;.ts/.qmrégénérés.
Amélioré — accueil de premier lancement plus « pro », icônes distinctes par moteur IA gratuit, réaffichage tant que l'IA n'est pas configurée
- Retour utilisateur (2 captures d'écran) : « améliore le rendu du message d'accueil d'explication du fonctionnement de la configuration de l'IA [...] faut vraiment un rendu pro et qui accompagne le plus possible l'utilisateur dans la bonne direction [...] c'est l'une des premières choses qu'il va voir en ouvrant l'application » + « si l'abonnement SlydeForge est actif alors mets le en avant » + « tant que l'utilisateur n'aura pas configuré d'IA il faudra continuer à ouvrir cette popup à l'ouverture » ; sur la popup de configuration des backends : « l'icône "Free" n'est pas lisible [...] il faudrait une icône plus en rapport avec le moteur d'IA pour les différencier » + traduction anglaise manquante sur ce même écran.
- ui/onboarding_dialog.py : l'ancien en-tête (un QLabel HTML brut) est remplacé par un bandeau "hero" en carte teintée expliquant en une phrase pourquoi une IA est nécessaire et quoi faire. La carte "Essai gratuit" porte désormais un bandeau "RECOMMANDÉ" (absent sur le réseau Socomec, où la carte GHE pré-remplie devient le chemin le plus simple). La carte "Abonnement SlydeForge", quand l'abonnement est déjà actif, passe EN TÊTE de rangée avec une mise en exergue visuelle (bordure/fond accentués, bandeau "✓ DÉJÀ ACTIF") plutôt que de rester une simple suggestion en 2e position — et le bandeau "RECOMMANDÉ" d'Essai gratuit s'efface alors pour ne pas concurrencer ce nouveau meilleur choix.
- app.py::_maybe_show_onboarding : ne se contente plus d'un unique passage au tout premier lancement — réaffiché à CHAQUE démarrage tant qu'aucune IA n'est configurée, jusqu'à ce qu'elle le soit (reste ensuite accessible depuis Aide > Configuration initiale de l'IA…). Le flag "1re exécution" (services/onboarding.py) continue néanmoins de ne se poser qu'une fois : il reste le signal utilisé ailleurs pour la langue par défaut d'une toute première installation, un usage indépendant de cette ré-ouverture.
- ai/settings.py::BACKEND_LABELS : les 3 essais gratuits (Gemini/Groq/OpenRouter) partageaient jusqu'ici le même badge générique "🆓", minuscule et strictement identique à la taille d'une combo — incapable de les distinguer au premier coup d'œil. Remplacé par un pictogramme propre à chaque moteur : ✨ Gemini, ⚡ Groq (réputation de vitesse), 🔀 OpenRouter (routage multi-modèles) ; le mot "essai gratuit" du libellé continue de porter l'information gratuité.
- Traduction anglaise manquante sur l'écran des backends : ai_settings.BACKEND_LABELS/PERSONAL_API_NOTICE/FREE_TIER_INFO (quotas, libellés de lien de création de clé) sont des dicts définis hors de ui/, indexés par variable — invisibles pour l'extraction i18n normale malgré des self.tr() désormais posés dans ui/settings_dialog.py/ui/onboarding_dialog.py. Ajoutés à i18n/extract_strings.py::_MANUAL_LITERALS (même mécanisme que TimelineEditDialog/AgendaEditDialog) + traductions dans i18n/translations_en.py ; .ts/.qm régénérés.
- +2 tests (test_subscribe_card_put_forward_when_already_active, réaffichage tant que l'IA n'est pas configurée dans tests/test_app_onboarding_wiring.py), tests existants adaptés (icônes distinctes plutôt que badge partagé).
- 2e passe (retour utilisateur, capture) : « je pense que tu peux faire un rendu bien plus stylé et pro [...] mettre la flamme déjà utilisée au lancement de l'application plutôt que la palette de peintre [...] je te laisse revoir les messages pour mieux accompagner l'utilisateur » :
- _icon_badge : chaque icône (bandeau d'en-tête ET les 3 cartes) est désormais posée dans un badge circulaire (fond blanc/neutre) plutôt qu'un emoji nu flottant sur fond plat ; le bandeau d'en-tête reprend paths.icon_png() — la vraie flamme de marque du splash screen/spinner — à la place de l'emoji palette 🎨, sans rapport avec l'identité de l'app. Le bandeau "meilleur choix" (RECOMMANDÉ/DÉJÀ ACTIF) devient une pastille à contour (_style_eyebrow) plutôt qu'un simple texte coloré.
- Textes réécrits pour mieux orienter : « Essai gratuit » précise maintenant que le quota gratuit du fournisseur ne convient pas à une présentation volumineuse/élaborée (pas seulement présenté comme gratuit et rapide) ; « Connecteur complet » précise qu'il vise un profil déjà à l'aise avec les API IA et possédant déjà ses accès (pas la voie par défaut pour un néophyte) ; les 2 états de la carte « Abonnement SlydeForge » mentionnent désormais aussi le menu Abonnement de la barre de menus (ui/main_window.py::_rebuild_subscription_menu), pas seulement Paramètres > IA.
- 3e passe (retour utilisateur) : « pour connecteur complet [...] revoir l'ordre pour être aligné avec la liste des backend (ça sera plus cohérent [...] car GHE pas très connu) et sélectionner par défaut API compatible OpenAI » : l'énumération des 3 backends (API compatible OpenAI / GitHub Models / GitHub Copilot Enterprise) suit désormais le même ordre que ai_settings.BACKENDS/le sélecteur de Paramètres > IA (générique accessible d'abord, GHE en dernier). SettingsDialog.select_backend (nouveau, factorisé avec prefill_free) + OnboardingDialog._configure_connector (dédié à cette seule carte — ne touche pas les 2 autres usages de _configure, ex. le bouton "Ouvrir Paramètres > IA…" de l'abonnement déjà actif) ouvrent désormais Paramètres > IA directement sur "API compatible OpenAI" plutôt que sur le dernier backend enregistré (souvent GitHub Models) ou GHE. +2 tests.
Corrigé — les puces de liste ne suivaient pas précisément leur point de texte quand un titre grossissait
- Retour utilisateur (capture d'écran, format 16:9, deck « Socomec ISOLA » réel inspecté en base) : une puce sans texte visible à côté, une autre "orpheline" flottant entre 2 points — alors que le modèle de charte "Conclusion" est parfaitement sain (3 puces bien pairées à leur point de texte, écart constant de ~1 point).
- Root cause : le correctif précédent (translation par la croissance de l'élément de flux qui "avale" une décoration, cf. entrée ci-dessous) décalait une puce de la croissance du PREMIER élément qui l'engloutit géométriquement — ici le TITRE — au lieu de celle de SON point de texte à elle. Le titre et son body pairé ne grossissent/se déplacent pas forcément de la même quantité (le body n'a besoin que de "juste assez" d'espace pour ne plus chevaucher, jamais la même translation que la croissance brute du titre) : la puce dérivait donc loin de la ligne qu'elle devait accompagner.
- Fix : une décoration IMMÉDIATEMENT SUIVIE, dans la liste des éléments, par un élément de flux (la convention de composition la plus courante d'un modèle de charte — puce juste avant son texte) est désormais explicitement pairée avec LUI : elle suit sa position FINALE en conservant l'écart D'ORIGINE entre les deux, peu importe ce qui a grossi ailleurs sur la slide. Mathématiquement un no-op si l'élément pairé n'a pas bougé — appliqué à TOUTE décoration pairée, pas seulement en cas de collision détectée. Repli sur l'ancienne translation-par-croissance pour les décorations SANS pairage direct (ex un bandeau d'en-tête). +2 tests, dont une reproduction directe du cas réel (écart de 1 point retrouvé exactement, peu importe la croissance du titre).
Ajouté — garantie systémique : le contenu ne peut plus jamais être recouvert par une décoration mal placée
- Retour utilisateur (captures d'écran, format 4:5, deck réel inspecté en base plutôt que deviné) : « il faudrait mettre de l'intelligence pour garantir que le rendu soit cohérent et esthétique [...] même si je sélectionne une charte je peux avoir des problèmes d'alignement ». Deux symptômes concrets sur le même deck : un bandeau de couleur en plein milieu d'un titre, une carte colorée recouvrant le texte de la carte voisine.
- Root cause commune, plus large que les 2 symptômes :
render/scene_builder.py::build_sceneetrender/pptx_renderer.pypeignent les éléments d'une slide dans l'ordre du tableauelements(« le z-order suit l'ordre du tableau », déjà documenté dans le 1er) — rien ne garantissait qu'un élémentrole:"decoration"reste TOUJOURS avant (donc sous) le contenu qu'il chevauche géométriquement. Sa position dans la liste dépend de là où le modèle de charte ou l'IA l'a insérée, sans aucun rapport avec sa fonction (fond vs. premier plan) — n'importe QUELLE cause de chevauchement (estimation de taille de texte, gabarit mal conçu, aléa de génération, adaptation de format…) peut donc, un jour ou l'autre, rendre du texte illisible. Plutôt que de traquer chaque cause une par une (illusoire à garantir exhaustivement), la garantie choisie est plus simple et universelle : le contenu passe TOUJOURS au-dessus. - Fix :
core/deck.py::enforce_decoration_behind_content— déplace juste avant le conflit une décoration qui chevauche RÉELLEMENT un élément de contenu antérieur dans la liste ; ne touche jamais une décoration qui ne chevauche rien (ex un filet fin posé dans l'espace vide entre un titre et son sous-titre) — un tri global ("toute décoration d'abord") avait été essayé puis abandonné : il perturbait aussi des décorations parfaitement inoffensives, cassant une bonne partie de la suite de tests existante sans aucun bénéfice réel. Câblée aux 4 mêmes points queapply_slide_type_images(génération, patch d'édition, réapplication de charte, forçage/restauration de type de slide) — jamais dans un chemin d'édition MANUELLE, pour ne jamais écraser un z-order que l'utilisateur a choisi lui-même via le canevas. - Complément esthétique :
generate/layout_guard.py::fix_text_overflowrepousse maintenant aussi toute décoration dont le CENTRE se retrouve à l'intérieur d'un élément de texte agrandi/déplacé (jamais si elle y était déjà avant — une superposition volontaire sur un texte COURT reste intacte) — sans ce complément, la garantie ci-dessus empêche le texte d'être caché mais laisse la décoration visuellement plantée AU TRAVERS de lui (dans les interlignes). - +10 tests (
tests/test_decoration_behind_content.py, 3 nouveaux cas danstests/test_layout_guard.py), 1 test existant adapté (tests/test_slide_type_background_fidelity.py: recherche par rôle plutôt que par index de tableau, désormais moins stable par construction).
Corrigé — sous-estimation de la hauteur d'un titre dans une colonne étroite (calcul de lignes ignorant les frontières de mot)
- Retour utilisateur (même deck, format 4:5) : le sous-titre d'une slide recouvrait la dernière ligne du titre juste au-dessus, alors que le deck réel montrait un écart correct entre les 2 rects (pas de chevauchement géométrique).
- Root cause :
generate/layout_guard.py::_paragraph_linesestimait le nombre de lignes parmath.ceil(caractères / caractères-par-ligne)— un calcul qui suppose implicitement qu'un mot peut être coupé n'importe où pour remplir une ligne. Un vrai retour à la ligne ne coupe jamais un mot : le titre « Des icônes vues en vrai » (colonne étroite, 42% de large en 4:5) a réellement occupé 4 lignes au rendu, contre 3 prédites — sa boîte n'a donc jamais été agrandie assez, la 4e ligne débordant directement dans l'espace laissé au sous-titre suivant. Écart resté invisible sur une large colonne 16:9 classique, flagrant dès qu'une colonne est étroite (portrait/carré) avec un texte à PEU de mots relativement longs. - Fix :
_wrapped_line_countsimule un retour à la ligne par mot entier — vérifié : retrouve exactement les 4 lignes réelles pour ce titre. +5 tests.
Corrigé — éléments décoratifs de charte étirés/déformés quand un deck est généré dans un format différent de celui de la charte
- Retour utilisateur (captures d'écran, charte « New York ») : « la déformation des slides lorsque l'on passe dans un autre format que celui défini dans la charte peut ne pas être vraiment esthétique (c'est surtout dans les éléments graphiques) [...] c'était directement depuis un brief IA (pas une conversion) » — les ellipses décoratives des cartes de statistiques apparaissaient nettement étirées.
- Root cause :
core/deck.py::apply_slide_type_images/_sync_decoration_shapesrecopient les élémentsrole:"decoration"d'un modèle de type de charte À L'IDENTIQUE (rect en %) sur la slide générée, quel que soit le format cible — or ces rects ont été pensés pour un format SOURCE précis (typiquement celui du deck où le modèle a été capturé), jamais enregistré nulle part. Un simple remap%par axe (déjà le comportement d'un changement de canevas SANS ce correctif) ne déforme pas la POSITION mais déforme bien la FORME d'un élément dont le rendu dépend de son ratio largeur/hauteur réel (ellipse, filet fin) dès que le ratio cible diffère du ratio source. - Fix : nouveau module pur
core/decoration_adapt.py— classification géométrique en 3 cas mesurée sur le rect d'origine (bandeau plein-axe / accent de bord ou coin / forme flottante), même logique que les contraintes "pin/scale" d'un outil de design pro (Figma/Keynote) : le ratio pixel réel de la forme est TOUJOURS préservé (jamais d'ellipse étirée), un bord touché reste ancré au même bord, une forme flottante reste centrée sur son centre d'origine. Un nouveau champtheme.slide_types[].template.native_format(deck.schema.json) enregistre le format dans lequel un modèle a été conçu — renseigné automatiquement, sans aucune action utilisateur : à la capture d'une slide comme modèle (ui/main_window.py::_save_slide_as_type_template), et déduit des dimensions réelles du PPTX source quand une charte est proposée par l'IA à partir d'un import (services/theme_service.py::analyze,formats/registry.py::nearest). Absent (modèle créé avant ce champ) : repli sur le format historiquement implicite de toute charte (16:9), comportement inchangé. Câblé aux 4 points qui recopient un modèle de charte (génération, patch d'édition, réapplication de charte, forçage/restauration de type de slide) ainsi qu'au moteur de conversion de format déterministe (conversion/engine.py::convert_slide, qui exclut désormais les éléments décoratifs du réagencement de contenu générique et les fait passer par le même moteur géométrique) — un seul mécanisme, quel que soit le chemin (génération directe ou conversion) et quelle que soit la paire de formats. - +15 tests (
tests/test_decoration_adapt.py,tests/test_decoration_adapt_wiring.py, 2 nouveaux cas danstests/test_theme_service_analyze.py). - Régression corrigée après un 1er test réel (captures d'écran, « Des monuments à voir ») : le fond bleu d'un panneau de composition (pas un simple filet décoratif — un grand panneau touchant 2 bords opposés d'un axe, donc classé BANDEAU) recouvrait le titre de la slide. Root cause : la préservation de taille PIXEL du correctif ci-dessus, sans plafond, pouvait faire GONFLER un tel panneau bien au-delà de sa largeur d'origine (45% -> plus de 100%) quand l'axe cible rétrécit en pixels (16:9 -> 9:16) — le panneau finissait par empiéter sur du texte positionné par le LLM en tenant pour acquis la géométrie D'ORIGINE du modèle. Fix : la préservation pixel d'un bandeau n'autorise plus désormais que le RÉTRÉCISSEMENT (jamais la croissance) — sans risque dans les 2 sens (un filet fin devient éventuellement plus fin encore, un panneau ne peut plus jamais gonfler). +2 tests de non-régression.
Corrigé — le titre provisoire d'un projet généré depuis un brief pouvait amputer un mot en plein milieu
- Retour utilisateur (contexte du test ci-dessus) : sur le brief « Faire une présentation [...] de la ville de New York. [...] », la coupure
[:60]brute deui/main_window.py::_new_from_brieftombait en plein milieu du mot « York » dans le cas général (elle tombait pile sur un espace pour ce brief précis, mais pas pour d'autres formulations) — un titre amputé de son sujet réel. - Pourquoi ça compte : ce titre provisoire sert d'ANCRAGE (
topic) à la recherche de photos libres de droits pour chaque slide tant que le titre définitif (synthétisé par l'IA en tâche de fond) n'est pas encore prêt, cf.services/stock_images.py::build_query_for_plan_item— chaque fois que le modèle omet le champ optionnelimage_query, un titre amputé en plein mot produit une requête incohérente. - Fix : nouveau
ui/main_window.py::_truncate_on_word_boundary— recule jusqu'au dernier espace avant la limite (sauf si la coupure tombe déjà sur une frontière de mot, auquel cas rien n'est retiré en plus), avec repli sur la coupure brute si aucun espace n'existe. +6 tests (tests/test_truncate_on_word_boundary.py).
Ajouté — état de la case « Utiliser des images libres de droits » visible dans le chat (activé/aucun fournisseur/résultat)
- Retour utilisateur : « serait-il possible de conserver l'information visible dans l'assistant lorsque l'on a coché la case [...] car je pense que je l'ai fait mais il n'y a aucune image [...] essaye de comprendre pourquoi ça s'est produit » — diagnostic confirmé via les logs (cf. journalisation ajoutée juste en dessous) : le fournisseur (Pexels) était bien configuré, la recherche a réussi à CHAQUE slide (3 candidats trouvés et téléchargés), mais l'IA n'en a utilisé AUCUN sur les 8 slides d'un deck touristique sur New York — un pipeline fonctionnel, mais aucune trace visible dans l'app pour le comprendre sans ouvrir les logs.
- Fix :
ui/main_window.py::_new_from_briefaffiche désormais un message dans le chat (persisté) dès le lancement — « Images libres de droits activé (Pexels) [...] » si un fournisseur est configuré, ou un avertissement explicite si la case est cochée SANS aucun fournisseur configuré (Paramètres > Images) — ce dernier cas ne déclenchait jusqu'ici AUCUNE recherche, silencieusement. Un résumé de fin de génération s'ajoute ensuite (« X photo(s) insérée(s) sur Y proposée(s) », ou explicitement « l'IA n'en a retenu aucune » si 0 malgré des candidats trouvés) —services/stock_images.py::apply_credit_captionsretourne maintenant le nombre de candidats effectivement utilisés pour l'alimenter. +4 tests (tests/test_stock_images_wiring.py).
Ajouté — journalisation des recherches d'images libres de droits (case « Utiliser des images libres de droits »)
- Retour utilisateur (capture à l'appui) : case cochée sur un brief IA, AUCUNE photo dans le deck entier — recherche manuelle immédiatement re-testée et fonctionnelle. Aucune anomalie de câblage trouvée dans
services/image_search.py/services/stock_images.py: le silence total en cas d'échec y est délibéré (jamais bloquant pour une génération), mais ne laissait justement aucune trace pour distinguer les 3 causes possibles d'un « 0 image » — aucun fournisseur configuré, recherche/téléchargement en échec (réseau, quota — un appel par slide sur un deck de plusieurs slides), ou candidats bien trouvés mais jamais utilisés par l'IA (choix éditorial légitime, usage facultatif par conception). - Fix :
services/image_search.py::search_and_download_candidatesetservices/stock_images.py::apply_credit_captionsjournalisent désormais chacun de ces cas (requête/fournisseurs, résultats, échecs réseau/téléchargement, candidats jamais crédités) — même discipline quecore/deck.py::apply_slide_type_backgroundpour la même raison : un prochain repro doit produire un LOG exploitable plutôt qu'une nouvelle capture d'écran à deviner. Comportement fonctionnel inchangé. - Confirmé au 1er repro réel (logs à l'appui) : ni le fournisseur, ni la recherche, ni le téléchargement n'étaient en cause (Pexels configuré, 3 candidats trouvés et téléchargés à CHAQUE slide d'un deck de 8) — l'IA n'en a simplement utilisé AUCUN. Complété par un log du contenu réellement proposé (requête + descriptions, absent du 1er passage de journalisation) pour trancher, au prochain repro, entre une requête incohérente et un rejet du modèle malgré des photos pertinentes — cf. aussi le correctif de troncature de titre ci-dessus (l'un des 2 ancrages possibles de cette requête) et la visibilité désormais ajoutée dans le chat.
Corrigé — le compteur « ≈ N tokens » ne comptait que la réponse du modèle, jamais le prompt envoyé
- Retour utilisateur : « gros décalage sur le nombre de token retourné par l'assistant SlydeForge que de ce qui est indiqué dans le retour de consommation des tokens retourné par la souscription SlydeForge » — sur un abonnement neuf, une génération « en 3 slides » annonçait « ≈ 2468 tokens » en fin de traitement alors que la jauge d'abonnement (
ui/ai_quota_gauge.py) affichait une consommation réelle bien supérieure pour ce même traitement. - Root cause :
ui/chat_panel.py::_token_chars(alimenté parappend_stream, caractères/4) ne comptait QUE le texte RENVOYÉ par le modèle à chaque appel. Il ne comptait jamais le PROMPT envoyé (system prompt : charte, catalogue d'assets/icônes, anatomie du layout, historique de conversation…) — potentiellement volumineux et répété à CHAQUE appel IA d'un même traitement (plan + une slide par appel + contrôle visuel/corrections = plusieurs appels pour un seul clic « Générer »).ai/streaming.py::_log_callcalculait déjà une estimation correcte (prompt_chars + response_chars) mais uniquement pour le log de debug, jamais remontée à l'UI. - Fix :
ai/streaming.py::complete_chat/stream_chatacceptent désormais unon_usageoptionnel (appelé UNE FOIS par appel HTTP réussi avec la taille du prompt en caractères), threadé en parallèle deon_chunkà traversgenerate/planner.py::make_plan,generate/slide_gen.py::generate_slide,services/visual_review.py::review_and_fix(+ vérif/correction) etservices/edit_service.py(édition par prompt + auto-critique). Séparé deon_chunkplutôt que fusionné :on_chunk/append_streamalimente aussi le panneau Détails avec le texte réellement renvoyé par le modèle, jamais avec le contenu du prompt. Côté UI,ui/chat_panel.py::add_prompt_charsalimente le même compteur sans toucher au panneau Détails ; câblé depuisui/main_window.py(flux « Nouveau depuis un brief » et édition par prompt) viaui/workers.py::UsageRelay, unQObjectséparé sur le même principe queLivePreviewRelaydéjà en place — la signatureWorker.job(emit_chunk, emit_phase)est utilisée par des dizaines d'appelants existants et ne doit pas changer. - +9 tests (
tests/test_ai_mocks.py,tests/test_token_estimation_wiring.py) vérifiant queon_usagereçoit la taille du prompt (pas la réponse), une seule fois par appel (pas par fragment SSE), et que le câblage bout-en-bout GUI alimente bienchat._token_chars.
Corrigé (génération : troncature JSON sur une slide riche en icônes, SANS modèle de charte)
- Retour utilisateur : « Échec : Génération de la slide « Des icônes qui définissent la ville » impossible : La réponse de l'IA semble avoir été coupée avant la fin (JSON incomplet) — probablement trop volumineuse pour le nombre de tokens autorisé. » sur un deck sans type de slide personnalisé (thème par défaut).
- Root cause : la consigne « tu PEUX simplifier le nombre de blocs purement décoratifs/répétitifs pour rester concis » (déjà ajoutée en 1.0.4 après un bug similaire) n'existait QUE dans
template_block/_slide_type_templates_block(generate/prompts.py) — c'est-à-dire uniquement quand la slide reprend un modèle de type personnalisé de la charte (generate/slide_gen.py::_template_for). Une slide sans ce modèle (cas le plus courant, ex une grille d'icônes de monuments générée librement) n'avait aucune limite de nombre de blocs ni consigne de sobriété JSON, et pouvait donc être tronquée parmax_tokensmalgré sa valeur déjà relevée (24000). - Fix : nouvelle
_CONCISION_NOTE(generate/prompts.py) — id courts, pas d'espace superflu, et un plafond explicite (4 à 6 blocs répétitifs) — incluse dans TOUS les appels desystem_generate_slide, avec ou sansslide_type_template. - +1 test dédié (
tests/test_layout_anatomy.py::test_system_prompt_includes_concision_note_without_theme_template).
Ajouté — 4ème backend IA « API SlydeForge » (jetons inclus dans l'abonnement)
- Contexte produit : le site slydeforge.com propose désormais un backend IA hébergé, mesuré en jetons et inclus dans certains paliers d'abonnement (
api/ai_proxy.php,api/payments_status.php::ai_usagecôté dépôtwww, désactivé par défaut côté serveur — aucun changement pour les utilisateurs actuels tant que rien n'est activé côté admin). Les 3 backends existants où l'utilisateur fournit sa propre clé (Copilot Enterprise, GitHub Models, API compatible OpenAI) sont regroupés conceptuellement sous l'étiquette « API personnelle » (ai_settings.PERSONAL_API_BACKENDS), avec un rappel systématique — « Sans limite imposée par SlydeForge — facturé par votre fournisseur selon ses propres tarifs (gratuit avec Ollama en local). » — remplaçant toute formulation pouvant laisser penser à de l'IA illimitée et gratuite. ai/client.py/ai/streaming.py:BACKEND_SLYDEFORGE_API— aucun identifiant à saisir (l'instance_iddeservices/license_service.py, déjà utilisé pour les achats marketplace, suffit). Jamais de streaming réel côté serveur (toujours une réponse JSON bloquante) : même repli « texte complet en un seul chunk » déjà utilisé pour Gemini. Chaque appel envoie unaction_type(generation/vision_qc/charter_extraction/patch_edit/layout_suggestion/writing_assist/presenter_notes) reflétant la capacité IA réellement déclenchée, propagé depuis ~18 points d'appel existants (génération, édition par patch, notes, révision visuelle, suggestions de mise en page/écriture…) — pour que le modèle ET le coût réel restent maîtrisés côté serveur, jamais pour que l'app choisisse elle-même un modèle.- Jauge de consommation (
ui/ai_quota_gauge.py) : alimentée au lancement parpayments_status.php::ai_usage, puis rafraîchie par la cléquotadéjà renvoyée par chaque appel IA (services/license_service.py::get_cached_quota()/set_cached_quota()) — jamais de requête réseau dédiée, et jamais un appel bloquant sur le thread GUI (lecture cache pure, y compris avant l'envoi d'un prompt). Alerte non bloquante à 80 %, état bloquant à 100 % avec 2 actions (« Passer au palier supérieur » / « Utiliser ma propre clé API »). - Erreurs 429/403 :
AIQuotaExceededError/AIPlanRequiredError(ai/client.py) portent un message auto-suffisant (date de renouvellement + les 2 actions ci-dessus en toutes lettres) — visible tel quel dans les ~20 affichages d'erreur existants de l'app (aucun n'a eu besoin d'être modifié). Un vrai dialogue à 2 boutons est en plus déclenché aux 2 points d'entrée IA les plus empruntés : la jauge de Paramètres > IA, et l'envoi de prompt principal (ui/main_window.py::_on_prompt, avec court-circuit proactif si le quota en cache indique déjà 100 %, pour éviter un aller-retour réseau inutile). - « Restaurer mes achats » (
ui/restore_purchases_dialog.py, accessible depuis Paramètres > IA et le menu Paramètres) : nécessaire dès maintenant qu'un abonnement peut être souscrit depuis le site AVANT même d'avoir installé l'app — l'achat reste « orphelin » tant que l'utilisateur ne passe pas par cet écran une fois l'app installée.services/license_service.py::claim_email()propage désormais le code d'erreur exact du serveur plutôt qu'un simple booléen.
Ajouté — gating des composants marketplace devenus payants (components/slide-components)
- Contexte produit : le site publie désormais un champ
pricingsur chaque entrée des cataloguescomponents/catalog.json(composants de workflow) etslide-components/catalog.json(composants interactifs de slide) — un composant peut donc devenir payant après avoir été installé gratuitement. Un composant déjà installé reste utilisable tel quel (jamais désinstallé/désactivé rétroactivement) ; seule une NOUVELLE installation ou mise à jour est désormais bloquée sans l'entitlement correspondant. services/marketplace_pricing.py(nouveau, partagé par les 2 marketplaces) :Pricing/parse_pricing()(tolérant, un catalogue plus ancien sanspricingreste gratuit),format_amount()/format_action_label()(« 4,99 € » / « S'abonner (4,99 €/mois) »),is_locked(),PaymentRequiredError.services/addon_marketplace.py::install()/services/slide_component_marketplace.py::install()vérifient l'entitlement (services/license_service.py::is_entitled()) AVANT tout téléchargement.ui/marketplace_payment_dialog.py(nouveau, partagé) : dialogue e-mail + choix Stripe/PayPal + bouton Acheter/S'abonner, déclenché à la place du message d'erreur générique dès qu'une installation est bloquée (ui/addon_marketplace_dialog.py/ui/slide_component_marketplace_dialog.pyaffichent aussi un badge prix et le libellé du bouton dès la sélection, avant même de cliquer).services/license_service.py::start_purchase()renvoie désormais unPurchaseStartstructuré (checkout_url/purchase_id/bypassed/error) au lieu d'une simple URL — gère le contournement développeur (bypassed) et chaque code d'erreur deapi/payments_checkout.phpavec un message dédié.docs/paid_components_integration.mdmis à jour pour refléter cette implémentation réelle (le document d'origine esquissait une forme depricingsansis_paid— corrigée).
Ajouté — menu « Abonnement » dédié + invitation à souscrire (Paramètres > IA, écran d'accueil)
- Contexte produit :
payments_status.phpexpose désormaispayments_enableden plus deentitlements/ai_usage— l'app peut donc distinguer 3 états (système de paiement éteint côté site / actif mais pas encore souscrit / abonnement actif) et adapter son affichage en conséquence, plutôt que de proposer une gestion d'abonnement noyée dans Paramètres sans jamais suggérer de souscrire. Retour utilisateur : « il manque l'essentiel : comment souscrire directement [...] un menu dédié pour ne pas tout mélanger ». services/license_service.py:SubscriptionStatus/get_cached_subscription_status()(lecture pure, jamais d'appel réseau — même discipline queget_cached_quota()) etrefresh_subscription_status()(réseau, à appeler uniquement depuis unWorker).pricing_url(lang=None)suit désormais la langue de l'interface (au lieu d'une URL FR figée).- Menu « Abonnement » (
ui/main_window.py, remplace les entrées « Restaurer mes achats… »/« Gérer mon abonnement… » de Paramètres) : invisible sipayments_enabled=false; sinon propose « Souscrire un abonnement… » + « J'ai déjà un abonnement (autre e-mail)… » si pas encore abonné, ou l'usage IA + « Gérer mon abonnement… » si abonnement actif. - Bandeau Paramètres > IA : quand l'API SlydeForge n'est pas encore incluse mais que
payments_enabled=true, un bandeau propose explicitement de souscrire (au lieu de ne rien afficher). - Écran d'accueil refondu en cartes cliquables (
ui/onboarding_dialog.py) — retour utilisateur : « pas très sexy et vendeur ». Le mur de texte markdown est remplacé par un en-tête court + des cartes (icône/titre/description/bouton) : Essai gratuit, puis (sipayments_enabled) Abonnement SlydeForge — toujours en 2e position, avant Connecteur complet, car c'est l'option la plus simple pour un utilisateur sans connaissances techniques — insérée dynamiquement une fois la vérification réseau résolue (jamais sur le thread GUI).