DEV Community

Mate Technologies
Mate Technologies

Posted on

๐ŸŽฌ Build a Relax Video Generator (Images + MP3 MP4) with Python & Tkinter

In this tutorial, weโ€™ll build a desktop GUI app that turns a folder of images and an MP3 file into a long relaxing MP4 video using FFmpeg.

This is perfect for:

Relax / meditation videos

YouTube ambience content

Learning GUI + FFmpeg automation in Python

Weโ€™ll go step by step, assuming youโ€™re a beginner.

๐Ÿงฐ What Weโ€™ll Use

Python

Tkinter (GUI)

ttkbootstrap (modern UI theme)

Pillow (PIL) for image previews

FFmpeg for video rendering

Threading (so the UI doesnโ€™t freeze)

๐Ÿ“ฆ Step 1: Install Requirements
pip install ttkbootstrap pillow

Make sure FFmpeg is installed and note its path:

C:\ffmpeg\bin\ffmpeg.exe

๐ŸชŸ Step 2: Create the Main App Window

We start by importing everything and creating the main window.

import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as tb
from PIL import Image, ImageTk
import subprocess
import os
import threading
import time
import re
Enter fullscreen mode Exit fullscreen mode

Now define the app window:

app = tb.Window(
    title="Relax Video Builder โ€“ Images + MP3 to MP4",
    themename="superhero",
    size=(950, 650),
    resizable=(False, False)
)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘‰ ttkbootstrap gives us modern styling with almost no extra work.

๐Ÿง  Step 3: App State Variables

These variables store selected files and app state.

image_files = []
mp3_path = tk.StringVar()
output_path = tk.StringVar()
hours_var = tk.IntVar(value=10)
Enter fullscreen mode Exit fullscreen mode

Rendering state:

process = None
rendering = False
total_seconds = 0

Enter fullscreen mode Exit fullscreen mode

FFmpeg path (โš ๏ธ change this):

FFMPEG_PATH = r"C:\ffmpeg\bin\ffmpeg.exe"

๐Ÿ–ผ Step 4: Selecting and Managing Images
Select images
def select_images():
    files = filedialog.askopenfilenames(
        filetypes=[("Images", "*.jpg *.png")]
    )
    if files:
        image_files.extend(files)
        refresh_images()
Enter fullscreen mode Exit fullscreen mode

Refresh image list

def refresh_images():
    image_listbox.delete(0, tk.END)
    for img in image_files:
        image_listbox.insert(tk.END, os.path.basename(img))
    image_count_label.config(text=f"{len(image_files)} image(s) selected")
Enter fullscreen mode Exit fullscreen mode

Remove images

def remove_selected_images():
    sel = image_listbox.curselection()
    for i in reversed(sel):
        del image_files[i]
    refresh_images()

def remove_all_images():
    image_files.clear()
    refresh_images()
    preview_label.config(image="")
Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘€ Step 5: Image Preview on Click

When you click an image, we show a preview.

def on_image_select(event):
    sel = image_listbox.curselection()
    if not sel:
        return

    img = Image.open(image_files[sel[0]])
    img.thumbnail((350, 250))
    tk_img = ImageTk.PhotoImage(img)

    preview_label.config(image=tk_img)
    preview_label.image = tk_img
Enter fullscreen mode Exit fullscreen mode

๐ŸŽต Step 6: MP3 Selection

def select_mp3():
    mp3 = filedialog.askopenfilename(
        filetypes=[("MP3", "*.mp3")]
    )
    if mp3:
        mp3_path.set(mp3)

def remove_mp3():
    mp3_path.set("")
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ Step 7: Output File Selection

def select_output():
    out = filedialog.asksaveasfilename(
        defaultextension=".mp4",
        filetypes=[("MP4", "*.mp4")]
    )
    if out:
        output_path.set(out)
Enter fullscreen mode Exit fullscreen mode

โ–ถ๏ธ Step 8: Start / Stop Rendering
Start button logic

def build_video():
    if rendering:
        return

    if not image_files or not mp3_path.get() or not output_path.get():
        messagebox.showerror("Error", "Missing images, MP3, or output file.")
        return

    threading.Thread(
        target=run_ffmpeg,
        daemon=True
    ).start()
Enter fullscreen mode Exit fullscreen mode

Stop button

def stop_video():
    global process, rendering
    if process:
        process.terminate()
        process = None
        rendering = False
        status_label.config(text="Rendering stopped.")
        resume_btn.config(state="normal")
Enter fullscreen mode Exit fullscreen mode

๐ŸŽž Step 9: FFmpeg Rendering Logic
Calculate duration per image

total_seconds = hours_var.get() * 3600
seconds_per_image = total_seconds / len(image_files)
Enter fullscreen mode Exit fullscreen mode

Create FFmpeg image list

list_file = "images.txt"
with open(list_file, "w", encoding="utf-8") as f:
    for img in image_files:
        f.write(f"file '{img}'\n")
        f.write(f"duration {seconds_per_image}\n")
    f.write(f"file '{image_files[-1]}'\n")
Enter fullscreen mode Exit fullscreen mode

FFmpeg command

cmd = [
    FFMPEG_PATH, "-y",
    "-stream_loop", "-1",
    "-i", mp3_path.get(),
    "-f", "concat", "-safe", "0",
    "-i", list_file,
    "-t", str(total_seconds),
    "-vf", "scale=1920:1080",
    "-c:v", "libx264",
    "-pix_fmt", "yuv420p",
    "-preset", "slow",
    "-crf", "18",
    "-c:a", "aac",
    "-b:a", "192k",
    output_path.get()
]
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Š Step 10: Progress Bar Tracking

We parse FFmpegโ€™s output to calculate progress.

time_pattern = re.compile(r"time=(\d+):(\d+):(\d+)")

for line in process.stderr:
    match = time_pattern.search(line)
    if match:
        h, m, s = map(int, match.groups())
        current = h * 3600 + m * 60 + s
        percent = (current / total_seconds) * 100

        progress_bar['value'] = percent
        status_label.config(
            text=f"Rendering... {int(percent)}%"
        )
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฑ Step 11: Build the UI Layout
Main container

main = tb.Frame(app, padding=15)
main.pack(fill="both", expand=True)

Left panel (images)
left = tb.Labelframe(main, text="Images", padding=10)
left.pack(side="left", fill="y")

Center preview
center = tb.Labelframe(main, text="Preview", padding=10)
center.pack(side="left", fill="both", expand=True)

Right settings panel
right = tb.Labelframe(main, text="Audio & Settings", padding=10)
right.pack(side="right", fill="y")
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Step 12: Run the App

app.mainloop()
Enter fullscreen mode Exit fullscreen mode

โœ… Final Result

You now have a fully working desktop app that:

Combines images + MP3

Builds long relaxing videos

Shows progress in real time

Uses a modern UI

Can be stopped and restarted safely

๐Ÿ’ก Ideas to Extend This

Add fade-in/out transitions

Randomize image order

Add text overlays

Remember last-used folders

Export presets for YouTube

Relax Video Builder โ€“ Images + MP3 to MP4

Top comments (0)