youngtsai commited on
Commit
01d9916
·
1 Parent(s): cf3160b

def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column

Browse files
Files changed (2) hide show
  1. app.py +148 -23
  2. sheet_service.py +128 -2
app.py CHANGED
@@ -2157,10 +2157,135 @@ def summary_add_markdown_version(video_id):
2157
  def get_sheet_data(sheet_url, range_name):
2158
  data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
2159
  flattened_data = SHEET_SERVICE.flatten_column_data(data)
2160
- return flattened_data
 
2161
 
 
 
 
 
2162
 
2163
- def refresh_video_LLM_all_content(video_ids):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2164
  # 輸入影片 id,以 , 逗號分隔 或是 \n 換行
2165
  video_id_list = video_ids.replace('\n', ',').split(',')
2166
  video_id_list = [vid.strip() for vid in video_id_list if vid.strip()]
@@ -2170,18 +2295,7 @@ def refresh_video_LLM_all_content(video_ids):
2170
 
2171
  for video_id in video_id_list:
2172
  try:
2173
- print(f"===refresh_all_LLM_content===")
2174
- print(f"video_id: {video_id}")
2175
- # 刪除 GCS 中所有以 video_id 開頭的檔案
2176
- print(f"===delete_blobs_by_folder_name: {video_id}===")
2177
- bucket_name = 'video_ai_assistant'
2178
- GCS_SERVICE.delete_blobs_by_folder_name(bucket_name, video_id)
2179
- print(f"所有以 {video_id} 開頭的檔案已刪除")
2180
-
2181
- # process_youtube_link
2182
- video_link = f"https://www.youtube.com/watch?v={video_id}"
2183
- process_youtube_link(PASSWORD, video_link)
2184
-
2185
  success_video_ids.append(video_id)
2186
  except Exception as e:
2187
  print(f"===refresh_all_LLM_content error===")
@@ -3663,16 +3777,11 @@ def create_app():
3663
  refresh_btn = gr.Button("refresh", variant="primary")
3664
  with gr.Tab("by sheets"):
3665
  sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
 
 
3666
  sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
3667
  sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
3668
- sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
3669
-
3670
- sheet_get_value_btn.click(
3671
- get_sheet_data,
3672
- inputs=[sheet_url, gr.Textbox(value="D:D", visible=True)], # 將範圍修改為 D 欄
3673
- outputs=[sheet_get_value_result]
3674
- )
3675
-
3676
  with gr.Row():
3677
  refresh_result = gr.JSON()
3678
 
@@ -3681,10 +3790,26 @@ def create_app():
3681
  inputs=[],
3682
  outputs=[refresh_btn]
