📁 Quản Lý & Sửa File Nhanh
Mở Trang Reels (Port 5000) ↗
Danh sách file trong /home/luan
📄 ai_post.py
📄 bai_dang_affiliate.txt
📄 batch_gen.py
📄 dashboard.log
📄 docker-compose.yml
📄 editor.log
📄 fb_auto_post.py
📄 file_editor.py
📄 filebrowser.log
📄 loi_binh.mp3
📄 products_state.json
📄 sensor.py
📄 web_dashboard.py
✏️ Đang sửa: web_dashboard.py
import os import json import time import datetime import threading import subprocess import requests from flask import Flask, request, redirect, url_for, render_template_string, send_file, jsonify from google import genai app = Flask(__name__) # CẤU HÌNH HỆ THỐNG & TOKEN VĨNH VIỄN GEMINI_KEYS = [ "AIzaSyAi-NmVlkUcDQQf9zy3qeXt2ZoaGTDcB1A", "AIzaSyD7aZkigWkRtVwnkWNu9aFpXbUR31InbR0" ] PAGE_TOKEN = "EAAUEpgcfQnQBSOUkKgRFpPB8csZBYyZB3DzGQP1zVMIdlxnfZBPicArMQ5TI2rXePUqPp5ca5VDBpWkLobXosUjmJe2OEgRjFZApS9mkHsPEZCAZCJgzkSlZC5cnPZAHZA5NVZCRU8bPQJ0c2crHWtZBshxixPjLkBOgaIALGXrMEwJjlEwgp4kV7jdteBZCZBbGjGZA8ZCs0ax" PAGE_ID = "1186120211260582" DATA_FILE = "/home/luan/products_state.json" PREVIEW_VIDEO = "/home/luan/preview_video.mp4" IS_CANCELLED = False # HÀM LẤY THÔNG SỐ CPU, RAM, NHIỆT ĐỘ CỦA ARMBIAN def get_system_stats(): stats = {"cpu": "0", "ram": "0", "temp": "0"} try: with open('/proc/meminfo', 'r') as f: mem = {} for line in f: parts = line.split() if len(parts) >= 2: mem[parts[0]] = int(parts[1]) total = mem.get('MemTotal:', 1) free = mem.get('MemFree:', 0) buffers = mem.get('Buffers:', 0) cached = mem.get('Cached:', 0) used = total - free - buffers - cached stats["ram"] = f"{(used / total) * 100:.1f}" if os.path.exists('/sys/class/thermal/thermal_zone0/temp'): with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f: stats["temp"] = f"{int(f.read().strip()) / 1000:.1f}" else: stats["temp"] = "--" load1 = os.getloadavg()[0] cores = os.cpu_count() or 4 cpu_pct = min((load1 / cores) * 100, 100.0) stats["cpu"] = f"{cpu_pct:.1f}" except Exception: pass return stats def load_data(): default = { "auto_enabled": False, "interval_hours": 3.0, "current_index": 0, "total_posts": 0, "last_post_time": 0, "history": [], "products": [], "draft": {"status": "idle", "product_idx": -1, "product_name": "", "caption": "", "msg": "", "progress": 0} } if not os.path.exists(DATA_FILE): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(default, f, ensure_ascii=False, indent=2) return default with open(DATA_FILE, "r", encoding="utf-8") as f: try: data = json.load(f) except: data = default for k, v in default.items(): if k not in data: data[k] = v for p in data.get("products", []): if "active" not in p: p["active"] = True if "draft" not in data: data["draft"] = default["draft"] if "progress" not in data["draft"]: data["draft"]["progress"] = 0 return data def save_data(data): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def update_progress(msg, pct, is_draft): if is_draft: db = load_data() db["draft"]["msg"] = msg db["draft"]["progress"] = pct save_data(db) def generate_voiceover(text, audio_path): global IS_CANCELLED if IS_CANCELLED: return False try: clean_text = text.replace("#", "").replace("*", "").replace("\n", " ") short_voice_text = clean_text[:280] cmd = f'nice -n 19 python3 -m edge_tts --voice "vi-VN-HoaiMyNeural" --text "{short_voice_text}" --write-media "{audio_path}"' subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return os.path.exists(audio_path) except Exception as e: print(f"Lỗi tạo voice: {e}") return False def generate_video_and_caption(sp, is_draft=False): global IS_CANCELLED caption = "" voice_script = f"Khám phá ngay {sp['ten']}, ưu đãi cực lớn hôm nay. Mua ngay kẻo lỡ!" if IS_CANCELLED: return None, None, "Đã dừng." # 1. AI viết bài update_progress("⏳ Đang kết nối AI Gemini soạn kịch bản...", 5, is_draft) time.sleep(1) prompt = f"Viết bài Facebook Reels bán hàng ngắn cho: {sp['ten']}.\nYêu cầu: 2 câu mở đầu giật tít, 3 ưu điểm nổi bật dạng bullet, 5 hashtag xu hướng." ai_success = False for key in GEMINI_KEYS: if ai_success or IS_CANCELLED: break if not key.strip(): continue try: client = genai.Client(api_key=key.strip()) for model_name in ['gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash']: try: ai_res = client.models.generate_content(model=model_name, contents=prompt) if ai_res and ai_res.text: caption = f"{ai_res.text}\n\n🛒 Mua ngay tại: {sp['link']}" voice_script = ai_res.text.split("\n\n")[0] ai_success = True break except: continue except: continue if not caption: caption = f"🔥 {sp['ten']} cực hot!\n👉 Mua ngay chính hãng tại: {sp['link']}\n\n#shopee #xuhuong #hotdeal" if IS_CANCELLED: return None, None, "Đã dừng." # 2. Tạo Giọng Đọc update_progress("🗣️ Đang tải dữ liệu lồng tiếng AI Tiếng Việt...", 15, is_draft) audio_file = "/home/luan/voice_temp.mp3" has_audio = generate_voiceover(voice_script, audio_file) # 3. Tải ảnh & Render (Chậm rãi, nhường CPU) raw_images = sp.get('anh', []) if isinstance(raw_images, str): raw_images = [raw_images] headers = {'User-Agent': 'Mozilla/5.0'} clip_files = [] temp_files = [audio_file] if has_audio else [] total_imgs = len(raw_images) for i, img_url in enumerate(raw_images): if IS_CANCELLED: break if not img_url.strip(): continue pct = 20 + int(((i) / total_imgs) * 60) update_progress(f"🖼️ Đang vẽ 3D ảnh {i+1}/{total_imgs} (Giới hạn CPU tránh quá tải)...", pct, is_draft) img_name = f"/home/luan/temp_img_{i}.jpg" clip_name = f"/home/luan/temp_clip_{i}.mp4" try: r = requests.get(img_url.strip(), headers=headers, timeout=15) if r.status_code == 200: with open(img_name, "wb") as f: f.write(r.content) temp_files.append(img_name) zoom_expr = "min(pzoom+0.003,1.25)" if (i % 2 == 0) else "if(eq(on,0),1.25,max(1.0,pzoom-0.003))" vf = ( f"split[bg_in][fg_in];" f"[bg_in]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=25:5[bg];" f"[fg_in]scale=1080:1080,zoompan=z='{zoom_expr}':d=75:s=1080x1080:fps=25[fg];" f"[bg][fg]overlay=(W-w)/2:(H-h)/2" ) cmd_clip = [ "nice", "-n", "19", "ffmpeg", "-threads", "1", "-y", "-loop", "1", "-t", "3.0", "-i", img_name, "-filter_complex", vf, "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p", "-r", "25", clip_name ] subprocess.run(cmd_clip, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if os.path.exists(clip_name): clip_files.append(clip_name) temp_files.append(clip_name) time.sleep(2) except Exception as e: print(f"Lỗi render {img_url}: {e}") if IS_CANCELLED or not clip_files: for tmp in temp_files: if os.path.exists(tmp): try: os.remove(tmp) except: pass return None, None, "Đã dừng hoặc lỗi ảnh!" try: update_progress("🎞️ Đang nối các đoạn video từ từ...", 85, is_draft) time.sleep(2) concat_file = "/home/luan/concat_list.txt" with open(concat_file, "w", encoding="utf-8") as f: for clip in clip_files: f.write(f"file '{clip}'\n") temp_files.append(concat_file) video_temp = "/home/luan/video_temp_nosound.mp4" temp_files.append(video_temp) subprocess.run([ "nice", "-n", "19", "ffmpeg", "-threads", "1", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, "-c", "copy", video_temp ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if has_audio and not IS_CANCELLED: update_progress("🎵 Mix âm thanh vào Video...", 95, is_draft) cmd_mix = [ "nice", "-n", "19", "ffmpeg", "-threads", "1", "-y", "-i", video_temp, "-i", audio_file, "-c:v", "copy", "-c:a", "aac", "-shortest", PREVIEW_VIDEO ] subprocess.run(cmd_mix, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: if os.path.exists(video_temp) and not IS_CANCELLED: os.replace(video_temp, PREVIEW_VIDEO) except Exception as e: return None, None, f"Lỗi ghép: {e}" finally: for tmp in temp_files: if os.path.exists(tmp): try: os.remove(tmp) except: pass if IS_CANCELLED or not os.path.exists(PREVIEW_VIDEO): return None, None, "Lỗi file cuối cùng!" update_progress("✅ Hoàn tất!", 100, is_draft) return PREVIEW_VIDEO, caption, None def post_video_to_facebook(video_path, caption, product_name): try: video_url = f"https://graph-video.facebook.com/v20.0/{PAGE_ID}/videos" with open(video_path, "rb") as video_file: res = requests.post( video_url, data={"description": caption, "access_token": PAGE_TOKEN}, files={"source": video_file}, timeout=60 ).json() if "id" in res: post_id = res["id"] db = load_data() db["total_posts"] += 1 db["last_post_time"] = time.time() now_str = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S") db["history"].insert(0, { "time": now_str, "product_name": product_name, "video_id": post_id, "fb_link": f"https://www.facebook.com/reel/{post_id}", "status": "Thành công" }) db["history"] = db["history"][:50] save_data(db) return True, post_id return False, res except Exception as e: return False, str(e) def task_generate_draft(product_idx): global IS_CANCELLED IS_CANCELLED = False db = load_data() if 0 <= product_idx < len(db["products"]): sp = db["products"][product_idx] db["draft"] = {"status": "generating", "product_idx": product_idx, "product_name": sp["ten"], "caption": "", "msg": "Chuẩn bị tiến trình...", "progress": 0} save_data(db) v_path, caption, err = generate_video_and_caption(sp, is_draft=True) if IS_CANCELLED: return db = load_data() if v_path and os.path.exists(v_path): db["draft"] = {"status": "ready", "product_idx": product_idx, "product_name": sp["ten"], "caption": caption, "msg": f"Đã tạo xong", "progress": 100} else: db["draft"] = {"status": "error", "product_idx": product_idx, "product_name": sp["ten"], "caption": "", "msg": f"Lỗi: {err}", "progress": 0} save_data(db) def background_scheduler(): while True: try: db = load_data() if db.get("auto_enabled", False): if (time.time() - db.get("last_post_time", 0)) >= db.get("interval_hours", 3.0) * 3600: products = [p for p in db.get("products", []) if p.get("active", True)] if products: idx = db.get("current_index", 0) % len(products) sp = products[idx] v_path, cap, err = generate_video_and_caption(sp, is_draft=False) if v_path: ok, pid = post_video_to_facebook(v_path, cap, sp["ten"]) if ok: db = load_data() db["current_index"] = (idx + 1) % len(products) save_data(db) if os.path.exists(PREVIEW_VIDEO): os.remove(PREVIEW_VIDEO) except: pass time.sleep(60) threading.Thread(target=background_scheduler, daemon=True).start() HTML_MAIN = """ <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Reels Studio - Hệ Thống Ổn Định</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <script> setInterval(async () => { try { let res = await fetch('/api/status'); let data = await res.json(); document.getElementById('sys-cpu').innerText = data.sys.cpu + '%'; document.getElementById('sys-ram').innerText = data.sys.ram + '%'; document.getElementById('sys-temp').innerText = data.sys.temp + '°C'; let draft = data.draft; if (draft.status === 'generating') { let pb = document.getElementById('draft-progress-bar'); let msg = document.getElementById('draft-msg'); if (pb) { pb.style.width = draft.progress + '%'; pb.innerText = draft.progress + '%'; msg.innerText = draft.msg; } else { window.location.reload(); } } else if (draft.status === 'ready' && document.getElementById('draft-progress-bar')) { window.location.reload(); } } catch (e) {} }, 2000); </script> </head> <body class="bg-light"> <div class="container py-4"> <div class="d-flex flex-wrap justify-content-between align-items-center mb-3 pb-2 border-bottom"> <div> <h3 class="text-primary fw-bold mb-0">🚀 Studio AI (Low CPU Mode)</h3> <small class="text-muted">Đầy đủ chức năng, giới hạn phần cứng an toàn</small> </div> <div class="mt-2 mt-sm-0 d-flex gap-2"> <a href="http://wedit.luanluan.duckdns.org/" target="_blank" class="btn btn-outline-dark fw-bold">📁 Sửa File Code</a> {% if db.auto_enabled %} <a href="/toggle_auto" class="btn btn-success fw-bold">🟢 Auto: ĐANG BẬT</a> {% else %} <a href="/toggle_auto" class="btn btn-outline-secondary fw-bold">🔴 Auto: TẮT</a> {% endif %} </div> </div> <div class="row text-center mb-4 g-3"> <div class="col-4"> <div class="p-2 border rounded bg-white shadow-sm"> <small class="text-muted fw-bold">TẢI CPU</small> <div class="fs-4 fw-bold text-primary" id="sys-cpu">--%</div> </div> </div> <div class="col-4"> <div class="p-2 border rounded bg-white shadow-sm"> <small class="text-muted fw-bold">RAM ĐÃ DÙNG</small> <div class="fs-4 fw-bold text-warning" id="sys-ram">--%</div> </div> </div> <div class="col-4"> <div class="p-2 border rounded bg-white shadow-sm"> <small class="text-muted fw-bold">NHIỆT ĐỘ</small> <div class="fs-4 fw-bold text-danger" id="sys-temp">--°C</div> </div> </div> </div> <div class="card shadow border-0 mb-4 {% if db.draft.status == 'ready' %}border-success{% endif %}"> <div class="card-header bg-dark text-white fw-bold d-flex justify-content-between align-items-center"> <span>🎬 KHUNG DUYỆT BÀI ĐĂNG</span> {% if db.draft.status == 'ready' %}<span class="badge bg-success">ĐÃ SẴN SÀNG</span> {% elif db.draft.status == 'generating' %}<span class="badge bg-warning text-dark">⏳ ĐANG XỬ LÝ</span> {% else %}<span class="badge bg-secondary">CHƯA CÓ</span>{% endif %} </div> <div class="card-body"> {% if db.draft.status == 'generating' %} <div class="text-center py-4"> <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;"></div> <h5 class="fw-bold text-dark">Hệ thống đang thong thả xử lý an toàn...</h5> <p class="text-primary fw-bold mb-1" id="draft-msg">{{ db.draft.msg }}</p> <div class="progress mx-auto mt-3 mb-4" style="height: 25px; max-width: 500px;"> <div class="progress-bar progress-bar-striped progress-bar-animated bg-success fw-bold fs-6" id="draft-progress-bar" role="progressbar" style="width: {{ db.draft.progress }}%;"> {{ db.draft.progress }}% </div> </div> <a href="/cancel_draft" class="btn btn-sm btn-danger fw-bold shadow-sm">⏹️ DỪNG TIẾN TRÌNH</a> </div> {% elif db.draft.status == 'ready' %} <div class="row g-3"> <div class="col-md-5 text-center"> <video width="260" height="420" controls autoplay loop class="rounded shadow-sm bg-black p-1"><source src="/stream_preview_video?t={{ db.last_post_time }}" type="video/mp4"></video> </div> <div class="col-md-7"> <form method="POST" action="/confirm_publish"> <textarea name="caption" rows="10" class="form-control font-monospace mb-3" style="font-size: 13px;" required>{{ db.draft.caption }}</textarea> <div class="d-flex gap-2"> <button type="submit" class="btn btn-success btn-lg fw-bold flex-grow-1">🚀 ĐĂNG NGAY LÊN FACEBOOK</button> <a href="/clear_draft" class="btn btn-outline-danger">✕ Hủy</a> </div> </form> </div> </div> {% elif db.draft.status == 'error' %} <div class="alert alert-danger mb-0">{{ db.draft.msg }} <a href="/clear_draft" class="btn btn-sm btn-outline-danger float-end">Đóng</a></div> {% else %} <div class="text-center py-3 text-muted">Bấm "🎬 Tạo Nháp" ở danh sách dưới để bắt đầu.</div> {% endif %} </div> </div> <div class="row g-4"> <div class="col-lg-5"> <div class="card shadow-sm border-0 mb-4"> <div class="card-header bg-primary text-white fw-bold">➕ Thêm Sản Phẩm Mới</div> <div class="card-body"> <form method="POST" action="/add"> <input type="text" name="ten" class="form-control mb-2" placeholder="Tên sản phẩm" required> <textarea name="anh" rows="4" class="form-control mb-2" placeholder="Link ảnh (.jpg)" required></textarea> <input type="url" name="link" class="form-control mb-3" placeholder="Link Shopee" required> <button type="submit" class="btn btn-success w-100 fw-bold">Lưu Sản Phẩm</button> </form> </div> </div> <div class="card shadow-sm border-0"> <div class="card-header bg-warning text-dark fw-bold">⏱️ Chu kỳ hẹn giờ Auto</div> <div class="card-body"> <form method="POST" action="/set_interval" class="d-flex gap-2"> <select name="hours" class="form-select form-select-sm"> <option value="1" {% if db.interval_hours == 1.0 %}selected{% endif %}>1 tiếng</option> <option value="3" {% if db.interval_hours == 3.0 %}selected{% endif %}>3 tiếng</option> <option value="6" {% if db.interval_hours == 6.0 %}selected{% endif %}>6 tiếng</option> </select> <button type="submit" class="btn btn-sm btn-dark">Lưu</button> </form> </div> </div> </div> <div class="col-lg-7"> <div class="card shadow-sm border-0 mb-4"> <div class="card-header bg-dark text-white fw-bold">📦 Danh Sách SP</div> <div class="card-body p-0"> <div class="table-responsive" style="max-height: 380px; overflow-y: auto;"> <table class="table table-hover align-middle mb-0"> <thead class="table-light sticky-top"><tr><th>Tên sản phẩm</th><th>Thao tác</th></tr></thead> <tbody> {% for p in db.products %} <tr> <td class="fw-bold">{{ p.ten }}</td> <td> <div class="d-flex gap-1"> <a href="/create_draft/{{ loop.index0 }}" class="btn btn-sm btn-danger fw-bold">🎬 Tạo Nháp</a> <a href="/toggle_product/{{ loop.index0 }}" class="btn btn-sm btn-outline-secondary">{% if p.active %}Tắt{% else %}Bật{% endif %}</a> <a href="/delete/{{ loop.index0 }}" class="btn btn-sm btn-outline-danger">✕</a> </div> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> <div class="card shadow-sm border-0"> <div class="card-header bg-success text-white fw-bold">📑 Lịch Sử Đã Đăng</div> <div class="card-body p-0"> <div class="table-responsive" style="max-height: 200px; overflow-y: auto;"> <table class="table table-hover align-middle mb-0"> <thead class="table-light sticky-top"><tr><th>Thời gian</th><th>Tên bài</th><th>Link</th></tr></thead> <tbody> {% for h in db.history %} <tr> <td><small class="text-muted">{{ h.time }}</small></td> <td><div class="text-truncate" style="max-width: 200px;">{{ h.product_name }}</div></td> <td><a href="{{ h.fb_link }}" target="_blank" class="btn btn-sm btn-outline-primary py-0">Xem Reel</a></td> </tr> {% else %} <tr><td colspan="3" class="text-center py-3 text-muted">Chưa có bài đăng nào.</td></tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> </div> </body> </html> """ @app.route("/") def index(): return render_template_string(HTML_MAIN, db=load_data()) @app.route("/api/status") def api_status(): db = load_data() return jsonify({ "sys": get_system_stats(), "draft": db.get("draft", {}) }) @app.route("/create_draft/<int:idx>") def create_draft(idx): th = threading.Thread(target=task_generate_draft, args=(idx,), daemon=True) th.start() return redirect("/") @app.route("/cancel_draft") def cancel_draft(): global IS_CANCELLED IS_CANCELLED = True try: subprocess.run("sudo pkill -9 -f ffmpeg", shell=True) subprocess.run("sudo pkill -9 -f edge_tts", shell=True) except: pass db = load_data() db["draft"] = {"status": "idle", "product_idx": -1, "product_name": "", "caption": "", "msg": "", "progress": 0} save_data(db) return redirect("/") @app.route("/clear_draft") def clear_draft(): db = load_data() db["draft"] = {"status": "idle", "product_idx": -1, "product_name": "", "caption": "", "msg": "", "progress": 0} save_data(db) return redirect("/") @app.route("/stream_preview_video") def stream_preview_video(): return send_file(PREVIEW_VIDEO, mimetype="video/mp4") @app.route("/confirm_publish", methods=["POST"]) def confirm_publish(): db = load_data() ok, res = post_video_to_facebook(PREVIEW_VIDEO, request.form.get("caption", ""), db["draft"]["product_name"]) if ok: clear_draft() return redirect("/") @app.route("/toggle_auto") def toggle_auto(): db = load_data() db["auto_enabled"] = not db.get("auto_enabled", False) save_data(db) return redirect("/") @app.route("/set_interval", methods=["POST"]) def set_interval(): db = load_data() db["interval_hours"] = float(request.form.get("hours", 3)) save_data(db) return redirect("/") @app.route("/toggle_product/<int:idx>") def toggle_product(idx): db = load_data() db["products"][idx]["active"] = not db["products"][idx].get("active", True) save_data(db) return redirect("/") @app.route("/add", methods=["POST"]) def add_product(): db = load_data() links = [l.strip() for l in request.form["anh"].strip().split("\n") if l.strip()] db["products"].append({"ten": request.form["ten"], "anh": links, "link": request.form["link"], "active": True}) save_data(db) return redirect("/") @app.route("/delete/<int:idx>") def delete_product(idx): db = load_data() db["products"].pop(idx) save_data(db) return redirect("/") if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)
💾 LƯU FILE & KHỞI ĐỘNG LẠI DASHBOARD