tbdavid2019 commited on
Commit
b199afe
·
1 Parent(s): 0bbe763

feat: 更新對話生成邏輯,增加分批生成機制及品質檢查,並優化提示詞模板

Browse files
Files changed (3) hide show
  1. __pycache__/app.cpython-311.pyc +0 -0
  2. app.py +198 -30
  3. prompts.py +17 -5
__pycache__/app.cpython-311.pyc CHANGED
Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ
 
app.py CHANGED
@@ -89,24 +89,15 @@ def generate_dialogue_via_requests(
89
 
90
  logger.info(f"輸入文本長度: {len(pdf_text)} 字符")
91
 
92
- # 基本提示詞
93
- base_prompt = f"""
94
- 以下是從 PDF 中擷取的文字內容,請參考並納入對話:
95
- ================================
96
- {pdf_text}
97
- ================================
98
- {intro_instructions}
99
- {text_instructions}
100
- <scratchpad>
101
- {scratch_pad_instructions}
102
- </scratchpad>
103
- {prelude_dialog}
104
- <podcast_dialogue>
105
- {podcast_dialog_instructions}
106
- </podcast_dialogue>
107
- {edited_transcript or ""}
108
- {user_feedback or ""}
109
- """
110
 
111
  headers = {
112
  "Authorization": f"Bearer {llm_api_key}",
@@ -124,7 +115,7 @@ def generate_dialogue_via_requests(
124
  max_retries = 5
125
  retry_delay = 5
126
 
127
- # 暫時使用簡化的單次生成
128
  payload = {
129
  "model": model,
130
  "messages": [
@@ -134,7 +125,8 @@ def generate_dialogue_via_requests(
134
  }
135
  ],
136
  "temperature": 0.7,
137
- "max_tokens": 8192
 
138
  }
139
 
140
  for attempt in range(max_retries):
@@ -168,10 +160,40 @@ def generate_dialogue_via_requests(
168
  if progress_callback:
169
  progress_callback("已成功從 LLM 獲取回應")
170
 
171
- # 進行品質檢查
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
 
173
  quality_report = quality_checker.check_dialogue_quality(generated_content, ['speaker-1', 'speaker-2'])
174
- logger.info(f"品質檢查分數: {quality_report.overall_score:.1f}")
175
  if progress_callback:
176
  progress_callback(f"品質檢查完成,分數: {quality_report.overall_score:.1f}/100")
177
  except Exception as e:
@@ -200,6 +222,147 @@ def generate_dialogue_via_requests(
200
  return "生成失敗"
201
 
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  def validate_and_generate_script(
204
  files,
205
  openai_api_key,
@@ -403,35 +566,40 @@ with gr.Blocks(title="Script Generator", css="""
403
  label="介紹提示詞 | Intro Instructions",
404
  lines=5,
405
  value=INSTRUCTION_TEMPLATES["podcast"]["intro"],
406
- interactive=True
 
407
  )
408
 
409
  text_instructions = gr.Textbox(
410
  label="文本分析提示詞 | Text Instructions",
411
  lines=5,
412
  value=INSTRUCTION_TEMPLATES["podcast"]["text_instructions"],
413
- interactive=True
 
414
  )
415
 
416
  scratch_pad = gr.Textbox(
417
  label="腦力激盪提示詞 | Scratch Pad",
418
  lines=5,
419
  value=INSTRUCTION_TEMPLATES["podcast"]["scratch_pad"],
420
- interactive=True
 
421
  )
422
 
423
  prelude = gr.Textbox(
424
  label="前導提示詞 | Prelude",
425
  lines=5,
426
  value=INSTRUCTION_TEMPLATES["podcast"]["prelude"],
427
- interactive=True
 
428
  )
429
 
430
  dialog = gr.Textbox(
431
- label="對話提示詞 | Dialog Instructions",
432
- lines=5,
433
- value=INSTRUCTION_TEMPLATES["podcast"]["dialog"],
434
- interactive=True
 
435
  )
436
 
437
  custom_prompt = gr.Textbox(
 
89
 
90
  logger.info(f"輸入文本長度: {len(pdf_text)} 字符")
91
 
92
+ # 使用簡化的模板化提示詞
93
+ base_prompt = podcast_dialog_instructions.format(content=pdf_text)
94
+
95
+ # 如果有自定義提示詞或編輯過的文稿,添加到提示中
96
+ if user_feedback:
97
+ base_prompt += f"\n\n【額外要求】\n{user_feedback}"
98
+
99
+ if edited_transcript:
100
+ base_prompt += f"\n\n【參考文稿】\n{edited_transcript}"
 
 
 
 
 
 
 
 
 
101
 
102
  headers = {
103
  "Authorization": f"Bearer {llm_api_key}",
 
115
  max_retries = 5
116
  retry_delay = 5
117
 
118
+ # 使用 Gemini Flash 2.5 的最大輸出 token 限制
119
  payload = {
120
  "model": model,
121
  "messages": [
 
125
  }
126
  ],
127
  "temperature": 0.7,
128
+ "max_tokens": 65536, # Gemini Flash 2.5 最大輸出 token 數
129
+ "stream": False # 先不用流式,確保穩定性
130
  }
131
 
132
  for attempt in range(max_retries):
 
160
  if progress_callback:
161
  progress_callback("已成功從 LLM 獲取回應")
162
 
163
+ # 檢查內容是否被截斷(更精確的檢查方式)
164
+ content_lines = generated_content.strip().split('\n')
165
+ last_line = content_lines[-1] if content_lines else ""
166
+
167
+ # 更精確的截斷檢測
168
+ is_truncated = (
169
+ len(generated_content) < 2000 or # 內容太短
170
+ not last_line.strip() or # 最後一行為空
171
+ (last_line.startswith('speaker-') and len(last_line.split(':', 1)) > 1 and
172
+ len(last_line.split(':', 1)[1].strip()) < 10) or # speaker 行內容太短
173
+ generated_content.strip().endswith(('在', '的', '了', '是', '會', '但', '因為', '所以', '這', '那'))
174
+ )
175
+
176
+ if is_truncated:
177
+ logger.warning("檢測到內容可能被截斷,嘗試分批生成...")
178
+ if progress_callback:
179
+ progress_callback("檢測到內容可能被截斷,嘗試分批生成...")
180
+
181
+ # 如果內容被截斷,使用分批生成
182
+ full_content = _generate_in_batches(
183
+ pdf_text, base_prompt, headers, url, model, num_parts,
184
+ progress_callback, max_retries, retry_delay
185
+ )
186
+ if full_content:
187
+ generated_content = full_content
188
+ logger.info("使用分批生成成功獲得完整內容")
189
+ else:
190
+ logger.warning("分批生成失敗,使用原始內容")
191
+
192
+ # **對完整文稿進行品質檢查**
193
  try:
194
+ logger.info("開始進行對話品質檢查")
195
  quality_report = quality_checker.check_dialogue_quality(generated_content, ['speaker-1', 'speaker-2'])
196
+ logger.info(f"品質檢查完成,總分: {quality_report.overall_score:.1f}")
197
  if progress_callback:
198
  progress_callback(f"品質檢查完成,分數: {quality_report.overall_score:.1f}/100")
199
  except Exception as e:
 
222
  return "生成失敗"
223
 
224
 
225
+ def _generate_in_batches(pdf_text, base_prompt, headers, url, model, num_parts, progress_callback, max_retries, retry_delay):
226
+ """
227
+ 分批生成的備用機制,只在單次生成被截斷時使用
228
+ """
229
+ try:
230
+ logger.info(f"開始分批生成,共 {num_parts} 個部分")
231
+
232
+ # 生成內容大綱(簡化版)
233
+ outline_prompt = f"""
234
+ 請為以下內容生成一個簡潔的討論大綱,包含 {num_parts} 個主要部分:
235
+
236
+ {pdf_text[:5000]}...
237
+
238
+ 請用繁體中文列出 {num_parts} 個主要討論主題,每個主題一行。
239
+ """
240
+
241
+ # 獲取大綱
242
+ outline_payload = {
243
+ "model": model,
244
+ "messages": [{"role": "user", "content": outline_prompt}],
245
+ "temperature": 0.3,
246
+ "max_tokens": 1000
247
+ }
248
+
249
+ outline_response = requests.post(url, headers=headers, json=outline_payload)
250
+ outline = ""
251
+ if outline_response.status_code == 200:
252
+ outline = outline_response.json()['choices'][0]['message']['content']
253
+ logger.info(f"獲得內容大綱: {outline[:100]}...")
254
+
255
+ # 分批生成
256
+ dialogue_parts = []
257
+ context_summary = ""
258
+
259
+ for part_index in range(num_parts):
260
+ is_first_part = part_index == 0
261
+ is_last_part = part_index == num_parts - 1
262
+
263
+ if is_first_part:
264
+ part_prompt = f"""
265
+ 將以下內容轉換成播客對話的第 1/{num_parts} 部分:
266
+
267
+ 【內容來源】
268
+ {pdf_text[:10000]}...
269
+
270
+ 【大綱參考】
271
+ {outline}
272
+
273
+ 【要求】
274
+ - 按照正常格式開場:speaker-1: 歡迎收聽 David888 Podcast,我是 David...
275
+ - speaker-2 首次發言時自我介紹為 Cordelia
276
+ - 討論前面的主題
277
+ - **不要結束對話**,在一個開放的討論點停止
278
+ - 必須使用繁體中文,格式為 speaker-1: 和 speaker-2:
279
+ """
280
+ elif is_last_part:
281
+ part_prompt = f"""
282
+ 延續之前的播客對話,這是第 {part_index+1}/{num_parts} 部分(最後一部分)。
283
+
284
+ 【前文摘要】
285
+ {context_summary[-3000:]}
286
+
287
+ 【大綱參考】
288
+ {outline}
289
+
290
+ 【內容來源】
291
+ {pdf_text}
292
+
293
+ 請:
294
+ 1. **不要重複開場**,直接繼續前面的對話
295
+ 2. 完成剩餘主題的討論
296
+ 3. **自然地結束對話**,包含總結和告別
297
+
298
+ **必須使用繁體中文,格式為 speaker-1: 和 speaker-2:**
299
+ """
300
+ else:
301
+ part_prompt = f"""
302
+ 延續之前的播客對話,這是第 {part_index+1}/{num_parts} 部分(中間部分)。
303
+
304
+ 【前文摘要】
305
+ {context_summary[-3000:]}
306
+
307
+ 【大綱參考】
308
+ {outline}
309
+
310
+ 【內容來源】
311
+ {pdf_text}
312
+
313
+ 請:
314
+ 1. **不要重複開場**,直接繼續前面的對話
315
+ 2. 討論相應的主題
316
+ 3. **不要結束對話**,在一個開放的討論點停止
317
+
318
+ **必須使用繁體中文,格式為 speaker-1: 和 speaker-2:**
319
+ """
320
+
321
+ # 生成當前部分
322
+ part_payload = {
323
+ "model": model,
324
+ "messages": [{"role": "user", "content": part_prompt}],
325
+ "temperature": 0.7,
326
+ "max_tokens": 8192
327
+ }
328
+
329
+ for attempt in range(max_retries):
330
+ try:
331
+ if progress_callback:
332
+ progress_callback(f"生成第 {part_index+1}/{num_parts} 部分 (嘗試 {attempt+1})...")
333
+
334
+ part_response = requests.post(url, headers=headers, json=part_payload)
335
+ part_response.raise_for_status()
336
+
337
+ current_part = part_response.json()['choices'][0]['message']['content']
338
+ dialogue_parts.append(current_part)
339
+
340
+ # 更新上下文摘要
341
+ if context_summary:
342
+ context_summary += "\n\n" + current_part
343
+ else:
344
+ context_summary = current_part
345
+
346
+ logger.info(f"完成第 {part_index+1}/{num_parts} 部分")
347
+ break
348
+
349
+ except Exception as e:
350
+ logger.error(f"生成第 {part_index+1} 部分失敗: {e}")
351
+ if attempt == max_retries - 1:
352
+ return None
353
+ time.sleep(retry_delay)
354
+
355
+ # 合併所有部分
356
+ full_dialogue = "\n\n".join(dialogue_parts)
357
+ logger.info(f"分批生成完成,總長度: {len(full_dialogue)} 字符")
358
+
359
+ return full_dialogue
360
+
361
+ except Exception as e:
362
+ logger.error(f"分批生成失敗: {e}")
363
+ return None
364
+
365
+
366
  def validate_and_generate_script(
367
  files,
368
  openai_api_key,
 
566
  label="介紹提示詞 | Intro Instructions",
567
  lines=5,
568
  value=INSTRUCTION_TEMPLATES["podcast"]["intro"],
569
+ interactive=True,
570
+ visible=False # 隱藏此欄位
571
  )
572
 
573
  text_instructions = gr.Textbox(
574
  label="文本分析提示詞 | Text Instructions",
575
  lines=5,
576
  value=INSTRUCTION_TEMPLATES["podcast"]["text_instructions"],
577
+ interactive=True,
578
+ visible=False # 隱藏此欄位
579
  )
580
 
581
  scratch_pad = gr.Textbox(
582
  label="腦力激盪提示詞 | Scratch Pad",
583
  lines=5,
584
  value=INSTRUCTION_TEMPLATES["podcast"]["scratch_pad"],
585
+ interactive=True,
586
+ visible=False # 隱藏此欄位
587
  )
588
 
589
  prelude = gr.Textbox(
590
  label="前導提示詞 | Prelude",
591
  lines=5,
592
  value=INSTRUCTION_TEMPLATES["podcast"]["prelude"],
593
+ interactive=True,
594
+ visible=False # 隱藏此欄位
595
  )
596
 
597
  dialog = gr.Textbox(
598
+ label="主要提示詞 | Main Prompt (預覽用,由模板自動設定)",
599
+ lines=8,
600
+ value=INSTRUCTION_TEMPLATES["podcast"]["dialog"],
601
+ interactive=True,
602
+ info="這是當前選擇模板的提示詞內容,通常不需要手動修改"
603
  )
604
 
605
  custom_prompt = gr.Textbox(
prompts.py CHANGED
@@ -14,19 +14,26 @@ PROMPTS = {
14
  - **speaker-2(Cordelia)**:共同主持人,專業理性,擅長深入分析
15
 
16
  【任務目標】
17
- - 將提供的文字內容轉換成自然流暢的雙人對話
18
  - 開場必須以 "speaker-1: 歡迎收聽 David888 Podcast,我是 David..." 開始
19
  - speaker-2 首次發言時自我介紹為 Cordelia
20
  - 對話風格輕鬆專業,類似 All-In-Podcast 的互動感
 
21
  - 適合語音播放,避免過於複雜的表述
22
 
 
 
 
 
 
 
23
  【輸出格式】
24
  - 使用 "speaker-1:" 和 "speaker-2:" 標記每句話
25
  - 不使用其他格式如 [主持人] 或括號
26
  - **必須使用繁體中文**
27
- - 對話長度根據內容適中,保持自然節奏
28
 
29
- 請將以下內容轉換成播客對話:
30
 
31
  {content}""",
32
 
@@ -36,17 +43,22 @@ PROMPTS = {
36
  - **speaker-1(David)**:主持人,風格親切專業,善於講解和分享
37
 
38
  【任務目標】
39
- - 將文字內容轉換成單人播客獨白
40
  - 開場必須以 "speaker-1: 歡迎收聽 David888 Podcast,我是 David..." 開始
41
  - 保持自然的語調和節奏感
42
  - 適合語音播放,內容豐富且易懂
43
 
 
 
 
 
 
44
  【輸出格式】
45
  - 所有內容使用 "speaker-1:" 標記
46
  - **必須使用繁體中文**
47
  - 保持自然的口語化表達
48
 
49
- 請將以下內容轉換成單人播客:
50
 
51
  {content}""",
52
 
 
14
  - **speaker-2(Cordelia)**:共同主持人,專業理性,擅長深入分析
15
 
16
  【任務目標】
17
+ - 將提供的文字內容**完整地**轉換成自然流暢的雙人對話
18
  - 開場必須以 "speaker-1: 歡迎收聽 David888 Podcast,我是 David..." 開始
19
  - speaker-2 首次發言時自我介紹為 Cordelia
20
  - 對話風格輕鬆專業,類似 All-In-Podcast 的互動感
21
+ - **重要**:必須涵蓋所有提供的內容,生成完整的對話直到自然結束
22
  - 適合語音播放,避免過於複雜的表述
23
 
24
+ 【長度要求】
25
+ - 根據內容長度,生成相應比例的完整對話(約 50-200 輪對話)
26
+ - **絕對不要提前結束**,確保所有重要內容都被完整討論
27
+ - 對話應該有自然的開場、充分的內容展開、深入討論和完整結尾
28
+ - 利用完整的輸出空間,創造豐富詳細的對話內容
29
+
30
  【輸出格式】
31
  - 使用 "speaker-1:" 和 "speaker-2:" 標記每句話
32
  - 不使用其他格式如 [主持人] 或括號
33
  - **必須使用繁體中文**
34
+ - 保持自然的口語化表達
35
 
36
+ 請將以下內容轉換成完整的播客對話:
37
 
38
  {content}""",
39
 
 
43
  - **speaker-1(David)**:主持人,風格親切專業,善於講解和分享
44
 
45
  【任務目標】
46
+ - 將文字內容**完整地**轉換成單人播客獨白
47
  - 開場必須以 "speaker-1: 歡迎收聽 David888 Podcast,我是 David..." 開始
48
  - 保持自然的語調和節奏感
49
  - 適合語音播放,內容豐富且易懂
50
 
51
+ 【長度要求】
52
+ - 生成豐富詳細的內容(約 30-100 段)
53
+ - **絕對不要提前結束**,確保所有內容都被完整覆蓋
54
+ - 利用完整的輸出空間創造深入的獨白內容
55
+
56
  【輸出格式】
57
  - 所有內容使用 "speaker-1:" 標記
58
  - **必須使用繁體中文**
59
  - 保持自然的口語化表達
60
 
61
+ 請將以下內容轉換成完整的單人播客:
62
 
63
  {content}""",
64