3683
  ).then(
3684
- refresh_video_LLM_all_content,
3685
  inputs=[refresh_video_ids],
3686
  outputs=[refresh_result]
3687
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3688
 
3689
 
3690
  # OPEN AI CHATBOT SELECT
 
2157
  def get_sheet_data(sheet_url, range_name):
2158
  data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
2159
  flattened_data = SHEET_SERVICE.flatten_column_data(data)
2160
+ flattened_data_string = ', '.join(flattened_data)
2161
+ return flattened_data_string
2162
 
2163
+ def update_sheet_data(sheet_url, qa_result, video_id):
2164
+ # 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
2165
+ sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
2166
+ print(f"sheet_data: {sheet_data}")
2167
 
2168
+ if not sheet_data or len(sheet_data) < 1:
2169
+ print("錯誤:工作表資料為空或缺少標頭列。")
2170
+ return
2171
+
2172
+ header_row = sheet_data[0]
2173
+ data_rows = sheet_data[1:] # 資料列(跳過標頭)
2174
+
2175
+ # 直接指定要尋找的欄位名稱
2176
+ target_column_name = '均一平台 YT Readable ID'
2177
+ target_qa_column_name = "QA"
2178
+
2179
+ try:
2180
+ # 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
2181
+ video_id_col_index = header_row.index(target_column_name)
2182
+ except ValueError:
2183
+ # 如果找不到指定的欄位名稱,印出錯誤並返回
2184
+ print(f"錯誤:在標頭列 {header_row} 中找不到指定的欄位名稱 '{target_column_name}'")
2185
+ return
2186
+
2187
+ # 2. 遍歷資料列,尋找 video_id 匹配的列索引 (row index)
2188
+ target_row_index = -1 # 初始化為 -1,表示未找到
2189
+ for i, row in enumerate(data_rows):
2190
+ # 確保該列有足夠的欄位,並且該欄位的值等於 video_id
2191
+ if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
2192
+ target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1
2193
+ break # 找到後即可跳出迴圈
2194
+
2195
+ if target_row_index != -1:
2196
+ print(f"找到 video_id '{video_id}' 於欄位 '{target_column_name}' 的第 {target_row_index + 1} 列 (工作表列號)") # +1 是為了顯示給人類看的行號
2197
+
2198
+ # --- 以下是更新邏輯 (需要取消註解並確保 qa_column 也正確) ---
2199
+ try:
2200
+ # 找到 QA 欄位的索引 (假設 qa_column 參數傳入的是正確的標題文字, e.g., 'QA')
2201
+ qa_col_index = header_row.index(target_qa_column_name)
2202
+
2203
+ # 更新 sheet 中的 qa_column 和 qa_result
2204
+ # 確保目標列有足夠的欄位可以更新
2205
+ if len(sheet_data[target_row_index]) > qa_col_index:
2206
+ # 注意:這裡直接修改 sheet_data 列表可能不會直接更新 Google Sheet
2207
+ # 你需要使用 SHEET_SERVICE 的更新方法
2208
+ # sheet_data[target_row_index][qa_col_index] = qa_result
2209
+ print(f"準備更新 第 {target_row_index + 1} 列, 欄位 '{target_qa_column_name}' (索引 {qa_col_index}) 為 '{qa_result}'")
2210
+ # --- 實際更新 Google Sheet 的程式碼
2211
+ SHEET_SERVICE.update_sheet_cell(sheet_url, target_row_index, qa_col_index, qa_result)
2212
+
2213
+ else:
2214
+ print(f"錯誤:第 {target_row_index + 1} 列沒有足夠的欄位來更新 QA (索引 {qa_col_index})。")
2215
+
2216
+ except ValueError:
2217
+ print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{qa_column}'")
2218
+ # --- 更新邏輯結束 ---
2219
+
2220
+ else:
2221
+ # 如果找不到 video_id,印出錯誤訊息
2222
+ print(f"錯誤:在欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
2223
+
2224
+ def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column
2225
+ video_ids = video_ids.replace('\n', ',').split(',')
2226
+ video_ids = [vid.strip() for vid in video_ids if vid.strip()]
2227
+
2228
+ success_video_ids = []
2229
+ failed_video_ids = []
2230
+
2231
+ sheet_qa_success_tag = "OO"
2232
+ sheet_qa_failed_tag = "XX"
2233
+
2234
+ for video_id in video_ids:
2235
+ try:
2236
+ # 確認 GCS 中是否存在 ID_transcript.json in GCS
2237
+ # 如果存在就 pass
2238
+ # 如果不存在就 refresh_video_LLM_all_content_by_id
2239
+ bucket_name = 'video_ai_assistant'
2240
+ file_name = f'{video_id}_transcript.json'
2241
+ blob_name = f"{video_id}/{file_name}"
2242
+ is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
2243
+ if not is_file_exists:
2244
+ print(f"{video_id} 不存在逐字稿")
2245
+ refresh_video_LLM_all_content_by_id(video_id)
2246
+ else:
2247
+ print(f"{video_id} 存在逐字稿")
2248
+
2249
+ qa_result = sheet_qa_success_tag
2250
+ # 更新呼叫,不再傳遞 sheet_video_column
2251
+ update_sheet_data(sheet_url, qa_result, video_id)
2252
+ success_video_ids.append(video_id) # 假設 update_sheet_data 成功就加入 success
2253
+ except Exception as e:
2254
+ print(f"===refresh_video_LLM_all_content_by_sheet error===")
2255
+ print(f"video_id: {video_id}")
2256
+ print(f"error: {str(e)}")
2257
+ print(f"===refresh_video_LLM_all_content_by_sheet error===")
2258
+ failed_video_ids.append(video_id)
2259
+ qa_result = sheet_qa_failed_tag
2260
+ try:
2261
+ # 即使前面出錯,還是嘗試更新 QA 狀態為 XX
2262
+ update_sheet_data(sheet_url, qa_result, video_id)
2263
+ except Exception as update_e:
2264
+ print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
2265
+ print(f"video_id: {video_id}")
2266
+ print(f"error: {str(update_e)}")
2267
+ print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
2268
+
2269
+
2270
+ result = {
2271
+ "success_video_ids": success_video_ids,
2272
+ "failed_video_ids": failed_video_ids
2273
+ }
2274
+ return result
2275
+
2276
+ def refresh_video_LLM_all_content_by_id(video_id):
2277
+ print(f"===refresh_all_LLM_content===")
2278
+ print(f"video_id: {video_id}")
2279
+ print(f"===delete_blobs_by_folder_name: {video_id}===")
2280
+ bucket_name = 'video_ai_assistant'
2281
+ GCS_SERVICE.delete_blobs_by_folder_name(bucket_name, video_id)
2282
+ print(f"所有以 {video_id} 開頭的檔案已刪除")
2283
+
2284
+ # process_youtube_link
2285
+ video_link = f"https://www.youtube.com/watch?v={video_id}"
2286
+ process_youtube_link(PASSWORD, video_link)
2287
+
2288
+ def refresh_video_LLM_all_content_by_ids(video_ids):
2289
  # 輸入影片 id,以 , 逗號分隔 或是 \n 換行
2290
  video_id_list = video_ids.replace('\n', ',').split(',')
2291
  video_id_list = [vid.strip() for vid in video_id_list if vid.strip()]
 
2295
 
2296
  for video_id in video_id_list:
2297
  try:
2298
+ refresh_video_LLM_all_content_by_id(video_id)
 
 
 
 
 
 
 
 
 
 
 
2299
  success_video_ids.append(video_id)
2300
  except Exception as e:
2301
  print(f"===refresh_all_LLM_content error===")
 
3777
  refresh_btn = gr.Button("refresh", variant="primary")
3778
  with gr.Tab("by sheets"):
3779
  sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
3780
+ sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="D:D")
3781
+ sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="F:F")
3782
  sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
