youngtsai commited on
Commit
4efcc29
·
1 Parent(s): 9931066

class SheetService:

Browse files
Files changed (2) hide show
  1. app.py +261 -131
  2. sheet_service.py +134 -77
app.py CHANGED
@@ -2164,7 +2164,7 @@ def get_sheet_data(sheet_url, range_name):
2164
  def update_sheet_data(sheet_url, qa_result, video_id):
2165
  # 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
2166
  sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
2167
- print(f"sheet_data: {sheet_data}")
2168
 
2169
  if not sheet_data or len(sheet_data) < 1:
2170
  print("錯誤:工作表資料為空或缺少標頭列。")
@@ -2175,7 +2175,7 @@ def update_sheet_data(sheet_url, qa_result, video_id):
2175
 
2176
  # 直接指定要尋找的欄位名稱
2177
  target_column_name = '均一平台 YT Readable ID'
2178
- target_qa_column_name = "QA"
2179
 
2180
  try:
2181
  # 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
@@ -2190,89 +2190,221 @@ def update_sheet_data(sheet_url, qa_result, video_id):
2190
  for i, row in enumerate(data_rows):
2191
  # 確保該列有足夠的欄位,並且該欄位的值等於 video_id
2192
  if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
2193
- target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1
2194
  break # 找到後即可跳出迴圈
2195
 
2196
  if target_row_index != -1:
2197
- print(f"找到 video_id '{video_id}' 於欄位 '{target_column_name}' 的第 {target_row_index + 1} 列 (工作表列號)") # +1 是為了顯示給人類看的行號
 
 
 
2198
 
2199
- # --- 以下是更新邏輯 (需要取消註解並確保 qa_column 也正確) ---
2200
  try:
2201
- # 找到 QA 欄位的索引 (假設 qa_column 參數傳入的是正確的標題文字, e.g., 'QA')
2202
  qa_col_index = header_row.index(target_qa_column_name)
2203
 
2204
- # 更新 sheet 中的 qa_column 和 qa_result
2205
- # 確保目標列有足夠的欄位可以更新
2206
- if len(sheet_data[target_row_index]) > qa_col_index:
2207
- # 注意:這裡直接修改 sheet_data 列表可能不會直接更新 Google Sheet
2208
- # 你需要使用 SHEET_SERVICE 的更新方法
2209
- # sheet_data[target_row_index][qa_col_index] = qa_result
2210
- print(f"準備更新 {target_row_index + 1} 列, 欄位 '{target_qa_column_name}' (索引 {qa_col_index}) 為 '{qa_result}'")
2211
- # --- 實際更新 Google Sheet 的程式碼
2212
- SHEET_SERVICE.update_sheet_cell(sheet_url, target_row_index, qa_col_index, qa_result)
 
 
 
 
 
2213
 
 
 
 
2214
  else:
2215
- print(f"錯誤:第 {target_row_index + 1} 列沒有足夠的欄位來更新 QA (索引 {qa_col_index})。")
 
2216
 
2217
  except ValueError:
2218
- print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{qa_column}'")
2219
  # --- 更新邏輯結束 ---
2220
 
2221
  else:
2222
  # 如果找不到 video_id,印出錯誤訊息
