Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- Dockerfile +19 -0
- README.md +49 -12
- package.json +19 -0
- public/index.html +48 -0
- public/script.js +234 -0
- public/style.css +94 -0
- server.js +75 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package files
|
| 6 |
+
COPY package.json ./
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy application files
|
| 12 |
+
COPY server.js ./
|
| 13 |
+
COPY public ./public
|
| 14 |
+
|
| 15 |
+
# Expose the port
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
# Start the server
|
| 19 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,12 +1,49 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Résumé Vidéo YouTube
|
| 2 |
+
|
| 3 |
+
Cette application web permet de résumer automatiquement le contenu d'une vidéo YouTube à partir de sa transcription.
|
| 4 |
+
|
| 5 |
+
## Fonctionnalités
|
| 6 |
+
|
| 7 |
+
- Extraction de la transcription de vidéos YouTube
|
| 8 |
+
- Résumé automatique par analyse statistique du texte
|
| 9 |
+
- Interface simple et réactive
|
| 10 |
+
|
| 11 |
+
## Comment utiliser
|
| 12 |
+
|
| 13 |
+
1. Entrez l'URL d'une vidéo YouTube dans le champ prévu
|
| 14 |
+
2. Sélectionnez le nombre de phrases souhaitées pour le résumé
|
| 15 |
+
3. Cliquez sur "Résumer la Vidéo"
|
| 16 |
+
4. Consultez la transcription complète et le résumé généré
|
| 17 |
+
|
| 18 |
+
## Limitations
|
| 19 |
+
|
| 20 |
+
- Fonctionne uniquement avec les vidéos YouTube disposant d'une transcription (activée par le créateur ou générée automatiquement par YouTube)
|
| 21 |
+
- L'algorithme de résumé est basique (sélection statistique des phrases importantes)
|
| 22 |
+
- Supporte principalement les textes en français
|
| 23 |
+
|
| 24 |
+
## Technique
|
| 25 |
+
|
| 26 |
+
- Frontend: HTML, CSS, JavaScript vanilla
|
| 27 |
+
- Backend: Node.js avec Express
|
| 28 |
+
- Conteneurisation: Docker
|
| 29 |
+
- Déployé sur Hugging Face Spaces
|
| 30 |
+
|
| 31 |
+
## Local Development
|
| 32 |
+
|
| 33 |
+
Pour exécuter l'application localement:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# Cloner le dépôt
|
| 37 |
+
git clone [URL_DU_REPO]
|
| 38 |
+
|
| 39 |
+
# Aller dans le répertoire
|
| 40 |
+
cd huggingface
|
| 41 |
+
|
| 42 |
+
# Installer les dépendances
|
| 43 |
+
npm install
|
| 44 |
+
|
| 45 |
+
# Démarrer le serveur
|
| 46 |
+
npm start
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
L'application sera disponible sur http://localhost:7860
|
package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "resume-video-youtube",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Application pour résumer des vidéos YouTube à partir de leur transcription",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"start": "node server.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"express": "^4.18.2",
|
| 12 |
+
"youtube-transcript": "^1.0.6"
|
| 13 |
+
},
|
| 14 |
+
"engines": {
|
| 15 |
+
"node": ">=18.0.0"
|
| 16 |
+
},
|
| 17 |
+
"author": "",
|
| 18 |
+
"license": "MIT"
|
| 19 |
+
}
|
public/index.html
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Résumé Vidéo YouTube</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
</head>
|
| 10 |
+
|
| 11 |
+
<body>
|
| 12 |
+
<div class="container">
|
| 13 |
+
<h1>Résumé de Vidéo YouTube</h1>
|
| 14 |
+
<p>
|
| 15 |
+
Cette application utilise un algorithme JavaScript exécuté dans votre navigateur
|
| 16 |
+
pour résumer le contenu textuel (transcription) d'une vidéo YouTube.
|
| 17 |
+
</p>
|
| 18 |
+
|
| 19 |
+
<label for="youtube-url">URL de la vidéo YouTube :</label>
|
| 20 |
+
<input type="text" id="youtube-url" placeholder="Ex: https://www.youtube.com/watch?v=dQw4w9WgXcQ">
|
| 21 |
+
|
| 22 |
+
<label for="summary-length">Nombre de phrases pour le résumé :</label>
|
| 23 |
+
<input type="number" id="summary-length" value="5" min="1" max="20">
|
| 24 |
+
|
| 25 |
+
<button id="summarize-btn">Résumer la Vidéo</button>
|
| 26 |
+
|
| 27 |
+
<div id="loading" style="display:none;">Chargement de la transcription et résumé en cours...</div>
|
| 28 |
+
<div id="error-message" class="error" style="display:none;"></div>
|
| 29 |
+
|
| 30 |
+
<h2>Transcription Complète :</h2>
|
| 31 |
+
<div id="transcript-output" class="output-box">
|
| 32 |
+
<p>La transcription apparaîtra ici...</p>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<h2>Résumé :</h2>
|
| 36 |
+
<div id="summary-output" class="output-box">
|
| 37 |
+
<p>Le résumé apparaîtra ici...</p>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<footer>
|
| 41 |
+
<p>Déployé sur <a href="https://huggingface.co/spaces" target="_blank">Hugging Face Spaces</a></p>
|
| 42 |
+
</footer>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<script src="script.js"></script>
|
| 46 |
+
</body>
|
| 47 |
+
|
| 48 |
+
</html>
|
public/script.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const urlInput = document.getElementById('youtube-url');
|
| 3 |
+
const summaryLengthInput = document.getElementById('summary-length');
|
| 4 |
+
const summarizeBtn = document.getElementById('summarize-btn');
|
| 5 |
+
const transcriptOutput = document.getElementById('transcript-output');
|
| 6 |
+
const summaryOutput = document.getElementById('summary-output');
|
| 7 |
+
const loadingDiv = document.getElementById('loading');
|
| 8 |
+
const errorDiv = document.getElementById('error-message');
|
| 9 |
+
|
| 10 |
+
// Liste de mots vides (français, à étendre)
|
| 11 |
+
// Source simple, pour une meilleure qualité, utiliser une liste plus complète
|
| 12 |
+
const STOP_WORDS_FR = new Set([
|
| 13 |
+
"a", "afin", "ah", "ai", "aie", "aient", "aies", "ailleurs", "ainsi", "ait",
|
| 14 |
+
"alors", "au", "aucun", "aucune", "aujourd", "aujourd'hui", "auquel", "aura",
|
| 15 |
+
"aurai", "auraient", "aurais", "aurait", "auras", "aurez", "auriez", "aurions",
|
| 16 |
+
"aurons", "auront", "aussi", "autre", "autres", "autrui", "aux", "auxquelles",
|
| 17 |
+
"auxquels", "avaient", "avais", "avait", "avant", "avec", "avez", "aviez",
|
| 18 |
+
"avions", "avoir", "avons", "ayant", "ayez", "ayons", "b", "bah", "beaucoup",
|
| 19 |
+
"bien", "bon", "c", "ça", "car", "ce", "ceci", "cela", "celle", "celle-ci",
|
| 20 |
+
"celle-là", "celles", "celles-ci", "celles-là", "celui", "celui-ci", "celui-là",
|
| 21 |
+
"cent", "cents", "cependant", "certain", "certaine", "certaines", "certains",
|
| 22 |
+
"ces", "cet", "cette", "ceux", "ceux-ci", "ceux-là", "chacun", "chacune",
|
| 23 |
+
"chaque", "cher", "chère", "chères", "chers", "chez", "ci", "cinq", "cinquante",
|
| 24 |
+
"cinquième", "comme", "comment", "d", "dans", "de", "debout", "dedans", "dehors",
|
| 25 |
+
"delà", "depuis", "dernier", "dernière", "dernières", "derniers", "des", "dès",
|
| 26 |
+
"deux", "deuxième", "devant", "devers", "devra", "devrai", "devraient", "devrais",
|
| 27 |
+
"devrait", "devras", "devrez", "devriez", "devrions", "devrons", "devront", " διαφορετικα ",
|
| 28 |
+
"dix", "dix-huit", "dix-neuf", "dix-sept", "dixième", "donc", "dont", "douze",
|
| 29 |
+
"douzième", "du", "dû", "duquel", "durant", "e", "eh", "elle", "elle-même",
|
| 30 |
+
"elles", "elles-mêmes", "en", "encore", "enfin", "entre", "envers", "environ",
|
| 31 |
+
"es", "ès", "est", "et", "etant", "étaient", "étais", "était", "étant", "etc",
|
| 32 |
+
"été", "êtes", "être", "eu", "eue", "eues", "eurent", "eus", "eusse", "eussent",
|
| 33 |
+
"eusses", "eussiez", "eussions", "eut", "eût", "eûmes", "eûtes", "eux", "eux-mêmes",
|
| 34 |
+
"f", "faire", "fais", "faisaient", "faisais", "faisait", "faisant", "faisons",
|
| 35 |
+
"fait", "faites", "faudra", "faudrait", "faut", "fi", "flac", "floc", "fois",
|
| 36 |
+
"font", "force", "fors", "g", "gens", "h", "ha", "hé", "hein", "hélas", "hem",
|
| 37 |
+
"hep", "hi", "ho", "holà", "hop", "hormis", "hors", "hou", "houp", "hue", "hui",
|
| 38 |
+
"huit", "huitième", "hum", "hurrah", "i", "ici", "il", "ils", "j", "j'", "je",
|
| 39 |
+
"jusqu'", "jusqu'au", "jusqu'aux", "jusqu'à", "jusque", "k", "l", "la", "là",
|
| 40 |
+
"laquelle", "le", "lequel", "les", "lès", "lesquelles", "lesquels", "leur",
|
| 41 |
+
"leurs", "lez", "loin", "longtemps", "lors", "lorsque", "lui", "lui-même", "m",
|
| 42 |
+
"ma", "maint", "maintenant", "mais", "malgré", "me", "même", "mêmes", "merci",
|
| 43 |
+
"mes", "mien", "mienne", "miennes", "miens", "mille", "mince", "mine", "moi",
|
| 44 |
+
"moi-même", "moins", "mon", "mot", "moyennant", "n", "na", "ne", "néanmoins",
|
| 45 |
+
"neuf", "neuvième", "ni", "nombreuses", "nombreux", "non", "nos", "notre",
|
| 46 |
+
"nôtre", "nôtres", "nous", "nous-mêmes", "nul", "o", "ô", "où", "oui", "on",
|
| 47 |
+
"ont", "onze", "onzième", "or", "ou", "où", "outre", "p", "par", "parce", "parmi",
|
| 48 |
+
"partant", "particulièrement", "pas", "passé", "pendant", "personne", "peu",
|
| 49 |
+
"peut", "peuvent", "peux", "pf", "pff", "pfi", "pfu", "pif", "plein", "plus",
|
| 50 |
+
"plusieurs", "plutôt", "pour", "pourquoi", "pourra", "pourrai", "pourraient",
|
| 51 |
+
"pourrais", "pourrait", "pourras", "pourrez", "pourriez", "pourrions", "pourrons",
|
| 52 |
+
"pourront", "pouvait", "pouvez", " pouvions ", "pouvons", "premier", "première",
|
| 53 |
+
"premièrement", "près", "presque", "prouf", "psitt", "pu", "puis", "puisque",
|
| 54 |
+
"q", "qu'", "quand", "quant", "quanta", "quant-à-soi", "quarante", "quatorze",
|
| 55 |
+
"quatre", "quatre-vingt", "quatre-vingt-dix", "quatre-vingt-onze", "quatre-vingt-un",
|
| 56 |
+
"quatrième", "quatrièmement", "que", "quel", "quelconque", "quelle", "quelles",
|
| 57 |
+
"quelqu'un", "quelque", "quelques", "quels", "qui", "quiconque", "quinze",
|
| 58 |
+
"quoi", "quoique", "r", "revoici", "revoilà", "rien", "s", "sa", "sacrebleu",
|
| 59 |
+
"sans", "sapristi", "sauf", "se", "seize", "selon", "sept", "septième", "sera",
|
| 60 |
+
"serai", "seraient", "serais", "serait", "seras", "serez", "seriez", "serions",
|
| 61 |
+
"serons", "seront", "ses", "seulement", "si", "sien", "sienne", "siennes",
|
| 62 |
+
"siens", "sinon", "six", "sixième", "soi", "soi-même", "soient", "sois", "soit",
|
| 63 |
+
"soixante", "sommes", "son", "sont", "sous", "stop", "suis", "suite", "sur",
|
| 64 |
+
"surtout", "t", "ta", "tac", "tandis", "tant", "tardivement", "te", "tel",
|
| 65 |
+
"telle", "telles", "tels", "tenant", "tes", "tic", "tien", "tienne", "tiennes",
|
| 66 |
+
"tiens", "toc", "toi", "toi-même", "ton", "tos", "tôt", "toute", "toutefois",
|
| 67 |
+
"toutes", "tous", "tout", "treize", "trente", "très", "trois", "troisième",
|
| 68 |
+
"troisièmement", "trop", "tu", "u", "un", "une", "unes", "uns", "v", "va", "vais",
|
| 69 |
+
"valeur", "vas", "vé", "vers", "via", "vif", "vifs", "vingt", "vivat", "vive",
|
| 70 |
+
"vives", "voici", "voilà", "vont", "vos", "votre", "vôtre", "vôtres", "vous",
|
| 71 |
+
"vous-mêmes", "vs", "vu", "w", "x", "y", "z", "zut", "alors", "au", "aucuns", "aussi",
|
| 72 |
+
"autre", "avant", "avec", "avoir", "bon", "car", "ce", "cela", "ces", "ceux",
|
| 73 |
+
"chaque", "ci", "comme", "comment", "dans", "des", "du", "dedans", "dehors",
|
| 74 |
+
"depuis", "deux", "devrait", "doit", "donc", "dos", "droite", "début", "elle",
|
| 75 |
+
"elles", "en", "encore", "essai", "est", "et", "eu", "fait", "faites", "fois",
|
| 76 |
+
"font", "force", "haut", "hors", "ici", "il", "ils", "je", "juste", "la", "le",
|
| 77 |
+
"les", "leur", "là", "ma", "maintenant", "mais", "mes", "mine", "moins", "mon",
|
| 78 |
+
"mot", "même", "ni", "nommés", "notre", "nous", "nouveaux", "ou", "où", "par",
|
| 79 |
+
"parce", "parole", "pas", "personnes", "peut", "peu", "pièce", "plupart", "pour",
|
| 80 |
+
"pourquoi", "quand", "que", "quel", "quelle", "quelles", "quels", "qui", "sa",
|
| 81 |
+
"sans", "ses", "seulement", "si", "sien", "son", "sont", "sous", "soyez", "sujet",
|
| 82 |
+
"sur", "ta", "tandis", "tellement", "tels", "tes", "ton", "tous", "tout", "trop",
|
| 83 |
+
"très", "tu", "valeur", "voie", "voient", "vont", "vos", "votre", "vous", "vu",
|
| 84 |
+
"ça", "étaient", "état", "étions", "été", "être", "pour", "que", "qui", "il", "elle", "on", "y"
|
| 85 |
+
]);
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
summarizeBtn.addEventListener('click', async () => {
|
| 89 |
+
const url = urlInput.value.trim();
|
| 90 |
+
const numSentences = parseInt(summaryLengthInput.value, 10);
|
| 91 |
+
|
| 92 |
+
if (!url) {
|
| 93 |
+
showError("Veuillez entrer une URL YouTube.");
|
| 94 |
+
return;
|
| 95 |
+
}
|
| 96 |
+
if (isNaN(numSentences) || numSentences < 1) {
|
| 97 |
+
showError("Veuillez entrer un nombre de phrases valide pour le résumé.");
|
| 98 |
+
return;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
showLoading(true);
|
| 102 |
+
hideError();
|
| 103 |
+
transcriptOutput.innerHTML = "<p>Récupération de la transcription...</p>";
|
| 104 |
+
summaryOutput.innerHTML = "<p>En attente de la transcription...</p>";
|
| 105 |
+
|
| 106 |
+
try {
|
| 107 |
+
const response = await fetch(`/get-transcript?url=${encodeURIComponent(url)}`);
|
| 108 |
+
const data = await response.json();
|
| 109 |
+
|
| 110 |
+
if (!response.ok) {
|
| 111 |
+
throw new Error(data.error || `Erreur HTTP: ${response.status}`);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const fullTranscript = data.transcript;
|
| 115 |
+
if (!fullTranscript || fullTranscript.trim() === "") {
|
| 116 |
+
showError("La transcription est vide ou n'a pas pu être récupérée.");
|
| 117 |
+
transcriptOutput.innerHTML = "<p>Aucune transcription reçue.</p>";
|
| 118 |
+
summaryOutput.innerHTML = "<p>Impossible de résumer sans transcription.</p>";
|
| 119 |
+
showLoading(false);
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
transcriptOutput.textContent = fullTranscript;
|
| 124 |
+
|
| 125 |
+
// "IA" locale : résumé
|
| 126 |
+
const summary = await summarizeText(fullTranscript, numSentences);
|
| 127 |
+
summaryOutput.textContent = summary;
|
| 128 |
+
|
| 129 |
+
} catch (err) {
|
| 130 |
+
console.error("Erreur dans le processus :", err);
|
| 131 |
+
showError(`Erreur: ${err.message}`);
|
| 132 |
+
transcriptOutput.innerHTML = "<p>Échec de la récupération.</p>";
|
| 133 |
+
summaryOutput.innerHTML = "<p>Échec du résumé.</p>";
|
| 134 |
+
} finally {
|
| 135 |
+
showLoading(false);
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
function showLoading(isLoading) {
|
| 140 |
+
loadingDiv.style.display = isLoading ? 'block' : 'none';
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function showError(message) {
|
| 144 |
+
errorDiv.textContent = message;
|
| 145 |
+
errorDiv.style.display = 'block';
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function hideError() {
|
| 149 |
+
errorDiv.style.display = 'none';
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// --- Fonctions de résumé (l'"IA" locale) ---
|
| 153 |
+
|
| 154 |
+
function tokenize(text) {
|
| 155 |
+
// Simpliste : enlève la ponctuation basique et met en minuscule
|
| 156 |
+
return text.toLowerCase().replace(/[^\w\s'-]|_/g, "").replace(/\s+/g, " ").trim().split(' ');
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function getSentences(text) {
|
| 160 |
+
// Segmentation basique par point, point d'interrogation, point d'exclamation.
|
| 161 |
+
// Peut être amélioré avec des expressions régulières plus complexes.
|
| 162 |
+
// Nettoie les timestamps typiques comme [00:00:00] ou (00:00:00)
|
| 163 |
+
const cleanedText = text.replace(/\[\d{2}:\d{2}:\d{2}\]/g, '')
|
| 164 |
+
.replace(/\(\d{2}:\d{2}:\d{2}\)/g, '');
|
| 165 |
+
return cleanedText.split(/[.!?]+\s*/).filter(s => s.trim().length > 0).map(s => s.trim());
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function calculateWordFrequencies(words) {
|
| 169 |
+
const freq = {};
|
| 170 |
+
words.forEach(word => {
|
| 171 |
+
if (!STOP_WORDS_FR.has(word) && word.length > 2) { // Ignore les mots vides et très courts
|
| 172 |
+
freq[word] = (freq[word] || 0) + 1;
|
| 173 |
+
}
|
| 174 |
+
});
|
| 175 |
+
return freq;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
function scoreSentences(sentences, wordFrequencies) {
|
| 179 |
+
const sentenceScores = [];
|
| 180 |
+
sentences.forEach((sentence, index) => {
|
| 181 |
+
const wordsInSentence = tokenize(sentence);
|
| 182 |
+
let score = 0;
|
| 183 |
+
wordsInSentence.forEach(word => {
|
| 184 |
+
if (wordFrequencies[word]) {
|
| 185 |
+
score += wordFrequencies[word];
|
| 186 |
+
}
|
| 187 |
+
});
|
| 188 |
+
// On pourrait ajouter un bonus pour la position (ex: premières phrases)
|
| 189 |
+
// if (index < 3) score *= 1.2; // Bonus pour les 3 premières phrases
|
| 190 |
+
sentenceScores.push({ sentence: sentence, score: score, index: index });
|
| 191 |
+
});
|
| 192 |
+
return sentenceScores;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
async function summarizeText(text, numSentences) {
|
| 196 |
+
if (!text || text.trim() === "") return "Le texte fourni est vide.";
|
| 197 |
+
|
| 198 |
+
try {
|
| 199 |
+
// 1. Découper le texte en phrases
|
| 200 |
+
const sentences = getSentences(text);
|
| 201 |
+
|
| 202 |
+
if (sentences.length <= numSentences) {
|
| 203 |
+
return text; // Si le texte est déjà plus court que le résumé demandé, retourner tout
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// 2. Tokeniser le texte pour l'analyse des fréquences
|
| 207 |
+
const words = tokenize(text);
|
| 208 |
+
|
| 209 |
+
// 3. Calculer les fréquences des mots (sauf mots vides)
|
| 210 |
+
const wordFrequencies = calculateWordFrequencies(words);
|
| 211 |
+
|
| 212 |
+
// 4. Calculer un score pour chaque phrase basé sur les mots importants
|
| 213 |
+
const sentenceScores = scoreSentences(sentences, wordFrequencies);
|
| 214 |
+
|
| 215 |
+
// 5. Trier les phrases par score (importance) décroissant
|
| 216 |
+
sentenceScores.sort((a, b) => b.score - a.score);
|
| 217 |
+
|
| 218 |
+
// 6. Prendre les N phrases les plus importantes
|
| 219 |
+
const topSentences = sentenceScores.slice(0, numSentences);
|
| 220 |
+
|
| 221 |
+
// 7. Retrier les phrases selon leur position originale dans le texte
|
| 222 |
+
topSentences.sort((a, b) => a.index - b.index);
|
| 223 |
+
|
| 224 |
+
// 8. Former le résumé final
|
| 225 |
+
const summary = topSentences.map(item => item.sentence).join('. ');
|
| 226 |
+
|
| 227 |
+
return summary + '.';
|
| 228 |
+
|
| 229 |
+
} catch (error) {
|
| 230 |
+
console.error("Erreur lors de la création du résumé:", error);
|
| 231 |
+
return "Une erreur s'est produite lors de la création du résumé.";
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
});
|
public/style.css
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: sans-serif;
|
| 3 |
+
line-height: 1.6;
|
| 4 |
+
margin: 0;
|
| 5 |
+
padding: 20px;
|
| 6 |
+
background-color: #f4f4f4;
|
| 7 |
+
color: #333;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.container {
|
| 11 |
+
max-width: 800px;
|
| 12 |
+
margin: auto;
|
| 13 |
+
background: #fff;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
border-radius: 8px;
|
| 16 |
+
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
h1, h2 {
|
| 20 |
+
color: #333;
|
| 21 |
+
text-align: center;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
label {
|
| 25 |
+
display: block;
|
| 26 |
+
margin-bottom: 5px;
|
| 27 |
+
font-weight: bold;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
input[type="text"], input[type="number"] {
|
| 31 |
+
width: calc(100% - 22px);
|
| 32 |
+
padding: 10px;
|
| 33 |
+
margin-bottom: 15px;
|
| 34 |
+
border: 1px solid #ddd;
|
| 35 |
+
border-radius: 4px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
button {
|
| 39 |
+
display: block;
|
| 40 |
+
width: 100%;
|
| 41 |
+
padding: 10px;
|
| 42 |
+
background-color: #5cb85c;
|
| 43 |
+
color: white;
|
| 44 |
+
border: none;
|
| 45 |
+
border-radius: 4px;
|
| 46 |
+
cursor: pointer;
|
| 47 |
+
font-size: 16px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
button:hover {
|
| 51 |
+
background-color: #4cae4c;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.output-box {
|
| 55 |
+
margin-top: 20px;
|
| 56 |
+
padding: 15px;
|
| 57 |
+
background-color: #e9e9e9;
|
| 58 |
+
border: 1px solid #ccc;
|
| 59 |
+
border-radius: 4px;
|
| 60 |
+
min-height: 100px;
|
| 61 |
+
white-space: pre-wrap; /* Conserve les sauts de ligne et espaces */
|
| 62 |
+
word-wrap: break-word;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.error {
|
| 66 |
+
color: red;
|
| 67 |
+
background-color: #ffe0e0;
|
| 68 |
+
border: 1px solid red;
|
| 69 |
+
padding: 10px;
|
| 70 |
+
margin-top: 15px;
|
| 71 |
+
border-radius: 4px;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
#loading {
|
| 75 |
+
text-align: center;
|
| 76 |
+
padding: 10px;
|
| 77 |
+
color: #555;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
footer {
|
| 81 |
+
margin-top: 20px;
|
| 82 |
+
text-align: center;
|
| 83 |
+
font-size: 0.9em;
|
| 84 |
+
color: #777;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
footer a {
|
| 88 |
+
color: #5cb85c;
|
| 89 |
+
text-decoration: none;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
footer a:hover {
|
| 93 |
+
text-decoration: underline;
|
| 94 |
+
}
|
server.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { YoutubeTranscript } from 'youtube-transcript';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
|
| 6 |
+
// Configuration pour __dirname avec ES Modules
|
| 7 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 8 |
+
const __dirname = path.dirname(__filename);
|
| 9 |
+
|
| 10 |
+
const app = express();
|
| 11 |
+
// Hugging Face Spaces utilise le port 7860 par défaut
|
| 12 |
+
const port = process.env.PORT || 7860;
|
| 13 |
+
|
| 14 |
+
// Servir les fichiers statiques du dossier 'public'
|
| 15 |
+
app.use(express.static(path.join(__dirname, 'public')));
|
| 16 |
+
|
| 17 |
+
app.get('/get-transcript', async (req, res) => {
|
| 18 |
+
const videoUrl = req.query.url;
|
| 19 |
+
if (!videoUrl) {
|
| 20 |
+
return res.status(400).json({ error: 'URL de la vidéo manquante' });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
// Essayer d'extraire l'ID de différentes formes d'URL YouTube
|
| 25 |
+
let videoId = '';
|
| 26 |
+
if (videoUrl.includes('v=')) {
|
| 27 |
+
videoId = videoUrl.split('v=')[1].split('&')[0];
|
| 28 |
+
} else if (videoUrl.includes('youtu.be/')) {
|
| 29 |
+
videoId = videoUrl.split('youtu.be/')[1].split('?')[0];
|
| 30 |
+
} else {
|
| 31 |
+
// On pourrait ajouter d'autres formats d'URL ici (shorts, etc.)
|
| 32 |
+
// Pour l'instant, on assume que c'est un ID direct si ce n'est pas une URL connue
|
| 33 |
+
videoId = videoUrl;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if (!videoId) {
|
| 37 |
+
return res.status(400).json({ error: "Impossible d'extraire l'ID de la vidéo depuis l'URL." });
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
console.log(`Fetching transcript for video ID: ${videoId}`);
|
| 41 |
+
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
| 42 |
+
|
| 43 |
+
if (!transcript || transcript.length === 0) {
|
| 44 |
+
return res.status(404).json({ error: 'Transcription non trouvée ou vide pour cette vidéo.' });
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Concaténer les textes de la transcription
|
| 48 |
+
const fullText = transcript.map(item => item.text).join(' ');
|
| 49 |
+
res.json({ transcript: fullText });
|
| 50 |
+
|
| 51 |
+
} catch (error) {
|
| 52 |
+
console.error('Erreur lors de la récupération de la transcription:', error);
|
| 53 |
+
if (error.message && error.message.includes('Could not find transcripts')) {
|
| 54 |
+
return res.status(404).json({ error: "Aucune transcription disponible pour cette vidéo (elles sont peut-être désactivées ou n'existent pas en auto-généré)." });
|
| 55 |
+
}
|
| 56 |
+
if (error.message && error.message.includes('is not a valid video ID')) {
|
| 57 |
+
return res.status(400).json({ error: `L'ID vidéo extrait ('${error.message.split("'")[1]}') n'est pas valide. Vérifiez l'URL.` });
|
| 58 |
+
}
|
| 59 |
+
res.status(500).json({ error: 'Erreur interne du serveur lors de la récupération de la transcription.' });
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
// Route pour vérifier l'état du serveur (utile pour Hugging Face)
|
| 64 |
+
app.get('/health', (req, res) => {
|
| 65 |
+
res.status(200).json({ status: 'ok' });
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Route par défaut
|
| 69 |
+
app.get('/', (req, res) => {
|
| 70 |
+
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
app.listen(port, '0.0.0.0', () => {
|
| 74 |
+
console.log(`Serveur démarré sur http://0.0.0.0:${port}`);
|
| 75 |
+
});
|