3783
  sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
3784
+ sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
 
 
 
 
 
 
 
3785
  with gr.Row():
3786
  refresh_result = gr.JSON()
3787
 
 
3790
  inputs=[],
3791
  outputs=[refresh_btn]
3792
  ).then(
3793
+ refresh_video_LLM_all_content_by_ids,
3794
  inputs=[refresh_video_ids],
3795
  outputs=[refresh_result]
3796
  )
3797
+
3798
+ sheet_get_value_btn.click(
3799
+ get_sheet_data,
3800
+ inputs=[sheet_url, sheet_video_column],
3801
+ outputs=[sheet_get_value_result]
3802
+ )
3803
+
3804
+ sheet_refresh_btn.click(
3805
+ lambda: gr.update(interactive=False),
3806
+ inputs=[],
3807
+ outputs=[sheet_refresh_btn]
3808
+ ).then(
3809
+ refresh_video_LLM_all_content_by_sheet,
3810
+ inputs=[sheet_url, sheet_QA_column, sheet_get_value_result],
3811
+ outputs=[refresh_result]
3812
+ )
3813
 
3814
 
3815
  # OPEN AI CHATBOT SELECT
sheet_service.py CHANGED
@@ -4,6 +4,7 @@ from google.oauth2 import service_account
4
  import json
5
  from urllib.parse import urlparse, parse_qs
6
  import logging # 建議使用 logging 而非 print
 
7
 
8
  # 設定基本的 logging
9
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -12,7 +13,7 @@ class SheetService:
12
  """
13
  一個用於與 Google Sheets API 互動的服務類別。
14
  """
15
- SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
16
 
17
  def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
18
  """
@@ -266,4 +267,129 @@ class SheetService:
266
  # 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
267
  # 並將其轉換為字串 str() 以確保類型一致性
268
  flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
269
- return flattened
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  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')
 
13
  """
14
  一個用於與 Google Sheets API 互動的服務類別。
15
  """
16
+ SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
17
 
18
  def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
19
  """
 
267
  # 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
268
  # 並將其轉換為字串 str() 以確保類型一致性
269
  flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
