Spaces:
Sleeping
Sleeping
def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column
Browse files- app.py +148 -23
- 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 |
-
|
|
|
2161 |
|
|
|
|
|
|
|
|
|
2162 |
|
2163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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
|
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
|