Spaces:
Running
Running
| import streamlit as st | |
| import tempfile | |
| import subprocess | |
| import os | |
| import re | |
| # Page settings | |
| st.set_page_config(page_title="Video Speed Tool", layout="centered") | |
| # CSS styling for mobile | |
| st.markdown(""" | |
| <style> | |
| /* Center title */ | |
| .title-center { | |
| text-align: center; | |
| font-size: 1.8em; | |
| font-weight: bold; | |
| margin-bottom: 20px; | |
| } | |
| /* Center all buttons */ | |
| div.stButton > button { | |
| font-weight: bold; | |
| width: 80%; | |
| border-radius: 8px; | |
| padding: 10px 0; | |
| font-size: 1.1em; | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| /* Center download button */ | |
| div.stDownloadButton > button { | |
| font-weight: bold; | |
| width: 80%; | |
| border-radius: 8px; | |
| padding: 10px 0; | |
| font-size: 1.1em; | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| /* Bigger + and - buttons in number input */ | |
| div[data-baseweb="input"] button { | |
| width: 40px !important; | |
| height: 40px !important; | |
| font-size: 1.2em !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Title | |
| st.markdown('<p class="title-center">π¬ Video Speed Tool</p>', unsafe_allow_html=True) | |
| # Upload file | |
| uploaded_file = st.file_uploader("Upload video", type=["mp4", "mov", "avi", "mkv"]) | |
| # Speed input | |
| speed = st.number_input( | |
| "Speed (0.25x - 4x)", | |
| min_value=0.25, | |
| max_value=4.0, | |
| value=1.5, | |
| step=0.05 | |
| ) | |
| if uploaded_file is not None: | |
| st.video(uploaded_file) | |
| if st.button("PROCESS VIDEO"): | |
| with st.spinner("Processing... Please wait"): | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_input: | |
| tmp_input.write(uploaded_file.read()) | |
| input_path = tmp_input.name | |
| output_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name | |
| video_speed = 1 / speed | |
| audio_speed = speed | |
| # Always compress | |
| cmd = [ | |
| "ffmpeg", "-i", input_path, | |
| "-filter_complex", f"[0:v]setpts={video_speed}*PTS[v];[0:a]atempo={audio_speed}[a]", | |
| "-map", "[v]", "-map", "[a]", | |
| "-vcodec", "libx264", "-crf", "28", "-preset", "fast", | |
| "-movflags", "+faststart", | |
| "-y", output_path | |
| ] | |
| # Progress bar | |
| progress_text = st.empty() | |
| progress_bar = st.progress(0) | |
| # Get video duration | |
| probe = subprocess.run( | |
| ["ffprobe", "-v", "error", "-show_entries", "format=duration", | |
| "-of", "default=noprint_wrappers=1:nokey=1", input_path], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True | |
| ) | |
| try: | |
| total_duration = float(probe.stdout.strip()) | |
| except: | |
| total_duration = None | |
| # Run ffmpeg with live progress | |
| process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) | |
| for line in process.stdout: | |
| if "time=" in line and total_duration: | |
| match = re.search(r"time=(\d+:\d+:\d+\.\d+)", line) | |
| if match: | |
| h, m, s = match.group(1).split(":") | |
| elapsed = int(h) * 3600 + int(m) * 60 + float(s) | |
| percent = min(int((elapsed / total_duration) * 100), 100) | |
| progress_bar.progress(percent) | |
| progress_text.text(f"Processing... {percent}%") | |
| process.wait() | |
| progress_bar.progress(100) | |
| progress_text.text("β Processing complete!") | |
| # Show processed video | |
| st.video(output_path) | |
| # Download button centered | |
| with open(output_path, "rb") as f: | |
| st.download_button( | |
| label="DOWNLOAD VIDEO", | |
| data=f, | |
| file_name="processed_video.mp4", | |
| mime="video/mp4" | |
| ) | |
| os.remove(input_path) | |
| os.remove(output_path) | |