Update app.py
Browse files
app.py
CHANGED
|
@@ -41,11 +41,11 @@ async def tts_interface(text, voice, rate_percentage, pitch_hz):
|
|
| 41 |
def play_saved_audio(audio_file):
|
| 42 |
return audio_file
|
| 43 |
|
| 44 |
-
async def podcast_produce(
|
| 45 |
rate = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%"
|
| 46 |
pitch = f"{'+' if pitch_hz >= 0 else ''}{pitch_hz}Hz"
|
| 47 |
audio_segments = []
|
| 48 |
-
for idx, text in enumerate(
|
| 49 |
if text.strip():
|
| 50 |
temp_audio = generate_unique_filename(PODCAST_DIR, prefix=f"segment{idx}")
|
| 51 |
communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
|
|
@@ -55,7 +55,7 @@ async def podcast_produce(script_list, voice, rate_percentage, pitch_hz, bgm_fil
|
|
| 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)
|
|
@@ -66,24 +66,30 @@ async def podcast_produce(script_list, voice, rate_percentage, pitch_hz, bgm_fil
|
|
| 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
|
| 72 |
|
| 73 |
def remove_paragraph(paragraphs):
|
|
|
|
| 74 |
if len(paragraphs) > 1:
|
| 75 |
paragraphs.pop()
|
| 76 |
return paragraphs
|
| 77 |
|
| 78 |
-
def
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def clear_textbox():
|
| 82 |
-
return ""
|
| 83 |
|
| 84 |
-
def
|
| 85 |
-
#
|
| 86 |
-
return
|
| 87 |
|
| 88 |
async def main():
|
| 89 |
voices = await get_voices()
|
|
@@ -113,57 +119,35 @@ async def main():
|
|
| 113 |
|
| 114 |
with gr.Tab("播客製作"):
|
| 115 |
gr.Markdown("### 📝 多段腳本輸入(可自由增減段落)")
|
| 116 |
-
|
| 117 |
paragraphs_state = gr.State([""])
|
|
|
|
| 118 |
|
| 119 |
-
#
|
| 120 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 139 |
-
# 新增段落
|
| 140 |
def on_add(paragraphs):
|
| 141 |
-
paragraphs
|
| 142 |
-
return
|
| 143 |
|
| 144 |
-
# 刪除段落
|
| 145 |
def on_remove(paragraphs):
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
return paragraphs
|
| 149 |
|
| 150 |
-
# 全部清空
|
| 151 |
def on_clear():
|
| 152 |
-
|
|
|
|
| 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)
|
| 157 |
-
|
| 158 |
-
# 監聽段落文字改變,更新狀態
|
| 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")
|
| 168 |
rate_input2 = gr.Slider(-50, 50, value=0, step=1, label="語速調整 (%)")
|
| 169 |
pitch_input2 = gr.Slider(-50, 50, value=0, step=1, label="音高調整 (Hz)")
|
|
@@ -173,28 +157,12 @@ async def main():
|
|
| 173 |
podcast_btn = gr.Button("生成播客")
|
| 174 |
podcast_output = gr.Audio(type="filepath", label="生成的播客音檔")
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 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 |
-
|
|
|
|
| 198 |
scripts = list(args[:n])
|
| 199 |
voice = args[n]
|
| 200 |
rate = args[n+1]
|
|
@@ -204,8 +172,15 @@ async def main():
|
|
| 204 |
desc = args[n+5]
|
| 205 |
return asyncio.run(podcast_produce(scripts, voice, rate, pitch, bgm, title, desc))
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
with gr.Tab("檢視已儲存播客"):
|
| 211 |
podcast_files = gr.Dropdown(list_saved_podcasts(), label="選擇已儲存播客檔案", interactive=True)
|
|
|
|
| 41 |
def play_saved_audio(audio_file):
|
| 42 |
return audio_file
|
| 43 |
|
| 44 |
+
async def podcast_produce(scripts, 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"
|
| 47 |
audio_segments = []
|
| 48 |
+
for idx, text in enumerate(scripts):
|
| 49 |
if text.strip():
|
| 50 |
temp_audio = generate_unique_filename(PODCAST_DIR, prefix=f"segment{idx}")
|
| 51 |
communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
|
|
|
|
| 55 |
if not audio_segments:
|
| 56 |
return None
|
| 57 |
podcast_audio = sum(audio_segments)
|
| 58 |
+
if bgm_file is not None and hasattr(bgm_file, "name") 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)
|
|
|
|
| 66 |
f.write(f"Title: {podcast_title}\nDescription: {podcast_desc}\n")
|
| 67 |
return podcast_file
|
| 68 |
|
| 69 |
+
def clear_textbox():
|
| 70 |
+
return ""
|
| 71 |
+
|
| 72 |
+
def clear_paragraphs():
|
| 73 |
+
return [""]
|
| 74 |
+
|
| 75 |
def add_paragraph(paragraphs):
|
| 76 |
+
paragraphs = paragraphs.copy()
|
| 77 |
paragraphs.append("")
|
| 78 |
return paragraphs
|
| 79 |
|
| 80 |
def remove_paragraph(paragraphs):
|
| 81 |
+
paragraphs = paragraphs.copy()
|
| 82 |
if len(paragraphs) > 1:
|
| 83 |
paragraphs.pop()
|
| 84 |
return paragraphs
|
| 85 |
|
| 86 |
+
def update_paragraphs_ui(paragraphs):
|
| 87 |
+
# 回傳一組 Textbox 元件
|
| 88 |
+
return [gr.Textbox(value=p, label=f"段落{i+1}內容", lines=3, interactive=True) for i, p in enumerate(paragraphs)]
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
def collect_paragraphs(*args):
|
| 91 |
+
# 收集所有段落內容
|
| 92 |
+
return list(args)
|
| 93 |
|
| 94 |
async def main():
|
| 95 |
voices = await get_voices()
|
|
|
|
| 119 |
|
| 120 |
with gr.Tab("播客製作"):
|
| 121 |
gr.Markdown("### 📝 多段腳本輸入(可自由增減段落)")
|
|
|
|
| 122 |
paragraphs_state = gr.State([""])
|
| 123 |
+
paragraphs_container = gr.Column()
|
| 124 |
|
| 125 |
+
# 初始渲染
|
| 126 |
+
paragraph_boxes = update_paragraphs_ui([""])
|
| 127 |
+
for tb in paragraph_boxes:
|
| 128 |
+
paragraphs_container.append(tb)
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
add_btn = gr.Button("新增段落")
|
| 131 |
remove_btn = gr.Button("刪除段落")
|
| 132 |
clear_all_btn = gr.Button("全部清空")
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def on_add(paragraphs):
|
| 135 |
+
new_paragraphs = add_paragraph(paragraphs)
|
| 136 |
+
return new_paragraphs, gr.update(components=update_paragraphs_ui(new_paragraphs))
|
| 137 |
|
|
|
|
| 138 |
def on_remove(paragraphs):
|
| 139 |
+
new_paragraphs = remove_paragraph(paragraphs)
|
| 140 |
+
return new_paragraphs, gr.update(components=update_paragraphs_ui(new_paragraphs))
|
|
|
|
| 141 |
|
|
|
|
| 142 |
def on_clear():
|
| 143 |
+
new_paragraphs = clear_paragraphs()
|
| 144 |
+
return new_paragraphs, gr.update(components=update_paragraphs_ui(new_paragraphs))
|
| 145 |
|
| 146 |
+
add_btn.click(on_add, inputs=paragraphs_state, outputs=[paragraphs_state, paragraphs_container])
|
| 147 |
+
remove_btn.click(on_remove, inputs=paragraphs_state, outputs=[paragraphs_state, paragraphs_container])
|
| 148 |
+
clear_all_btn.click(on_clear, outputs=[paragraphs_state, paragraphs_container])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
# 參數設定
|
| 151 |
voice_input2 = gr.Dropdown(voices, label="選擇語音", value="zh-CN-XiaoxiaoNeural")
|
| 152 |
rate_input2 = gr.Slider(-50, 50, value=0, step=1, label="語速調整 (%)")
|
| 153 |
pitch_input2 = gr.Slider(-50, 50, value=0, step=1, label="音高調整 (Hz)")
|
|
|
|
| 157 |
podcast_btn = gr.Button("生成播客")
|
| 158 |
podcast_output = gr.Audio(type="filepath", label="生成的播客音檔")
|
| 159 |
|
| 160 |
+
def gather_scripts(*args):
|
| 161 |
+
return list(args)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
|
|
|
| 163 |
def on_podcast_btn_click(*args):
|
| 164 |
+
# args: 段落內容 + 參數
|
| 165 |
+
n = len(paragraphs_state.value)
|
| 166 |
scripts = list(args[:n])
|
| 167 |
voice = args[n]
|
| 168 |
rate = args[n+1]
|
|
|
|
| 172 |
desc = args[n+5]
|
| 173 |
return asyncio.run(podcast_produce(scripts, voice, rate, pitch, bgm, title, desc))
|
| 174 |
|
| 175 |
+
# 這裡需要用最新的段落數來組合 inputs
|
| 176 |
+
def get_inputs():
|
| 177 |
+
return [tb for tb in paragraphs_container.children] + [voice_input2, rate_input2, pitch_input2, bgm_input, podcast_title, podcast_desc]
|
| 178 |
+
|
| 179 |
+
podcast_btn.click(
|
| 180 |
+
fn=on_podcast_btn_click,
|
| 181 |
+
inputs=lambda: get_inputs(),
|
| 182 |
+
outputs=podcast_output
|
| 183 |
+
)
|
| 184 |
|
| 185 |
with gr.Tab("檢視已儲存播客"):
|
| 186 |
podcast_files = gr.Dropdown(list_saved_podcasts(), label="選擇已儲存播客檔案", interactive=True)
|