2223
  print(f"錯誤:��欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
2224
 
2225
- def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column
2226
- video_ids = video_ids.replace('\n', ',').split(',')
2227
- video_ids = [vid.strip() for vid in video_ids if vid.strip()]
2228
-
2229
- success_video_ids = []
2230
- failed_video_ids = []
2231
-
2232
  sheet_qa_success_tag = "OO"
2233
  sheet_qa_failed_tag = "XX"
 
2234
 
2235
- for video_id in video_ids:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2236
  try:
2237
- # 確認 GCS 中是否存在 ID_transcript.json in GCS
2238
- # 如果存在就 pass
2239
- # 如果不存在就 refresh_video_LLM_all_content_by_id
2240
- bucket_name = 'video_ai_assistant'
2241
- file_name = f'{video_id}_transcript.json'
2242
- blob_name = f"{video_id}/{file_name}"
2243
  is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
2244
- if not is_file_exists:
2245
- print(f"{video_id} 不存在逐字稿")
2246
- refresh_video_LLM_all_content_by_id(video_id)
2247
  else:
2248
- print(f"{video_id} 存在逐字稿")
2249
-
2250
- qa_result = sheet_qa_success_tag
2251
- time.sleep(1)
2252
- # 更新呼叫,不再傳遞 sheet_video_column
2253
- update_sheet_data(sheet_url, qa_result, video_id)
2254
- success_video_ids.append(video_id) # 假設 update_sheet_data 成功就加入 success
2255
  except Exception as e:
2256
- print(f"===refresh_video_LLM_all_content_by_sheet error===")
2257
- print(f"video_id: {video_id}")
2258
- print(f"error: {str(e)}")
2259
- print(f"===refresh_video_LLM_all_content_by_sheet error===")
2260
- failed_video_ids.append(video_id)
2261
- qa_result = sheet_qa_failed_tag
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2262
  try:
2263
- # 即使前面出錯,還是嘗試更新 QA 狀態為 XX
2264
- update_sheet_data(sheet_url, qa_result, video_id)
2265
- time.sleep(1)
2266
- except Exception as update_e:
2267
- print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
2268
- print(f"video_id: {video_id}")
2269
- print(f"error: {str(update_e)}")
2270
- print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2271
 
 
 
 
2272
 
2273
  result = {
2274
- "success_video_ids": success_video_ids,
2275
- "failed_video_ids": failed_video_ids
2276
  }
2277
  return result
2278
 
@@ -3599,81 +3731,79 @@ def create_app():
3599
  with gr.Row():
3600
  with gr.Tab("學習單"):
3601
  with gr.Row():
3602
- with gr.Column(scale=1):
3603
- with gr.Row():
3604
- worksheet_content_type_name = gr.Textbox(value="worksheet", visible=False)
3605
- worksheet_algorithm = gr.Dropdown(label="選擇教學策略或理論", choices=["Bloom認知階層理論", "Polya數學解題法", "CRA教學法"], value="Bloom認知階層理論", visible=False)
3606
- worksheet_content_btn = gr.Button("生成學習單 📄", variant="primary", visible=True)
3607
- with gr.Accordion("微調", open=False):
3608
- worksheet_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3609
- worksheet_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3610
- worksheet_result_retrun_original = gr.Button("返回原始結果")
3611
- with gr.Accordion("prompt", open=False) as worksheet_accordion:
3612
- worksheet_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3613
- with gr.Column(scale=2):
3614
- # 生成對應不同模式的結果
3615
- worksheet_result_prompt = gr.Textbox(visible=False)
3616
- worksheet_result_original = gr.Textbox(visible=False)
3617
- worksheet_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3618
- worksheet_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3619
- worksheet_result_word_link = gr.File(label="Download Word")
3620
- with gr.Tab("教案"):
3621
- with gr.Row():
3622
- with gr.Column(scale=1):
3623
- with gr.Row():
3624
- lesson_plan_content_type_name = gr.Textbox(value="lesson_plan", visible=False)
3625
- lesson_plan_time = gr.Slider(label="選擇課程時間(分鐘)", minimum=10, maximum=120, step=5, value=40)
3626
- lesson_plan_btn = gr.Button("生成教案 📕", variant="primary", visible=True)
3627
- with gr.Accordion("微調", open=False):
3628
- lesson_plan_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3629
- lesson_plan_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3630
- lesson_plan_result_retrun_original = gr.Button("返回原始結果")
3631
- with gr.Accordion("prompt", open=False) as lesson_plan_accordion:
3632
- lesson_plan_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3633
- with gr.Column(scale=2):
3634
- # 生成對應不同模式的結果
3635
- lesson_plan_result_prompt = gr.Textbox(visible=False)
3636
- lesson_plan_result_original = gr.Textbox(visible=False)
3637
- lesson_plan_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3638
-
3639
- lesson_plan_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3640
- lesson_plan_result_word_link = gr.File(label="Download Word")
3641
- with gr.Tab("出場券"):
3642
- with gr.Row():
3643
- with gr.Column(scale=1):
3644
- with gr.Row():
3645
- exit_ticket_content_type_name = gr.Textbox(value="exit_ticket", visible=False)
3646
- exit_ticket_time = gr.Slider(label="選擇出場券時間(分鐘)", minimum=5, maximum=10, step=1, value=8)
3647
- exit_ticket_btn = gr.Button("生成出場券 🎟️", variant="primary", visible=True)
3648
- with gr.Accordion("微調", open=False):
3649
- exit_ticket_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3650
- exit_ticket_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3651
- exit_ticket_result_retrun_original = gr.Button("返回原始結果")
3652
- with gr.Accordion("prompt", open=False) as exit_ticket_accordion:
3653
- exit_ticket_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3654
- with gr.Column(scale=2):
3655
- # 生成對應不同模式的結果
3656
- exit_ticket_result_prompt = gr.Textbox(visible=False)
3657
- exit_ticket_result_original = gr.Textbox(visible=False)
3658
- exit_ticket_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3659
-
3660
- exit_ticket_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3661
- exit_ticket_result_word_link = gr.File(label="Download Word")
3662
 
3663
 
3664
- # with gr.Tab("素養導向閱讀題組"):
3665
- # literacy_oriented_reading_content = gr.Textbox(label="輸入閱讀材料")
3666
- # literacy_oriented_reading_content_btn = gr.Button("生成閱讀理解題")
3667
 
3668
- # with gr.Tab("自我評估"):
3669
- # self_assessment_content = gr.Textbox(label="輸入自評問卷或檢查表")
3670
- # self_assessment_content_btn = gr.Button("生成自評問卷")
3671
- # with gr.Tab("自我反思評量"):
3672
- # self_reflection_content = gr.Textbox(label="輸入自我反思活動")
3673
- # self_reflection_content_btn = gr.Button("生成自我反思活動")
3674
- # with gr.Tab("後設認知"):
3675
- # metacognition_content = gr.Textbox(label="輸入後設認知相關問題")
3676
- # metacognition_content_btn = gr.Button("生成後設認知問題")
3677
 
3678
  with gr.Accordion("免責聲明", open=True):
3679
  gr.Markdown("""
@@ -3780,8 +3910,8 @@ def create_app():
3780
  refresh_btn = gr.Button("refresh", variant="primary")
3781
  with gr.Tab("by sheets"):
3782
  sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
3783
- sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="D:D")
3784
- sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="F:F")
3785
  sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
3786
  sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
3787
  sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
 
2164
  def update_sheet_data(sheet_url, qa_result, video_id):
2165
  # 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
2166
  sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
2167
+ # print(f"sheet_data: {sheet_data}")
2168
 
2169
  if not sheet_data or len(sheet_data) < 1:
2170
  print("錯誤:工作表資料為空或缺少標頭列。")
 
2175
 
2176
  # 直接指定要尋找的欄位名稱
2177
  target_column_name = '均一平台 YT Readable ID'
2178
+ target_qa_column_name = "QA" # 假設 QA 欄位的標題是 "QA"
2179
 
2180
  try:
2181
  # 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
 
2190
  for i, row in enumerate(data_rows):
2191
  # 確保該列有足夠的欄位,並且該欄位的值等於 video_id
2192
  if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
2193
+ target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1 (因為 data_rows 從 sheet_data[1] 開始)
2194
  break # 找到後即可跳出迴圈
2195
 
2196
  if target_row_index != -1:
2197
+ # target_row_index video_id sheet_data (含標頭) 中的 0-based 索引
2198
+ # 例如,第一筆資料的 target_row_index 是 1
2199
+ actual_target_row_number = target_row_index + 1 # 這是實際的 1-based 工作表列號 (例如 2)
2200
+ print(f"找到 video_id '{video_id}' 於欄位 '{target_column_name}' 的第 {actual_target_row_number} 列 (工作表列號)")
2201
 
2202
+ # --- 以下是更新邏輯 ---
2203
  try:
2204
+ # 找到 QA 欄位的索引
2205
  qa_col_index = header_row.index(target_qa_column_name)
2206
 
2207
+ # 取得目標列的實際資料
2208
+ target_row_data = sheet_data[target_row_index]
2209
+
2210
+ # 檢查目標列是否有足夠的欄位可以更新
2211
+ if len(target_row_data) > qa_col_index:
2212
+ # *** 修正點:傳遞給 update_sheet_cell 的列號減 1 ***
2213
+ # 因為觀察到 update_sheet_cell(N) 會更新到 N+1
2214
+ # 所以我們傳遞 N-1,讓它內部 +1 後剛好是 N
2215
+ row_index_to_pass = actual_target_row_number - 1 # 將 1-based 列號減 1
2216
+
2217
+ # 為了 log 清晰,仍然顯示 1-based 的目標列號
2218
+ print(f"準備更新 第 {actual_target_row_number} 列, 欄位 '{target_qa_column_name}' (索引 {qa_col_index}) 為 '{qa_result}'")
2219
+ # 添加一行 log 來確認傳遞的值
2220
+ print(f" (傳遞給 update_sheet_cell 的 row_index: {row_index_to_pass})")
2221
 
2222
+ # --- 實際更新 Google Sheet 的程式碼 ---
2223
+ # 傳遞調整後的索引 row_index_to_pass
2224
+ SHEET_SERVICE.update_sheet_cell(sheet_url, row_index_to_pass, qa_col_index, qa_result)
2225
  else:
2226
+ # 如果目標列的長度不足以包含 qa_col_index
2227
+ print(f"錯誤:第 {actual_target_row_number} 列 (資料長度 {len(target_row_data)}) 沒有足夠的欄位來更新 QA (需要索引 {qa_col_index})。")
2228
 
2229
  except ValueError:
2230
+ print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{target_qa_column_name}'")
2231
  # --- 更新邏輯結束 ---
2232
 
2233
  else:
2234
  # 如果找不到 video_id,印出錯誤訊息
2235
  print(f"錯誤:��欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
2236
 
2237
+ def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids_input): # 參數名稱改為 video_ids_input 避免混淆
2238
+ print("=== 開始批次處理工作表刷新 ===")
2239
+ target_id_column_name = '均一平台 YT Readable ID'
2240
+ target_qa_column_name = "QA"
 
 
 
2241
  sheet_qa_success_tag = "OO"
2242
  sheet_qa_failed_tag = "XX"
2243
+ bucket_name = 'video_ai_assistant'
2244
 
2245
+ # 1. 解析輸入的 video_ids
2246
+ video_ids_list = video_ids_input.replace('\n', ',').split(',')
2247
+ video_ids_list = [vid.strip() for vid in video_ids_list if vid.strip() and vid.strip() != target_id_column_name]
2248
+ print(f"準備處理的 Video IDs: {video_ids_list}")
2249
+
2250
+ if not video_ids_list:
2251
+ print("沒有有效的 Video ID 需要處理。")
2252
+ return {"success_video_ids": [], "failed_video_ids": []}
2253
+
2254
+ # 2. 一次性讀取工作表資料
2255
+ try:
2256
+ print(f"正在從 {sheet_url} 讀取工作表資料...")
2257
+ sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
2258
+ if not sheet_data or len(sheet_data) < 1:
2259
+ print("錯誤:工作表資料為空或缺少標頭列。")
2260
+ return {"success_video_ids": [], "failed_video_ids": []}
2261
+ print(f"成功讀取 {len(sheet_data)} 列資料。")
2262
+ header_row = sheet_data[0]
2263
+ data_rows = sheet_data[1:]
2264
+ except Exception as e:
2265
+ print(f"讀取工作表時發生錯誤: {e}")
2266
+ return {"success_video_ids": [], "failed_video_ids": []}
2267
+
2268
+ # 3. 找到目標欄位索引並建立 video_id 到 row_number 的映射
2269
+ try:
2270
+ video_id_col_index = header_row.index(target_id_column_name)
2271
+ qa_col_index = header_row.index(target_qa_column_name)
2272
+ # Google Sheet 的欄位字母 (A=0, B=1, ...)
2273
+ qa_col_letter = chr(ord('A') + qa_col_index)
2274
+ print(f"'{target_id_column_name}' 欄位索引: {video_id_col_index}")
2275
+ print(f"'{target_qa_column_name}' 欄位索引: {qa_col_index} (欄位 {qa_col_letter})")
2276
+ except ValueError as e:
2277
+ print(f"錯誤:在標頭列 {header_row} 中找不到必要的欄位: {e}")
2278
+ return {"success_video_ids": [], "failed_video_ids": []}
2279
+
2280
+ video_id_to_row_map = {}
2281
+ for i, row in enumerate(data_rows):
2282
+ if len(row) > video_id_col_index and row[video_id_col_index]:
2283
+ # 儲存 1-based 的工作表列號
2284
+ video_id_to_row_map[row[video_id_col_index]] = i + 2 # 資料列從第 2 列開始 (1-based)
2285
+
2286
+ # 4. 批次檢查 GCS 並分類
2287
+ ids_to_set_oo_phase1 = [] # (video_id, row_number)
2288
+ ids_to_refresh = [] # video_id
2289
+ processed_ids = set() # 追蹤已處理的ID,避免重複
2290
+
2291
+ print("開始檢查 GCS 檔案狀態...")
2292
+ for video_id in video_ids_list:
2293
+ if video_id in processed_ids:
2294
+ continue
2295
+ processed_ids.add(video_id)
2296
+
2297
+ if video_id not in video_id_to_row_map:
2298
+ print(f"警告:輸入的 video_id '{video_id}' 在工作表的 '{target_id_column_name}' 欄位中找不到,將跳過。")
2299
+ continue
2300
+
2301
+ row_number = video_id_to_row_map[video_id]
2302
+ file_name = f'{video_id}_transcript.json'
2303
+ blob_name = f"{video_id}/{file_name}"
2304
  try:
 
 
 
 
 
 
2305
  is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
2306
+ if is_file_exists:
2307
+ print(f" - {video_id} (第 {row_number} 列): GCS 檔案存在,標記為 OO。")
2308
+ ids_to_set_oo_phase1.append((video_id, row_number))
2309
  else:
2310
+ print(f" - {video_id} (第 {row_number} 列): GCS 檔案不存在,需要刷新。")
2311
+ ids_to_refresh.append(video_id)
 
 
 
 
 
2312
  except Exception as e:
2313
+ print(f"檢查 GCS 檔案 {blob_name} 時發生錯誤: {e},將嘗試刷新。")
2314
+ ids_to_refresh.append(video_id) # 如果檢查出錯,也歸類為需要刷新
2315
+
2316
+ # 5. 批次更新 "OO" (Phase 1: GCS 已存在)
2317
+ if ids_to_set_oo_phase1:
2318
+ print(f"\n準備批次更新 {len(ids_to_set_oo_phase1)} 個 Video ID 的 QA 為 '{sheet_qa_success_tag}' (GCS 已存在)...")
2319
+ update_data_oo1 = []
2320
+ sheet_name = SHEET_SERVICE.get_sheet_name_by_url(sheet_url) # 需要 SHEET_SERVICE 提供此方法
2321
+ if not sheet_name:
2322
+ print("錯誤:無法獲取工作表名稱,無法執行批次更新。")
2323
+ # 或者可以嘗試從 sheet_data 推斷,但不夠通用
2324
+ else:
2325
+ for vid, row_num in ids_to_set_oo_phase1:
2326
+ update_data_oo1.append({
2327
+ 'range': f"{sheet_name}!{qa_col_letter}{row_num}",
2328
+ 'value': sheet_qa_success_tag
2329
+ })
2330
+ try:
2331
+ # *** 假設 SHEET_SERVICE.batch_update_cells 存在 ***
2332
+ SHEET_SERVICE.batch_update_cells(sheet_url, update_data_oo1)
2333
+ print(f"成功批次更新 {len(update_data_oo1)} 個儲存格為 '{sheet_qa_success_tag}'。")
2334
+ except Exception as e:
2335
+ print(f"批次更新 QA 為 '{sheet_qa_success_tag}' 時發生錯誤: {e}")
2336
+ # 這裡可以考慮是否要將這些 ID 移到失敗列表
2337
+
2338
+ # 6. 處理需要刷新的 ID
2339
+ successfully_refreshed_ids = [] # (video_id, row_number)
2340
+ failed_refresh_ids = [] # (video_id, row_number)
2341
+
2342
+ if ids_to_refresh:
2343
+ print(f"\n開始處理 {len(ids_to_refresh)} 個需要刷新的 Video ID...")
2344
+ for video_id in ids_to_refresh:
2345
+ row_number = video_id_to_row_map[video_id] # 必定存在,前面已檢查
2346
+ print(f" 正在刷新 {video_id} (第 {row_number} 列)...")
2347
  try:
2348
+ refresh_video_LLM_all_content_by_id(video_id)
2349
+ print(f" - {video_id} 刷新成功。")
2350
+ successfully_refreshed_ids.append((video_id, row_number))
2351
+ time.sleep(1) # 避免過於頻繁觸發其他 API (例如 GCS 刪除/上傳)
2352
+ except Exception as e:
2353
+ print(f" - {video_id} 刷新失敗: {str(e)}")
2354
+ failed_refresh_ids.append((video_id, row_number))
2355
+ time.sleep(5) # 失敗時稍作停頓
2356
+
2357
+ # 7. 批次更新刷新結果 (OO for success, XX for failure)
2358
+ update_data_oo2 = []
2359
+ update_data_xx = []
2360
+ sheet_name = SHEET_SERVICE.get_sheet_name_by_url(sheet_url) # 再次獲取或使用之前獲取的
2361
+
2362
+ if sheet_name:
2363
+ for vid, row_num in successfully_refreshed_ids:
2364
+ update_data_oo2.append({
2365
+ 'range': f"{sheet_name}!{qa_col_letter}{row_num}",
2366
+ 'value': sheet_qa_success_tag
2367
+ })
2368
+ for vid, row_num in failed_refresh_ids:
2369
+ update_data_xx.append({
2370
+ 'range': f"{sheet_name}!{qa_col_letter}{row_num}",
2371
+ 'value': sheet_qa_failed_tag
2372
+ })
2373
+
2374
+ if update_data_oo2:
2375
+ print(f"\n準備批次更新 {len(update_data_oo2)} 個 Video ID 的 QA 為 '{sheet_qa_success_tag}' (刷新成功)...")
2376
+ try:
2377
+ SHEET_SERVICE.batch_update_cells(sheet_url, update_data_oo2)
2378
+ print(f"成功批次更新 {len(update_data_oo2)} 個儲存格為 '{sheet_qa_success_tag}'。")
2379
+ except Exception as e:
2380
+ print(f"批次更新 QA 為 '{sheet_qa_success_tag}' (刷新成功) 時發生錯誤: {e}")
2381
+
2382
+ if update_data_xx:
2383
+ print(f"\n準備批次更新 {len(update_data_xx)} 個 Video ID 的 QA 為 '{sheet_qa_failed_tag}' (刷新失敗)...")
2384
+ try:
2385
+ SHEET_SERVICE.batch_update_cells(sheet_url, update_data_xx)
2386
+ print(f"成功批次更新 {len(update_data_xx)} 個儲存格為 '{sheet_qa_failed_tag}'。")
2387
+ except Exception as e:
2388
+ print(f"批次更新 QA 為 '{sheet_qa_failed_tag}' 時發生錯誤: {e}")
2389
+ else:
2390
+ if successfully_refreshed_ids or failed_refresh_ids:
2391
+ print("錯誤:無法獲取工作表名稱,無法批次更新刷新結果。")
2392
+
2393
+
2394
+ # 8. 整理最終結果
2395
+ final_success_ids = [item[0] for item in ids_to_set_oo_phase1] + [item[0] for item in successfully_refreshed_ids]
2396
+ final_failed_ids = [item[0] for item in failed_refresh_ids]
2397
+ # 將在工作表中找不到的 ID 也視為失敗
2398
+ initial_not_found = [vid for vid in video_ids_list if vid not in video_id_to_row_map]
2399
+ final_failed_ids.extend(initial_not_found)
2400
 
2401
+ print("\n=== 批次處理完成 ===")
2402
+ print(f"成功處理 (或 GCS 已存在): {len(final_success_ids)} 個")
2403
+ print(f"處理失敗 (或未找到/刷新失敗): {len(final_failed_ids)} 個")
2404
 
2405
  result = {
2406
+ "success_video_ids": final_success_ids,
2407
+ "failed_video_ids": list(set(final_failed_ids)) # 去重
2408
  }
2409
  return result
2410
 
 
3731
  with gr.Row():
3732
  with gr.Tab("學習單"):
3733
  with gr.Row():
3734
+ worksheet_content_type_name = gr.Textbox(value="worksheet", visible=False)
3735
+ worksheet_algorithm = gr.Dropdown(label="選擇教學策略或理論", choices=["Bloom認知階層理論", "Polya數學解題法", "CRA教學法"], value="Bloom認知階層理論", visible=False)
3736
+ worksheet_content_btn = gr.Button("生成學習單 📄", variant="primary", visible=True)
3737
+ with gr.Accordion("微調", open=False):
3738
+ worksheet_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3739
+ worksheet_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3740
+ worksheet_result_retrun_original = gr.Button("返回原始結果")
3741
+ with gr.Accordion("prompt", open=False) as worksheet_accordion:
3742
+ worksheet_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3743
+ with gr.Column(scale=2):
3744
+ # 生成對應不同模式的結果
3745
+ worksheet_result_prompt = gr.Textbox(visible=False)
3746
+ worksheet_result_original = gr.Textbox(visible=False)
3747
+ worksheet_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3748
+ worksheet_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3749
+ worksheet_result_word_link = gr.File(label="Download Word")
3750
+ with gr.Tab("教案"):
3751
+ with gr.Row():
3752
+ with gr.Column(scale=1):
3753
+ with gr.Row():
3754
+ lesson_plan_content_type_name = gr.Textbox(value="lesson_plan", visible=False)
3755
+ lesson_plan_time = gr.Slider(label="選擇課程時間(分鐘)", minimum=10, maximum=120, step=5, value=40)
3756
+ lesson_plan_btn = gr.Button("生成教案 📕", variant="primary", visible=True)
3757
+ with gr.Accordion("微調", open=False):
3758
+ lesson_plan_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3759
+ lesson_plan_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3760
+ lesson_plan_result_retrun_original = gr.Button("返回原始結果")
3761
+ with gr.Accordion("prompt", open=False) as lesson_plan_accordion:
3762
+ lesson_plan_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3763
+ with gr.Column(scale=2):
3764
+ # 生成對應不同模式的結果
3765
+ lesson_plan_result_prompt = gr.Textbox(visible=False)
3766
+ lesson_plan_result_original = gr.Textbox(visible=False)
3767
+ lesson_plan_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3768
+
3769
+ lesson_plan_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3770
+ lesson_plan_result_word_link = gr.File(label="Download Word")
3771
+ with gr.Tab("出場券"):
3772
+ with gr.Row():
3773
+ with gr.Column(scale=1):
3774
+ with gr.Row():
3775
+ exit_ticket_content_type_name = gr.Textbox(value="exit_ticket", visible=False)
3776
+ exit_ticket_time = gr.Slider(label="選擇出場券時間(分鐘)", minimum=5, maximum=10, step=1, value=8)
3777
+ exit_ticket_btn = gr.Button("生成出場券 🎟️", variant="primary", visible=True)
3778
+ with gr.Accordion("微調", open=False):
3779
+ exit_ticket_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
3780
+ exit_ticket_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
3781
+ exit_ticket_result_retrun_original = gr.Button("返回原始結果")
3782
+ with gr.Accordion("prompt", open=False) as exit_ticket_accordion:
3783
+ exit_ticket_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
3784
+ with gr.Column(scale=2):
3785
+ # 生成對應不同模式的結果
3786
+ exit_ticket_result_prompt = gr.Textbox(visible=False)
3787
+ exit_ticket_result_original = gr.Textbox(visible=False)
3788
+ exit_ticket_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
3789
+
3790
+ exit_ticket_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
3791
+ exit_ticket_result_word_link = gr.File(label="Download Word")
 
 
3792
 
3793
 
3794
+ # with gr.Tab("素養導向閱讀題組"):
3795
+ # literacy_oriented_reading_content = gr.Textbox(label="輸入閱讀材料")
3796
+ # literacy_oriented_reading_content_btn = gr.Button("生成閱讀理解題")
3797
 
3798
+ # with gr.Tab("自我評估"):
3799
+ # self_assessment_content = gr.Textbox(label="輸入自評問卷或檢查表")
3800
+ # self_assessment_content_btn = gr.Button("生成自評問卷")
3801
+ # with gr.Tab("自我反思評量"):
3802
+ # self_reflection_content = gr.Textbox(label="輸入自我反思活動")
3803
+ # self_reflection_content_btn = gr.Button("生成自我反思活動")
3804
+ # with gr.Tab("後設認知"):
3805
+ # metacognition_content = gr.Textbox(label="輸入後設認知相關問題")
3806
+ # metacognition_content_btn = gr.Button("生成後設認知問題")
3807
 
3808
  with gr.Accordion("免責聲明", open=True):
3809
  gr.Markdown("""
 
3910
  refresh_btn = gr.Button("refresh", variant="primary")
3911
  with gr.Tab("by sheets"):
3912
  sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
3913
+ sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="D2:D")
3914
+ sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="F2:F")
3915
  sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
