Skip to content

Commit 9b531c0

Browse files
authored
Create relax_video_gui.py
1 parent 84e12a4 commit 9b531c0

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import tkinter as tk
2+
from tkinter import filedialog, messagebox
3+
import ttkbootstrap as tb
4+
from PIL import Image, ImageTk
5+
import subprocess
6+
import os
7+
import threading
8+
import time
9+
import re
10+
11+
# ================= APP =================
12+
app = tb.Window(
13+
title="Relax Video Builder – Images + MP3 to MP4",
14+
themename="superhero",
15+
size=(950, 650),
16+
resizable=(False, False)
17+
)
18+
19+
# ================= VARIABLES =================
20+
image_files = []
21+
mp3_path = tk.StringVar()
22+
output_path = tk.StringVar()
23+
hours_var = tk.IntVar(value=10)
24+
25+
process = None
26+
rendering = False
27+
total_seconds = 0
28+
29+
FFMPEG_PATH = r"C:\ffmpeg\bin\ffmpeg.exe" # CHANGE THIS
30+
31+
# ================= FUNCTIONS =================
32+
def select_images():
33+
files = filedialog.askopenfilenames(
34+
filetypes=[("Images", "*.jpg *.png")]
35+
)
36+
if files:
37+
image_files.extend(files)
38+
refresh_images()
39+
40+
def refresh_images():
41+
image_listbox.delete(0, tk.END)
42+
for img in image_files:
43+
image_listbox.insert(tk.END, os.path.basename(img))
44+
image_count_label.config(text=f"{len(image_files)} image(s) selected")
45+
46+
def remove_selected_images():
47+
sel = image_listbox.curselection()
48+
for i in reversed(sel):
49+
del image_files[i]
50+
refresh_images()
51+
52+
def remove_all_images():
53+
image_files.clear()
54+
refresh_images()
55+
preview_label.config(image="")
56+
57+
def on_image_select(event):
58+
sel = image_listbox.curselection()
59+
if not sel:
60+
return
61+
img = Image.open(image_files[sel[0]])
62+
img.thumbnail((350, 250))
63+
tk_img = ImageTk.PhotoImage(img)
64+
preview_label.config(image=tk_img)
65+
preview_label.image = tk_img
66+
67+
def select_mp3():
68+
mp3 = filedialog.askopenfilename(filetypes=[("MP3", "*.mp3")])
69+
if mp3:
70+
mp3_path.set(mp3)
71+
72+
def remove_mp3():
73+
mp3_path.set("")
74+
75+
def select_output():
76+
out = filedialog.asksaveasfilename(
77+
defaultextension=".mp4",
78+
filetypes=[("MP4", "*.mp4")]
79+
)
80+
if out:
81+
output_path.set(out)
82+
83+
def build_video():
84+
if rendering:
85+
return
86+
if not image_files or not mp3_path.get() or not output_path.get():
87+
messagebox.showerror("Error", "Missing images, MP3, or output file.")
88+
return
89+
90+
threading.Thread(target=run_ffmpeg, daemon=True).start()
91+
92+
def stop_video():
93+
global process, rendering
94+
if process:
95+
process.terminate()
96+
process = None
97+
rendering = False
98+
status_label.config(text="Rendering stopped.")
99+
resume_btn.config(state="normal")
100+
101+
def run_ffmpeg():
102+
global process, rendering, total_seconds
103+
rendering = True
104+
resume_btn.config(state="disabled")
105+
progress_bar['value'] = 0
106+
107+
total_seconds = hours_var.get() * 3600
108+
seconds_per_image = total_seconds / len(image_files)
109+
110+
list_file = "images.txt"
111+
with open(list_file, "w", encoding="utf-8") as f:
112+
for img in image_files:
113+
f.write(f"file '{img}'\n")
114+
f.write(f"duration {seconds_per_image}\n")
115+
f.write(f"file '{image_files[-1]}'\n")
116+
117+
cmd = [
118+
FFMPEG_PATH, "-y",
119+
"-stream_loop", "-1",
120+
"-i", mp3_path.get(),
121+
"-f", "concat", "-safe", "0",
122+
"-i", list_file,
123+
"-t", str(total_seconds),
124+
"-vf", "scale=1920:1080",
125+
"-c:v", "libx264",
126+
"-pix_fmt", "yuv420p",
127+
"-preset", "slow",
128+
"-crf", "18",
129+
"-c:a", "aac",
130+
"-b:a", "192k",
131+
output_path.get()
132+
]
133+
134+
process = subprocess.Popen(
135+
cmd,
136+
stderr=subprocess.PIPE,
137+
universal_newlines=True
138+
)
139+
140+
time_pattern = re.compile(r"time=(\d+):(\d+):(\d+)")
141+
142+
for line in process.stderr:
143+
match = time_pattern.search(line)
144+
if match:
145+
h, m, s = map(int, match.groups())
146+
current = h * 3600 + m * 60 + s
147+
percent = (current / total_seconds) * 100
148+
progress_bar['value'] = percent
149+
status_label.config(
150+
text=f"Rendering... {int(percent)}%"
151+
)
152+
153+
process.wait()
154+
rendering = False
155+
os.remove(list_file)
156+
157+
if process.returncode == 0:
158+
status_label.config(text="Rendering complete!")
159+
messagebox.showinfo("Done", "Relax video created successfully.")
160+
else:
161+
status_label.config(text="Rendering stopped.")
162+
163+
# ================= UI =================
164+
main = tb.Frame(app, padding=15)
165+
main.pack(fill="both", expand=True)
166+
167+
tb.Label(main, text="Relax Video Builder", font=("Segoe UI", 18, "bold")).pack()
168+
169+
content = tb.Frame(main)
170+
content.pack(fill="both", expand=True, pady=10)
171+
172+
# LEFT
173+
left = tb.Labelframe(content, text="Images", padding=10)
174+
left.pack(side="left", fill="y")
175+
176+
tb.Button(left, text="Add Images", command=select_images).pack(fill="x")
177+
image_listbox = tk.Listbox(left, height=15)
178+
image_listbox.pack(pady=5)
179+
image_listbox.bind("<<ListboxSelect>>", on_image_select)
180+
image_count_label = tb.Label(left, text="0 image(s)")
181+
image_count_label.pack()
182+
tb.Button(left, text="Remove Selected", command=remove_selected_images).pack(fill="x")
183+
tb.Button(left, text="Remove All", command=remove_all_images).pack(fill="x")
184+
185+
# CENTER
186+
center = tb.Labelframe(content, text="Preview", padding=10)
187+
center.pack(side="left", fill="both", expand=True)
188+
preview_label = tb.Label(center)
189+
preview_label.pack(expand=True)
190+
191+
# RIGHT
192+
right = tb.Labelframe(content, text="Audio & Settings", padding=10)
193+
right.pack(side="right", fill="y")
194+
195+
tb.Button(right, text="Select MP3", command=select_mp3).pack(fill="x")
196+
tb.Entry(right, textvariable=mp3_path, width=30).pack()
197+
tb.Button(right, text="Remove MP3", command=remove_mp3).pack(fill="x", pady=5)
198+
199+
tb.Label(right, text="Duration (Hours)").pack()
200+
tb.Spinbox(right, from_=1, to=10, textvariable=hours_var, width=8).pack()
201+
202+
tb.Button(right, text="Output MP4", command=select_output).pack(fill="x", pady=10)
203+
tb.Entry(right, textvariable=output_path, width=30).pack()
204+
205+
tb.Button(right, text="▶ Start", bootstyle="success", command=build_video).pack(fill="x", pady=5)
206+
tb.Button(right, text="⛔ Stop", bootstyle="danger", command=stop_video).pack(fill="x", pady=5)
207+
resume_btn = tb.Button(right, text="🔁 Resume (Restart)", bootstyle="warning", command=build_video)
208+
resume_btn.pack(fill="x")
209+
210+
# BOTTOM
211+
progress_bar = tb.Progressbar(main, length=600)
212+
progress_bar.pack(pady=10)
213+
214+
status_label = tb.Label(main, text="Idle")
215+
status_label.pack()
216+
217+
app.mainloop()

0 commit comments

Comments
 (0)