Ivan000 commited on
Commit
2516b23
·
verified ·
1 Parent(s): 5f47c12

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -76
app.py CHANGED
@@ -1,9 +1,6 @@
1
  # audio_split_zip_gradio.py
2
- # Gradio app: upload huge audio -> split into N-second parts -> download ZIP (mp3 parts)
3
- # - Auto bitrate = same as source if detectable; fallback to 192 kbps
4
- # - Custom bitrate slider appears only in Custom mode
5
- # - Mobile-friendly UI (bigger text, simpler layout)
6
- # - Uses ffmpeg/ffprobe (system dependency)
7
 
8
  import os
9
  import math
@@ -36,7 +33,6 @@ def ffprobe_info(path: str) -> dict:
36
  """
37
  info = {"duration_sec": None, "bitrate_bps": None}
38
 
39
- # duration + format bitrate
40
  out, _ = _run([
41
  "ffprobe", "-v", "error",
42
  "-show_entries", "format=duration,bit_rate",
@@ -59,7 +55,6 @@ def ffprobe_info(path: str) -> dict:
59
  except Exception:
60
  pass
61
 
62
- # prefer audio stream bitrate if available
63
  out2, _ = _run([
64
  "ffprobe", "-v", "error",
65
  "-select_streams", "a:0",
@@ -104,10 +99,6 @@ def format_seconds(s: float) -> str:
104
  # -------------------------
105
 
106
  def cleanup_tmpdirs(prefix="audiosplit_", older_than_seconds=6 * 3600):
107
- """
108
- Best-effort cleanup of temp dirs created by this app.
109
- Removes dirs in system temp older than N seconds.
110
- """
111
  tmp_root = Path(tempfile.gettempdir())
112
  now = time.time()
113
  for p in tmp_root.glob(prefix + "*"):
@@ -116,7 +107,6 @@ def cleanup_tmpdirs(prefix="audiosplit_", older_than_seconds=6 * 3600):
116
  continue
117
  age = now - p.stat().st_mtime
118
  if age > older_than_seconds:
119
- # avoid importing shutil globally; but it's fine:
120
  import shutil
121
  shutil.rmtree(p, ignore_errors=True)
122
  except Exception:
@@ -129,23 +119,23 @@ def cleanup_tmpdirs(prefix="audiosplit_", older_than_seconds=6 * 3600):
129
 
130
  def split_and_zip(file_path: str, chunk_seconds: int, quality_mode: str, custom_kbps: int):
131
  if not file_path or not os.path.exists(file_path):
132
- raise gr.Error("Загрузите аудиофайл.")
133
 
134
  try:
135
  chunk_seconds = int(chunk_seconds)
136
  except Exception:
137
- raise gr.Error("Длина фрагмента должна быть целым числом (секунды).")
138
  if chunk_seconds <= 0:
139
- raise gr.Error("Длина фрагмента должна быть больше 0.")
140
 
141
- cleanup_tmpdirs() # occasional cleanup
142
 
143
  info = ffprobe_info(file_path)
144
  duration = info.get("duration_sec")
145
  src_kbps = to_kbps(info.get("bitrate_bps"))
146
 
147
  if duration is None:
148
- raise gr.Error("Не удалось определить длительность файла. Проверьте ffmpeg/ffprobe и формат.")
149
 
150
  # bitrate selection
151
  if quality_mode == "Auto (same as source)":
@@ -176,7 +166,6 @@ def split_and_zip(file_path: str, chunk_seconds: int, quality_mode: str, custom_
176
  )
177
  out_path = os.path.join(parts_dir, out_name)
178
 
179
- # Re-encode to mp3 for consistent output and compatibility
180
  cmd = [
181
  "ffmpeg", "-y",
182
  "-ss", str(start),
@@ -195,82 +184,204 @@ def split_and_zip(file_path: str, chunk_seconds: int, quality_mode: str, custom_
195
  for p in created:
196
  zf.write(p, arcname=os.path.basename(p))
197
 
198
- src_txt = f"{src_kbps} kbps" if src_kbps else "не удалось определить"
199
  status = (
200
- f"Файл: {Path(file_path).name}\n"
201
- f"Длительность: {duration:.2f} сек\n"
202
- f"Фрагмент: {chunk_seconds} сек\n"
203
- f"Частей: {total_parts}\n"
204
- f"Битрейт источника: {src_txt}\n"
205
- f"Выход: mp3\n"
206
- f"Качество: {out_quality_text}\n"
207
- f"ZIP готов."
208
  )
209
 
210
  return zip_path, status
211
 
212
 
213
  # -------------------------
214
- # UI (mobile-friendly)
215
  # -------------------------
216
 
217
- CSS = """
218
- :root { --text-lg: 18px; --text-md: 16px; }
219
- #app { max-width: 980px; margin: 0 auto; }
220
- #title h2 { font-size: 22px; margin-bottom: 6px; }
221
- .gradio-container { font-size: var(--text-md); }
222
- label, .wrap, .prose { font-size: var(--text-md) !important; }
223
- textarea, input, button { font-size: var(--text-md) !important; }
224
- button { padding: 12px 16px !important; }
225
- .smallnote { font-size: 13px; opacity: 0.85; }
 
 
 
 
 
 
 
 
226
 