3916
  sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
3917
  sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
sheet_service.py CHANGED
@@ -5,6 +5,7 @@ import json
5
  from urllib.parse import urlparse, parse_qs
6
  import logging # 建議使用 logging 而非 print
7
  import string # 導入 string 模組用於轉換欄位索引
 
8
 
9
  # 設定基本的 logging
10
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -44,69 +45,62 @@ class SheetService:
44
  self.service = None
45
  self.sheet = None
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
48
  """
49
  從 Google Sheets URL 中提取試算表 ID。
50
  """
51
- parsed_url = urlparse(sheet_url)
52
- path_parts = parsed_url.path.split('/')
53
- try:
54
- # Google Sheet URL 格式通常是 /spreadsheets/d/SPREADSHEET_ID/edit...
55
- if 'd' in path_parts:
56
- id_index = path_parts.index('d') + 1
57
- if id_index < len(path_parts):
58
- spreadsheet_id = path_parts[id_index]
59
- # 進行一些基本檢查,確保它看起來像一個 ID
60
- if len(spreadsheet_id) > 30: # Google Sheet ID 通常很長
61
- return spreadsheet_id
62
- except ValueError:
63
- pass # 'd' 不在路徑中
64
-
65
- logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
66
- return None
67
 
68
  def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
69
  """
70
  從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
71
- 返回整數型別的 gid 或 None。
72
  """
