algorembrant commited on
Commit
7af876f
·
verified ·
1 Parent(s): 30db981

Upload 8 files

Browse files
Files changed (8) hide show
  1. .gitignore +32 -0
  2. LICENSE +21 -0
  3. STRUCTURE.md +14 -0
  4. TECHSTACK.md +12 -0
  5. app.py +227 -0
  6. db.py +42 -0
  7. history.db +0 -0
  8. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ nodler/
2
+ # Large binaries
3
+ ffmpeg.exe
4
+ ffprobe.exe
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ .env
28
+ .venv
29
+ venv/
30
+ ENV/
31
+ env.bak/
32
+ venv.bak/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rembrant Oyangoren Albeos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
STRUCTURE.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Structure
2
+
3
+ ```text
4
+ video-compressor/
5
+ ├── .gitignore
6
+ ├── app.py
7
+ ├── db.py
8
+ ├── ffmpeg.exe
9
+ ├── ffprobe.exe
10
+ ├── history.db
11
+ ├── LICENSE
12
+ ├── requirements.txt
13
+ └── TECHSTACK.md
14
+ ```
TECHSTACK.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Techstack
2
+
3
+ Audit of **video-compressor** project files (excluding environment and cache):
4
+
5
+ | File Type | Count | Size (KB) |
6
+ | :--- | :--- | :--- |
7
+ | (no extension) | 2 | 1.3 |
8
+ | Python (.py) | 2 | 10.3 |
9
+ | Windows Executable (.exe) | 2 | 261,912.5 |
10
+ | Database (.db) | 1 | 12.0 |
11
+ | Plain Text (.txt) | 1 | 0.0 |
12
+ | **Total** | **8** | **261,936.2** |
app.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import tempfile
3
+ import tempfile
4
+ import os
5
+ import time
6
+ import ffmpeg
7
+ import subprocess
8
+ import platform
9
+ from db import init_db, insert_record, get_records
10
+
11
+ # Initialize Database
12
+ init_db()
13
+
14
+ st.set_page_config(page_title="Video Compressor", layout="wide")
15
+
16
+ def get_video_info(file_path):
17
+ try:
18
+ probe = ffmpeg.probe(file_path)
19
+ video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
20
+ audio_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None)
21
+
22
+ duration = float(probe['format']['duration'])
23
+ width = int(video_stream['width'])
24
+ height = int(video_stream['height'])
25
+
26
+ # Audio bitrate defaults to 128k if not found or 0
27
+ audio_bitrate = 128000
28
+ if audio_stream and 'bit_rate' in audio_stream:
29
+ audio_bitrate = int(audio_stream['bit_rate'])
30
+ elif 'bit_rate' in probe['format']:
31
+ # fallback approximation
32
+ audio_bitrate = 128000
33
+
34
+ return {
35
+ 'duration': duration,
36
+ 'width': width,
37
+ 'height': height,
38
+ 'audio_bitrate': audio_bitrate
39
+ }
40
+ except Exception as e:
41
+ st.error(f"Error probing video: {e}")
42
+ return None
43
+
44
+ def compress_video_2pass(input_path, output_path, start_time, end_time, target_size_mb, resolution, fps, progress_bar):
45
+ info = get_video_info(input_path)
46
+ if not info:
47
+ return False
48
+
49
+ duration = end_time - start_time
50
+ if duration <= 0:
51
+ st.error("Invalid trim times (End time must be greater than Start time).")
52
+ return False
53
+
54
+ # Calculate bitrates
55
+ # Target size in bits
56
+ target_size_bits = target_size_mb * 8388608 # MB to bits (1024 * 1024 * 8)
57
+
58
+ # Audio bitrate in bits per second
59
+ audio_bitrate = info['audio_bitrate']
60
+
61
+ # Required total bitrate (b/s)
62
+ total_bitrate = target_size_bits / duration
63
+
64
+ # Required video bitrate (b/s)
65
+ video_bitrate = total_bitrate - audio_bitrate
66
+
67
+ # Ensure a minimum video bitrate to avoid errors
68
+ if video_bitrate < 10000:
69
+ st.warning("Target size is too small for this duration. Video quality will be extremely poor or fail. Setting a minimum bitrate.")
70
+ video_bitrate = 10000
71
+
72
+ video_bitrate_k = int(video_bitrate / 1000)
73
+ audio_bitrate_k = int(audio_bitrate / 1000)
74
+
75
+ # Resolution scaling parameter
76
+ scale_param = ""
77
+ if resolution != "Original":
78
+ height = int(resolution.replace("p", ""))
79
+ scale_param = f"scale=-2:{height}"
80
+
81
+ devnull = os.devnull
82
+
83
+ input_kwargs = {'ss': start_time, 'to': end_time}
84
+
85
+ # Setup Streams
86
+ v_ext = ffmpeg.input(input_path, **input_kwargs)
87
+ a_ext = v_ext.audio
88
+ v = v_ext.video
89
+
90
+ if scale_param:
91
+ v = v.filter('scale', -2, int(resolution.replace('p', '')))
92
+
93
+ if fps and fps != "Original":
94
+ v = v.filter('fps', fps=int(fps))
95
+
96
+ # Pass 1
97
+ progress_bar.progress(20, text="Pass 1/2: Analyzing Video...")
98
+ passlog_base = os.path.join(tempfile.gettempdir(), f"ffmpeg2pass_{int(time.time())}")
99
+ try:
100
+ pass1_args = {
101
+ 'c:v': 'libx264',
102
+ 'b:v': f'{video_bitrate_k}k',
103
+ 'pass': 1,
104
+ 'passlogfile': passlog_base,
105
+ 'f': 'mp4',
106
+ 'y': None # overwrite
107
+ }
108
+ # In Windows, pass 1 log file must be handled properly, ffmpeg-python creates ffmpeg2pass-0.log in current dir
109
+
110
+ # We construct the pass 1 output
111
+ out1 = ffmpeg.output(v, devnull, **pass1_args)
112
+ ffmpeg.run(out1, quiet=True, overwrite_output=True)
113
+ except ffmpeg.Error as e:
114
+ if e.stderr:
115
+ st.error(f"Pass 1 Error: {e.stderr.decode('utf8')}")
116
+ return False
117
+
118
+ # Pass 2
119
+ progress_bar.progress(60, text="Pass 2/2: Compressing Video...")
120
+ try:
121
+ pass2_args = {
122
+ 'c:v': 'libx264',
123
+ 'b:v': f'{video_bitrate_k}k',
124
+ 'pass': 2,
125
+ 'passlogfile': passlog_base,
126
+ 'c:a': 'aac',
127
+ 'b:a': f'{audio_bitrate_k}k',
128
+ 'y': None # overwrite
129
+ }
130
+ out2 = ffmpeg.output(v, a_ext, output_path, **pass2_args)
131
+ ffmpeg.run(out2, quiet=True, overwrite_output=True)
132
+
133
+ except ffmpeg.Error as e:
134
+ if e.stderr:
135
+ st.error(f"Pass 2 Error: {e.stderr.decode('utf8')}")
136
+ return False
137
+ finally:
138
+ # Clean up pass log files
139
+ for ext in ['-0.log', '-0.log.mbtree']:
140
+ logfile = f"{passlog_base}{ext}"
141
+ if os.path.exists(logfile):
142
+ try:
143
+ os.remove(logfile)
144
+ except:
145
+ pass
146
+
147
+ progress_bar.progress(100, text="Done!")
148
+ return True
149
+
150
+ st.title("Video Compressor & Trimmer")
151
+ st.markdown("Upload a video, trim it, set a target size, and this tool will use **2-pass encoding** to perfectly hit your target size (MB).")
152
+
153
+ tab1, tab2 = st.tabs(["Compressor", "History"])
154
+
155
+ with tab1:
156
+ uploaded_file = st.file_uploader("Upload Video", type=['mp4', 'mov', 'avi', 'mkv'])
157
+
158
+ if uploaded_file is not None:
159
+ # Save temp file
160
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_input:
161
+ temp_input.write(uploaded_file.read())
162
+ temp_input_path = temp_input.name
163
+
164
+ info = get_video_info(temp_input_path)
165
+ if info:
166
+ st.success(f"Video Loaded: {info['duration']:.2f} seconds | {info['width']}x{info['height']}")
167
+
168
+ col1, col2 = st.columns(2)
169
+ with col1:
170
+ start_time = st.number_input("Start Time (seconds)", min_value=0.0, max_value=info['duration']-0.1, value=0.0, step=1.0)
171
+ with col2:
172
+ end_time = st.number_input("End Time (seconds)", min_value=0.1, max_value=info['duration'], value=info['duration'], step=1.0)
173
+
174
+ col3, col4, col5 = st.columns(3)
175
+ with col3:
176
+ resolution = st.selectbox("Resolution", ["Original", "1080p", "720p", "480p", "360p"])
177
+ with col4:
178
+ fps = st.selectbox("FPS", ["Original", "60", "30", "24", "15"])
179
+ with col5:
180
+ target_size = st.number_input("Target Size (MB)", min_value=0.1, value=10.0, step=1.0)
181
+
182
+ if st.button("Compress & Export"):
183
+ st.info("Ensure you have `ffmpeg` installed on your system.")
184
+ progress_bar = st.progress(0, text="Initializing...")
185
+
186
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_output:
187
+ temp_output_path = temp_output.name
188
+
189
+ start_perf = time.time()
190
+ success = compress_video_2pass(temp_input_path, temp_output_path, start_time, end_time, target_size, resolution, fps, progress_bar)
191
+ end_perf = time.time()
192
+
193
+ if success:
194
+ final_size_bytes = os.path.getsize(temp_output_path)
195
+ final_size_mb = final_size_bytes / (1024 * 1024)
196
+ duration_sec = end_time - start_time
197
+
198
+ st.success(f"Compression Successful! Time taken: {end_perf - start_perf:.2f}s")
199
+ st.metric("Final Size", f"{final_size_mb:.2f} MB")
200
+
201
+ with open(temp_output_path, "rb") as file:
202
+ btn = st.download_button(
203
+ label="Download Compressed Video",
204
+ data=file,
205
+ file_name=f"compressed_{uploaded_file.name}",
206
+ mime="video/mp4"
207
+ )
208
+
209
+ # Log to DB
210
+ insert_record(uploaded_file.name, target_size, final_size_mb, duration_sec, resolution, fps if fps != "Original" else info.get('r_frame_rate', 0))
211
+
212
+ # cleanup output temp file after offering download
213
+ # os.remove(temp_output_path) -> actually we shouldn't remove immediately if download button needs it
214
+ # Streamlit's download button reads from memory here since we opened it.
215
+
216
+ # Cleanup input temp file if we change files or stop
217
+ # (This is a simplistic approach, in production we'd use a more robust cleanup)
218
+
219
+ with tab2:
220
+ st.header("Compression History")
221
+ records = get_records()
222
+ if not records:
223
+ st.write("No compression history found.")
224
+ else:
225
+ import pandas as pd
226
+ df = pd.DataFrame(records, columns=['ID', 'Original File', 'Target Size (MB)', 'Final Size (MB)', 'Duration (s)', 'Resolution', 'FPS', 'Timestamp'])
227
+ st.dataframe(df, hide_index=True)
db.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import os
3
+ from datetime import datetime
4
+
5
+ DB_PATH = os.path.join(os.path.dirname(__file__), 'history.db')
6
+
7
+ def init_db():
8
+ conn = sqlite3.connect(DB_PATH)
9
+ cursor = conn.cursor()
10
+ cursor.execute('''
11
+ CREATE TABLE IF NOT EXISTS compression_history (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ original_filename TEXT NOT NULL,
14
+ target_size_mb REAL NOT NULL,
15
+ final_size_mb REAL NOT NULL,
16
+ duration_sec REAL NOT NULL,
17
+ resolution TEXT,
18
+ fps INTEGER,
19
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
20
+ )
21
+ ''')
22
+ conn.commit()
23
+ conn.close()
24
+
25
+ def insert_record(original_filename, target_size_mb, final_size_mb, duration_sec, resolution, fps):
26
+ conn = sqlite3.connect(DB_PATH)
27
+ cursor = conn.cursor()
28
+ cursor.execute('''
29
+ INSERT INTO compression_history
30
+ (original_filename, target_size_mb, final_size_mb, duration_sec, resolution, fps, timestamp)
31
+ VALUES (?, ?, ?, ?, ?, ?, ?)
32
+ ''', (original_filename, target_size_mb, final_size_mb, duration_sec, resolution, fps, datetime.now()))
33
+ conn.commit()
34
+ conn.close()
35
+
36
+ def get_records():
37
+ conn = sqlite3.connect(DB_PATH)
38
+ cursor = conn.cursor()
39
+ cursor.execute('SELECT * FROM compression_history ORDER BY timestamp DESC')
40
+ records = cursor.fetchall()
41
+ conn.close()
42
+ return records
history.db ADDED
Binary file (12.3 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ streamlit
2
+ ffmpeg-python
3
+ SQLAlchemy