Ryanus commited on
Commit
ebd5e0a
·
verified ·
1 Parent(s): 5cd4332

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +38 -48
app.py CHANGED
@@ -14,14 +14,12 @@ def generate_unique_filename(folder, prefix="audio", ext="mp3"):
14
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
15
  return os.path.join(folder, f"{prefix}_{timestamp}.{ext}")
16
 
17
- # Edge TTS 語音合成
18
  async def generate_speech(text, voice, rate, pitch, folder=AUDIO_DIR):
19
  output_file = generate_unique_filename(folder)
20
  communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
21
  await communicate.save(output_file)
22
  return output_file
23
 
24
- # 取得 Edge TTS 可用語音
25
  async def get_voices():
26
  voices = await edge_tts.list_voices()
27
  return [voice["ShortName"] for voice in voices]
@@ -34,7 +32,6 @@ def list_saved_podcasts():
34
  files = sorted(os.listdir(PODCAST_DIR), reverse=True)
35
  return [os.path.join(PODCAST_DIR, f) for f in files if f.endswith(".mp3")]
36
 
37
- # 單段語音合成介面
38
  async def tts_interface(text, voice, rate_percentage, pitch_hz):
39
  rate = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%"
40
  pitch = f"{'+' if pitch_hz >= 0 else ''}{pitch_hz}Hz"
@@ -44,7 +41,6 @@ async def tts_interface(text, voice, rate_percentage, pitch_hz):
44
  def play_saved_audio(audio_file):
45
  return audio_file
46
 
47
- # 播客製作:多段腳本合成並拼接、可插入背景音樂
48
  async def podcast_produce(script_list, voice, rate_percentage, pitch_hz, bgm_file, podcast_title, podcast_desc):
49
  rate = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%"
50
  pitch = f"{'+' if pitch_hz >= 0 else ''}{pitch_hz}Hz"