73
- parsed_url = urlparse(sheet_url)
74
- query_params = parse_qs(parsed_url.query)
75
- fragment_params = parse_qs(parsed_url.fragment) # gid 也可能在 # 後面
76
-
77
- gid_str = None
78
- if 'gid' in query_params:
79
- gid_str = query_params['gid'][0]
80
- elif 'gid' in fragment_params:
81
- gid_str = fragment_params['gid'][0]
82
-
83
- if gid_str:
84
- try:
85
- return int(gid_str)
86
- except ValueError:
87
- logging.warning(f"URL 中的 gid 不是有效的整數: {gid_str}")
88
- return None
89
- else:
90
- # logging.info(f"URL 中未找到 gid 參數,將嘗試使用第一個工作表: {sheet_url}")
91
- # 如果 URL 沒有 gid,通常表示是第一個工作表,其 gid 通常是 0
92
- # 但我們在這裡返回 None,讓後續邏輯決定如何處理
93
- return None
94
 
95
  def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
96
  """
97
  使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
98
- 如果 gid 為 None,則返回第一個工作表的名稱。
99
-
100
- Args:
101
- spreadsheet_id (str): Google 試算表的 ID。
102
- gid (int | None): 目標工作表分頁的 ID。如果為 None,則獲取第一個工作表。
103
-
104
- Returns:
105
- str | None: 工作表的名稱 (title),如果找不到或發生錯誤則返回 None。
106
  """
