18 janvier 2026

Découvrez comment j’ai créé mon App nocode YouTube Transcriber & Cutter avec l’IA

Résumé express

Dans cette vidéo, je vous dévoile les coulisses de la création de mon application innovante qui transcrit et découpe des vidéos YouTube grâce à l’intelligence artificielle. Je vous partage mon parcours, mes outils favoris, et mes astuces pour automatiser un processus souvent chronophage. Si vous êtes passionné par le développement web, l’IA et le montage vidéo, cet article est fait pour vous !

Pourquoi j’ai créé cette application

L’idée m’est venue en cherchant à optimiser mon temps de travail lors de l’analyse de contenus vidéo. Extraire des segments précis d’une vidéo manuellement est fastidieux et source d’erreurs. J’ai donc décidé de mettre à contribution mes compétences en Intelligence Artificielle pour concevoir une solution automatisée.

Les outils utilisés pour développer l’application

Pour mener à bien ce projet, ChatGPT o3-mini-high a choisi un ensemble d’outils performants et complémentaires :

Les fonctionnalités clés de l’application

Transcription automatique

Découpe vidéo précise

Interface utilisateur optimisée

Étapes de développement et challenges rencontrés

Créer cette application n’a pas été un long fleuve tranquille. Voici quelques étapes clés et défis que j’ai dû relever :

  1. Conception de l’architecture : J’ai structuré l’application autour de Flask, en séparant clairement le backend de l’interface utilisateur. L’organisation du projet, avec notamment le fichier index.html dans un dossier dédié aux templates, a été primordiale pour la maintenabilité du code.
  2. Gestion des téléchargements et transcriptions : L’intégration de yt-dlp et de YouTubeTranscriptApi a nécessité une gestion fine des erreurs et une synchronisation précise entre le téléchargement de la vidéo et la récupération de ses métadonnées.
  3. Implémentation de la découpe vidéo : Utiliser FFmpeg pour découper les vidéos selon des repères textuels a demandé une manipulation précise des timestamps extraits du transcript, afin de gérer à la fois des coupes simples et des traitements complexes en batch.
  4. Amélioration de l’expérience utilisateur (UX) : J’ai mis un point d’honneur à concevoir une interface épurée avec des animations légères, pour rendre l’outil accessible même à ceux qui ne sont pas familiers avec la technologie.
  5. Tests et retours utilisateurs : Les phases de tests ont été essentielles pour affiner les fonctionnalités, corriger des bugs et intégrer des retours d’utilisateurs pour une interface encore plus fluide.

Impact et perspectives d’évolution

Depuis la mise en ligne, les retours ont été très positifs. Les utilisateurs apprécient la simplicité d’utilisation et la précision des découpes. Voici quelques axes d’amélioration envisagés pour l’avenir :

Mais pourquooooiiii ?

En créant cette application, j’ai voulu démontrer que l’innovation passe par l’automatisation et l’optimisation des tâches quotidiennes. Mon objectif est de rendre la manipulation de contenus vidéo plus accessible et efficace grâce à l’intelligence artificielle et aux outils modernes. Cette vidéo est une invitation à explorer ces technologies et à oser les intégrer dans vos projets, qu’ils soient personnels ou professionnels.

Si vous êtes intéressé par le développement d’outils innovants, je vous encourage à tester l’application, partager vos impressions et proposer des idées pour l’améliorer.

Les deux fichiers du projet

Avant d’utiliser cette application, assurez-vous d’avoir Python 3.6 (ou une version supérieure) installé. Ensuite, suivez ces instructions :

1. Installer les dépendances Python

Créez un fichier nommé requirements.txt avec le contenu suivant, puis copiez-collez ce contenu :

Flask
yt-dlp
youtube-transcript-api

Ensuite, ouvrez votre terminal et exécutez la commande suivante :

pip install -r requirements.txt

2. Installer FFmpeg

Selon votre système d’exploitation, copiez-collez l’une des commandes ci-dessous :

sudo apt update && sudo apt install ffmpeg
brew install ffmpeg

Copiez-collez ces instructions dans votre terminal pour préparer votre environnement avant de lancer l’application. Ensuite télécharger les deux fichiers dans votre dossier d’application.

import os
import re
import json
import subprocess
from flask import Flask, render_template, request
from youtube_transcript_api import YouTubeTranscriptApi

app = Flask(__name__)

def extract_video_id(url):
    """Extract the 11-character YouTube video ID from the URL."""
    pattern = r"(?:v=|\/)([a-zA-Z0-9_-]{11})"
    match = re.search(pattern, url)
    if match:
        return match.group(1)
    return None

