# PdfScrapper Navigateur visuel qui permet de sélectionner du texte sur des pages web et de l'accumuler dans un PDF, au fil de la navigation. ## Fonctionnalités - **Navigateur intégré** (moteur Chromium via QtWebEngine) : barre d'adresse, précédent / suivant / recharger, session persistante (cookies/connexions conservées entre deux lancements). - **Sauvegarde de projet** (*"📂 Ouvrir…"* / *"💾 Enregistrer"* / *"💾 Enregistrer sous…"*, dans la barre d'outils) : un projet regroupe les extraits capturés, le style PDF et la page en cours, dans un fichier `.pdfscrap` (JSON). *Enregistrer* réutilise le fichier déjà nommé (ou propose *Enregistrer sous* la première fois) ; *Ouvrir* recharge un projet précédent (avec confirmation si des extraits non enregistrés seraient remplacés). Une **sauvegarde automatique** tourne aussi en tâche de fond, toutes les 60 secondes et à la fermeture de l'appli : tant qu'aucun fichier n'a été nommé explicitement, elle écrit dans un fichier de récupération par défaut, rechargé automatiquement au prochain lancement (comme la restauration de session d'un navigateur) — dès qu'un *Enregistrer sous* est fait, elle bascule sur ce fichier. - **Capture de texte (sélection manuelle)** : sélectionnez du texte dans la page puis cliquez sur *"➕ Ajouter la sélection au PDF"* (ou `Ctrl+Shift+A`). L'extrait est stocké avec le titre de la page, son URL et l'heure de capture. - **Capture de bloc au survol** : le bloc de contenu sous le curseur (paragraphe, item de liste, section, article...) se surligne automatiquement quand le mode est actif ; un clic dessus l'ajoute directement au PDF, sans avoir à sélectionner le texte à la main. Deux façons de l'activer, combinables : - **Maintenir `Ctrl` enfoncé** dans la page : actif tant que la touche est tenue, désactivé dès qu'on la relâche (pratique pour capturer un bloc à la volée sans repasser par la souris ensuite). - **`Ctrl+Shift+B`** (ou le bouton *"🎯 Mode capture au survol"*) : reste actif en continu jusqu'à nouvelle bascule, y compris après avoir suivi un lien vers la page suivante. Tant que le mode est actif (par l'une ou l'autre méthode), le clic normal (suivre les liens) est suspendu sur la page. - **Panneau latéral** : liste des extraits capturés (réorganisables, supprimables, **renommables**). Si un extrait est sélectionné dans la liste, chaque nouvelle capture s'insère juste à côté de lui — **au-dessus** ou **en dessous** selon la case à cocher *"Insérer les nouvelles captures au-dessus de la sélection"* (cochée par défaut = au-dessus) — et les captures suivantes continuent de s'enchaîner du même côté du repère sans avoir à re-sélectionner à chaque fois. Sans sélection, les captures s'accumulent simplement à la fin, quel que soit ce réglage. - **Renommer un extrait** : double-cliquez sur un extrait dans la liste (ou sélectionnez-le puis *"✏️ Renommer"*) pour changer son titre affiché — utilisé aussi comme titre de section dans le PDF exporté. - **Tableau des extraits** : le panneau latéral affiche les extraits capturés dans un vrai tableau (colonnes *Heure*, *Titre*, *Aperçu*, *Lien*, avec en-tête), plutôt qu'une simple liste de texte. - **Lier plusieurs extraits en un seul chapitre** : quand un chapitre est réparti sur plusieurs pages web (donc plusieurs extraits capturés séparément), l'icône de la colonne *Lien* permet de relier un extrait au **suivant** dans le tableau — à la manière du maillon de chaîne entre calques adjacents dans GIMP. Les extraits reliés sont exportés comme **une seule section** dans le PDF : un seul titre (celui du premier extrait du groupe, toujours renommable) et une seule entrée de sommaire, sans saut de page entre eux. Les extraits qui suivent le premier d'un groupe sont repérés par un préfixe "↳" (colonne *Titre*, et onglet "Corriger le texte"). Deux extraits directement reliés ne peuvent pas être réordonnés (▲/▼) sans être déliés au préalable, pour éviter d'inverser silencieusement le sens du lien. - **Aperçu et style du PDF** (*"🖌 Aperçu et style du PDF…"*, barre d'outils) : fenêtre dédiée qui affiche un rendu en direct du PDF final (via QtPdf) pendant que vous ajustez les réglages, répartis en deux onglets : - **🎨 Style** : titre du document, format de page (A4/Letter), police globale (Helvetica/Times/Courier — s'applique à tout le document), taille du texte et des titres, couleur des titres, couleur du texte, **couleur d'arrière-plan**, marges, affichage ou non de l'URL/date sous chaque titre, saut de page entre extraits ou non, et **inclusion d'un sommaire** (voir ci-dessous). - **✏️ Corriger le texte** : liste des extraits, avec un champ titre et une zone de texte éditable pour corriger le contenu capturé directement (fautes de frappe, nettoyage de texte scrappé, etc.) — les modifications s'appliquent immédiatement à l'extrait (y compris dans le panneau latéral de la fenêtre principale) et l'aperçu se régénère automatiquement. L'aperçu se régénère automatiquement (avec un léger délai anti-rebond) à chaque changement. Les réglages de style et les corrections de texte faits ici restent actifs pour l'export "normal" depuis le panneau latéral, même après avoir fermé cette fenêtre. - **Sommaire cliquable** : activable depuis l'onglet Style (*"Inclure un sommaire"*, actif par défaut dès qu'il y a plus d'un extrait). Insère une page de sommaire juste après le titre du document, avec un lien cliquable vers chaque section — liens internes PDF standard (annotations `/Link` avec destination directe), utilisables dans n'importe quel lecteur PDF. - **Détection de liens de "suite"** : les liens dont le texte — ou le texte d'un conteneur voisin proche (icône + légende, ex. "Partie Suivante" à côté du lien) — ressemble à *suivant / next / chapitre suivant / lire la suite / part 2* etc. (ou une balise ``) sont surlignés en orange et listés dans le panneau, pour enchaîner facilement la lecture et la capture sans les chercher manuellement. La détection tourne en continu (`MutationObserver` JS, résultats poussés vers Python via le pont QWebChannel) plutôt qu'une seule fois au chargement — nécessaire car beaucoup de sites (SPA, pagination injectée après coup en JavaScript) ajoutent ce genre de lien bien après le signal de fin de chargement. - **Scraping assisté par IA locale** (llama-cpp-python, aucune donnée envoyée en ligne) : configurez un modèle via *"⚙️ Configurer l'IA locale"* (fichier `.gguf` de votre choix — non fourni avec l'appli, liens de téléchargement vérifiés dans la section [Installation](#modèles-gguf-recommandés) ci-dessous). Trois usages, combinables : - **🤖 Extraire (IA)** : demande au modèle d'extraire le texte narratif principal de la page (menus/pubs/commentaires écartés) et l'ajoute au PDF — alternative à la sélection manuelle/au mode survol, utile sur les pages au contenu trop fragmenté pour un clic unique. - **🤖 Lien suivant (IA)** : demande au modèle de désigner, parmi *tous* les liens de la page, celui qui mène le plus probablement à la suite — complète la détection heuristique quand celle-ci ne trouve rien (structures de page trop inhabituelles pour un motif texte). Pour chaque lien, le texte du **conteneur parent proche** est aussi transmis au modèle quand il diffère du texte du lien lui-même — utile pour les liens de pagination réduits à une icône/un chiffre sans texte propre (ex. "2", "›"), fréquents quand un chapitre est réparti sur plusieurs pages web. La **fin du contenu de la page actuelle** (déjà extrait, voir 🤖 Extraire (IA) — ou relu pour l'occasion pour le bouton manuel) est aussi transmise : un texte qui s'interrompt en plein milieu d'une scène est un indice fort de pagination interne, qu'un simple texte de lien ne suffit pas toujours à révéler. Le modèle indique aussi si le lien choisi mène à la suite du **même chapitre** (pagination interne) ou à un **nouveau chapitre**. - **🔁 Scraping auto (IA)** : combine les deux en boucle — extrait, capture, trouve et suit le lien suivant, répète — jusqu'à un nombre de chapitres maximum fixé au démarrage (garde-fou demandé à chaque lancement, **0 = illimité**, voir ci-dessous) ou jusqu'à ce qu'aucune suite ne soit trouvée ; une détection de boucle (retour vers une page déjà visitée) arrête aussi le processus automatiquement. Le bouton devient "⏹ Arrêter le scraping" tant qu'il tourne. L'inférence tourne sur un thread dédié (en streaming) pour ne jamais geler l'interface, avec un **délai d'inactivité** (2 min sans le moindre fragment de réponse généré) au-delà duquel le scraping s'arrête proprement plutôt que de rester bloqué indéfiniment si l'IA ne répond vraiment plus jamais (modèle figé, fichier corrompu...) — chaque fragment généré réarme ce délai, donc une analyse simplement lente (mais qui progresse encore) n'est jamais interrompue à tort, contrairement à une limite sur la durée totale de l'appel. Les éventuelles erreurs d'inférence n'affichent plus de boîte de dialogue bloquante pendant un scraping automatique (juste un arrêt + un message dans le journal), pour ne jamais donner l'impression que l'appli s'est figée faute d'un clic sur une fenêtre passée inaperçue. Une **fenêtre de suivi** (non modale, se réaffiche à chaque lancement) s'ouvre en même temps : journal horodaté de chaque étape (lecture de la page, extraction IA, recherche puis choix du lien suivant, arrêt et sa raison...), barre de progression "chapitre X/N" (animation indéterminée en mode illimité), et son propre bouton *"⏹ Arrêter le scraping"*. La fermer (X) la masque seulement — le scraping continue en arrière-plan si on ne l'arrête pas, la fenêtre se rouvrant au prochain démarrage de scraping. **Chapitres multipages liés automatiquement** : quand le modèle juge que la page suivante est la suite du même chapitre (voir ci-dessus), l'extrait qui vient d'être capturé est automatiquement marqué 🔗 (même mécanisme que la liaison manuelle, voir *Lier plusieurs extraits en un seul chapitre* ci-dessous) — les pages d'un même chapitre réparti sur plusieurs URLs ressortent donc comme une seule section dans le PDF final, sans action manuelle. - **Détection des tables des matières** : avant de tenter d'en extraire le contenu, chaque page est d'abord analysée pour savoir si c'est une table des matières / liste de chapitres plutôt qu'un chapitre en soi (page d'accueil d'une série, sommaire...). Si oui : aucun faux "chapitre" n'est capturé depuis cette liste de liens, et la recherche du lien suivant vise explicitement le **premier chapitre** de la liste plutôt qu'une suite arbitraire parmi tous les chapitres listés — sans cette détection, le scraping automatique tend à se perdre dès qu'il démarre sur une page de ce type. S'applique aussi bien au scraping automatique qu'au bouton manuel *"🤖 Lien suivant (IA)"*. - **Limite "0" = illimité / détection automatique** : entrer 0 comme nombre maximum de chapitres tente d'abord de déterminer le nombre total de chapitres à partir du titre/texte de la page de départ (ex. une table des matières, "Chapitre 3 sur 12"...) avant même de commencer — trouvé, ce nombre devient la limite ; indéterminable, le scraping tourne sans limite fixe et s'arrête de lui-même dès qu'aucune suite n'est trouvée (voir garde-fous ci-dessus). - **Prompt personnalisable** : le prompt système utilisé pour le choix du lien suivant (Lien suivant (IA) et Scraping auto) est éditable dans *"⚙️ Configurer l'IA locale"* — utile pour affiner le comportement sur un site particulièrement atypique. Un bouton *"↺ Réinitialiser au défaut"* restaure le texte intégré à l'appli. Laisser le champ tel quel revient à utiliser ce prompt par défaut ; le modifier doit conserver le format de réponse JSON décrit dans le texte (`index`/`same_chapter`), sous peine de casser l'analyse de la réponse du modèle. Tous les boutons ci-dessus (projet, capture, aperçu, IA, export) sont regroupés dans la **barre d'outils** en haut de la fenêtre ; le panneau latéral ne garde que le tableau des extraits, ses actions de réorganisation, et les liens suggérés sur la page courante. ## Installation ```bash python -m venv .venv .venv\Scripts\activate # Windows pip install -r requirements.txt ``` ## Lancement ```bash python run.py ``` `llama-cpp-python` compile une extension native au moment de l'installation (pas de wheel précompilé pour toutes les configurations) — l'installation peut donc prendre plusieurs minutes selon la machine. Les fonctionnalités IA restent optionnelles : sans modèle configuré, le reste de l'application fonctionne normalement (les boutons IA affichent un message clair invitant à configurer un modèle plutôt que d'échouer silencieusement). ### IA locale (optionnel) 1. Téléchargez un modèle au format **GGUF** (liens vérifiés ci-dessous — le fichier n'est pas fourni avec l'appli, à télécharger une seule fois). 2. Dans PdfScrapper, ouvrez *"⚙️ Configurer l'IA locale"* et indiquez le chemin du fichier `.gguf`. 3. Les boutons *"🤖 Extraire (IA)"*, *"🤖 Lien suivant (IA)"* et *"🔁 Scraping automatique (IA)"* sont alors utilisables. #### Modèles GGUF recommandés Existence, nom de fichier exact et taille vérifiés au moment de la rédaction. Prendre la variante **Q4_K_M** (bon compromis qualité/vitesse/ taille pour du CPU) sauf indication contraire — c'est le lien direct donné ci-dessous. | Modèle | Taille (Q4_K_M) | Pourquoi celui-ci | Téléchargement | |---|---|---|---| | **Qwen3-4B-Instruct-2507** (recommandé par défaut) | ~2,5 Go | Le plus récent de la liste, très bon suivi d'instructions (utile pour la sortie JSON du choix de lien) et bon en français, reste rapide sur CPU | [lien direct](https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_M.gguf) · [page du modèle](https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF) | | **Qwen2.5-3B-Instruct** (le plus léger) | ~1,9 Go | Le plus petit/rapide de la liste, correct en français, suffisant pour dépanner sur une machine modeste | [lien direct](https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF/resolve/main/Qwen2.5-3B-Instruct-Q4_K_M.gguf) · [page du modèle](https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF) | | **Mistral-7B-Instruct-v0.3** (meilleur français) | ~4,4 Go | Modèle Mistral AI (français), référence historique en qualité de français ; demande plus de RAM/CPU | [lien direct](https://huggingface.co/bartowski/Mistral-7B-Instruct-v0.3-GGUF/resolve/main/Mistral-7B-Instruct-v0.3-Q4_K_M.gguf) · [page du modèle](https://huggingface.co/bartowski/Mistral-7B-Instruct-v0.3-GGUF) | | **Llama-3.2-3B-Instruct** (alternative légère) | ~2,0 Go | Généraliste léger, bonne alternative si Qwen ne convient pas | [lien direct](https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_K_M.gguf) · [page du modèle](https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF) | | **Qwen2.5-7B-Instruct** (meilleure qualité) | ~4,7 Go | Si ≥ 8 Go de RAM disponibles et qu'on veut la meilleure qualité d'extraction | [lien direct](https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf) · [page du modèle](https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF) | Pour une machine avec peu de RAM, préférer les modèles 3-4B. Pour plus de qualité (et si le CPU/la RAM suit), les 7B donnent de meilleurs résultats sur l'extraction de contenu comme sur le choix du lien suivant. ## Structure du projet ``` pdfscrapper/ main.py # point d'entrée (QApplication) main_window.py # fenêtre principale : navigateur + panneau latéral link_detector.py # heuristiques JS de détection des liens "suivant" block_picker.py # heuristique JS de surlignage/capture de bloc au survol bridge.py # pont QWebChannel (JS -> Python) pour la capture de bloc clip_table.py # tableau des extraits (panneau latéral) : colonnes Heure/Titre/Aperçu/Lien pdf_style.py # dataclass PdfStyle (réglages de mise en forme du PDF) pdf_builder.py # génération du PDF (reportlab) : style, fond, sommaire/liens preview_window.py # fenêtre d'aperçu en direct (QtPdf), style et correction du texte ai_config.py # dataclass AiConfig, persistée via QSettings ai_settings_dialog.py # fenêtre de configuration du modèle IA local (.gguf) ai_worker.py # exécution des appels IA sur un thread dédié (non bloquant) local_ai.py # inférence llama-cpp-python : extraction + choix du lien suivant auto_scrape_dialog.py # fenêtre de suivi du scraping auto (journal + progression) models.py # dataclass Clip (un extrait capturé) + regroupement en chapitres project_io.py # sauvegarde/chargement de projet (.pdfscrap) + pointeur de session run.py # lance l'application ``` ## Pistes d'évolution - Réglage des motifs de détection de liens depuis l'interface. - Export additionnel en `.txt` / `.epub`.