107
  if not self.service:
108
  logging.error("Sheet API 服務未成功初始化。")
109
  return None
 
 
 
110
  try:
111
  # 使用 spreadsheets.get 獲取試算表的中繼資料
112
  # fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
@@ -120,39 +114,36 @@ class SheetService:
120
  logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
121
  return None
122
 
123
- if gid is not None:
124
- # 如果提供了 gid,尋找匹配的工作表
125
- for sheet in sheets:
126
- properties = sheet.get('properties', {})
127
- if properties.get('sheetId') == gid:
128
- sheet_title = properties.get('title')
129
- if sheet_title:
130
- logging.info(f"找到 gid={gid} 對應的工作表名稱: '{sheet_title}'")
131
- return sheet_title
132
- else:
133
- logging.warning(f"找到 gid={gid} 但缺少 title 屬性。")
134
- return None
135
- # 如果遍歷完畢沒有找到匹配的 gid
136
- logging.warning(f"在試算表 {spreadsheet_id} 中未找到 gid={gid} 的工作表。")
137
- return None
138
- else:
139
- # 如果 gid 為 None,返回第一個工作表的名稱
140
- first_sheet_properties = sheets[0].get('properties', {})
141
- first_sheet_title = first_sheet_properties.get('title')
142
- first_sheet_gid = first_sheet_properties.get('sheetId', '未知')
143
- if first_sheet_title:
144
- logging.info(f"未提供 gid,使用第一個工作表 (gid={first_sheet_gid}): '{first_sheet_title}'")
145
- return first_sheet_title
146
- else:
147
- logging.warning(f"第一個工作表 (gid={first_sheet_gid}) 缺少 title 屬性。")
148
- return None
149
-
150
- except googleapiclient.errors.HttpError as error:
151
- logging.error(f"獲取工作表名稱時發生 API 錯誤: {error}")
152
  return None
 
