Update app.py
Browse files
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 |
-
|
| 121 |
paragraphs_state = gr.State([""])
|
| 122 |
-
# 初始一個段落 Textbox
|
| 123 |
-
paragraph_boxes = []
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 166 |
-
|
| 167 |
-
|
| 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 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
#
|
| 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]
|