227
- @media (max-width: 640px) {
228
- #app { padding: 10px; }
229
- #title h2 { font-size: 20px; }
230
- .gradio-container { font-size: var(--text-lg); }
231
- label, textarea, input, button { font-size: var(--text-lg) !important; }
 
 
232
  }
233
- """
234
 
235
- with gr.Blocks() as demo:
236
- with gr.Column(elem_id="app"):
237
- gr.Markdown("## Audio Splitter → ZIP (mp3)", elem_id="title")
238
- gr.Markdown(
239
- "<div class='smallnote'>"
240
- "Загрузите аудиофайл, задайте длину фрагмента в секундах. "
241
- "На выходе будет ZIP с mp3-частями."
242
- "</div>"
243
- )
244
 
245
- inp = gr.File(label="Аудиофайл", file_count="single", type="filepath")
 
 
 
 
 
 
 
 
 
246
 
247
- chunk_seconds = gr.Number(label="Длина одного фрагмента (сек)", value=10, precision=0)
 
 
 
 
 
 
 
248
 
249
- quality_mode = gr.Radio(
250
- label="Качество (битрейт)",
251
- choices=["Auto (same as source)", "Custom (8..320 kbps)"],
252
- value="Auto (same as source)"
253
- )
 
 
 
 
 
 
 
254
 
255
- custom_bitrate = gr.Slider(
256
- label="Custom bitrate (kbps)",
257
- minimum=8, maximum=320, value=192, step=1,
258
- visible=False, interactive=True
259
- )
260
 
261
- gr.Markdown(
262
- "<div class='smallnote'>"
263
- "Auto: пытаемся взять битрейт из исходника через ffprobe. Если не получилось — 192 kbps."
264
- "</div>"
265
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
- btn = gr.Button("Split & Download ZIP", variant="primary")
 
 
 
 
 
 
 
 
268
 
269
- out_zip = gr.File(label="ZIP архив (скачать)")
270
- status = gr.Textbox(label="Статус", lines=8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
  def toggle_custom(mode):
273
- return gr.update(visible=(mode.startswith("Custom")))
274
 
275
  quality_mode.change(toggle_custom, inputs=quality_mode, outputs=custom_bitrate)
276
 
@@ -281,9 +392,6 @@ with gr.Blocks() as demo:
281
  )
282
 
283
  if __name__ == "__main__":
284
- # Key changes:
285
- # - css moved to launch() to avoid warning
286
- # - ssr_mode disabled to remove SSR experimental mode
287
  demo.launch(
288
  server_name="0.0.0.0",
289
  server_port=7860,
 
1
  # audio_split_zip_gradio.py
2
+ # Gradio app: upload big audio -> split into N-second parts -> download ZIP (mp3 parts)
3
+ # UI: English + custom neon/glass style with subtle animations
 
 
 
4
 
5
  import os
6
  import math
 
33
  """
34
  info = {"duration_sec": None, "bitrate_bps": None}