153
  except Exception as e:
154
- logging.error(f"獲取工作表名稱時發生未知錯誤: {e}")
 
 
 
 
 
 
 
 
 
155
  return None
 
 
156
 
157
  def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
158
  """
@@ -392,4 +383,70 @@ class SheetService:
392
  return False
393
  except Exception as e:
394
  logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
395
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from urllib.parse import urlparse, parse_qs
6
  import logging # 建議使用 logging 而非 print
7
  import string # 導入 string 模組用於轉換欄位索引
8
+ import re # 導入 re 模組
9
 
10
  # 設定基本的 logging
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
45
  self.service = None
46
  self.sheet = None
47
 
48
+ def _extract_ids_from_url(self, sheet_url):
49
+ """
50
+ (私有方法) 從 Google Sheets URL 中提取 spreadsheetId 和 gid。
51
+ """
52
+ spreadsheet_id = None
53
+ gid = None
54
+ # 嘗試從 URL 中提取 spreadsheetId (通常在 /d/ 和 /edit 之間)
55
+ match_id = re.search(r'/d/([a-zA-Z0-9-_]+)', sheet_url)
56
+ if match_id:
57
+ spreadsheet_id = match_id.group(1)
58
+ # 嘗試從 URL 中提取 gid (通常在 gid= 後面)
59
+ match_gid = re.search(r'[#&]gid=(\d+)', sheet_url)
60
+ if match_gid:
61
+ gid = int(match_gid.group(1)) # gid 是數字
62
+ else:
63
+ # 如果 URL 沒有 gid,通常指向第一個工作表,gid 為 0
64
+ # logging.info(f"URL 中未找到 gid,假設為第一個工作表 (gid=0): {sheet_url}")
65
+ gid = 0
66
+ return spreadsheet_id, gid
67
+
68
  def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
69
  """
