RandomPersonRR commited on
Commit
906ec90
·
verified ·
1 Parent(s): 9a9f9e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +463 -197
app.py CHANGED
@@ -1,225 +1,491 @@
1
- import os
2
  import asyncio
3
- import subprocess
 
 
4
  import uuid
5
  import glob
6
- import shutil
 
7
  import gradio as gr
8
 
9
- allowed_extensions = [
10
- ".3g2", ".3gp", ".3gpp", ".avi", ".cavs", ".dv", ".dvr", ".flv", ".m2ts",
11
- ".m4v", ".mkv", ".mod", ".mov", ".mp4", ".mpeg", ".mpg", ".mts", ".mxf",
12
- ".ogg", ".rm", ".rmvb", ".swf", ".ts", ".vob", ".webm", ".wmv", ".wtv",
13
- ".ogv", ".opus", ".aac", ".ac3", ".aif", ".aifc", ".aiff", ".amr", ".au",
14
- ".caf", ".dss", ".flac", ".m4a", ".m4b", ".mp3", ".oga", ".voc", ".wav",
15
- ".weba", ".wma"
16
- ]
17
-
18
- os.system("chmod +x fdkaac") # Make sure fdkaac is executable
19
-
20
- accel = 'auto'
21
- video_base_opts = ['-crf', '63', '-c:v', 'libx264', '-tune', 'zerolatency']
22
-
23
- UPLOAD_FOLDER = 'uploads'
24
- CONVERTED_FOLDER = 'converted'
25
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
26
- os.makedirs(CONVERTED_FOLDER, exist_ok=True)
27
-
28
- async def run_subprocess(cmd, use_fdkaac=False):
29
- env = os.environ.copy()
30
- if use_fdkaac:
31
- env["LD_LIBRARY_PATH"] = os.path.abspath("./") + ":" + env.get("LD_LIBRARY_PATH", "")
32
- print(f"[DEBUG] Running command:\n{' '.join(cmd)}\n")
33
- process = await asyncio.create_subprocess_exec(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *cmd,
 
 
35
  stdout=asyncio.subprocess.PIPE,
36
  stderr=asyncio.subprocess.PIPE,
37
- env=env
38
  )
39
- stdout, stderr = await process.communicate()
40
- if process.returncode != 0:
41
- print(f"[ERROR] Command failed:\n{stderr.decode()}\n")
42
- raise subprocess.CalledProcessError(process.returncode, cmd, stderr.decode())
43
- print(f"[DEBUG] Command succeeded:\n{stdout.decode()}\n")
44
- return stdout.decode(), stderr.decode()
45
-
46
- async def convert_video_task(input_path, downscale, faster, use_mp3, audio_only, custom_bitrate, video_bitrate):
47
- if audio_only:
48
- if use_mp3:
49
- # Directly convert to MP3 via ffmpeg
50
- output_audio = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.mp3")
51
- ffmpeg_audio_cmd = [
52
- 'ffmpeg', '-y', '-i', input_path,
53
- '-c:a', 'libmp3lame', '-b:a', '8k', '-ar', '24000', '-ac', '1',
54
- output_audio
55
- ]
56
- await run_subprocess(ffmpeg_audio_cmd)
57
- return output_audio, None
58
- else:
59
- # Convert to WAV, then AAC via fdkaac
60
- audio_wav = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.wav")
61
- audio_output = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.m4a")
62
- await run_subprocess([
63
- 'ffmpeg', '-y', '-i', input_path, '-ac', '1', '-ar', '8000', audio_wav
64
- ])
65
- await run_subprocess([
66
- './fdkaac', '-b', '1k', '-C', '-f', '2', '-G', '1', '-w', '8000',
67
- '-o', audio_output, audio_wav
68
- ], use_fdkaac=True)
69
- os.remove(audio_wav)
70
- return audio_output, None
71
-
72
- # Video conversion paths
73
- if use_mp3:
74
- output_video = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.mp4")
75
- ffmpeg_video_cmd = [
76
- 'ffmpeg', '-y', '-hwaccel', accel, '-i', input_path
77
- ]
78
-
79
- if custom_bitrate:
80
- ffmpeg_video_cmd += ['-b:v', f"{int(video_bitrate)}k"]
81
- else:
82
- ffmpeg_video_cmd += video_base_opts
83
-
84
- if faster:
85
- ffmpeg_video_cmd.extend(['-preset', 'ultrafast'])
86
-
87
- ffmpeg_video_cmd += [
88
- '-c:a', 'libmp3lame', '-b:a', '8k', '-ar', '24000', '-ac', '1',
89
- output_video
90
  ]
