saltudio commited on
Commit
b471059
·
verified ·
1 Parent(s): 80d374c
Files changed (1) hide show
  1. app.py +81 -35
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 start_ms, end_ms in silent_regions:
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 generate_timecodes(
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
- # ---------- UI ----------
109
- with gr.Blocks(title="YouTube Timecode Generator | City Pop Edition") as demo:
110
- gr.Markdown(
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
- with gr.Row():
119
- audio_file = gr.File(label="Upload audio (mp3/wav/flac)", file_types=[".mp3", ".wav", ".flac", ".m4a"])
120
- mode = gr.Radio(choices=["Auto (detect silence)", "Manual (comma-separated starts)"], value="Auto (detect silence)", label="Mode")
121
 
122
- with gr.Row():
123
- 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)
124
- titles_text = gr.Textbox(label="Titles (one per line, optional)", placeholder="Overture\nShadow Love\nOffice Groove\n...")
 
 
 
 
 
 
125
 
126
- with gr.Accordion("Auto-detect parameters", open=False):
127
- silence_thresh = gr.Slider(-60, 0, value=-30, step=1, label="Silence threshold (dBFS)")
128
- min_silence_ms = gr.Slider(200, 4000, value=1500, step=50, label="Minimum silence length (ms)")
129
- seek_step_ms = gr.Slider(1, 30, value=5, step=1, label="Seek step (ms)")
130
 
131
- run_btn = gr.Button("Generate Timecodes", variant="primary")
132
- output_text = gr.Textbox(label="Timecodes", lines=18)
133
- download = gr.File(label="Download timecodes.txt")
134
 
135
- run_btn.click(
136
- fn=generate_timecodes,
137
- inputs=[audio_file, mode, manual_starts_text, titles_text, silence_thresh, min_silence_ms, seek_step_ms],
138
- outputs=[output_text, download]
 
 
 
 
 
 
 
 
 
 
139
  )
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  gr.Markdown(
142
- "Tips:\n"
143
- "- Live/concert audio naikkan `Minimum silence` & turunkan `Silence threshold` agar tidak sensitif.\n"
144
- "- Studio mix yang keras coba `silence-threshold -26` s.d. `-22`.\n"
145
- "- Manual mode paling akurat untuk album tanpa jeda hening."
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__":