70
  從 Google Sheets URL 中提取試算表 ID。
71
  """
72
+ spreadsheet_id, _ = self._extract_ids_from_url(sheet_url)
73
+ if spreadsheet_id:
74
+ # 可以添加一些基本檢查,確保它看起來像一個 ID
75
+ if len(spreadsheet_id) > 30: # Google Sheet ID 通常很長
76
+ return spreadsheet_id
77
+ else:
78
+ logging.warning(f"從 URL 提取的 ID '{spreadsheet_id}' 看起來太短,可能無效: {sheet_url}")
79
+ return None
80
+ else:
81
+ logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
82
+ return None
 
 
 
 
 
83
 
84
  def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
85
  """
86
  從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
87
+ 返回整數型別的 gid 或 None (如果未指定則返回 0)
88
  """
89
+ _, gid = self._extract_ids_from_url(sheet_url)
90
+ # 即使 gid 是 0 (預設),也將其視為有效的 gid
91
+ return gid
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
94
  """
95
  使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
96
+ 如果 gid 為 None,則返回第一個工作表的名稱 (gid=0)。
 
 
 
 
 
 
 
97
  """
98
  if not self.service:
99
  logging.error("Sheet API 服務未成功初始化。")
100
  return None
101
+ # 如果傳入的 gid 是 None,我們將其視為 0
102
+ target_gid = gid if gid is not None else 0
103
+
104
  try:
105
  # 使用 spreadsheets.get 獲取試算表的中繼資料
106
  # fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
 
114
  logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
115
  return None
116
 
117
+ for sheet in sheets:
118
+ properties = sheet.get('properties', {})
119
+ if properties.get('sheetId') == target_gid:
120
+ sheet_title = properties.get('title')
121
+ logging.info(f"找到 gid={target_gid} 對應的工作表名稱: '{sheet_title}'")
122
+ return sheet_title
123
+
124
+ # 如果循環結束還沒找到
125
+ logging.warning(f"在試算表 {spreadsheet_id} 中未找到 gid={target_gid} 對應的工作表。")
126
+ # 如果 gid 是 0 但找不到,可能試算表是空的或有問題,返回第一個工作表的名稱作為備選
127
+ if target_gid == 0 and sheets:
128
+ first_sheet_title = sheets[0].get('properties', {}).get('title')
129
+ logging.warning(f"返回第一個工作表的名稱 '{first_sheet_title}' 作為 gid=0 的備選。")
130
+ return first_sheet_title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  return None
132
+
133
  except Exception as e:
134
+ logging.error(f"獲取工作表名稱時發生 API 錯誤 (ID: {spreadsheet_id}, GID: {target_gid}): {e}")
135
+ return None
136
+
137
+ def get_sheet_name_by_url(self, sheet_url: str) -> str | None:
138
+ """
139
+ 從 Google Sheets URL 中提取 gid 並獲取對應的工作表名稱。
140
+ """
141
+ spreadsheet_id, gid = self._extract_ids_from_url(sheet_url)
142
+ if not spreadsheet_id:
143
+ logging.error(f"無法從 URL {sheet_url} 中提取 Spreadsheet ID。")
144
  return None
145
+ # 直接調用已有的 get_sheet_name_by_gid 方法
146
+ return self.get_sheet_name_by_gid(spreadsheet_id, gid)
147
 
148
  def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
149
  """
 
