Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -52,9 +52,8 @@ class NotebookLMTool:
|
|
| 52 |
def _call_gemini_with_retry(self, model_name, contents, config=None, retries=5):
|
| 53 |
"""
|
| 54 |
封裝 Gemini 呼叫,加入指數退避重試機制 (Exponential Backoff)
|
| 55 |
-
專門處理 429 Resource Exhausted 錯誤
|
| 56 |
"""
|
| 57 |
-
delay =
|
| 58 |
|
| 59 |
for attempt in range(retries):
|
| 60 |
try:
|
|
@@ -65,22 +64,22 @@ class NotebookLMTool:
|
|
| 65 |
)
|
| 66 |
return response
|
| 67 |
except Exception as e:
|
| 68 |
-
# 檢查是否為 Rate Limit 相關錯誤 (包含 429 或 Service Unavailable)
|
| 69 |
error_str = str(e)
|
|
|
|
| 70 |
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str or "503" in error_str:
|
| 71 |
-
wait_time = delay + random.uniform(0,
|
| 72 |
-
print(f"⚠️
|
| 73 |
time.sleep(wait_time)
|
| 74 |
-
delay *=
|
| 75 |
else:
|
| 76 |
-
raise e #
|
| 77 |
|
| 78 |
-
raise Exception("API
|
| 79 |
|
| 80 |
# --- 單頁處理邏輯 ---
|
| 81 |
def process_single_page(self, page_index, img, img_output_dir):
|
| 82 |
"""處理單一頁面的:去字(背景) + 文字分析(Layout)"""
|
| 83 |
-
print(f"🚀 [Page {page_index+1}]
|
| 84 |
|
| 85 |
result = {
|
| 86 |
"index": page_index,
|
|
@@ -96,7 +95,9 @@ class NotebookLMTool:
|
|
| 96 |
final_bg_path = os.path.join(img_output_dir, save_name)
|
| 97 |
bg_success = False
|
| 98 |
|
|
|
|
| 99 |
# 1. 背景去字 (Image Cleaning)
|
|
|
|
| 100 |
try:
|
| 101 |
clean_prompt = """
|
| 102 |
Strictly remove all text, titles, text-boxes, and bullet points from this slide image.
|
|
@@ -106,9 +107,9 @@ class NotebookLMTool:
|
|
| 106 |
3. Output ONLY the image.
|
| 107 |
"""
|
| 108 |
|
| 109 |
-
#
|
| 110 |
resp_img = self._call_gemini_with_retry(
|
| 111 |
-
model_name="gemini-2.
|
| 112 |
contents=[clean_prompt, img],
|
| 113 |
config=types.GenerateContentConfig(response_modalities=["IMAGE"])
|
| 114 |
)
|
|
@@ -133,28 +134,30 @@ class NotebookLMTool:
|
|
| 133 |
result["bg_path"] = final_bg_path
|
| 134 |
result["preview"] = (final_bg_path, f"Page {page_index+1} Cleaned")
|
| 135 |
else:
|
| 136 |
-
print(f"⚠️ [Page {page_index+1}] 去字失敗:
|
| 137 |
|
| 138 |
except Exception as e:
|
| 139 |
print(f"❌ [Page {page_index+1}] Clean Error: {e}", flush=True)
|
| 140 |
|
| 141 |
-
# 失敗回退原圖
|
| 142 |
if not bg_success:
|
| 143 |
img.save(final_bg_path)
|
| 144 |
result["bg_path"] = final_bg_path
|
| 145 |
result["preview"] = (final_bg_path, f"Page {page_index+1} (Original)")
|
| 146 |
-
result["log"] += f"[P{page_index+1}] Warning: Background cleaning failed
|
| 147 |
|
|
|
|
| 148 |
# 2. 文字與佈局分析 (Layout Analysis)
|
|
|
|
| 149 |
try:
|
| 150 |
layout_prompt = """
|
| 151 |
Analyze this slide. Return a JSON list of all text blocks.
|
| 152 |
Each item: {"text": string, "box_2d": [ymin, xmin, ymax, xmax] (0-1000), "font_size": int, "color": hex, "is_bold": bool}
|
| 153 |
"""
|
| 154 |
|
| 155 |
-
#
|
| 156 |
resp_layout = self._call_gemini_with_retry(
|
| 157 |
-
model_name="gemini-2.
|
| 158 |
contents=[layout_prompt, img],
|
| 159 |
config=types.GenerateContentConfig(response_mime_type="application/json")
|
| 160 |
)
|
|
@@ -211,14 +214,12 @@ class NotebookLMTool:
|
|
| 211 |
progress(0.2, desc="🚀 AI 處理中 (已啟用速率保護)...")
|
| 212 |
|
| 213 |
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 214 |
-
# 提交任務,但加入微小延遲避免瞬間併發過高
|
| 215 |
future_to_page = {}
|
| 216 |
for i, img in enumerate(images):
|
| 217 |
-
time.sleep(1) #
|
| 218 |
future = executor.submit(self.process_single_page, i, img, img_output_dir)
|
| 219 |
future_to_page[future] = i
|
| 220 |
|
| 221 |
-
# 等待完成
|
| 222 |
for future in concurrent.futures.as_completed(future_to_page):
|
| 223 |
try:
|
| 224 |
res = future.result()
|
|
@@ -234,8 +235,7 @@ class NotebookLMTool:
|
|
| 234 |
cleaned_images_paths = []
|
| 235 |
|
| 236 |
for i in range(len(images)):
|
| 237 |
-
if i not in results_map:
|
| 238 |
-
continue
|
| 239 |
res = results_map[i]
|
| 240 |
|
| 241 |
full_text_log += res["log"]
|
|
@@ -331,7 +331,7 @@ with gr.Blocks(title="NotebookLM Slide Restorer,PPT.404", theme=gr.themes.Soft
|
|
| 331 |
|
| 332 |
gr.Markdown("---")
|
| 333 |
pdf_input = gr.File(label="上傳 PDF")
|
| 334 |
-
btn_process = gr.Button("🚀 開始還原 PPTX (
|
| 335 |
|
| 336 |
with gr.Column():
|
| 337 |
out_zip = gr.File(label="📦 下載完整包")
|
|
|
|
| 52 |
def _call_gemini_with_retry(self, model_name, contents, config=None, retries=5):
|
| 53 |
"""
|
| 54 |
封裝 Gemini 呼叫,加入指數退避重試機制 (Exponential Backoff)
|
|
|
|
| 55 |
"""
|
| 56 |
+
delay = 5 # 初始等待秒數
|
| 57 |
|
| 58 |
for attempt in range(retries):
|
| 59 |
try:
|
|
|
|
| 64 |
)
|
| 65 |
return response
|
| 66 |
except Exception as e:
|
|
|
|
| 67 |
error_str = str(e)
|
| 68 |
+
# 檢查是否為 Rate Limit 相關錯誤 (429, 503, Resource Exhausted)
|
| 69 |
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str or "503" in error_str:
|
| 70 |
+
wait_time = delay + random.uniform(0, 3)
|
| 71 |
+
print(f"⚠️ API 忙碌 (Attempt {attempt+1}/{retries}),休息 {wait_time:.1f} 秒...", flush=True)
|
| 72 |
time.sleep(wait_time)
|
| 73 |
+
delay *= 1.5 # 遞增等待時間
|
| 74 |
else:
|
| 75 |
+
raise e # 其他錯誤 (如 400) 直接拋出
|
| 76 |
|
| 77 |
+
raise Exception("API 重試多次失敗,請檢查配額。")
|
| 78 |
|
| 79 |
# --- 單頁處理邏輯 ---
|
| 80 |
def process_single_page(self, page_index, img, img_output_dir):
|
| 81 |
"""處理單一頁面的:去字(背景) + 文字分析(Layout)"""
|
| 82 |
+
print(f"🚀 [Page {page_index+1}] 啟動處理...", flush=True)
|
| 83 |
|
| 84 |
result = {
|
| 85 |
"index": page_index,
|
|
|
|
| 95 |
final_bg_path = os.path.join(img_output_dir, save_name)
|
| 96 |
bg_success = False
|
| 97 |
|
| 98 |
+
# ==========================================
|
| 99 |
# 1. 背景去字 (Image Cleaning)
|
| 100 |
+
# ==========================================
|
| 101 |
try:
|
| 102 |
clean_prompt = """
|
| 103 |
Strictly remove all text, titles, text-boxes, and bullet points from this slide image.
|
|
|
|
| 107 |
3. Output ONLY the image.
|
| 108 |
"""
|
| 109 |
|
| 110 |
+
# ✅ 修正點 2: 使用 _call_gemini_with_retry 確保 429 時會重試
|
| 111 |
resp_img = self._call_gemini_with_retry(
|
| 112 |
+
model_name="gemini-2.5-flash-image",
|
| 113 |
contents=[clean_prompt, img],
|
| 114 |
config=types.GenerateContentConfig(response_modalities=["IMAGE"])
|
| 115 |
)
|
|
|
|
| 134 |
result["bg_path"] = final_bg_path
|
| 135 |
result["preview"] = (final_bg_path, f"Page {page_index+1} Cleaned")
|
| 136 |
else:
|
| 137 |
+
print(f"⚠️ [Page {page_index+1}] 去字失敗: 模型未回傳圖片", flush=True)
|
| 138 |
|
| 139 |
except Exception as e:
|
| 140 |
print(f"❌ [Page {page_index+1}] Clean Error: {e}", flush=True)
|
| 141 |
|
| 142 |
+
# 失敗回退原圖 (但標記為失敗)
|
| 143 |
if not bg_success:
|
| 144 |
img.save(final_bg_path)
|
| 145 |
result["bg_path"] = final_bg_path
|
| 146 |
result["preview"] = (final_bg_path, f"Page {page_index+1} (Original)")
|
| 147 |
+
result["log"] += f"[P{page_index+1}] Warning: Background cleaning failed. Used original image.\n"
|
| 148 |
|
| 149 |
+
# ==========================================
|
| 150 |
# 2. 文字與佈局分析 (Layout Analysis)
|
| 151 |
+
# ==========================================
|
| 152 |
try:
|
| 153 |
layout_prompt = """
|
| 154 |
Analyze this slide. Return a JSON list of all text blocks.
|
| 155 |
Each item: {"text": string, "box_2d": [ymin, xmin, ymax, xmax] (0-1000), "font_size": int, "color": hex, "is_bold": bool}
|
| 156 |
"""
|
| 157 |
|
| 158 |
+
# ✅ 修正點 3: 使用 _call_gemini_with_retry
|
| 159 |
resp_layout = self._call_gemini_with_retry(
|
| 160 |
+
model_name="gemini-2.5-flash",
|
| 161 |
contents=[layout_prompt, img],
|
| 162 |
config=types.GenerateContentConfig(response_mime_type="application/json")
|
| 163 |
)
|
|
|
|
| 214 |
progress(0.2, desc="🚀 AI 處理中 (已啟用速率保護)...")
|
| 215 |
|
| 216 |
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
|
| 217 |
future_to_page = {}
|
| 218 |
for i, img in enumerate(images):
|
| 219 |
+
time.sleep(1.5) # 稍微加大間隔,避免同時撞牆
|
| 220 |
future = executor.submit(self.process_single_page, i, img, img_output_dir)
|
| 221 |
future_to_page[future] = i
|
| 222 |
|
|
|
|
| 223 |
for future in concurrent.futures.as_completed(future_to_page):
|
| 224 |
try:
|
| 225 |
res = future.result()
|
|
|
|
| 235 |
cleaned_images_paths = []
|
| 236 |
|
| 237 |
for i in range(len(images)):
|
| 238 |
+
if i not in results_map: continue
|
|
|
|
| 239 |
res = results_map[i]
|
| 240 |
|
| 241 |
full_text_log += res["log"]
|
|
|
|
| 331 |
|
| 332 |
gr.Markdown("---")
|
| 333 |
pdf_input = gr.File(label="上傳 PDF")
|
| 334 |
+
btn_process = gr.Button("🚀 開始還原 PPTX (穩定修復版)", variant="primary")
|
| 335 |
|
| 336 |
with gr.Column():
|
| 337 |
out_zip = gr.File(label="📦 下載完整包")
|