91
- await run_subprocess(ffmpeg_video_cmd)
92
- return None, output_video
93
-
94
- # AAC video + fdkaac audio merge
95
- audio_wav = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.wav")
96
- audio_output = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.m4a")
97
- video_output = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.mp4")
98
-
99
- await run_subprocess([
100
- 'ffmpeg', '-y', '-i', input_path, '-ac', '1', '-ar', '8000', audio_wav
101
- ])
102
-
103
- await run_subprocess([
104
- './fdkaac', '-b', '1k', '-C', '-f', '2', '-G', '1', '-w', '8000',
105
- '-o', audio_output, audio_wav
106
- ], use_fdkaac=True)
107
-
108
- video_cmd = ['ffmpeg', '-y', '-hwaccel', accel, '-i', input_path]
109
- if downscale:
110
- video_cmd += ['-vf', 'scale=-2:144']
111
- if custom_bitrate:
112
- video_cmd += ['-b:v', f"{int(video_bitrate)}k"]
113
- else:
114
- video_cmd += video_base_opts
115
- if faster:
116
- video_cmd.extend(['-preset', 'ultrafast'])
117
- video_cmd += ['-an', video_output]
118
- await run_subprocess(video_cmd)
119
 
120
- merged_output = os.path.join(CONVERTED_FOLDER, f"{uuid.uuid4()}.mp4")
121
- await run_subprocess([
122
- 'ffmpeg', '-y', '-i', video_output, '-i', audio_output, '-c', 'copy', merged_output
123
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- for f in [audio_wav, audio_output, video_output]:
126
- try:
127
- os.remove(f)
128
- except FileNotFoundError:
129
- pass
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- return None, merged_output
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- async def process_conversion(use_youtube, youtube_url, video_file, downscale, faster, use_mp3, audio_only, custom_bitrate, video_bitrate):
134
  try:
135
  if use_youtube:
136
  if not youtube_url:
137
- return "Error: YouTube URL required.", None
138
- yt_uuid = str(uuid.uuid4())
139
- yt_out = os.path.join(UPLOAD_FOLDER, yt_uuid + ".%(ext)s")
140
- yt_cmd = ['yt-dlp', '-o', yt_out, '-f', 'b', youtube_url]
141
- await run_subprocess(yt_cmd)
142
- pattern = os.path.join(UPLOAD_FOLDER, yt_uuid + ".*")
143
- files = glob.glob(pattern)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  if not files:
145
- return "Download failed.", None
 
 
146
  input_path = files[0]
 
 
147
  else:
148
  if not video_file:
149
- return "No video provided.", None
150
- ext = os.path.splitext(video_file.name)[1].lower()
151
- if ext not in allowed_extensions:
152
- return f"Unsupported file type: {ext}", None
153
- input_path = os.path.join(UPLOAD_FOLDER, f"{uuid.uuid4()}{ext}")
 
 
 
 
 
 
 
 
154
  shutil.copy2(video_file.name, input_path)
 
155
 
156
- audio_out, video_out = await convert_video_task(
157
- input_path, downscale, faster, use_mp3, audio_only, custom_bitrate, video_bitrate
158
- )
 
 
 
 
 
 
159
 
 
160
  if audio_only:
161
- return audio_out, audio_out
162
- return video_out, video_out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  except Exception as e:
164
- return f"Error: {str(e)}", None
165
-
166
- def convert_video(*args):
167
- return asyncio.run(process_conversion(*args))
168
-
169
- with gr.Blocks(theme=gr.themes.Default(primary_hue="rose")) as demo:
170
- gr.Markdown("""
171
- # **Low Quality Video Inator**
172
-
173
- Upload a video or paste a YouTube URL below, then tweak the settings.
174
- **Note:** YouTube downloads almost never work on HuggingFace.
175
- """)
176
-
177
- with gr.Group():
178
- with gr.Row():
179
- use_youtube = gr.Checkbox(label="🔗 Use YouTube URL (usually broken here)", value=False)
180
- youtube_url = gr.Textbox(label="YouTube URL", placeholder="Paste YouTube URL here")
181
- video_file = gr.File(label="📁 Upload Video File", file_types=[
182
- ".3g2", ".3gp", ".3gpp", ".avi", ".cavs", ".dv", ".dvr", ".flv", ".m2ts", ".m4v",
183
- ".mkv", ".mod", ".mov", ".mp4", ".mpeg", ".mpg", ".mts", ".mxf", ".ogg", ".rm",
184
- ".rmvb", ".swf", ".ts", ".vob", ".webm", ".wmv", ".wtv", ".ogv", ".opus", ".aac",
185
- ".ac3", ".aif", ".aifc", ".aiff", ".amr", ".au", ".caf", ".dss", ".flac", ".m4a",
186
- ".m4b", ".mp3", ".oga", ".voc", ".wav", ".weba", ".wma"
187
- ])
188
-
189
- gr.Markdown("### ⚙️ **Conversion Settings**")
190
- with gr.Group():
191
- with gr.Row():
192
- downscale = gr.Checkbox(label="Downscale Video to 144p", value=False)
193
- faster = gr.Checkbox(label="Faster Video Compression (pixelated, faster)", value=False)
194
- with gr.Row():
195
- use_mp3 = gr.Checkbox(label="Use MP3 Audio (better compatibility, AAC is lower quality)", value=False)
196
- audio_only = gr.Checkbox(label="Audio Only (no video output)", value=False)
197
- with gr.Row():
198
- custom_bitrate = gr.Checkbox(label="Use Custom Video Bitrate", value=False)
199
- video_bitrate = gr.Number(label="Video Bitrate (kbps)", visible=False)
200
-
201
- custom_bitrate.change(
202
- lambda checked: gr.update(visible=checked),
203
- inputs=[custom_bitrate],
204
- outputs=[video_bitrate]
205
- )
206
-
207
- convert_button = gr.Button("Convert Now", variant="primary")
208
-
209
- gr.Markdown("### **Conversion Preview**")
210
- video_preview = gr.Video(label="Preview Output")
211
-
212
- gr.Markdown("### **Download Your Masterpiece**")
213
- file_download = gr.File(label="Download Result")
214
-
215
- convert_button.click(
216
- convert_video,
217
- inputs=[
218
- use_youtube, youtube_url, video_file,
219
- downscale, faster, use_mp3, audio_only,
220
- custom_bitrate, video_bitrate
221
- ],
222
- outputs=[video_preview, file_download]
223
  )
224
 
225
- demo.launch()
 
 
1
+ # low_quality_video_inator.py
2
  import asyncio
3
+ import os
4
+ import re
5
+ import shutil
6
  import uuid
7
  import glob
8
+ import subprocess
9
+ from pathlib import Path
10
  import gradio as gr
11
 
12
+ # -------------------------
13
+ # Config / constants
14
+ # -------------------------
15
+ UPLOAD_FOLDER = Path("uploads")
16
+ CONVERTED_FOLDER = Path("converted")
17
+ UPLOAD_FOLDER.mkdir(exist_ok=True)
18
+ CONVERTED_FOLDER.mkdir(exist_ok=True)
19
+
20
+ ALLOWED_EXTENSIONS = {
21
+ ".3g2", ".3gp", ".3gpp", ".avi", ".cavs", ".dv", ".dvr", ".flv",
22
+ ".m2ts", ".m4v", ".mkv", ".mod", ".mov", ".mp4", ".mpeg", ".mpg",
23
+ ".mts", ".mxf", ".ogg", ".rm", ".rmvb", ".swf", ".ts", ".vob",
24
+ ".webm", ".wmv", ".wtv", ".ogv", ".opus", ".aac", ".ac3", ".aif",
25
+ ".aifc", ".aiff", ".amr", ".au", ".caf", ".dss", ".flac", ".m4a",
26
+ ".m4b", ".mp3", ".oga", ".voc", ".wav", ".weba", ".wma"
27
+ }
28
+
29
+ # FFmpeg defaults
30
+ VIDEO_BASE_OPTS = ["-crf", "63", "-c:v", "libx264", "-tune", "zerolatency"]
31
+ ACCEL = "auto"
32
+
33
+ # Regex to find ffmpeg "time=HH:MM:SS.mmm" in stderr
34
+ FFMPEG_TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+\.\d+)")
35
+
36
+ # Helper HTML progress bar generator (friendly and portable)
37
+ def progress_html(percent: float, step: str) -> str:
38
+ pct = max(0.0, min(100.0, percent))
39
+ bar = f"""
40
+ <div style="font-family:system-ui,Segoe UI,Roboto,Arial;margin-bottom:6px;">
41
+ <div style="font-size:14px;margin-bottom:4px;"><strong>Step:</strong> {gr.utils.html.escape(step)}</div>
42
+ <div style="background:#eee;border-radius:6px;overflow:hidden;border:1px solid #ddd;">
43
+ <div style="width:{pct:.2f}%;padding:8px 0;text-align:center;white-space:nowrap;">
44
+ <span style="color:#111;font-weight:600;">{pct:.1f}%</span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ """
49
+ return bar
50
+
51
+ # -------------------------
52
+ # Async process helpers
53
+ # -------------------------
54
+ async def run_command_capture(cmd, cwd=None, env=None):
55
+ """Run command to completion, return (stdout, stderr, returncode)."""
56
+ proc = await asyncio.create_subprocess_exec(
57
  *cmd,
58
+ cwd=cwd,
59
+ env=env or os.environ.copy(),
60
  stdout=asyncio.subprocess.PIPE,
61
  stderr=asyncio.subprocess.PIPE,
 
62
  )
63
+ stdout, stderr = await proc.communicate()
64
+ return stdout.decode(errors="ignore"), stderr.decode(errors="ignore"), proc.returncode
65
+
66
+ async def get_duration_seconds(path: Path) -> float | None:
67
+ """Get media duration using ffprobe. Returns seconds or None."""
68
+ cmd = [
69
+ "ffprobe", "-v", "error",
70
+ "-select_streams", "v:0",
71
+ "-show_entries", "format=duration",
72
+ "-of", "default=noprint_wrappers=1:nokey=1",
73
+ str(path)
74
+ ]
75
+ stdout, stderr, rc = await run_command_capture(cmd)
76
+ if rc != 0:
77
+ # try generic format probing
78
+ cmd2 = [
79
+ "ffprobe", "-v", "error",
80
+ "-show_entries", "format=duration",
81
+ "-of", "default=noprint_wrappers=1:nokey=1",
82
+ str(path),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  ]
84
+ stdout, stderr, rc = await run_command_capture(cmd2)
85
+ if rc != 0:
86
+ return None
87
+ try:
88
+ return float(stdout.strip())
89
+ except Exception:
90
+ return None
91
+
92
+ async def stream_ffmpeg_with_progress(cmd, total_seconds, step_name, yield_fn):
93
+ """
94
+ Run ffmpeg command and stream progress updates via yield_fn(percent, step_name).
95
+ yield_fn is a coroutine function that accepts (percent: float, step_text: str)
96
+ """
97
+ # Start process, read stderr line-by-line
98
+ proc = await asyncio.create_subprocess_exec(
99
+ *cmd,
100
+ stdout=asyncio.subprocess.PIPE,
101
+ stderr=asyncio.subprocess.PIPE,
102
+ env=os.environ.copy()
103
+ )
 
 
 
 
 
 
 
 
104
 
105
+ last_percent = 0.0
106
+ # Read stderr bytes as they arrive
107
+ while True:
108
+ line = await proc.stderr.readline()
109
+ if not line:
110
+ break
111
+ text = line.decode(errors="ignore").strip()
112
+ # parse time=HH:MM:SS.ms
113
+ m = FFMPEG_TIME_RE.search(text)
114
+ if m and total_seconds and total_seconds > 0.0:
115
+ hh, mm, ss = m.groups()
116
+ current = int(hh) * 3600 + int(mm) * 60 + float(ss)
117
+ percent = (current / total_seconds) * 100.0
118
+ # Only yield when percent changed enough to avoid UI spam
119
+ if percent - last_percent >= 0.5:
120
+ last_percent = percent
121
+ await yield_fn(percent, step_name)
122
+ # Optionally we can yield textual debug lines (not required)
123
+ await proc.wait()
124
+ return proc.returncode
125
 
126
+ # -------------------------
127
+ # Conversion logic (async generator feeding Gradio)
128
+ # -------------------------
129
+ async def convert_stream(
130
+ use_youtube: bool,
131
+ youtube_url: str,
132
+ video_file, # gr.File-like arg when uploaded
133
+ downscale: bool,
134
+ faster: bool,
135
+ use_mp3: bool,
136
+ audio_only: bool,
137
+ custom_bitrate: bool,
138
+ video_bitrate: float
139
+ ):
140
+ # Convenience small helper to push progress to UI (Gradio generator will yield)
141
+ async def push(percent: float, step_text: str):
142
+ yield progress_html(percent, step_text), step_text, None, None
143
 
144
+ # Another helper to yield final outputs
145
+ def final_yield(percent, step_text, preview_path, download_path):
146
+ # Gradio generator expects plain values, not coroutine - so we return these
147
+ return progress_html(percent, step_text), step_text, str(preview_path) if preview_path else None, str(download_path) if download_path else None
148
+
149
+ # Start
150
+ step = "Starting..."
151
+ yield progress_html(0.0, step), step, None, None
152
+
153
+ # Choose input source
154
+ input_path: Path | None = None
155
+ temp_files = []
156
 
 
157
  try:
158
  if use_youtube:
159
  if not youtube_url:
160
+ step = "Error: YouTube URL required."
161
+ yield progress_html(0.0, step), step, None, None
162
+ return
163
+
164
+ step = "Attempting to download from YouTube (yt-dlp)..."
165
+ yield progress_html(1.0, step), step, None, None
166
+
167
+ # Build output template
168
+ out_uuid = uuid.uuid4().hex
169
+ out_template = str(UPLOAD_FOLDER / f"{out_uuid}.%(ext)s")
170
+ ytdlp_cmd = ["yt-dlp", "-f", "b", "-o", out_template, youtube_url]
171
+
172
+ # Run and capture stderr to inspect errors
173
+ stdout, stderr, rc = await run_command_capture(ytdlp_cmd)
174
+ if rc != 0:
175
+ # Inspect stderr for "Video unavailable"
176
+ combined = (stdout or "") + "\n" + (stderr or "")
177
+ if "Video unavailable" in combined or "This video is unavailable" in combined:
178
+ step = "Error: Video unavailable on YouTube (removed / private / blocked by uploader)."
179
+ yield progress_html(0.0, step), step, None, None
180
+ return
181
+ else:
182
+ # Assume server/cloud IP is blocked by YouTube; instruct user to upload
183
+ step = (
184
+ "Could not download from YouTube from this server. "
185
+ "This is commonly caused by YouTube blocking cloud host IPs. "
186
+ "Please upload the file manually using the 'Upload Video File' control, or try again from another network."
187
+ )
188
+ yield progress_html(0.0, step), step, None, None
189
+ return
190
+ # find the downloaded file
191
+ files = list(UPLOAD_FOLDER.glob(f"{out_uuid}.*"))
192
  if not files:
193
+ step = "Download completed but file not found."
194
+ yield progress_html(0.0, step), step, None, None
195
+ return
196
  input_path = files[0]
197
+ temp_files.append(input_path)
198
+
199
  else:
200
  if not video_file:
201
+ step = "No video provided. Upload a file or use a YouTube URL."
202
+ yield progress_html(0.0, step), step, None, None
203
+ return
204
+ # gr.File's .name gives a local temp path in many deployments
205
+ try:
206
+ ext = Path(video_file.name).suffix.lower()
207
+ except Exception:
208
+ ext = None
209
+ if ext not in ALLOWED_EXTENSIONS:
210
+ step = f"Unsupported file type: {ext}"
211
+ yield progress_html(0.0, step), step, None, None
212
+ return
213
+ input_path = UPLOAD_FOLDER / f"{uuid.uuid4().hex}{ext}"
214
  shutil.copy2(video_file.name, input_path)
215
+ temp_files.append(input_path)
216
 
217
+ # We now have input_path
218
+ step = "Probing file duration..."
219
+ yield progress_html(2.0, step), step, None, None
220
+ total = await get_duration_seconds(input_path)
221
+ if not total:
222
+ # Might still proceed but progress can't show percent; we will show step-based progress
223
+ step = "Warning: could not read duration; progress will be step-based."
224
+ yield progress_html(0.0, step), step, None, None
225
+ total = None
226
 
227
+ # Handle audio-only or full conversion
228
  if audio_only:
229
+ if use_mp3:
230
+ step = "Converting audio to MP3..."
231
+ yield progress_html(1.0, step), step, None, None
232
+ out_audio = CONVERTED_FOLDER / f"{uuid.uuid4().hex}.mp3"
233
+ # ffmpeg low bitrate mp3 example:
234
+ ffmpeg_cmd = [
235
+ "ffmpeg", "-y", "-i", str(input_path),
236
+ "-ac", "1", "-ar", "24000", "-b:a", "8k",
237
+ str(out_audio)
238
+ ]
239
+ # If we have duration, stream progress parsing
240
+ if total:
241
+ async def _yield_fn(p, s):
242
+ yield progress_html(p, s), s, None, None
243
+ # Because run_ffmpeg_with_progress expects a coroutine yield_fn, but Gradio expects
244
+ # yields from this generator directly, implement manual streaming:
245
+ proc = await asyncio.create_subprocess_exec(*ffmpeg_cmd, stderr=asyncio.subprocess.PIPE)
246
+ last = 0.0
247
+ while True:
248
+ line = await proc.stderr.readline()
249
+ if not line:
250
+ break
251
+ txt = line.decode(errors="ignore")
252
+ m = FFMPEG_TIME_RE.search(txt)
253
+ if m and total:
254
+ hh, mm, ss = m.groups()
255
+ current = int(hh) * 3600 + int(mm) * 60 + float(ss)
256
+ percent = (current / total) * 100.0
257
+ if percent - last >= 0.5:
258
+ last = percent
259
+ yield progress_html(percent, step), step, None, None
260
+ await proc.wait()
261
+ if proc.returncode != 0:
262
+ raise RuntimeError("ffmpeg audio conversion failed")
263
+ else:
264
+ # fallback synchronous run
265
+ stdout, stderr, rc = await run_command_capture(ffmpeg_cmd)
266
+ if rc != 0:
267
+ raise RuntimeError(f"ffmpeg failed: {stderr[:200]}")
268
+ # Done
269
+ yield final_yield(100.0, "Audio conversion finished.", out_audio, out_audio)
270
+ return
271
+ else:
272
+ # AAC path (use ffmpeg's aac if fdkaac missing)
273
+ step = "Extracting audio and encoding AAC..."
274
+ yield progress_html(1.0, step), step, None, None
275
+ out_audio = CONVERTED_FOLDER / f"{uuid.uuid4().hex}.m4a"
276
+ ffmpeg_cmd = [
277
+ "ffmpeg", "-y", "-i", str(input_path),
278
+ "-ac", "1", "-ar", "8000", "-c:a", "aac", "-b:a", "12k",
279
+ str(out_audio)
280
+ ]
281
+ # stream progress if possible
282
+ if total:
283
+ proc = await asyncio.create_subprocess_exec(*ffmpeg_cmd, stderr=asyncio.subprocess.PIPE)
284
+ last = 0.0
285
+ while True:
286
+ line = await proc.stderr.readline()
287
+ if not line:
288
+ break
289
+ txt = line.decode(errors="ignore")
290
+ m = FFMPEG_TIME_RE.search(txt)
291
+ if m and total:
292
+ hh, mm, ss = m.groups()
293
+ current = int(hh) * 3600 + int(mm) * 60 + float(ss)
294
+ percent = (current / total) * 100.0
295
+ if percent - last >= 0.5:
296
+ last = percent
297
+ yield progress_html(percent, step), step, None, None
298
+ await proc.wait()
299
+ if proc.returncode != 0:
300
+ raise RuntimeError("ffmpeg aac audio encoding failed")
301
+ else:
302
+ stdout, stderr, rc = await run_command_capture(ffmpeg_cmd)
303
+ if rc != 0:
304
+ raise RuntimeError(f"ffmpeg failed: {stderr[:200]}")
305
+
306
+ yield final_yield(100.0, "Audio conversion finished.", out_audio, out_audio)
307
+ return
308
+
309
+ # ---------- Video (with audio) flow ----------
310
+ # 1) encode audio (unless we're merging in single pass). We'll encode audio then encode video then merge.
311
+ step = "Encoding audio track..."
312
+ yield progress_html(1.0, step), step, None, None
313
+ out_audio = CONVERTED_FOLDER / f"{uuid.uuid4().hex}.m4a"
314
+ ffmpeg_audio_cmd = [
315
+ "ffmpeg", "-y", "-i", str(input_path),
316
+ "-ac", "1", "-ar", "8000", "-c:a"
317
+ ]
318
+ if use_mp3:
319
+ ffmpeg_audio_cmd += ["libmp3lame", "-b:a", "8k", str(out_audio)]
320
+ else:
321
+ ffmpeg_audio_cmd += ["aac", "-b:a", "12k", str(out_audio)]
322
+
323
+ if total:
324
+ proc = await asyncio.create_subprocess_exec(*ffmpeg_audio_cmd, stderr=asyncio.subprocess.PIPE)
325
+ last = 0.0
326
+ while True:
327
+ line = await proc.stderr.readline()
328
+ if not line:
329
+ break
330
+ txt = line.decode(errors="ignore")
331
+ m = FFMPEG_TIME_RE.search(txt)
332
+ if m and total:
333
+ hh, mm, ss = m.groups()
334
+ current = int(hh) * 3600 + int(mm) * 60 + float(ss)
335
+ percent = (current / total) * 100.0 * 0.20 # audio is 20% of total pipeline (visual weighting)
336
+ if percent - last >= 0.5:
337
+ last = percent
338
+ yield progress_html(percent, step), step, None, None
339
+ await proc.wait()
340
+ if proc.returncode != 0:
341
+ raise RuntimeError("ffmpeg audio encoding failed")
342
+ else:
343
+ stdout, stderr, rc = await run_command_capture(ffmpeg_audio_cmd)
344
+ if rc != 0:
345
+ raise RuntimeError("ffmpeg audio encoding failed (no duration)")
346
+
347
+ # 2) encode video-only
348
+ step = "Encoding video track..."
349
+ yield progress_html(5.0, step), step, None, None
350
+ out_video = CONVERTED_FOLDER / f"{uuid.uuid4().hex}.mp4"
351
+ ffmpeg_video_cmd = ["ffmpeg", "-y", "-hwaccel", ACCEL, "-i", str(input_path)]
352
+
353
+ # Downscale if requested
354
+ if downscale:
355
+ ffmpeg_video_cmd += ["-vf", "scale=-2:144"]
356
+
357
+ # Bitrate handling
358
+ if custom_bitrate and video_bitrate:
359
+ ffmpeg_video_cmd += ["-b:v", f"{int(video_bitrate)}k"]
360
+ else:
361
+ ffmpeg_video_cmd += VIDEO_BASE_OPTS
362
+
363
+ if faster:
364
+ ffmpeg_video_cmd += ["-preset", "ultrafast"]
365
+
366
+ ffmpeg_video_cmd += ["-an", str(out_video)]
367
+
368
+ if total:
369
+ # We'll treat video encoding as 70% of pipeline
370
+ proc = await asyncio.create_subprocess_exec(*ffmpeg_video_cmd, stderr=asyncio.subprocess.PIPE)
371
+ last = 0.0
372
+ while True:
373
+ line = await proc.stderr.readline()
374
+ if not line:
375
+ break
376
+ txt = line.decode(errors="ignore")
377
+ m = FFMPEG_TIME_RE.search(txt)
378
+ if m and total:
379
+ hh, mm, ss = m.groups()
380
+ current = int(hh) * 3600 + int(mm) * 60 + float(ss)
381
+ percent_video = (current / total) * 100.0
382
+ # Map video portion to a weighted chunk (20% done already for audio, video 70% here)
383
+ combined_percent = 20.0 + (percent_video * 0.70)
384
+ if combined_percent - last >= 0.5:
385
+ last = combined_percent
386
+ yield progress_html(combined_percent, step), step, None, None
387
+ await proc.wait()
388
+ if proc.returncode != 0:
389
+ raise RuntimeError("ffmpeg video encoding failed")
390
+ else:
391
+ stdout, stderr, rc = await run_command_capture(ffmpeg_video_cmd)
392
+ if rc != 0:
393
+ raise RuntimeError("ffmpeg video encoding failed (no duration)")
394
+
395
+ # 3) merge audio+video (fast copy)
396
+ step = "Merging audio and video..."
397
+ yield progress_html(95.0, step), step, None, None
398
+ merged_out = CONVERTED_FOLDER / f"{uuid.uuid4().hex}.mp4"
399
+ merge_cmd = [
400
+ "ffmpeg", "-y",
401
+ "-i", str(out_video),
402
+ "-i", str(out_audio),
403
+ "-c", "copy",
404
+ str(merged_out)
405
+ ]
406
+ stdout, stderr, rc = await run_command_capture(merge_cmd)
407
+ if rc != 0:
408
+ # sometimes copy fails due to container specifics; fallback to re-encoding mux
409
+ merge_cmd = [
410
+ "ffmpeg", "-y",
411
+ "-i", str(out_video),
412
+ "-i", str(out_audio),
413
+ "-c:v", "copy", "-c:a", "aac",
414
+ str(merged_out)
415
+ ]
416
+ stdout, stderr, rc = await run_command_capture(merge_cmd)
417
+ if rc != 0:
418
+ raise RuntimeError("Merging audio and video failed")
419
+
420
+ # cleanup temp pieces
421
+ for p in (out_audio, out_video):
422
+ try:
423
+ p.unlink()
424
+ except Exception:
425
+ pass
426
+
427
+ # Done
428
+ step = "Finished"
429
+ yield final_yield(100.0, "Conversion complete!", merged_out, merged_out)
430
+ return
431
+
432
  except Exception as e:
433
+ err = f"Error: {e}"
434
+ yield progress_html(0.0, err), err, None, None
435
+ return
436
+ finally:
437
+ # remove temporary download files if any
438
+ for p in temp_files:
439
+ try:
440
+ p.unlink()
441
+ except Exception:
442
+ pass
443
+
444
+ # -------------------------
445
+ # Gradio UI
446
+ # -------------------------
447
+ with gr.Blocks(title="Low Quality Video Inator", css="""
448
+ .gradio-container { font-family: system-ui, 'Segoe UI', Roboto, Arial; }
449
+ """) as demo:
450
+
451
+ gr.Markdown("# Low Quality Video Inator\nUpload a video file or paste a YouTube URL. The interface will stream a step-aware progress bar while ffmpeg runs.")
452
+
453
+ with gr.Row():
454
+ use_youtube = gr.Checkbox(label="Use YouTube URL (try server download first)", value=False)
455
+ youtube_url = gr.Textbox(label="YouTube URL", placeholder="https://youtube.com/...", lines=1)
456
+
457
+ video_file = gr.File(label="Upload Video File", file_types=list(ALLOWED_EXTENSIONS), file_count="single")
458
+
459
+ gr.Markdown("### Conversion Settings")
460
+ with gr.Row():
461
+ downscale = gr.Checkbox(label="Downscale to 144p", value=False)
462
+ faster = gr.Checkbox(label="Faster encoding (lower quality)", value=False)
463
+ with gr.Row():
464
+ use_mp3 = gr.Checkbox(label="Use MP3 audio", value=False)
465
+ audio_only = gr.Checkbox(label="Audio Only", value=False)
466
+ with gr.Row():
467
+ custom_bitrate = gr.Checkbox(label="Use Custom Video Bitrate (kbps)", value=False)
468
+ video_bitrate = gr.Number(label="Video Bitrate (kbps)", value=64, visible=False)
469
+
470
+ # Toggle visibility of bitrate input
471
+ def toggle_bitrate_checkbox(use: bool):
472
+ return gr.update(visible=use)
473
+ custom_bitrate.change(toggle_bitrate_checkbox, inputs=[custom_bitrate], outputs=[video_bitrate])
474
+
475
+ convert_btn = gr.Button("Convert Now", variant="primary")
476
+
477
+ with gr.Column():
478
+ progress_area = gr.HTML("<div>Ready.</div>", label="Progress")
479
+ step_text = gr.Textbox(value="Idle", label="Current Step", interactive=False)
480
+ preview = gr.Video(label="Preview Output")
481
+ download_file = gr.File(label="Download Result")
482
+
483
+ # Hook up generator -> UI
484
+ convert_btn.click(
485
+ fn=convert_stream,
486
+ inputs=[use_youtube, youtube_url, video_file, downscale, faster, use_mp3, audio_only, custom_bitrate, video_bitrate],
487
+ outputs=[progress_area, step_text, preview, download_file]
 
 
 
 
488
  )
489
 
490
+ # Safety / hosting configuration: no flagging UI
491
+ demo.launch(share=False, prevent_thread_lock=True)