383
  return False
384
  except Exception as e:
385
  logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
386
+ return False
387
+
388
+ def batch_update_cells(self, sheet_url: str, update_data: list[dict]) -> dict | None:
389
+ """
390
+ 批次更新 Google Sheet 中的多個儲存格。
391
+
392
+ Args:
393
+ sheet_url (str): Google Sheet 的 URL。
394
+ update_data (list[dict]): 一個字典列表,每個字典包含:
395
+ 'range' (str): 要更新的範圍 (例如 '工作表名!A1')
396
+ 'value' (str): 要寫入的值
397
+
398
+ Returns:
399
+ dict | None: Google API 的返回結果,如果成功。否則返回 None。
400
+ """
401
+ spreadsheet_id, _ = self._extract_ids_from_url(sheet_url)
402
+ if not spreadsheet_id:
403
+ logging.error(f"錯誤:無法從 URL {sheet_url} 中提取 Spreadsheet ID 進行批次更新。")
404
+ return None
405
+ if not self.service:
406
+ logging.error("Sheet API 服務未成功初始化。")
407
+ return None
408
+ if not update_data:
409
+ logging.warning("沒有需要批次更新的資料。")
410
+ return None
411
+
412
+ # 確保 update_data 中的 range 包含 sheet_name
413
+ # (調用此方法前應已獲取 sheet_name 並填入 range)
414
+ # 例如 update_data = [{'range': '測試!F2', 'value': 'OO'}, ...]
415
+
416
+ body = {
417
+ 'valueInputOption': 'USER_ENTERED', # 或者 'RAW'
418
+ 'data': []
419
+ }
420
+ for item in update_data:
421
+ # 基本的範圍和值檢查
422
+ if 'range' not in item or 'value' not in item:
423
+ logging.warning(f"跳過格式錯誤的更新項目: {item}")
424
+ continue
425
+ if '!' not in item['range'] or len(item['range'].split('!')[1]) < 2: # 簡單檢查範圍格式
426
+ logging.warning(f"跳過範圍格式可能錯誤的更新項目: {item['range']}")
427
+ continue
428
+
429
+ body['data'].append({
430
+ 'range': item['range'],
431
+ 'values': [[item['value']]] # value 需要是二維列表
432
+ })
433
+
434
+ if not body['data']:
435
+ logging.warning("沒有有效的資料可以進行批次更新。")
436
+ return None
437
+
438
+ try:
439
+ logging.info(f"準備批次更新 Spreadsheet ID: {spreadsheet_id},共 {len(body['data'])} 個儲存格...")
440
+ result = self.service.spreadsheets().values().batchUpdate(
441
+ spreadsheetId=spreadsheet_id,
442
+ body=body
443
+ ).execute()
444
+ total_updated = result.get('totalUpdatedCells', 0)
445
+ logging.info(f"批次更新完成。共更新了 {total_updated} 個儲存格。")
446
+ return result
447
+ except Exception as e:
448
+ logging.error(f"批次更新儲存格時發生 API 錯誤: {e}")
449
+ # from googleapiclient.errors import HttpError
450
+ # if isinstance(e, HttpError):
451
+ # logging.error(f"錯誤詳情: {e.content}")
452
+ return None