270
+ return flattened
271
+
272
+ def update_sheet_data_by_url(self, sheet_url, data):
273
+ spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
274
+ self.sheet.values().update(
275
+ spreadsheetId=spreadsheet_id,
276
+ range='A1:Z1000',
277
+ valueInputOption='USER_ENTERED',
278
+ body={'values': data}
279
+ ).execute()
280
+
281
+ return True
282
+
283
+ @staticmethod
284
+ def _col_index_to_letter(col_index: int) -> str:
285
+ """將 0-based 的欄位索引轉換為 A1 表示法的字母。"""
286
+ if col_index < 0:
287
+ raise ValueError("Column index must be non-negative")
288
+ letters = ''
289
+ while col_index >= 0:
290
+ col_index, remainder = divmod(col_index, 26)
291
+ letters = string.ascii_uppercase[remainder] + letters
292
+ col_index -= 1 # 因為 divmod 是 0-based,但 A=1, Z=26, AA=27 的轉換需要調整
293
+ if col_index < -1: # 修正邊界條件
294
+ break
295
+ # 修正:上面的邏輯有點複雜且可能有誤,改用更簡單的方式
296
+ letters = ''
297
+ dividend = col_index + 1 # 轉為 1-based
298
+ while dividend > 0:
299
+ module = (dividend - 1) % 26
300
+ letters = string.ascii_uppercase[module] + letters
301
+ dividend = (dividend - module) // 26
302
+ return letters if letters else "A" # 確保至少返回 "A"
303
+
304
+ @staticmethod
305
+ def _col_index_to_letter_simple(col_index: int) -> str:
306
+ """將 0-based 的欄位索引轉換為 A1 表示法的字母 (簡化版,適用於 A-ZZ)。"""
307
+ if col_index < 0:
308
+ raise ValueError("Column index must be non-negative")
309
+ letters = ""
310
+ while col_index >= 0:
311
+ letters = string.ascii_uppercase[col_index % 26] + letters
312
+ col_index = col_index // 26 - 1
313
+ return letters
314
+
315
+ # SHEET_SERVICE.update_sheet_cell(sheet_url, target_row_index, qa_col_index, qa_result)
316
+ def update_sheet_cell(self, sheet_url: str, target_row_index_in_data: int, qa_col_index: int, qa_result: str) -> bool:
317
+ """
318
+ 更新指定 URL 的 Google Sheet 中的單一儲存格。
319
+
320
+ Args:
321
+ sheet_url (str): Google 試算表的完整 URL。
322
+ target_row_index_in_data (int): 目標列在 get_sheet_data_by_url 返回的列表中的索引
323
+ (從 1 開始算,因為 0 是標頭)。
324
+ qa_col_index (int): 目標欄位的索引 (從 0 開始算)。
325
+ qa_result (str): 要寫入儲存格的值。
326
+
327
+ Returns:
328
+ bool: 更新是否成功。
329
+ """
330
+ if not self.sheet:
331
+ logging.error("Sheet API 服務未成功初始化。")
332
+ return False
333
+
334
+ spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
335
+ if not spreadsheet_id:
336
+ logging.error(f"無法從 URL 獲取 Spreadsheet ID: {sheet_url}")
337
+ return False
338
+
339
+ gid = self.get_sheet_gid_by_url(sheet_url)
340
+ sheet_name = self.get_sheet_name_by_gid(spreadsheet_id, gid)
341
+ if not sheet_name:
342
+ logging.error(f"無法根據 URL ({sheet_url}) 確定要更新的工作表名稱。")
343
+ return False
344
+
345
+ # 將 0-based 的欄位索引轉換為字母
346
+ try:
347
+ col_letter = self._col_index_to_letter_simple(qa_col_index)
348
+ except ValueError as e:
349
+ logging.error(f"無效的欄位索引 {qa_col_index}: {e}")
350
+ return False
351
+
352
+ # 計算實際的工作表列號
353
+ # target_row_index_in_data 是 sheet_data 中的索引 (1-based)
354
+ # 實際列號是 target_row_index_in_data + 1 (因為工作表通常從第 1 列開始)
355
+ actual_sheet_row = target_row_index_in_data + 1
356
+
357
+ # 構建 A1 表示法的範圍,例如 '測試'!F3
358
+ # 需要正確處理工作表名稱中的特殊字符(例如空格)
359
+ if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name or "'" in sheet_name:
360
+ # 修正:使用三引號定義 f-string 以避免引號衝突
361
+ safe_sheet_name = f"""'{sheet_name.replace("'", "''")}'""" # 單引號用兩個單引號轉義
362
+ else:
363
+ safe_sheet_name = sheet_name
364
+ range_name = f"{safe_sheet_name}!{col_letter}{actual_sheet_row}"
365
+
366
+ logging.info(f"準備更新 Spreadsheet ID: {spreadsheet_id}, Range: {range_name}, Value: {qa_result}")
367
+
368
+ try:
369
+ body = {
370
+ 'values': [[qa_result]] # 更新單一儲存格的值需要是 list of lists
371
+ }
372
+ result = self.sheet.values().update(
373
+ spreadsheetId=spreadsheet_id,
374
+ range=range_name,
375
+ valueInputOption='USER_ENTERED', # 或者 'RAW' 如果你不需要 Google Sheets 解釋輸入
376
+ body=body
377
+ ).execute()
378
+ logging.info(f"成功更新儲存格 {range_name}。 更新了 {result.get('updatedCells')} 個儲存格。")
379
+ return True
380
+ except googleapiclient.errors.HttpError as error:
381
+ # 記錄更詳細的錯誤
382
+ error_details = error.resp.get('content', '{}')
383
+ try:
384
+ error_json = json.loads(error_details)
385
+ error_message = error_json.get('error', {}).get('message', str(error))
386
+ except json.JSONDecodeError:
387
+ error_message = str(error)
388
+ logging.error(f"更新儲存格時發生 API 錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {error_message}")
389
+ # 檢查是否是權限錯誤
390
+ if error.resp.status == 403:
391
+ logging.error("錯誤 403:權限不足。請檢查服務帳戶是否有目標工作表的編輯權限,以及 API 金鑰是否啟用了正確的範圍 (scopes)。")
392
+ return False
393
+ except Exception as e:
394
+ logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
395
+ return False