35
 
 
36
  out, _ = _run([
37
  "ffprobe", "-v", "error",
38
  "-show_entries", "format=duration,bit_rate",
 
55
  except Exception:
56
  pass
57
 
 
58
  out2, _ = _run([
59
  "ffprobe", "-v", "error",
60
  "-select_streams", "a:0",
 
99
  # -------------------------
100
 
101
  def cleanup_tmpdirs(prefix="audiosplit_", older_than_seconds=6 * 3600):
 
 
 
 
102
  tmp_root = Path(tempfile.gettempdir())
103
  now = time.time()
104
  for p in tmp_root.glob(prefix + "*"):
 
107
  continue
108
  age = now - p.stat().st_mtime
109
  if age > older_than_seconds:
 
110
  import shutil
111
  shutil.rmtree(p, ignore_errors=True)
112
  except Exception:
 
119
 
120
  def split_and_zip(file_path: str, chunk_seconds: int, quality_mode: str, custom_kbps: int):
121
  if not file_path or not os.path.exists(file_path):
122
+ raise gr.Error("Please upload an audio file.")
123
 
124
  try:
125
  chunk_seconds = int(chunk_seconds)
126
  except Exception:
127
+ raise gr.Error("Chunk length must be an integer (seconds).")
128
  if chunk_seconds <= 0:
129
+ raise gr.Error("Chunk length must be greater than 0 seconds.")
130
 
131
+ cleanup_tmpdirs()
132
 
133
  info = ffprobe_info(file_path)
134
  duration = info.get("duration_sec")
135
  src_kbps = to_kbps(info.get("bitrate_bps"))
136
 
137
  if duration is None:
138
+ raise gr.Error("Couldn't detect audio duration. Check ffmpeg/ffprobe and the file format.")
139
 
140
  # bitrate selection
141
  if quality_mode == "Auto (same as source)":
 
166
  )
167
  out_path = os.path.join(parts_dir, out_name)
168
 
 
169
  cmd = [
170
  "ffmpeg", "-y",
171
  "-ss", str(start),
 
184
  for p in created:
185
  zf.write(p, arcname=os.path.basename(p))
186
 
187
+ src_txt = f"{src_kbps} kbps" if src_kbps else "unknown"
188
  status = (
189
+ f"Input: {Path(file_path).name}\n"
190
+ f"Duration: {duration:.2f} sec\n"
191
+ f"Chunk length: {chunk_seconds} sec\n"
192
+ f"Parts: {total_parts}\n"
193
+ f"Source bitrate: {src_txt}\n"
194
+ f"Output: mp3\n"
195
+ f"Quality: {out_quality_text}\n"
196
+ f"ZIP is ready."
197
  )
198
 
199
  return zip_path, status
200
 
201
 
202
  # -------------------------
203
+ # UI styling
204
  # -------------------------
205
 
206
+ CSS = r"""
207
+ /* --------- Neon / Glass Theme (subtle animations) --------- */
208
+ :root{
209
+ --bg0:#05060a;
210
+ --bg1:#070a12;
211
+ --panel: rgba(255,255,255,0.06);
212
+ --panel2: rgba(0,0,0,0.25);
213
+ --stroke: rgba(255,255,255,0.14);
214
+ --txt: rgba(255,255,255,0.92);
215
+ --muted: rgba(255,255,255,0.70);
216
+
217
+ --neon1:#7c3aed; /* purple */
218
+ --neon2:#22d3ee; /* cyan */
219
+ --neon3:#f97316; /* orange */
220
+ --good:#34d399;
221
+ --bad:#fb7185;
222
+ }
223
 
224
+ .gradio-container {
225
+ color: var(--txt) !important;
226
+ background:
227
+ radial-gradient(900px 600px at 20% 10%, rgba(124,58,237,0.35), transparent 55%),
228
+ radial-gradient(700px 500px at 80% 20%, rgba(34,211,238,0.25), transparent 60%),
229
+ radial-gradient(700px 500px at 50% 90%, rgba(249,115,22,0.15), transparent 60%),
230
+ linear-gradient(180deg, var(--bg0), var(--bg1)) !important;
231
  }
 
232
 
233
+ #app {
234
+ max-width: 980px;
235
+ margin: 0 auto;
236
+ padding: 18px 14px 28px;
237
+ }
 
 
 
 
238
 
239
+ .glass {
240
+ background: var(--panel);
241
+ border: 1px solid var(--stroke);
242
+ border-radius: 18px;
243
+ padding: 16px;
244
+ backdrop-filter: blur(10px);
245
+ box-shadow:
246
+ 0 0 0 1px rgba(255,255,255,0.04) inset,
247
+ 0 14px 50px rgba(0,0,0,0.55);
248
+ }
249
 
250
+ .header {
251
+ border-radius: 18px;
252
+ padding: 16px 18px;
253
+ border: 1px solid rgba(255,255,255,0.12);
254
+ background:
255
+ linear-gradient(135deg, rgba(124,58,237,0.16), rgba(34,211,238,0.10) 55%, rgba(249,115,22,0.07));
256
+ box-shadow: 0 14px 50px rgba(0,0,0,0.55);
257
+ }
258
 
259
+ #title {
260
+ margin: 0;
261
+ font-size: 24px;
262
+ letter-spacing: 0.2px;
263
+ line-height: 1.15;
264
+ background: linear-gradient(90deg, var(--neon2), var(--neon1), var(--neon3));
265
+ background-size: 200% 100%;
266
+ -webkit-background-clip: text;
267
+ background-clip: text;
268
+ color: transparent;
269
+ animation: hueflow 6s ease-in-out infinite;
270
+ }
271
 
272
+ @keyframes hueflow {
273
+ 0% { background-position: 0% 50%; filter: drop-shadow(0 0 10px rgba(34,211,238,0.18)); }
274
+ 50% { background-position: 100% 50%; filter: drop-shadow(0 0 14px rgba(124,58,237,0.22)); }
275
+ 100% { background-position: 0% 50%; filter: drop-shadow(0 0 10px rgba(249,115,22,0.15)); }
276
+ }
277
 
278
+ .subtitle {
279
+ margin-top: 6px;
280
+ color: var(--muted);
281
+ font-size: 13px;
282
+ }
283
+
284
+ .smallnote {
285
+ color: var(--muted);
286
+ font-size: 13px;
287
+ }
288
+
289
+ /* Make inputs feel more "custom" */
290
+ .gradio-container input[type="text"],
291
+ .gradio-container input[type="number"],
292
+ .gradio-container textarea {
293
+ background: rgba(0,0,0,0.25) !important;
294
+ border: 1px solid rgba(255,255,255,0.14) !important;
295
+ border-radius: 14px !important;
296
+ }
297
+
298
+ .gradio-container label { color: var(--muted) !important; }
299
+
300
+ /* Button: neon glow + hover lift */
301
+ .gradio-container button.primary {
302
+ border-radius: 16px !important;
303
+ border: 1px solid rgba(255,255,255,0.16) !important;
304
+ background: linear-gradient(90deg, rgba(34,211,238,0.22), rgba(124,58,237,0.22), rgba(249,115,22,0.18)) !important;
305
+ box-shadow:
306
+ 0 10px 30px rgba(0,0,0,0.55),
307
+ 0 0 0 1px rgba(255,255,255,0.05) inset;
308
+ transition: transform .15s ease, box-shadow .15s ease, filter .15s ease;
309
+ }
310
+
311
+ .gradio-container button.primary:hover {
312
+ transform: translateY(-1px);
313
+ filter: brightness(1.05);
314
+ box-shadow:
315
+ 0 14px 44px rgba(0,0,0,0.60),
316
+ 0 0 22px rgba(34,211,238,0.12),
317
+ 0 0 24px rgba(124,58,237,0.10);
318
+ }
319
+
320
+ /* File input area a bit nicer */
321
+ .gradio-container .file-preview,
322
+ .gradio-container .upload-container {
323
+ border-radius: 16px !important;
324
+ border: 1px dashed rgba(255,255,255,0.18) !important;
325
+ background: rgba(0,0,0,0.16) !important;
326
+ }
327
 
328
+ /* Mobile: bigger text and spacing */
329
+ @media (max-width: 640px) {
330
+ #app { padding: 12px 10px 24px; }
331
+ #title { font-size: 20px; }
332
+ .subtitle, .smallnote { font-size: 13px; }
333
+ .glass { padding: 14px; }
334
+ .gradio-container button.primary { width: 100%; }
335
+ }
336
+ """
337
 
338
+
339
+ # -------------------------
340
+ # UI
341
+ # -------------------------
342
+
343
+ with gr.Blocks() as demo:
344
+ with gr.Column(elem_id="app"):
345
+ with gr.Column(elem_classes=["header"]):
346
+ gr.Markdown("## Neon Audio Splitter → ZIP", elem_id="title")
347
+ gr.Markdown(
348
+ "<div class='subtitle'>"
349
+ "Upload a large audio file, choose chunk length, optionally override bitrate, and download a ZIP of MP3 parts."
350
+ "</div>"
351
+ )
352
+
353
+ with gr.Column(elem_classes=["glass"]):
354
+ inp = gr.File(label="Audio file", file_count="single", type="filepath")
355
+
356
+ chunk_seconds = gr.Number(label="Chunk length (seconds)", value=10, precision=0)
357
+
358
+ quality_mode = gr.Radio(
359
+ label="Bitrate mode",
360
+ choices=["Auto (same as source)", "Custom (8..320 kbps)"],
361
+ value="Auto (same as source)"
362
+ )
363
+
364
+ custom_bitrate = gr.Slider(
365
+ label="Custom bitrate (kbps)",
366
+ minimum=8, maximum=320, value=192, step=1,
367
+ visible=False, interactive=True
368
+ )
369
+
370
+ gr.Markdown(
371
+ "<div class='smallnote'>"
372
+ "<b>Auto</b> tries to read the original bitrate via <code>ffprobe</code>. "
373
+ "If it can't be detected, it uses <b>192 kbps</b> as a fallback."
374
+ "</div>"
375
+ )
376
+
377
+ btn = gr.Button("Split & Download ZIP", variant="primary")
378
+
379
+ with gr.Column(elem_classes=["glass"]):
380
+ out_zip = gr.File(label="ZIP archive (download)")
381
+ status = gr.Textbox(label="Status", lines=8)
382
 
383
  def toggle_custom(mode):
384
+ return gr.update(visible=mode.startswith("Custom"))
385
 
386
  quality_mode.change(toggle_custom, inputs=quality_mode, outputs=custom_bitrate)
387
 
 
392
  )
393
 
394
  if __name__ == "__main__":
 
 
 
395
  demo.launch(
396
  server_name="0.0.0.0",
397
  server_port=7860,