# app_ava_tabs_final.py — AVATabs Voice (bản build-ready trên T4) import gradio as gr import asyncio import json, datetime, threading, os, pytz # ===================== # QUOTA DEMO LOGIC # ===================== import json, time, os from datetime import datetime, timedelta QUOTA_FILE = "quota_demo.json" MAX_CHARS = 100_000 MAX_DAYS = 3 START_TIME = None def load_quota(): global START_TIME if os.path.exists(QUOTA_FILE): with open(QUOTA_FILE, "r", encoding="utf-8") as f: data = json.load(f) else: data = {"chars_used": 0, "start": time.time(), "gmail": "demo@gmail.com"} with open(QUOTA_FILE, "w", encoding="utf-8") as f: json.dump(data, f) START_TIME = data["start"] return data def save_quota(chars_used): data = load_quota() data["chars_used"] = chars_used with open(QUOTA_FILE, "w", encoding="utf-8") as f: json.dump(data, f) def get_quota_status(): data = load_quota() days_used = (time.time() - data["start"]) / 86400 days_left = max(0, MAX_DAYS - int(days_used)) chars_used = data["chars_used"] expired = chars_used >= MAX_CHARS or days_used >= MAX_DAYS return chars_used, days_left, expired # ===================================================== # GLOBAL CSS (Header + Layout + Slider + Footer) # ===================================================== GLOBAL_CSS = """ body, html, .gradio-container {margin:0;padding:0;height:100%;overflow:hidden;} .gradio-container {display:flex;flex-direction:column;min-height:100vh;} .header-row { display:flex;align-items:center;justify-content:space-between; border-bottom:1px solid #ccc;background:#fff; padding:6px 14px;position:sticky;top:0;z-index:100;margin:0; top:0 !important; padding-top:0 !important; margin-top:0 !important; } /* ===== ÉP BODY SÁT HEADER ===== */ .gradio-tabs, .tab-nav, .tabitem, .tab-content { margin-top:0 !important; padding-top:0 !important; } .header-btn { border-radius:10px;padding:5px 12px;font-size:14px;font-weight:600; border:none;cursor:pointer; } .login-btn {background:#e0e0e0;color:#000;} .free-btn {background:#000;color:#fff;} :root, .gradio-container { --color-accent:#000;--button-primary-background:#000;--button-primary-text:#fff; --link-color:#000;--panel-background:#fff; } .gradio-tabs .tab-nav button[aria-selected="true"] { color:#000 !important;border-bottom:2px solid #000 !important; } .gradio-tabs .tab-nav button {color:#000 !important;font-weight:600;} #textbox textarea {resize:none !important;} /* ===== BODY ===== */ .gr-blocks {display:flex;flex-direction:column;height:100%;box-sizing:border-box;} .gr-column, .gr-row {margin:0 !important;padding:0 !important;gap:6px !important;} /* ===== BỘ ĐẾM KÝ TỰ & NGÀY ===== */ #char_usage_row { display:flex;justify-content:space-between;align-items:center; font-size:14px;margin-top:0;margin-bottom:0;padding:0 2px; } #char_usage_row + .gr-row { margin-top:-4px !important;padding-top:0 !important; } /* ===== Ô AUDIO ===== */ .audio-box { border:1px solid #ccc;border-radius:10px;padding:10px;margin-top:6px; background:#fff;min-height:110px;height:auto; display:flex;flex-direction:column;justify-content:space-between; box-sizing:border-box;align-self:flex-end; } /* ===== CÂN CHIỀU CAO 2 CỘT ===== */ #clone_cols, #tts_cols {display:flex;align-items:stretch;justify-content:space-between;gap:10px;} #clone_cols .audio-box, #tts_cols .audio-box { flex:1 1 50%;height:100%;display:flex;flex-direction:column;justify-content:space-between; } /* ===== NÚT & SLIDER ===== */ .clone-control {display:flex;align-items:center;justify-content:space-between;margin-top:10px;gap:10px;} .download-btn,.clone-btn {background:#555!important;color:#fff!important;border:none!important;border-radius:10px!important;padding:6px 12px!important;flex:0 0 42%!important;font-weight:600!important;text-align:center!important;cursor:pointer!important;transition:opacity .2s ease,background .2s ease;} .download-btn:hover,.clone-btn:hover {background:#333!important;opacity:.9;} } .download-btn,.clone-btn{background:#4d4d4d;color:#fff;border:none;border-radius:10px; padding:8px 14px;font-weight:600;width:45%!important;text-align:center;cursor:pointer; transition:opacity .2s,background .2s;} input[type="range"] { -webkit-appearance:none;width:100%;height:4px;border-radius:4px;background:#000;cursor:pointer; } input[type="range"]::-webkit-slider-thumb,input[type="range"]::-moz-range-thumb{width:9px!important;height:9px!important;border-radius:50%!important;background:#000!important;cursor:pointer;transition:transform .15s ease;}input[type="range"]:hover::-webkit-slider-thumb,input[type="range"]:hover::-moz-range-thumb{transform:scale(1.2);} .range-value { position:absolute;background:#000;color:#fff;padding:2px 6px;border-radius:4px;font-size:12px; transform:translate(-50%,-160%);white-space:nowrap;pointer-events:none;z-index:100; } .loading-spinner { display:inline-block;width:18px;height:18px;border:2px solid #000; border-top-color:transparent;border-radius:50%;animation:spin .9s linear infinite; } @keyframes spin {from{transform:rotate(0deg);}to{transform:rotate(360deg);}} input[type="range"]::-webkit-slider-thumb,input[type="range"]::-moz-range-thumb{ width:9px!important;height:9px!important;border-radius:50%!important;background:#000!important;border:none!important; cursor:pointer!important;transition:transform .15s ease!important;} input[type="range"]:hover::-webkit-slider-thumb,input[type="range"]:hover::-moz-range-thumb{transform:scale(1.2);} button.download-btn, button.clone-btn { flex:0 0 auto !important; width:auto !important; min-width:80px !important; } button.download-btn, button.clone-btn { flex:0 0 auto !important; width:100px !important; /* ép hai nút bằng nhau */ min-width:100px !important; background:#555 !important; color:#fff !important; border-radius:8px !important; padding:6px 10px !important; } .clone-control { display:flex !important; align-items:center !important; justify-content:space-between !important; gap:10px !important; flex-wrap:nowrap !important; /* KHÔNG cho xuống hàng */ } #char_usage_row { margin-top:-6px !important; margin-bottom:0 !important; padding:0 !important; line-height:1 !important; } .audio-box{width:100%!important;} """ HEADER_HTML = """
🎯 AVATabs Voice
📱 Tham gia nhóm Zalo
""" # ===================================================== # MAIN APP # ===================================================== with gr.Blocks(css=GLOBAL_CSS) as demo: gr.HTML(HEADER_HTML) with gr.Tabs(): # ========== CLONE VOICE ========== with gr.Tab("Clone Voice"): text_input = gr.Textbox(label="Nhập văn bản tối đa 60.000 ký tự (Chỉ áp dụng cho Tiếng Anh & Tiếng Việt)", lines=20, placeholder="⚠️ Ứng dụng này đọc 100% ký tự chính xác, không bỏ sót hoặc rút ngắn nội dung văn bản.\n\nNhập nội dung cần đọc. Có thể thêm dấu chấm, dấu phẩy để ngắt nghỉ giúp giọng đọc tự nhiên hơn." ) with gr.Row(elem_id="char_usage_row"): gr.HTML("
✅ 0/100.000 ký tự⏳ Hạn sử dụng: 0/3 ngày
") with gr.Row(elem_id="clone_cols"): with gr.Column(): # Cột trái gr.HTML('
Chưa chọn file
Tốc độ giọng đọc1.0x
') with gr.Column(): # Cột phải gr.HTML('''
Kết quả Clone
Chưa bắt đầu
''') # ========== TEXT TO SPEECH ========== with gr.Tab("Text to Speech"): text_input_tts = gr.Textbox( label="Nhập văn bản chỉ dùng cho tiếng Việt (tối đa 100.000 ký tự)", lines=20, placeholder="⚠️ Ứng dụng này đọc 100% ký tự chính xác, không bỏ sót hoặc rút ngắn nội dung văn bản.\n\nNhập nội dung cần đọc. Có thể thêm dấu chấm, dấu phẩy để ngắt nghỉ giúp giọng đọc tự nhiên hơn." ) with gr.Row(elem_id="char_usage_row"): gr.HTML("
✅ 0/100.000 ký tự⏳ Hạn sử dụng: 0/30 ngày
Chọn giọng
Anh Khoa
Tốc độ giọng đọc 1.0x
''') with gr.Column(): # Cột giữa gr.HTML('''
Cảm xúc giọng đọc
''') with gr.Column(): # Cột phải gr.HTML('''
Kết quả TTS
Chưa bắt đầu
''') # ========== CĂN ĐỀU 3 CỘT ========== gr.HTML("") # ===================================================== # CHẠY ỨNG DỤNG # ===================================================== gr.HTML('') # ========================= # JS POPUP + LOGIC CHECK # ========================= gr.HTML(""" """) # ===================== # FRONTEND QUOTA CHECK (JS + backend event) # ===================== def update_quota(char_count): data = load_quota() used = data["chars_used"] + char_count save_quota(used) chars_used, days_left, expired = get_quota_status() return f"✅ {chars_used:,}/{MAX_CHARS:,} ký tự", f"⏳ Hạn sử dụng: {MAX_DAYS - days_left}/{MAX_DAYS} ngày", expired # Hàm giả lập clone/TTS có quota thật def run_with_quota(text): chars = len(text) chars_used, days_left, expired = get_quota_status() if expired: return "⚠️ Hết hạn dùng thử. Hãy nâng cấp hoặc liên hệ Admin.", gr.update(interactive=False) new_counter, new_time, expired = update_quota(chars) return f"✅ Render {chars} ký tự xong.", gr.update(interactive=True) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, share=False)