Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- config.py +25 -0
- facebook_utils.py +50 -0
- google_utils.py +63 -0
- main.py +60 -0
- processor.py +103 -0
config.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# Cấu hình chung
|
| 4 |
+
DRIVE_EXCEL_FILE_ID = "ID_FILE_EXCEL_CUA_BAN" # Thay ID file input.xlsx trên Drive vào đây
|
| 5 |
+
LOCAL_EXCEL_PATH = "input.xlsx"
|
| 6 |
+
TEMP_VIDEO_DIR = "temp_videos"
|
| 7 |
+
|
| 8 |
+
# Cấu hình các Page (Thêm Page mới thì thêm 1 block vào đây)
|
| 9 |
+
PAGES_CONFIG = [
|
| 10 |
+
{
|
| 11 |
+
"sheet_name": "BeYeu", # Tên Sheet trong Excel
|
| 12 |
+
"page_id": os.getenv("PAGE_ID_BEYEU"),
|
| 13 |
+
"access_token": os.getenv("TOKEN_BEYEU"),
|
| 14 |
+
"schedule_times": ["07:00", "19:00"], # Giờ đăng bài
|
| 15 |
+
"active": True
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"sheet_name": "BeYeu2",
|
| 19 |
+
"page_id": os.getenv("PAGE_ID_BEYEU2"),
|
| 20 |
+
"access_token": os.getenv("TOKEN_BEYEU2"),
|
| 21 |
+
"schedule_times": ["08:00", "20:00"],
|
| 22 |
+
"active": True
|
| 23 |
+
},
|
| 24 |
+
# Muốn thêm BeYeu3, chỉ cần copy block trên và đổi thông tin
|
| 25 |
+
]
|
facebook_utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
async def upload_reel(page_id, access_token, video_path, description):
|
| 6 |
+
api_version = "v19.0"
|
| 7 |
+
base_url = f"https://graph.facebook.com/{api_version}"
|
| 8 |
+
|
| 9 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 10 |
+
# 1. Init
|
| 11 |
+
init_res = await client.post(
|
| 12 |
+
f"{base_url}/{page_id}/video_reels",
|
| 13 |
+
data={"upload_phase": "start", "access_token": access_token}
|
| 14 |
+
)
|
| 15 |
+
init_data = init_res.json()
|
| 16 |
+
video_id = init_data.get("video_id")
|
| 17 |
+
upload_url = init_data.get("upload_url")
|
| 18 |
+
|
| 19 |
+
if not video_id: raise Exception(f"Init Error: {init_data}")
|
| 20 |
+
|
| 21 |
+
# 2. Upload
|
| 22 |
+
file_size = os.path.getsize(video_path)
|
| 23 |
+
with open(video_path, "rb") as f:
|
| 24 |
+
video_data = f.read()
|
| 25 |
+
|
| 26 |
+
headers = {"Authorization": f"OAuth {access_token}", "offset": "0", "file_size": str(file_size)}
|
| 27 |
+
upload_res = await client.post(upload_url, content=video_data, headers=headers)
|
| 28 |
+
if upload_res.status_code != 200: raise Exception(f"Upload Error: {upload_res.text}")
|
| 29 |
+
|
| 30 |
+
# 3. Publish
|
| 31 |
+
pub_res = await client.post(
|
| 32 |
+
f"{base_url}/{page_id}/video_reels",
|
| 33 |
+
data={
|
| 34 |
+
"access_token": access_token,
|
| 35 |
+
"video_id": video_id,
|
| 36 |
+
"upload_phase": "finish",
|
| 37 |
+
"video_state": "PUBLISHED",
|
| 38 |
+
"description": description
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
if not pub_res.json().get("success"): raise Exception(f"Publish Error: {pub_res.text}")
|
| 42 |
+
|
| 43 |
+
return video_id
|
| 44 |
+
|
| 45 |
+
async def comment_on_video(video_id, access_token, message):
|
| 46 |
+
# Đợi 1 chút để video xử lý
|
| 47 |
+
await asyncio.sleep(15)
|
| 48 |
+
url = f"https://graph.facebook.com/v19.0/{video_id}/comments"
|
| 49 |
+
async with httpx.AsyncClient() as client:
|
| 50 |
+
await client.post(url, data={"message": message, "access_token": access_token})
|
google_utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import random
|
| 4 |
+
import asyncio
|
| 5 |
+
import os
|
| 6 |
+
from googleapiclient.discovery import build
|
| 7 |
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
| 8 |
+
import io
|
| 9 |
+
import pickle
|
| 10 |
+
|
| 11 |
+
# --- Biến toàn cục cho Gemini Keys ---
|
| 12 |
+
GEMINI_KEYS = []
|
| 13 |
+
|
| 14 |
+
async def load_gemini_keys(excel_path):
|
| 15 |
+
"""Đọc list API Key từ sheet APIKey"""
|
| 16 |
+
global GEMINI_KEYS
|
| 17 |
+
try:
|
| 18 |
+
df = pd.read_excel(excel_path, sheet_name='APIKey', header=None)
|
| 19 |
+
# Giả sử key nằm ở cột A, từ dòng 2
|
| 20 |
+
GEMINI_KEYS = df.iloc[1:, 0].dropna().astype(str).tolist()
|
| 21 |
+
print(f"✅ Đã load {len(GEMINI_KEYS)} Gemini Keys.")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"❌ Lỗi load Gemini Keys: {e}")
|
| 24 |
+
|
| 25 |
+
async def generate_content_with_gemini(product_desc):
|
| 26 |
+
"""Viết lại nội dung bằng Gemini với cơ chế xoay vòng Key"""
|
| 27 |
+
if not GEMINI_KEYS:
|
| 28 |
+
return product_desc # Fallback nếu lỗi
|
| 29 |
+
|
| 30 |
+
prompt = (
|
| 31 |
+
f"Viết một caption Facebook Reels ngắn gọn, hấp dẫn, bắt trend "
|
| 32 |
+
f"cho sản phẩm sau. Có icon sinh động và kêu gọi mua hàng khéo léo.\n"
|
| 33 |
+
f"Sản phẩm: {product_desc}"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Thử ngẫu nhiên key cho đến khi thành công
|
| 37 |
+
for _ in range(3):
|
| 38 |
+
key = random.choice(GEMINI_KEYS)
|
| 39 |
+
genai.configure(api_key=key)
|
| 40 |
+
try:
|
| 41 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 42 |
+
response = await model.generate_content_async(prompt)
|
| 43 |
+
return response.text.strip()
|
| 44 |
+
except Exception:
|
| 45 |
+
continue
|
| 46 |
+
return product_desc
|
| 47 |
+
|
| 48 |
+
# --- Hàm Drive (Tái sử dụng code của bạn) ---
|
| 49 |
+
# (Bạn giữ nguyên các hàm get_drive_credentials, download_file_from_drive, upload_file_to_drive cũ)
|
| 50 |
+
# Chỉ thêm hàm download video theo ID:
|
| 51 |
+
|
| 52 |
+
async def download_video_by_id(drive_service, file_id, save_path):
|
| 53 |
+
if os.path.exists(save_path):
|
| 54 |
+
os.remove(save_path) # Xóa file cũ nếu có
|
| 55 |
+
request = drive_service.files().get_media(fileId=file_id)
|
| 56 |
+
fh = io.BytesIO()
|
| 57 |
+
downloader = MediaIoBaseDownload(fh, request)
|
| 58 |
+
done = False
|
| 59 |
+
while not done:
|
| 60 |
+
status, done = downloader.next_chunk()
|
| 61 |
+
with open(save_path, 'wb') as f:
|
| 62 |
+
f.write(fh.getvalue())
|
| 63 |
+
return save_path
|
main.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 3 |
+
from apscheduler.triggers.cron import CronTrigger
|
| 4 |
+
import asyncio
|
| 5 |
+
from contextlib import asynccontextmanager
|
| 6 |
+
import config
|
| 7 |
+
import processor
|
| 8 |
+
import google_utils
|
| 9 |
+
|
| 10 |
+
# --- Khởi tạo Scheduler ---
|
| 11 |
+
scheduler = AsyncIOScheduler()
|
| 12 |
+
|
| 13 |
+
async def job_runner(sheet_name):
|
| 14 |
+
"""Hàm wrapper để chạy job"""
|
| 15 |
+
# Tìm config dựa trên sheet_name
|
| 16 |
+
page_cfg = next((p for p in config.PAGES_CONFIG if p['sheet_name'] == sheet_name), None)
|
| 17 |
+
if not page_cfg: return
|
| 18 |
+
|
| 19 |
+
# Lấy credentials (dùng lại hàm cũ của bạn)
|
| 20 |
+
# Lưu ý: cần truyền chat_id nếu dùng hàm cũ, hoặc sửa hàm get_drive_credentials để không cần chat_id
|
| 21 |
+
credentials = await google_utils.get_drive_credentials_no_chat_id()
|
| 22 |
+
if credentials:
|
| 23 |
+
await google_utils.load_gemini_keys(config.LOCAL_EXCEL_PATH) # Load key mới nhất
|
| 24 |
+
await processor.process_single_page(page_cfg, credentials)
|
| 25 |
+
|
| 26 |
+
@asynccontextmanager
|
| 27 |
+
async def lifespan(app: FastAPI):
|
| 28 |
+
print("🚀 Bot khởi động...")
|
| 29 |
+
|
| 30 |
+
# 1. Tải file Excel lần đầu để lấy API Key
|
| 31 |
+
# (Thực hiện logic tải file và load key ở đây)
|
| 32 |
+
|
| 33 |
+
# 2. Đăng ký lịch chạy từ Config
|
| 34 |
+
for page in config.PAGES_CONFIG:
|
| 35 |
+
if page['active']:
|
| 36 |
+
for time_str in page['schedule_times']:
|
| 37 |
+
hour, minute = time_str.split(':')
|
| 38 |
+
scheduler.add_job(
|
| 39 |
+
job_runner,
|
| 40 |
+
CronTrigger(hour=int(hour), minute=int(minute)),
|
| 41 |
+
args=[page['sheet_name']],
|
| 42 |
+
name=f"{page['sheet_name']}_{time_str}"
|
| 43 |
+
)
|
| 44 |
+
print(f"📅 Đã hẹn giờ: {page['sheet_name']} lúc {time_str}")
|
| 45 |
+
|
| 46 |
+
scheduler.start()
|
| 47 |
+
yield
|
| 48 |
+
print("🛑 Bot dừng.")
|
| 49 |
+
|
| 50 |
+
app = FastAPI(lifespan=lifespan)
|
| 51 |
+
|
| 52 |
+
@app.get("/")
|
| 53 |
+
def health_check():
|
| 54 |
+
return {"status": "Running", "jobs": [str(j) for j in scheduler.get_jobs()]}
|
| 55 |
+
|
| 56 |
+
# Endpoint để test thủ công 1 page
|
| 57 |
+
@app.post("/trigger/{sheet_name}")
|
| 58 |
+
async def manual_trigger(sheet_name: str):
|
| 59 |
+
await job_runner(sheet_name)
|
| 60 |
+
return {"message": f"Triggered {sheet_name}"}
|
processor.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
import asyncio
|
| 5 |
+
from googleapiclient.discovery import build
|
| 6 |
+
from config import LOCAL_EXCEL_PATH, DRIVE_EXCEL_FILE_ID, TEMP_VIDEO_DIR
|
| 7 |
+
import google_utils
|
| 8 |
+
import facebook_utils
|
| 9 |
+
|
| 10 |
+
# Lock để tránh 2 page cùng ghi file Excel 1 lúc
|
| 11 |
+
excel_lock = asyncio.Lock()
|
| 12 |
+
|
| 13 |
+
async def process_single_page(page_config, drive_credentials):
|
| 14 |
+
sheet_name = page_config['sheet_name']
|
| 15 |
+
print(f"🚀 [Processor] Bắt đầu xử lý cho Sheet: {sheet_name}")
|
| 16 |
+
|
| 17 |
+
drive_service = build('drive', 'v3', credentials=drive_credentials)
|
| 18 |
+
|
| 19 |
+
async with excel_lock:
|
| 20 |
+
# 1. Tải file Excel mới nhất về
|
| 21 |
+
await google_utils.download_file_from_drive(drive_service, DRIVE_EXCEL_FILE_ID, LOCAL_EXCEL_PATH)
|
| 22 |
+
|
| 23 |
+
# 2. Đọc dữ liệu
|
| 24 |
+
try:
|
| 25 |
+
df = pd.read_excel(LOCAL_EXCEL_PATH, sheet_name=sheet_name)
|
| 26 |
+
except ValueError:
|
| 27 |
+
print(f"❌ Không tìm thấy sheet {sheet_name}")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
# 3. Lọc các dòng NYS (Giả sử cột Status là cột E - index 4)
|
| 31 |
+
# Cấu trúc: A:STT, B:Desc, C:Shopee, D:DriveID, E:Status
|
| 32 |
+
# Pandas index cột: 0, 1, 2, 3, 4
|
| 33 |
+
|
| 34 |
+
# Chuẩn hóa tên cột để dễ gọi
|
| 35 |
+
df.columns = ['STT', 'Desc', 'ShopeeLink', 'DriveVideoID', 'Status', 'NewContent'] # Thêm cột NewContent nếu cần
|
| 36 |
+
|
| 37 |
+
nys_rows = df[df['Status'] == 'NYS']
|
| 38 |
+
|
| 39 |
+
if nys_rows.empty:
|
| 40 |
+
print(f"⚠️ Sheet {sheet_name} đã hết bài (không còn NYS).")
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
# 4. Chọn ngẫu nhiên
|
| 44 |
+
selected_row_index = random.choice(nys_rows.index)
|
| 45 |
+
row_data = df.loc[selected_row_index]
|
| 46 |
+
|
| 47 |
+
print(f"--- Đã chọn dòng {selected_row_index + 2} ---")
|
| 48 |
+
|
| 49 |
+
# (Nhả lock Excel ra để xử lý nặng)
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
# 5. Gemini viết lại nội dung
|
| 53 |
+
original_desc = str(row_data['Desc'])
|
| 54 |
+
new_content = await google_utils.generate_content_with_gemini(original_desc)
|
| 55 |
+
|
| 56 |
+
# 6. Tải video
|
| 57 |
+
video_drive_id = str(row_data['DriveVideoID'])
|
| 58 |
+
if not os.path.exists(TEMP_VIDEO_DIR): os.makedirs(TEMP_VIDEO_DIR)
|
| 59 |
+
local_video_path = os.path.join(TEMP_VIDEO_DIR, f"{sheet_name}_{video_drive_id}.mp4")
|
| 60 |
+
|
| 61 |
+
await google_utils.download_video_by_id(drive_service, video_drive_id, local_video_path)
|
| 62 |
+
|
| 63 |
+
# 7. Upload Facebook
|
| 64 |
+
video_id = await facebook_utils.upload_reel(
|
| 65 |
+
page_config['page_id'],
|
| 66 |
+
page_config['access_token'],
|
| 67 |
+
local_video_path,
|
| 68 |
+
new_content
|
| 69 |
+
)
|
| 70 |
+
print(f"✅ Upload thành công. ID: {video_id}")
|
| 71 |
+
|
| 72 |
+
# 8. Comment
|
| 73 |
+
cta = "🔥 Săn ngay tại đây cả nhà ơi: "
|
| 74 |
+
comment_msg = f"{cta} {row_data['ShopeeLink']}"
|
| 75 |
+
await facebook_utils.comment_on_video(video_id, page_config['access_token'], comment_msg)
|
| 76 |
+
|
| 77 |
+
# 9. Cập nhật Excel (Cần Lock lại)
|
| 78 |
+
async with excel_lock:
|
| 79 |
+
# Đọc lại file để đảm bảo không đè dữ liệu của luồng khác
|
| 80 |
+
# (Trong thực tế nên dùng Google Sheets API update cell trực tiếp sẽ tốt hơn download/upload)
|
| 81 |
+
# Nhưng ở đây ta làm theo logic file Excel local
|
| 82 |
+
df_update = pd.read_excel(LOCAL_EXCEL_PATH, sheet_name=None) # Đọc tất cả sheet
|
| 83 |
+
|
| 84 |
+
# Cập nhật sheet hiện tại
|
| 85 |
+
sheet_df = df_update[sheet_name]
|
| 86 |
+
sheet_df.at[selected_row_index, 'Status'] = 'Uploaded'
|
| 87 |
+
# Giả sử cột F là nội dung mới
|
| 88 |
+
if len(sheet_df.columns) > 5:
|
| 89 |
+
sheet_df.iloc[selected_row_index, 5] = new_content
|
| 90 |
+
|
| 91 |
+
# Lưu và Upload
|
| 92 |
+
with pd.ExcelWriter(LOCAL_EXCEL_PATH, engine='openpyxl') as writer:
|
| 93 |
+
for s_name, s_df in df_update.items():
|
| 94 |
+
s_df.to_excel(writer, sheet_name=s_name, index=False)
|
| 95 |
+
|
| 96 |
+
await google_utils.upload_file_to_drive(drive_service, LOCAL_EXCEL_PATH, DRIVE_EXCEL_FILE_ID)
|
| 97 |
+
print(f"✅ Đã cập nhật Excel lên Drive.")
|
| 98 |
+
|
| 99 |
+
# Dọn dẹp
|
| 100 |
+
if os.path.exists(local_video_path): os.remove(local_video_path)
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"❌ Lỗi xử lý Page {sheet_name}: {e}")
|