tridanindi commited on
Commit
dfe52de
·
verified ·
1 Parent(s): ed54ab1

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +0 -0
  2. spanish_processor.py +297 -0
app.py ADDED
The diff for this file is too large to render. See raw diff
 
spanish_processor.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # english_processor.py
2
+
3
+ import os
4
+ import re
5
+ import json
6
+ import base64
7
+ import httpx
8
+ import asyncio
9
+ import traceback
10
+ import pandas as pd
11
+ from urllib.parse import urljoin
12
+ import datetime
13
+ import random
14
+ import subprocess
15
+
16
+ # --- CÁC HÀM TIỆN ÍCH SẼ ĐƯỢC TRUYỀN VÀO TỪ app.py ---
17
+ # Chúng ta sẽ không định nghĩa lại các hàm này ở đây,
18
+ # mà sẽ nhận chúng dưới dạng tham số để tránh trùng lặp code.
19
+ # Các hàm tiện ích bao gồm:
20
+ # - generate_with_gemini_auto_retry
21
+ # - send_telegram_message
22
+ # - get_random_common_images_from_drive
23
+ # - move_file_on_drive
24
+ # - append_row_to_excel
25
+ # - upload_file_to_drive
26
+ # - _split_sentences
27
+
28
+ #==============================================================================
29
+ # >>>>> LOGIC XÀO NỘI DUNG SANG TIẾNG ANH <<<<<
30
+ #==============================================================================
31
+
32
+
33
+
34
+
35
+ async def rewrite_content_with_gemini_en(
36
+ title: str,
37
+ content: str,
38
+ generate_with_gemini_auto_retry # Nhận hàm tiện ích làm tham số
39
+ ) -> dict | None:
40
+ """
41
+ Hàm mới, tách biệt logic để dịch và viết lại nội dung sang Tiếng Anh.
42
+ """
43
+ try:
44
+ sanitized_title = title.replace('"', '\\"')
45
+ sanitized_content = content.replace('"', '\\"')
46
+ current_date = datetime.datetime.now().strftime("%m/%d")
47
+
48
+ prompt = (
49
+ f"Eres un editor de noticias profesional para canales de YouTube y TikTok dirigidos a una audiencia de habla HISPANA.\n"
50
+ f"Basado en el artículo original en vietnamita, crea el contenido requerido en ESPAÑOL. "
51
+ f"SIEMPRE elimina las citas de fuentes como 'según Dan Tri', 'fuentes dijeron'.\n\n"
52
+ f"--- REQUISITOS ---\n"
53
+ f"1. **youtube_title:** Un título principal pegadizo y sensacional para YouTube/TikTok. Comienza con 'Noticias del Mundo {current_date}:'. **IMPORTANTE: Debe tener menos de 100 caracteres.**\n"
54
+ f"2. **image_title:** Un título muy corto y potente (5-9 palabras) para la superposición de la imagen.\n"
55
+ f"3. **youtube_description:** Un resumen corto y profesional (2-3 frases) para la publicación de YouTube con una llamada a la acción.\n"
56
+ f"4. **tts_script:** Un objeto JSON que contenga exactamente 3 claves para el guion de la voz en off:\n"
57
+ f" - **intro:** Una única frase de bienvenida. Ejemplo: 'Bienvenidos a su actualización de noticias mundiales.'\n"
58
+ f" - **main_content:** El contenido principal de la noticia, reescrito a partir del artículo original. **Limitar a menos de 300 palabras. NO DEBE contener frases de introducción o cierre.**\n"
59
+ f" - **outro:** Una única frase de cierre. Ejemplo: 'Gracias por sintonizarnos.'\n\n"
60
+ f"--- ARTÍCULO ORIGINAL EN VIETNAMITA ---\n"
61
+ f"**Original Title:** {sanitized_title}\n"
62
+ f"**Original Content:** {sanitized_content}\n\n"
63
+ f"--- FORMATO DE SALIDA JSON (DEVOLVER SOLO EL JSON) ---\n"
64
+ f'{{"youtube_title": "[Contenido para el Título de YouTube]", "image_title": "[Contenido para el Título de la Imagen]", "youtube_description": "[Contenido para la Descripción de YouTube]", "tts_script": {{"intro": "[Frase única de bienvenida]", "main_content": "[Contenido principal de la noticia]", "outro": "[Frase única de cierre]"}}}}'
65
+ )
66
+
67
+
68
+ response = await generate_with_gemini_auto_retry(prompt)
69
+ cleaned_response_text = response.text.strip().replace("```json", "").replace("```", "").strip()
70
+
71
+ json_match = re.search(r'\{.*\}', cleaned_response_text, re.DOTALL)
72
+ if json_match:
73
+ return json.loads(json_match.group(0))
74
+
75
+ print("❌ English Rewrite Error: Gemini did not return valid JSON.")
76
+ print(" Raw response:", response.text)
77
+ return None
78
+
79
+ except Exception as e:
80
+ print(f"❌ Error during English content rewrite: {e}")
81
+ return None
82
+
83
+ #==============================================================================
84
+ # >>>>> LOGIC TẠO ÂM THANH TIẾNG ANH <<<<<
85
+ #==============================================================================
86
+
87
+ async def convert_text_to_mp3_en(
88
+ text_or_script: str | dict,
89
+ chat_id: int,
90
+ gemini_api_keys: list,
91
+ temp_path: str,
92
+ mp3_save_path: str,
93
+ send_telegram_message,
94
+ _split_sentences
95
+ ) -> tuple[str | None, str | None]:
96
+ """
97
+ [PHIÊN BẢN ĐỒNG BỘ 1:1 VỚI BẢN TIẾNG VIỆT]
98
+ Tạo file âm thanh Tiếng Anh, sử dụng logic và endpoint TTS mới nhất.
99
+ """
100
+ async def send_error_log(base_message: str):
101
+ error_traceback = traceback.format_exc()
102
+ detailed_error_message = (f"❌ **Lỗi tạo Audio (SPAIN)**\n\n{base_message}\n\n{error_traceback}")
103
+ await send_telegram_message(chat_id, detailed_error_message[-4000:])
104
+
105
+ if not gemini_api_keys: return None, "Config Error: Gemini API key list is empty (SPAIN)."
106
+
107
+ full_text = ""
108
+ if isinstance(text_or_script, dict):
109
+ # Lấy đối tượng tts_script nếu có
110
+ tts_script = text_or_script.get('tts_script', {}) if 'tts_script' in text_or_script else text_or_script
111
+ intro = tts_script.get('intro', '').strip()
112
+ main_content = tts_script.get('main_content', '').strip()
113
+ outro = tts_script.get('outro', '').strip()
114
+ full_text = ' '.join(filter(None, [intro, main_content, outro]))
115
+ else:
116
+ full_text = text_or_script
117
+
118
+ sentences = _split_sentences(full_text)
119
+ if not sentences: return None, "Content Error: Input text contains no valid sentences (SPAIN)."
120
+
121
+ temp_wav_files, last_api_error = [], "No specific API error."
122
+ base_delay, max_delay, max_retries_per_sentence = 2, 60, len(gemini_api_keys) * 2
123
+
124
+ for i, sentence in enumerate(sentences):
125
+ start_index, attempts, success = random.randint(0, len(gemini_api_keys) - 1), 0, False
126
+ current_index = start_index
127
+
128
+ while attempts < max_retries_per_sentence:
129
+ styled_text = f"{sentence}"
130
+ api_key = gemini_api_keys[current_index]
131
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key={api_key}"
132
+ payload = { "model": "gemini-2.5-flash-preview-tts", "contents": [{"parts": [{"text": styled_text}]}], "generationConfig": { "responseModalities": ["AUDIO"], "speechConfig": { "voiceConfig": { "prebuiltVoiceConfig": { "voiceName": "Alnilam" }}}}}
133
+
134
+ try:
135
+ async with httpx.AsyncClient(timeout=60.0) as client:
136
+ response = await client.post(url, json=payload)
137
+ response.raise_for_status()
138
+
139
+ audio_content = response.json().get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("inlineData", {}).get("data")
140
+ if audio_content:
141
+ audio_bytes = base64.b64decode(audio_content)
142
+ temp_raw_path = os.path.join(temp_path, f"part_{i}_en.raw")
143
+ with open(temp_raw_path, "wb") as f: f.write(audio_bytes)
144
+
145
+ temp_wav_path = os.path.join(temp_path, f"part_{i}_en.wav")
146
+ ffmpeg_pcm_to_wav = ['ffmpeg', '-y', '-f', 's16le', '-ar', '24000', '-ac', '1', '-i', temp_raw_path, temp_wav_path]
147
+ process = await asyncio.create_subprocess_exec(*ffmpeg_pcm_to_wav, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
148
+ await process.communicate()
149
+ if process.returncode != 0: raise ValueError("FFmpeg failed to convert raw PCM to WAV.")
150
+
151
+ temp_wav_files.append(temp_wav_path)
152
+ os.remove(temp_raw_path)
153
+ success = True
154
+ break
155
+ else:
156
+ raise ValueError("API response did not contain audio data (SPAIN).")
157
+ except httpx.HTTPStatusError as e:
158
+ last_api_error = str(e); await asyncio.sleep(1)
159
+ except Exception as e:
160
+ last_api_error = str(e); await asyncio.sleep(1)
161
+
162
+ current_index = (current_index + 1) % len(gemini_api_keys)
163
+ attempts += 1
164
+
165
+ if not success:
166
+ error_msg = f"All API keys failed for sentence {i+1} (SPAIN). Last error: {last_api_error}"
167
+ await send_error_log(error_msg)
168
+ return None, error_msg
169
+
170
+ if not temp_wav_files: return None, "No temporary audio files were created (SPAIN)."
171
+
172
+ try:
173
+ final_wav_path = os.path.join(mp3_save_path, f"autogen_en_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.wav")
174
+ concat_list_path = os.path.join(temp_path, "concat_en.txt")
175
+ silence_wav_path = os.path.join(temp_path, "silence.wav")
176
+
177
+ if not os.path.exists(silence_wav_path):
178
+ ffmpeg_create_silence = ['ffmpeg', '-y', '-f', 'lavfi', '-i', 'anullsrc=r=24000:cl=mono', '-t', '0.1', silence_wav_path]
179
+ process = await asyncio.create_subprocess_exec(*ffmpeg_create_silence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
180
+ if (await process.wait()) != 0: raise ValueError("Could not create silence file.")
181
+
182
+ with open(concat_list_path, "w") as f:
183
+ for i, wav_file in enumerate(temp_wav_files):
184
+ f.write(f"file '{os.path.abspath(wav_file)}'\n")
185
+ if i < len(temp_wav_files) - 1:
186
+ f.write(f"file '{os.path.abspath(silence_wav_path)}'\n")
187
+
188
+ ffmpeg_cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_list_path, '-c', 'copy', final_wav_path]
189
+ process = await asyncio.create_subprocess_exec(*ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
190
+ _, stderr = await process.communicate()
191
+ if process.returncode != 0:
192
+ raise subprocess.CalledProcessError(process.returncode, ffmpeg_cmd, stderr=stderr)
193
+
194
+ return final_wav_path, None
195
+ except Exception as e:
196
+ error_detail = f"FFMPEG error during concatenation (EN): {e}"
197
+ await send_error_log(error_detail)
198
+ return None, error_detail
199
+ finally:
200
+ # Dọn dẹp tất cả các file tạm
201
+ files_to_clean_after_concat = temp_wav_files + [concat_list_path]
202
+ for f in files_to_clean_after_concat:
203
+ if os.path.exists(f): os.remove(f)
204
+
205
+ #==============================================================================
206
+ # >>>>> HÀM ORCHESTRATOR CHÍNH CHO TIẾNG ANH <<<<<
207
+ #==============================================================================
208
+
209
+ async def auto_process_and_save_article_en(
210
+ # Các service và dữ liệu cần thiết từ app.py
211
+ drive_service,
212
+ article: dict,
213
+ category_name: str,
214
+ chat_id: int,
215
+
216
+ # Các đường dẫn và cấu hình
217
+ working_excel_path: str,
218
+ common_image_folder_id: str,
219
+ processed_image_folder_id: str,
220
+ rewritten_data: dict, # <-- THÊM THAM SỐ MỚI
221
+ drive_excel_file_id: str, # <<< THÊM THAM SỐ MỚI VÀO ĐÂY
222
+ # Các hàm tiện ích
223
+ send_telegram_message,
224
+ get_random_common_images_from_drive,
225
+ move_file_on_drive,
226
+ append_row_to_excel,
227
+ upload_file_to_drive
228
+ ):
229
+ """
230
+ Hàm chính điều phối toàn bộ quy trình xử lý và lưu trữ cho một bài báo Tiếng Anh.
231
+ """
232
+ await send_telegram_message(chat_id, f"🇬🇧 Đang xử lý Tiếng Anh cho: *{article['title'][:50]}...*")
233
+ common_image_files_info = []
234
+ try:
235
+ # 1. Dùng Gemini để dịch và viết lại nội dung
236
+ #rewritten_data = await rewrite_content_with_gemini(article['title'], article['content'])
237
+ if not rewritten_data: raise ValueError("Rewrite content failed (EN)")
238
+
239
+ # <<< THÊM LOGIC MỚI: Ghép các phần của kịch bản thành một chuỗi duy nhất >>>
240
+ tts_script = rewritten_data.get('tts_script', {})
241
+ full_tts_content = ' '.join(filter(None, [
242
+ tts_script.get('intro', ''),
243
+ tts_script.get('main_content', ''),
244
+ tts_script.get('outro', '')
245
+ ])).strip()
246
+
247
+ # 2. Lấy 3 ảnh ngẫu nhiên từ thư mục common Tiếng Anh
248
+ if not common_image_folder_id: raise ValueError(f"Image folder for '{category_name}' not configured (EN)")
249
+ common_image_files_info = await get_random_common_images_from_drive(drive_service, common_image_folder_id, chat_id, count=3)
250
+ if not common_image_files_info: raise ValueError("Failed to get common images (EN)")
251
+
252
+ # 3. Di chuyển ảnh đã chọn sang thư mục đã xử lý
253
+ image_links, image_ids = ["", "", ""], ["", "", ""]
254
+ for i, (original_id, local_path) in enumerate(common_image_files_info):
255
+ moved_file_info = await move_file_on_drive(drive_service, original_id, processed_image_folder_id)
256
+ if moved_file_info:
257
+ image_links[i], image_ids[i] = moved_file_info['link'], moved_file_info['id']
258
+
259
+ if not image_links[0]: raise ValueError("Failed to move common images (EN)")
260
+
261
+ # 4. Tìm tài khoản YouTube tương ứng trong file Excel
262
+ df_acc = pd.read_excel(working_excel_path, sheet_name='AccDanTri', header=None)
263
+ account_key = f"{category_name}_EN" # Key để tìm tài khoản, ví dụ: "Giáo Dục_EN"
264
+ account_row = df_acc[df_acc.iloc[:, 0] == account_key]
265
+ if account_row.empty: raise ValueError(f"YouTube account '{account_key}' not found in AccDanTri sheet")
266
+ account_pickle = account_row.iloc[0, 1]
267
+
268
+ # 5. Chuẩn bị và ghi dữ liệu vào file Excel
269
+ final_data_row = [
270
+ account_key,
271
+ rewritten_data.get('youtube_title'), rewritten_data.get('image_title'),
272
+ rewritten_data.get('youtube_description'),
273
+ full_tts_content, # <<< SỬ DỤNG BIẾN MỚI ĐÃ ĐƯỢC GHÉP
274
+ article['publish_time'],
275
+ image_links[0], image_ids[0],
276
+ image_links[1], image_ids[1],
277
+ image_links[2], image_ids[2],
278
+ account_pickle, "NYS", "", "", "", ""
279
+ ]
280
+ await append_row_to_excel(working_excel_path, "DanTri", final_data_row)
281
+
282
+ # 6. Tải file Excel đã cập nhật lên Google Drive
283
+ await upload_file_to_drive(drive_service, working_excel_path, drive_excel_file_id)
284
+
285
+ except Exception as e:
286
+ # Báo lỗi chi tiết nếu có vấn đề
287
+ await send_telegram_message(chat_id, f"⚠️ (Auto EN) Error: {e}\n{traceback.format_exc()[-1000:]}")
288
+
289
+ finally:
290
+ # Dọn dẹp các file ảnh tạm đã tải về
291
+ if common_image_files_info:
292
+ for _, f_path in common_image_files_info:
293
+ if f_path and os.path.exists(f_path):
294
+ try:
295
+ os.remove(f_path)
296
+ except OSError:
297
+ pass