Spaces:
Sleeping
Sleeping
up logic
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import tempfile
|
| 3 |
import os
|
|
|
|
| 4 |
from typing import List, Optional, Tuple
|
| 5 |
from pydub import AudioSegment
|
| 6 |
from pydub.silence import detect_silence
|
|
@@ -26,6 +27,15 @@ def ms_to_hhmmss(ms: int) -> str:
|
|
| 26 |
s = seconds % 60
|
| 27 |
return f"{h:02d}:{m:02d}:{s:02d}" if h > 0 else f"{m:02d}:{s:02d}"
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def read_titles_from_text(titles_text: Optional[str], n_tracks: int) -> List[str]:
|
| 30 |
if not titles_text:
|
| 31 |
return [f"Track {i+1:02d}" for i in range(n_tracks)]
|
|
@@ -43,7 +53,7 @@ def detect_starts_by_silence(audio: AudioSegment, silence_thresh_db: float, min_
|
|
| 43 |
)
|
| 44 |
starts = [0]
|
| 45 |
silent_regions.sort(key=lambda x: x[0])
|
| 46 |
-
for
|
| 47 |
if end_ms - starts[-1] > 1000:
|
| 48 |
starts.append(end_ms)
|
| 49 |
starts = sorted(set(starts))
|
|
@@ -59,7 +69,7 @@ def clamp_starts(starts: List[int], audio_len: int) -> List[int]:
|
|
| 59 |
return clean
|
| 60 |
|
| 61 |
# ---------- Core ----------
|
| 62 |
-
def
|
| 63 |
audio_file,
|
| 64 |
mode,
|
| 65 |
manual_starts_text,
|
|
@@ -71,7 +81,6 @@ def generate_timecodes(
|
|
| 71 |
if audio_file is None:
|
| 72 |
return "Please upload an audio file.", None
|
| 73 |
|
| 74 |
-
# Load audio with pydub/ffmpeg
|
| 75 |
audio = AudioSegment.from_file(audio_file.name)
|
| 76 |
audio_len = len(audio)
|
| 77 |
|
|
@@ -91,13 +100,11 @@ def generate_timecodes(
|
|
| 91 |
starts = clamp_starts(starts, audio_len)
|
| 92 |
titles = read_titles_from_text(titles_text, len(starts))
|
| 93 |
|
| 94 |
-
# Compose output lines
|
| 95 |
lines = []
|
| 96 |
for i, start_ms in enumerate(starts):
|
| 97 |
ts = ms_to_hhmmss(start_ms)
|
| 98 |
lines.append(f"{ts} – {titles[i]}")
|
| 99 |
|
| 100 |
-
# Save to temp file
|
| 101 |
tmpdir = tempfile.mkdtemp()
|
| 102 |
out_path = os.path.join(tmpdir, "timecodes.txt")
|
| 103 |
with open(out_path, "w", encoding="utf-8") as f:
|
|
@@ -105,44 +112,83 @@ def generate_timecodes(
|
|
| 105 |
|
| 106 |
return "\n".join(lines), out_path
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
"## 🎧 YouTube Timecode Generator\n"
|
| 112 |
-
"Generate **YouTube-style timecodes** from a long audio file.\n"
|
| 113 |
-
"- **Auto mode**: detect track starts from silence (adjust thresholds).\n"
|
| 114 |
-
"- **Manual mode**: enter start times like `0, 215, 07:10, 12:35`.\n"
|
| 115 |
-
"- Paste a **title list** (one per line) to label tracks."
|
| 116 |
-
)
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
mode = gr.Radio(choices=["Auto (detect silence)", "Manual (comma-separated starts)"], value="Auto (detect silence)", label="Mode")
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
gr.Markdown(
|
| 142 |
-
"Tips
|
| 143 |
-
"-
|
| 144 |
-
"-
|
| 145 |
-
"-
|
| 146 |
)
|
| 147 |
|
| 148 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import tempfile
|
| 3 |
import os
|
| 4 |
+
import re
|
| 5 |
from typing import List, Optional, Tuple
|
| 6 |
from pydub import AudioSegment
|
| 7 |
from pydub.silence import detect_silence
|
|
|
|
| 27 |
s = seconds % 60
|
| 28 |
return f"{h:02d}:{m:02d}:{s:02d}" if h > 0 else f"{m:02d}:{s:02d}"
|
| 29 |
|
| 30 |
+
def clean_title_from_filename(name: str) -> str:
|
| 31 |
+
# remove extension
|
| 32 |
+
base = os.path.splitext(os.path.basename(name))[0]
|
| 33 |
+
# remove leading track numbers and separators like "01. " / "1 - " / "01_" etc
|
| 34 |
+
base = re.sub(r"^\s*\d{1,3}[\.\-\)_\s]+", "", base)
|
| 35 |
+
# squeeze spaces
|
| 36 |
+
base = re.sub(r"\s{2,}", " ", base).strip()
|
| 37 |
+
return base
|
| 38 |
+
|
| 39 |
def read_titles_from_text(titles_text: Optional[str], n_tracks: int) -> List[str]:
|
| 40 |
if not titles_text:
|
| 41 |
return [f"Track {i+1:02d}" for i in range(n_tracks)]
|
|
|
|
| 53 |
)
|
| 54 |
starts = [0]
|
| 55 |
silent_regions.sort(key=lambda x: x[0])
|
| 56 |
+
for _, end_ms in silent_regions:
|
| 57 |
if end_ms - starts[-1] > 1000:
|
| 58 |
starts.append(end_ms)
|
| 59 |
starts = sorted(set(starts))
|
|
|
|
| 69 |
return clean
|
| 70 |
|
| 71 |
# ---------- Core ----------
|
| 72 |
+
def generate_timecodes_single(
|
| 73 |
audio_file,
|
| 74 |
mode,
|
| 75 |
manual_starts_text,
|
|
|
|
| 81 |
if audio_file is None:
|
| 82 |
return "Please upload an audio file.", None
|
| 83 |
|
|
|
|
| 84 |
audio = AudioSegment.from_file(audio_file.name)
|
| 85 |
audio_len = len(audio)
|
| 86 |
|
|
|
|
| 100 |
starts = clamp_starts(starts, audio_len)
|
| 101 |
titles = read_titles_from_text(titles_text, len(starts))
|
| 102 |
|
|
|
|
| 103 |
lines = []
|
| 104 |
for i, start_ms in enumerate(starts):
|
| 105 |
ts = ms_to_hhmmss(start_ms)
|
| 106 |
lines.append(f"{ts} – {titles[i]}")
|
| 107 |
|
|
|
|
| 108 |
tmpdir = tempfile.mkdtemp()
|
| 109 |
out_path = os.path.join(tmpdir, "timecodes.txt")
|
| 110 |
with open(out_path, "w", encoding="utf-8") as f:
|
|
|
|
| 112 |
|
| 113 |
return "\n".join(lines), out_path
|
| 114 |
|
| 115 |
+
def generate_timecodes_multi(files, titles_text, natural_sort=True) -> Tuple[str, str]:
|
| 116 |
+
if not files:
|
| 117 |
+
return "Please upload multiple audio files.", None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
# Sort files by filename for consistent album order
|
| 120 |
+
file_objs = sorted(files, key=lambda f: f.name if natural_sort else files.index(f))
|
|
|
|
| 121 |
|
| 122 |
+
# Calculate cumulative starts
|
| 123 |
+
starts_ms = []
|
| 124 |
+
titles = []
|
| 125 |
+
cursor = 0
|
| 126 |
+
for f in file_objs:
|
| 127 |
+
starts_ms.append(cursor)
|
| 128 |
+
titles.append(clean_title_from_filename(f.name))
|
| 129 |
+
seg = AudioSegment.from_file(f.name)
|
| 130 |
+
cursor += len(seg)
|
| 131 |
|
| 132 |
+
# If user provided custom titles, override
|
| 133 |
+
custom_titles = read_titles_from_text(titles_text, len(titles))
|
| 134 |
+
if titles_text:
|
| 135 |
+
titles = custom_titles
|
| 136 |
|
| 137 |
+
lines = []
|
| 138 |
+
for ts_ms, title in zip(starts_ms, titles):
|
| 139 |
+
lines.append(f"{ms_to_hhmmss(ts_ms)} – {title}")
|
| 140 |
|
| 141 |
+
tmpdir = tempfile.mkdtemp()
|
| 142 |
+
out_path = os.path.join(tmpdir, "timecodes.txt")
|
| 143 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 144 |
+
f.write("\n".join(lines))
|
| 145 |
+
|
| 146 |
+
return "\n".join(lines), out_path
|
| 147 |
+
|
| 148 |
+
# ---------- UI ----------
|
| 149 |
+
with gr.Blocks(title="YouTube Timecode Generator | Single or Multiple Audio") as demo:
|
| 150 |
+
gr.Markdown(
|
| 151 |
+
"## 🎧 YouTube Timecode Generator\n"
|
| 152 |
+
"Make YouTube-style **timecodes** from either **one long audio** or **multiple track files**.\n"
|
| 153 |
+
"- **Multiple Files**: upload each song file → timecodes use cumulative Durations.\n"
|
| 154 |
+
"- **Single File**: auto-detect with silence or enter manual start times.\n"
|
| 155 |
)
|
| 156 |
|
| 157 |
+
with gr.Tabs():
|
| 158 |
+
with gr.Tab("Multiple Files (recommended)"):
|
| 159 |
+
multi_files = gr.Files(label="Upload multiple audio files (mp3/wav/flac/m4a)", file_types=[".mp3", ".wav", ".flac", ".m4a"])
|
| 160 |
+
multi_titles = gr.Textbox(label="Optional: Titles (one per line; leave blank to use file names)", lines=6, placeholder="Morning Light\nDaydream Train\n...")
|
| 161 |
+
gen_multi = gr.Button("Generate Timecodes", variant="primary")
|
| 162 |
+
out_multi = gr.Textbox(label="Timecodes", lines=18)
|
| 163 |
+
dl_multi = gr.File(label="Download timecodes.txt")
|
| 164 |
+
gen_multi.click(fn=generate_timecodes_multi, inputs=[multi_files, multi_titles], outputs=[out_multi, dl_multi])
|
| 165 |
+
|
| 166 |
+
with gr.Tab("Single File (auto/manual)"):
|
| 167 |
+
audio_file = gr.File(label="Upload audio (mp3/wav/flac/m4a)", file_types=[".mp3", ".wav", ".flac", ".m4a"])
|
| 168 |
+
mode = gr.Radio(choices=["Auto (detect silence)", "Manual (comma-separated starts)"], value="Auto (detect silence)", label="Mode")
|
| 169 |
+
manual_starts_text = gr.Textbox(label="Manual starts (seconds or mm:ss or hh:mm:ss, comma-separated)", placeholder="0, 215, 07:10, 12:35", lines=2)
|
| 170 |
+
titles_text = gr.Textbox(label="Titles (one per line, optional)", placeholder="Overture\nShadow Love\nOffice Groove\n...", lines=6)
|
| 171 |
+
|
| 172 |
+
with gr.Accordion("Auto-detect parameters", open=False):
|
| 173 |
+
silence_thresh = gr.Slider(-60, 0, value=-30, step=1, label="Silence threshold (dBFS)")
|
| 174 |
+
min_silence_ms = gr.Slider(200, 4000, value=1500, step=50, label="Minimum silence length (ms)")
|
| 175 |
+
seek_step_ms = gr.Slider(1, 30, value=5, step=1, label="Seek step (ms)")
|
| 176 |
+
|
| 177 |
+
gen_single = gr.Button("Generate Timecodes", variant="primary")
|
| 178 |
+
out_single = gr.Textbox(label="Timecodes", lines=18)
|
| 179 |
+
dl_single = gr.File(label="Download timecodes.txt")
|
| 180 |
+
|
| 181 |
+
gen_single.click(
|
| 182 |
+
fn=generate_timecodes_single,
|
| 183 |
+
inputs=[audio_file, mode, manual_starts_text, titles_text, silence_thresh, min_silence_ms, seek_step_ms],
|
| 184 |
+
outputs=[out_single, dl_single]
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
gr.Markdown(
|
| 188 |
+
"### Tips\n"
|
| 189 |
+
"- **Multiple files** cocok untuk folder lagu terpisah (paling mudah & akurat).\n"
|
| 190 |
+
"- **Auto mode**: untuk satu file album; naikkan `Minimum silence` & turunkan `Silence threshold` bila live/crowded.\n"
|
| 191 |
+
"- Output sudah siap **copy–paste** ke deskripsi YouTube."
|
| 192 |
)
|
| 193 |
|
| 194 |
if __name__ == "__main__":
|