@@ -59,21 +55,17 @@ async def podcast_produce(script_list, voice, rate_percentage, pitch_hz, bgm_fil
59
  if not audio_segments:
60
  return None
61
  podcast_audio = sum(audio_segments)
62
- # 插入背景音樂(可選)
63
- if bgm_file is not None and os.path.isfile(bgm_file):
64
  bgm = AudioSegment.from_file(bgm_file.name).apply_gain(-10)
65
  bgm = bgm[:len(podcast_audio)]
66
  podcast_audio = podcast_audio.overlay(bgm)
67
- # 儲存播客音檔
68
  podcast_file = generate_unique_filename(PODCAST_DIR, prefix="podcast")
69
  podcast_audio.export(podcast_file, format="mp3")
70
- # 儲存元資料
71
  meta_file = podcast_file.replace(".mp3", ".txt")
72
  with open(meta_file, "w", encoding="utf-8") as f:
73
  f.write(f"Title: {podcast_title}\nDescription: {podcast_desc}\n")
74
  return podcast_file
75
 
76
- # 動態段落管理函數
77
  def add_paragraph(paragraphs):
78
  paragraphs.append("")
79
  return paragraphs
@@ -89,6 +81,10 @@ def clear_paragraphs():
89
  def clear_textbox():
90
  return ""
91
 
 
 
 
 
92
  async def main():
93
  voices = await get_voices()
94
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
@@ -117,24 +113,26 @@ async def main():
117
 
118
  with gr.Tab("播客製作"):
119
  gr.Markdown("### 📝 多段腳本輸入(可自由增減段落)")
120
- # 用 state 儲存段落文字
121
  paragraphs_state = gr.State([""])
122
- # 初始一個段落 Textbox
123
- paragraph_boxes = []
124
 
125
- def render_paragraphs(paragraphs):
126
- # 回傳多個 Textbox,value 對應段落內容
127
- return [gr.Textbox(value=p, label=f"段落{i+1}內容", lines=3, elem_id=f"para_{i}") for i, p in enumerate(paragraphs)]
 
 
 
 
 
128
 
129
- # 初始渲染
130
- paragraph_boxes = render_paragraphs(paragraphs_state.value)
131
 
132
  # 按鈕
133
  add_btn = gr.Button("新增段落")
134
  remove_btn = gr.Button("刪除段落")
135
  clear_all_btn = gr.Button("全部清空")
136
 
137
- # 更新段落文字用
138
  def update_paragraphs(*texts):
139
  return list(texts)
140
 
@@ -153,7 +151,6 @@ async def main():
153
  def on_clear():
154
  return [""]
155
 
156
- # 連結按鈕與狀態
157
  add_btn.click(on_add, inputs=paragraphs_state, outputs=paragraphs_state)
158
  remove_btn.click(on_remove, inputs=paragraphs_state, outputs=paragraphs_state)
159
  clear_all_btn.click(on_clear, outputs=paragraphs_state)
@@ -162,30 +159,9 @@ async def main():
162
  def on_text_change(*texts):
163
  return list(texts)
164
 
165
- # 這裡用一個 container 放段落 Textbox,之後會更新
166
- paragraphs_container = gr.Column()
167
- # 用函數渲染段落 Textbox
168
- def render_paragraphs_ui(paragraphs):
169
- # 清空 container
170
- paragraphs_container.clear()
171
- boxes = []
172
- for i, p in enumerate(paragraphs):
173
- tb = gr.Textbox(value=p, label=f"段落{i+1}內容", lines=3)
174
- boxes.append(tb)
175
- paragraphs_container.append(tb)
176
- return boxes
177
-
178
- # 初始化
179
- paragraph_boxes = render_paragraphs_ui(paragraphs_state.value)
180
-
181
- # 當 paragraphs_state 改變時,重新渲染段落輸入框
182
- def on_paragraphs_state_change(paragraphs):
183
- # 重新渲染
184
- nonlocal paragraph_boxes
185
- paragraph_boxes = render_paragraphs_ui(paragraphs)
186
- return paragraphs_state
187
-
188
- paragraphs_state.change(on_paragraphs_state_change, inputs=paragraphs_state, outputs=paragraphs_state)
189
 
190
  # 播客參數輸入
191
  voice_input2 = gr.Dropdown(voices, label="選擇語音", value="zh-CN-XiaoxiaoNeural")
@@ -197,13 +173,27 @@ async def main():
197
  podcast_btn = gr.Button("生成播客")
198
  podcast_output = gr.Audio(type="filepath", label="生成的播客音檔")
199
 
200
- # 合成播客按鈕事件
201
- def gather_paragraph_texts(*args):
202
- return list(args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
- # podcast_btn 觸發時,先收集所有段落文字,再呼叫播客合成
205
  def on_podcast_btn_click(*args):
206
- # args: 段落文字 + 參數
207
  n = len(paragraph_boxes)
208
  scripts = list(args[:n])
209
  voice = args[n]
 
14
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
15
  return os.path.join(folder, f"{prefix}_{timestamp}.{ext}")
16
 
 
17
  async def generate_speech(text, voice, rate, pitch, folder=AUDIO_DIR):
18
  output_file = generate_unique_filename(folder)
19
  communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
20
  await communicate.save(output_file)
21
  return output_file
22
 
 
23
  async def get_voices():
24
  voices = await edge_tts.list_voices()
25
  return [voice["ShortName"] for voice in voices]
 
32
  files = sorted(os.listdir(PODCAST_DIR), reverse=True)
33
  return [os.path.join(PODCAST_DIR, f) for f in files if f.endswith(".mp3")]
34
 
 
35
  async def tts_interface(text, voice, rate_percentage, pitch_hz):
36
  rate = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%"
37
  pitch = f"{'+' if pitch_hz >= 0 else ''}{pitch_hz}Hz"
 
41
  def play_saved_audio(audio_file):
42
  return audio_file
43
 
 
44
  async def podcast_produce(script_list, voice, rate_percentage, pitch_hz, bgm_file, podcast_title, podcast_desc):
45
  rate = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%"
46
  pitch = f"{'+' if pitch_hz >= 0 else ''}{pitch_hz}Hz"
 
55
  if not audio_segments:
56
  return None
57
  podcast_audio = sum(audio_segments)
58
+ if bgm_file is not None and os.path.isfile(bgm_file.name):
 
59
  bgm = AudioSegment.from_file(bgm_file.name).apply_gain(-10)
60
  bgm = bgm[:len(podcast_audio)]
61
  podcast_audio = podcast_audio.overlay(bgm)
 
62
  podcast_file = generate_unique_filename(PODCAST_DIR, prefix="podcast")
63
  podcast_audio.export(podcast_file, format="mp3")
 
64
  meta_file = podcast_file.replace(".mp3", ".txt")
65
  with open(meta_file, "w", encoding="utf-8") as f:
66
  f.write(f"Title: {podcast_title}\nDescription: {podcast_desc}\n")
67
  return podcast_file
68
 
 
69
  def add_paragraph(paragraphs):
70
  paragraphs.append("")
71
  return paragraphs
 
81
  def clear_textbox():
82
  return ""
83
 
84
+ def render_paragraphs(paragraphs):
85
+ # 回傳一組 Textbox 的 dict,方便動態更新
86
+ return {f"para_{i}": p for i, p in enumerate(paragraphs)}
87
+
88
  async def main():
89
  voices = await get_voices()
90
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
 
113
 
114
  with gr.Tab("播客製作"):
115
  gr.Markdown("### 📝 多段腳本輸入(可自由增減段落)")
116
+
117
  paragraphs_state = gr.State([""])
 
 
118
 
119
+ # 動態產生多個 Textbox,並用 dictionary 存放元件
120
+ paragraph_textboxes = {}
121
+
122
+ def render_paragraph_boxes(paragraphs):
123
+ boxes = {}
124
+ for i, p in enumerate(paragraphs):
125
+ boxes[f"para_{i}"] = gr.Textbox(value=p, label=f"段落{i+1}內容", lines=3)
126
+ return boxes
127
 
128
+ paragraph_textboxes = render_paragraph_boxes(paragraphs_state.value)
 
129
 
130
  # 按鈕
131
  add_btn = gr.Button("新增段落")
132
  remove_btn = gr.Button("刪除段落")
133
  clear_all_btn = gr.Button("全部清空")
134
 
135
+ # 更新段落文字
136
  def update_paragraphs(*texts):
137
  return list(texts)
138
 
 
151
  def on_clear():
152
  return [""]
153
 
 
154
  add_btn.click(on_add, inputs=paragraphs_state, outputs=paragraphs_state)
155
  remove_btn.click(on_remove, inputs=paragraphs_state, outputs=paragraphs_state)
156
  clear_all_btn.click(on_clear, outputs=paragraphs_state)
 
159
  def on_text_change(*texts):
160
  return list(texts)
161
 
162
+ # 將段落文字元件放入列表,方便傳入合成函數
163
+ def get_paragraph_values(*args):
164
+ return list(args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  # 播客參數輸入
167
  voice_input2 = gr.Dropdown(voices, label="選擇語音", value="zh-CN-XiaoxiaoNeural")
 
173
  podcast_btn = gr.Button("生成播客")
174
  podcast_output = gr.Audio(type="filepath", label="生成的播客音檔")
175
 
176
+ # 動態建立段落輸入元件區塊
177
+ with gr.Column() as paragraphs_container:
178
+ def build_paragraph_boxes(paragraphs):
179
+ boxes = []
180
+ for i, p in enumerate(paragraphs):
181
+ tb = gr.Textbox(value=p, label=f"段落{i+1}內容", lines=3)
182
+ boxes.append(tb)
183
+ return boxes
184
+
185
+ paragraph_boxes = build_paragraph_boxes(paragraphs_state.value)
186
+
187
+ # 重新渲染段落輸入框
188
+ def rerender_paragraph_boxes(paragraphs):
189
+ nonlocal paragraph_boxes
190
+ paragraph_boxes = build_paragraph_boxes(paragraphs)
191
+ return paragraph_boxes
192
+
193
+ paragraphs_state.change(rerender_paragraph_boxes, inputs=paragraphs_state, outputs=paragraph_boxes)
194
 
195
+ # 播客合成按鈕事件
196
  def on_podcast_btn_click(*args):
 
197
  n = len(paragraph_boxes)
198
  scripts = list(args[:n])
199
  voice = args[n]