def download_video(yt_url, output_file):
    """Download the YouTube video using yt-dlp."""
    command = ["yt-dlp", "-f", "best", "-o", output_file, yt_url]
    subprocess.run(command, check=True)

def get_video_length(yt_url):
    """Retrieve video length (in seconds) from yt-dlp metadata."""
    command = ["yt-dlp", "--dump-json", yt_url]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    data = json.loads(result.stdout)
    return data.get("duration", 0)

def cut_video(input_file, start_time, end_time, output_file):
    """Cut the video segment using FFmpeg from start_time to end_time."""
    duration = end_time - start_time
    command = [
        "ffmpeg",
        "-y",  # Overwrite output file if exists
        "-ss", str(start_time),
        "-i", input_file,
        "-t", str(duration),
        "-c", "copy",
        output_file
    ]
    subprocess.run(command, check=True)

def merge_videos(video_files, merged_filename):
    """Merge video segments using FFmpeg's concat demuxer."""
    list_filename = "segments_list.txt"
    with open(list_filename, "w") as f:
        for vf in video_files:
            f.write(f"file '{os.path.abspath(vf)}'\n")
    merge_cmd = [
        "ffmpeg",
        "-y",  # Overwrite output file if exists
        "-f", "concat",
        "-safe", "0",
        "-i", list_filename,
        "-c", "copy",
        merged_filename
    ]
    subprocess.run(merge_cmd, check=True)
    os.remove(list_filename)

@app.route("/", methods=["GET", "POST"])
def index():
    message = ""
    transcript_text = ""
    cut_results = []      # Messages for each extracted segment
    output_files = []     # List of extracted segment filenames

    if request.method == "POST":
        yt_url = request.form.get("yt_url")
        cut_method = request.form.get("cut_method")  # "between" or "around"
        trans_lang = request.form.get("trans_lang", "en").strip()  # Selected language
        merge_option = request.form.get("merge_segments")  # "on" if checked
        delete_option = request.form.get("delete_segments")  # "on" if checked
        video_id = extract_video_id(yt_url)

        if not video_id:
            message = "Invalid URL. Please enter a valid YouTube video URL."
        else:
            try:
                # Retrieve transcript using the selected language
                transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[trans_lang])
                transcript_text = "\n".join([f"[{seg['start']:.2f}] {seg['text']}" for seg in transcript])
                
                # Download the video and get its length
                video_filename = f"{video_id}.mp4"
                download_video(yt_url, video_filename)
                video_length = get_video_length(yt_url)
                
                if cut_method == "between":
                    # Option 1: Between Precise Keywords
                    keyword_pairs_input = request.form.get("keyword_pairs", "").strip()
                    if keyword_pairs_input:
                        pairs = []
                        for line in keyword_pairs_input.splitlines():
                            if "," in line:
                                start_kw, end_kw = line.split(",", 1)
                                pairs.append((start_kw.strip().lower(), end_kw.strip().lower()))
                    else:
                        start_kw = request.form.get("start_keyword", "").strip().lower()
                        end_kw = request.form.get("end_keyword", "").strip().lower()
                        if start_kw and end_kw:
                            pairs = [(start_kw, end_kw)]
                        else:
                            pairs = []
                    
                    if not pairs:
                        message = "Please provide at least one valid keyword pair."
                    else:
                        for idx, (start_keyword, end_keyword) in enumerate(pairs, start=1):
                            start_index = None
                            end_index = None
                            for i, seg in enumerate(transcript):
                                text_lower = seg['text'].lower()
                                if start_index is None and start_keyword in text_lower:
                                    start_index = i
                                if start_index is not None and end_keyword in text_lower:
                                    end_index = i
                                    break
                            if start_index is None:
                                cut_results.append(f"Pair {idx}: Start keyword '{start_keyword}' not found.")
                                continue
                            if end_index is None:
                                cut_results.append(f"Pair {idx}: End keyword '{end_keyword}' not found after start keyword.")
                                continue
                            
                            seg_start = transcript[start_index]['start']
                            seg_end = transcript[end_index]['start']
                            output_filename = f"{video_id}_between_pair_{idx}.mp4"
                            cut_video(video_filename, seg_start, seg_end, output_filename)
                            output_files.append(output_filename)
                            cut_results.append(f"Pair {idx}: Segment created: {output_filename} (from {seg_start:.2f}s to {seg_end:.2f}s)")
                        message = "Processing complete for 'Between Precise Keywords' method."
                        
                elif cut_method == "around":
                    # Option 2: Around KW with separate durations
                    around_keywords_input = request.form.get("around_keywords", "").strip()
                    d_before_str = request.form.get("duration_before", "").strip()
                    d_after_str = request.form.get("duration_after", "").strip()
                    process_all = request.form.get("process_all")  # "on" if checked
                    
                    if not around_keywords_input or not d_before_str or not d_after_str:
                        message = "Please provide at least one keyword, a 'duration before', and a 'duration after' (in seconds) for the 'Around KW' option."
                    else:
                        try:
                            d_before = float(d_before_str)
                            d_after = float(d_after_str)
                        except ValueError:
                            message = "Both durations must be numeric values (in seconds)."
                            return render_template("index.html", message=message, transcript=transcript_text, cut_results=cut_results)
                        
                        # Split keywords on commas (entered on one line)
                        keywords = [kw.strip().lower() for kw in around_keywords_input.split(",") if kw.strip()]
                        if not keywords:
                            message = "Please provide at least one valid keyword."
                        else:
                            for kw in keywords:
                                occurrences = []
                                for seg in transcript:
                                    if kw in seg['text'].lower():
                                        occurrences.append(seg['start'])
                                
                                if not occurrences:
                                    cut_results.append(f"Keyword '{kw}' not found in the transcript.")
                                    continue
                                
                                if process_all != "on":
                                    occurrences = occurrences[:1]
                                
                                for idx, kw_time in enumerate(occurrences, start=1):
                                    start_time = max(0, kw_time - d_before)
                                    end_time = min(video_length, kw_time + d_after)
                                    output_filename = f"{video_id}_around_{kw}_{idx}.mp4"
                                    cut_video(video_filename, start_time, end_time, output_filename)
                                    output_files.append(output_filename)
                                    cut_results.append(f"Around '{kw}' occurrence {idx}: Segment created: {output_filename} (from {start_time:.2f}s to {end_time:.2f}s)")
                            message = "Processing complete for 'Around KW' method."
                else:
                    message = "Please choose a cutting method."
                
                # If merge option is checked and there are output files, merge them.
                if merge_option == "on" and output_files:
                    merged_filename = f"{video_id}_merged.mp4"
                    merge_videos(output_files, merged_filename)
                    cut_results.append(f"Merged video created: {merged_filename}")
                    message += " All segments have been merged."
                    
                    # If delete option is also checked, remove all individual segments and downloaded video.
                    if delete_option == "on":
                        for file in output_files:
                            if os.path.exists(file):
                                os.remove(file)
                        if os.path.exists(video_filename):
                            os.remove(video_filename)
                        cut_results.append("All individual segment files and the downloaded video have been deleted.")
            except Exception as e:
                message = f"Error during processing: {e}"
    return render_template("index.html", message=message, transcript=transcript_text, cut_results=cut_results)

if __name__ == "__main__":
    app.run(debug=True)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>YouTube Transcriber & Cutter</title>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        /* Reset & Base Styles */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Roboto', sans-serif;
            background: linear-gradient(135deg, #eef2f3, #8e9eab);
            color: #333;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
            padding: 30px;
        }
        h1 {
            text-align: center;
            margin-bottom: 20px;
            font-size: 2.2em;
            font-weight: 700;
            color: #2c3e50;
        }
        h2 {
            margin-bottom: 10px;
            color: #2c3e50;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: 500;
            color: #2c3e50;
        }
        input[type="text"],
        input[type="number"],
        textarea,
        select {
            width: 100%;
            padding: 12px;
            margin-bottom: 20px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 1em;
        }
        input[type="radio"],
        input[type="checkbox"] {
            margin-right: 5px;
        }
        input[type="submit"] {
            width: 100%;
            padding: 15px;
            background: #3498db;
            color: #fff;
            border: none;
            font-size: 1.1em;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.3s ease;
        }
        input[type="submit"]:hover {
            background: #2980b9;
        }
        .result {
            background: #f7f7f7;
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 4px;
            margin-bottom: 20px;
        }
        .error {
            color: #e74c3c;
            margin-bottom: 20px;
            font-weight: 500;
        }
        p {
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>YouTube Transcriber & Cutter</h1>
        <form method="post">
            <label for="yt_url">Enter YouTube Video URL:</label>
            <input type="text" id="yt_url" name="yt_url" placeholder="https://www.youtube.com/watch?v=..." required>

            <label for="trans_lang">Select Transcription Language:</label>
            <select name="trans_lang" id="trans_lang">
                <option value="en">English</option>
                <option value="es">Spanish</option>
                <option value="zh">Chinese</option>
                <option value="hi">Hindi</option>
                <option value="ar">Arabic</option>
                <option value="fr">French</option>
                <option value="ru">Russian</option>
                <option value="pt">Portuguese</option>
                <option value="id">Indonesian</option>
                <option value="bn">Bengali</option>
            </select>

            <label>Select Cutting Method:</label>
            <input type="radio" id="between" name="cut_method" value="between" checked>
            <label for="between" style="display: inline;">Between Precise Keywords</label><br>
            <input type="radio" id="around" name="cut_method" value="around">
            <label for="around" style="display: inline;">Around KW</label>
            <br><br>
            
            <!-- Fields for "Between Precise Keywords" option -->
            <div id="between_fields">
                <p>You can either enter a single keyword pair:</p>
                <label for="start_keyword">Start Keyword:</label>
                <input type="text" id="start_keyword" name="start_keyword" placeholder="e.g., Introduction">
                <label for="end_keyword">End Keyword:</label>
                <input type="text" id="end_keyword" name="end_keyword" placeholder="e.g., Conclusion">
                <p>Or provide multiple keyword pairs (one pair per line in the format: start_keyword,end_keyword):</p>
                <textarea id="keyword_pairs" name="keyword_pairs" rows="4" placeholder="e.g., Introduction, Conclusion&#10;Overview, Summary"></textarea>
            </div>

            <!-- Fields for "Around KW" option -->
            <div id="around_fields" style="display:none;">
                <p>Enter one or more keywords on a single line (separated by commas):</p>
                <input type="text" id="around_keywords" name="around_keywords" placeholder="e.g., key insight, critical point">
                <label for="duration_before">Duration Before Keyword (in seconds):</label>
                <input type="number" id="duration_before" name="duration_before" placeholder="e.g., 5" step="0.1">
                <label for="duration_after">Duration After Keyword (in seconds):</label>
                <input type="number" id="duration_after" name="duration_after" placeholder="e.g., 10" step="0.1">
                <input type="checkbox" id="process_all" name="process_all">
                <label for="process_all" style="display: inline;">Process all keyword occurrences (if unchecked, only the first occurrence is processed)</label>
            </div>
            
            <!-- Merge option for both cutting methods -->
            <div>
                <input type="checkbox" id="merge_segments" name="merge_segments">
                <label for="merge_segments" style="display: inline;">Merge all extracted segments into one video file</label>
            </div>
            <br>
            <!-- Delete option (only applicable if merge is checked) -->
            <div>
                <input type="checkbox" id="delete_segments" name="delete_segments">
                <label for="delete_segments" style="display: inline;">Delete individual segments and downloaded video after merging</label>
            </div>
            <br>
            <input type="submit" value="Process Video">
        </form>

        {% if message %}
            <p><strong>{{ message }}</strong></p>
        {% endif %}

        {% if transcript %}
            <h2>Full Transcript:</h2>
            <div class="result">
                <textarea rows="15" readonly>{{ transcript }}</textarea>
            </div>
        {% endif %}

        {% if cut_results %}
            <h2>Video Cut Results:</h2>
            <div class="result">
                <ul>
                {% for result in cut_results %}
                    <li>{{ result }}</li>
                {% endfor %}
                </ul>
            </div>
        {% endif %}
    </div>

    <script>
        // Toggle form fields based on selected cutting method
        const betweenRadio = document.getElementById("between");
        const aroundRadio = document.getElementById("around");
        const betweenFields = document.getElementById("between_fields");
        const aroundFields = document.getElementById("around_fields");

        function toggleFields() {
            if (betweenRadio.checked) {
                betweenFields.style.display = "block";
                aroundFields.style.display = "none";
            } else if (aroundRadio.checked) {
                betweenFields.style.display = "none";
                aroundFields.style.display = "block";
            }
        }

        betweenRadio.addEventListener("change", toggleFields);
        aroundRadio.addEventListener("change", toggleFields);
        // Initialize on page load
        toggleFields();
    </script>
</body>
</html>
Laurent, AI Sherpa et créateur YouTube. Diplômé Audencia Business School et Master Sciences de l’Éducation, je propose un écosystème dont le but est de devenir un professionnel augmenté par l’IA, sans subir. Toujours professeur et père de famille expatrié, je partage mon parcours avec transparence pour vous aider à tirer le meilleur de ces nouveaux outils.
Laurent
Fondateur, MintAvocado

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *