AVATabsDemo / app.py
Su189's picture
Rename app_demo.py to app.py
43025ff verified
# 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 = """
<div class='header-row'>
<div style="display:flex;align-items:center;gap:8px;">
<b style="font-size:18px;color:#1a365d;">🎯 AVATabs Voice</b>
</div>
<div style="text-align:center;flex:1;">
<a href="https://zalo.me/g/skbjyj852" target="_blank"
style="color:#1a365d;text-decoration:none;font-weight:500;">
📱 Tham gia nhóm Zalo
</a>
</div>
<div style="display:flex;gap:10px;">
<button class='header-btn login-btn'>Đăng nhập</button>
<button class='header-btn free-btn'>Dùng thử Free</button>
</div>
</div>
"""
# =====================================================
# 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("<div style='position:relative;display:flex;align-items:center;justify-content:space-between;width:100%;gap:8px;'><span style='white-space:nowrap;color:#000;'>✅ 0/100.000 ký tự</span><span style='position:absolute;left:50%;transform:translateX(-50%);white-space:nowrap;color:#000;'>⏳ Hạn sử dụng: 0/3 ngày</span><label for='pdf-upload' style='cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px;color:#1a365d;font-weight:500;'><span style='font-size:16px;'>📄</span>Tải bản PDF</label><input id='pdf-upload' type='file' accept='.pdf' style='display:none;' onchange='(async function(f){if(f.files.length){const file=f.files[0];const formData=new FormData();formData.append(\"file\",file);const resp=await fetch(\"/load_pdf\",{method:\"POST\",body:formData});if(resp.ok){const text=await resp.text();const textarea=document.querySelector(\"textarea\");if(textarea){textarea.value=text;textarea.dispatchEvent(new Event(\"input\",{bubbles:true}));}}else{alert(\"Không thể đọc file PDF.\");}}})(this)'/></div><div style='font-size:12px;color:#888;margin-top:2px;'</div>")
with gr.Row(elem_id="clone_cols"):
with gr.Column(): # Cột trái
gr.HTML('<div class="audio-box"><div style="display:flex;align-items:center;gap:8px;width:100%;"><label for="sample-file" style="background:#4d4d4d;color:#fff;padding:6px 12px;border-radius:8px;cursor:pointer;white-space:nowrap;">Tải Mp3 mẫu</label><input type="file" accept=".mp3,.wav" id="sample-file" style="display:none;"><audio id="sample-audio" controls style="flex-grow:1;height:28px;"></audio></div><div id="file-name" style="font-size:13px;color:#444;margin-top:4px;text-align:left;">Chưa chọn file</div><div style="display:flex;align-items:center;justify-content:space-between;margin-top:8px;width:100%;"><span style="flex-shrink:0;">Tốc độ giọng đọc</span><input type="range" min="0.5" max="2" step="0.1" value="1" style="flex-grow:1;height:4px;border-radius:4px;background:#000;appearance:none;outline:none;cursor:pointer;margin:0 8px;vertical-align:middle;"><span style="width:38px;text-align:right;">1.0x</span></div></div>')
with gr.Column(): # Cột phải
gr.HTML('''<div class="audio-box"><div style="display:flex;align-items:center;gap:8px;width:100%;"><span style="font-weight:600;">Kết quả Clone</span><audio controls style="flex-grow:1;height:28px;margin-left:8px;"><source src="result.mp3" type="audio/mpeg"></audio></div><div class="clone-control"><button class="download-btn" onclick="(function(){let a=document.querySelector('audio');let s=a?(a.currentSrc||a.src):null;if(!s){alert('Không có file để tải');return;}let l=document.createElement('a');l.href=s;l.download='clone_result.mp3';document.body.appendChild(l);l.click();document.body.removeChild(l);})()">⬇ Tải xuống</button><div id="clone-status" style="text-align:center;flex-grow:1;">Chưa bắt đầu</div><button class="clone-btn" onclick="(function(){let s=document.getElementById('clone-status');s.innerHTML='<div class=loading-spinner></div>';setTimeout(()=>{s.innerText='✅ Đã hoàn thành';},4000);})()">🚀 Bắt đầu Clone</button></div></div>''')
# ========== 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("<div style='position:relative;display:flex;align-items:center;justify-content:space-between;width:100%;gap:8px;'><span style='white-space:nowrap;color:#000;'>✅ 0/100.000 ký tự</span><span style='position:absolute;left:50%;transform:translateX(-50%);white-space:nowrap;color:#000;'>⏳ Hạn sử dụng: 0/30 ngày</span><label for='pdf-upload' style='cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px;color:#1a365d;font-weight:500;'><span style='font-size:16px;'>📄</span>Tải bản PDF</label><input id='pdf-upload' type='file' accept='.pdf' style='display:none;' onchange='(async function(f){if(f.files.length){const file=f.files[0];const formData=new FormData();formData.append(\"file\",file);const resp=await fetch(\"/load_pdf\",{method:\"POST\",body:formData});if(resp.ok){const text=await resp.text();const textarea=document.querySelector(\"textarea\");if(textarea){textarea.value=text;textarea.dispatchEvent(new Event(\"input\",{bubbles:true}));}}else{alert(\"Không thể đọc file PDF.\");}}})(this)'/></div><div style='font-size:12px;color:#888;margin-top:2px;</div>")
with gr.Row(elem_id="tts_cols"):
with gr.Column(): # Cột trái
with gr.Row():
gr.HTML('''<div class="audio-box" style="position:relative;padding:10px;background:#fff;border:1px solid #ccc;border-radius:8px;height:95px;display:flex;flex-direction:column;justify-content:flex-start;overflow:hidden;">
<!-- Dòng 1: Chọn giọng -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
<span style="font-weight:600;">Chọn giọng</span>
<div id="voice-select" style="border:1px solid #ccc;border-radius:8px;padding:6px 12px;width:75%;display:flex;align-items:center;justify-content:space-between;cursor:pointer;background:#fff;">
<span id="selected-voice">Anh Khoa</span>
<span style="color:#555;">▲</span>
</div>
</div>
<!-- Dòng 2: Tốc độ giọng đọc -->
<div style="display:flex;align-items:center;gap:2px;width:100%;margin-top:4px;">
<span style="white-space:nowrap;">Tốc độ giọng đọc</span>
<input type="range" min="0.5" max="2" step="0.1" value="1" style="flex-grow:1;height:4px;border-radius:4px;background:#000;appearance:none;cursor:pointer;">
<span>1.0x</span>
</div>
<style>
/* Thanh slider: núm tròn đen đường kính 7px */
input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
width:5px; height:5px; border-radius:50%;
background:#000; cursor:pointer;
}
</style>
</div>
<!-- Dropdown 10 giọng -->
<div id="voice-dropdown" style="display:none;position:fixed;width:60%;background:#fff;border:8px solid #ccc;border-radius:10px;box-shadow:0 6px 12px rgba(0,0,0,0.15);overflow-y:auto;max-height:260px;z-index:9999;animation:slideUp 0.25s ease forwards;">
<script>
const voices = [
["Anh Khoa", "Giọng nam truyền cảm, miền Bắc", "#4dabf7"],
["Bích Hồng", "Giọng nữ ấm áp, miền Nam", "#ff6b6b"],
["Đăng Khôi", "Giọng nam trẻ trung, tự tin", "#f06595"],
["Diễm Quỳnh", "Giọng nữ thanh thoát, chuẩn miền Bắc", "#ffd43b"],
["Hoàng Dung", "Giọng nữ nhẹ nhàng, dễ nghe", "#69db7c"],
["Hồng Ngọc", "Giọng nữ miền Trung, khỏe khoắn", "#845ef7"],
["Kim Huệ", "Giọng nữ trẻ, rõ ràng", "#ffa94d"],
["Kim Ngân", "Giọng nữ ngọt ngào, dễ thương", "#63e6be"],
["Mạnh Hùng", "Giọng nam trầm ấm, điềm tĩnh", "#748ffc"],
["Tuấn Hải", "Giọng nam năng động, hiện đại", "#f783ac"]
];
document.write(voices.map(([name, desc, color]) => `
<div class='voice-item' style='display:grid;grid-template-columns:auto 1fr auto;align-items:center;padding:8px 10px;border-bottom:1px solid #eee;gap:8px;'>
<div style='width:36px;height:36px;border-radius:50%;background:${color};'></div>
<div style='display:flex;flex-direction:column;'>
<span style='font-weight:600;font-size:15px;'>${name}</span>
<span style='font-size:13px;color:#777;'>${desc}</span>
</div>
<div style='display:flex;align-items:center;gap:8px;'>
<button class='voice-play' data-voice='${name}' style='background:#777;color:#fff;border:none;width:26px;height:26px;border-radius:50%;font-weight:bold;cursor:pointer;'>▶</button>
<div class='voice-radio' data-voice='${name}' style='width:16px;height:16px;border:2px solid #000;border-radius:50%;cursor:pointer;'></div>
</div>
</div>`).join(""));
</script>
</div>
<!-- Script logic -->
<script>
const sel = document.getElementById("voice-select"),
drop = document.getElementById("voice-dropdown"),
selName = document.getElementById("selected-voice");
sel.onclick = () => {
if (drop.style.display === "block") { drop.style.display = "none"; return; }
document.body.appendChild(drop);
const r = sel.getBoundingClientRect();
drop.style.left = r.left + "px";
drop.style.top = (r.top - drop.offsetHeight - 8) + "px";
drop.style.display = "block";
};
document.addEventListener("click", e => {
if (!sel.contains(e.target) && !drop.contains(e.target)) drop.style.display = "none";
});
setTimeout(() => {
document.querySelectorAll(".voice-radio").forEach(r => {
r.onclick = () => {
document.querySelectorAll(".voice-radio").forEach(x => x.style.background = "transparent");
r.style.background = "#0078ff";
selName.textContent = r.dataset.voice;
drop.style.display = "none";
};
});
document.querySelectorAll(".voice-play").forEach(p => {
p.onclick = () => {
if (p.textContent === "▶") {
p.textContent = "⏸";
const a = new Audio("assets/sample_audio.mp3");
a.play(); a.onended = () => { p.textContent = "▶"; };
} else p.textContent = "▶";
};
});
}, 500);
const s = document.getElementById("speed-slider"),
val = document.getElementById("speed-value");
s.oninput = () => { val.textContent = parseFloat(s.value).toFixed(1) + "x"; };
</script>
<!-- CSS -->
<style>
@keyframes slideUp { from {opacity:0;transform:translateY(10px);} to {opacity:1;transform:translateY(0);} }
input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
width:9px;height:9px;border-radius:50%;background:#000;cursor:pointer;
}
#voice-dropdown::-webkit-scrollbar { width:8px; }
#voice-dropdown::-webkit-scrollbar-thumb { background:rgba(0,0,0,0.25);border-radius:4px; }
</style>
</div>
''')
with gr.Column(): # Cột giữa
gr.HTML('''<div class="audio-box" style="padding:10px;background:#fff;border:1px solid #ccc;border-radius:8px;height:95px;display:flex;flex-direction:column;justify-content:center;">
<span style="font-weight:600;margin-bottom:4px;">Cảm xúc giọng đọc</span>
<div style="display:grid;grid-template-columns:auto auto auto;row-gap:4px;column-gap:12px;">
<label><input type="checkbox"> Cảm xúc vui</label>
<label><input type="checkbox"> Buồn</label>
<label><input type="checkbox"> Giọng âm vang</label>
<label><input type="checkbox"> Năng động</label>
<label><input type="checkbox"> Nghiêm túc</label>
</div></div>''')
with gr.Column(): # Cột phải
gr.HTML('''<div class="audio-box" style="display:flex;flex-direction:column;gap:1px;background:#fff;border:1px solid #ccc;border-radius:8px;height:95px;">
<div style="display:flex;align-items:center;gap:1px;width:100%;"><span style="font-weight:600;">Kết quả TTS</span><audio controls style="flex-grow:1;height:28px;margin-left:8px;"><source src="result.mp3" type="audio/mpeg"></audio></div>
<div class="clone-control" style="display:flex;align-items:center;justify-content:space-between;width:100%;margin-top:4px;">
<button class="download-btn" style="flex:0 0 38px;height:38px;background:#555;color:#fff;border:none;border-radius:20px;cursor:pointer;" onclick="(function(){let a=document.querySelector('audio');let s=a?(a.currentSrc||a.src):null;if(!s){alert('Không có file để tải');return;}let l=document.createElement('a');l.href=s;l.download='tts_result.mp3';document.body.appendChild(l);l.click();document.body.removeChild(l);})()">⬇ Tải xuống</button>
<div id="tts-status" style="text-align:center;flex-grow:1;font-weight:500;">Chưa bắt đầu</div>
<button class="clone-btn" style="flex:0 0 38px;height:38px;background:#555;color:#fff;border:none;border-radius:20px;cursor:pointer;" onclick="(function(){let s=document.getElementById('tts-status');s.innerHTML='<div class=loading-spinner></div>';setTimeout(()=>{s.innerText='✅ Đã hoàn thành';},4000);})()">Bắt đầu TTS</button></div></div>''')
# ========== CĂN ĐỀU 3 CỘT ==========
gr.HTML("<style>#tts_cols{display:flex;gap:1px !important;}#tts_cols>div{flex:1 !important;}</style>")
# =====================================================
# CHẠY ỨNG DỤNG
# =====================================================
gr.HTML('<script>(function(){document.addEventListener("input",e=>{if(e.target.type==="range"){let s=e.target;let t=s.nextElementSibling;if(t&&t.tagName==="SPAN"&&t.style.width==="38px")t.textContent=parseFloat(s.value).toFixed(1)+"x";}});})();</script>')
# =========================
# JS POPUP + LOGIC CHECK
# =========================
gr.HTML("""
<script>
// ===== Popup Đăng ký =====
function showRegisterPopup() {
const overlay = document.createElement("div");
overlay.id = "popup-overlay";
overlay.style = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3); /* Nền mờ 30% */
z-index: 9999;
display: flex; justify-content: center; align-items: center;
`;
const popup = document.createElement("div");
popup.id = "register-popup";
popup.style = `
background: #fff;
border-radius: 12px;
padding: 25px 35px;
width: 360px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
`;
popup.innerHTML = `
<h2 style="color:#1a365d; margin-bottom:10px;">Xin mời đăng ký</h2>
<label>Email:</label><br/>
<input type="email" id="reg-email" placeholder="abc@gmail.com"
style="width:90%; padding:8px; margin:6px 0; border:1px solid #ccc; border-radius:6px;"><br/>
<label>Điện thoại:</label><br/>
<input type="text" id="reg-phone" placeholder="(+84)"
style="width:90%; padding:8px; margin:6px 0; border:1px solid #ccc; border-radius:6px;"><br/>
<div style="margin-top:15px;">
<button id="cancel-btn"
style="padding:8px 20px; margin-right:10px; border:none; border-radius:20px;
background:#ccc; color:#000; cursor:pointer;">Hủy</button>
<button id="register-btn"
style="padding:8px 20px; border:none; border-radius:20px;
background:#007bff; color:#fff; cursor:pointer;">Đăng ký</button>
</div>
`;
overlay.appendChild(popup);
document.body.appendChild(overlay);
document.getElementById("cancel-btn").onclick = () => document.body.removeChild(overlay);
document.getElementById("register-btn").onclick = () => {
const email = document.getElementById("reg-email").value.trim();
const phone = document.getElementById("reg-phone").value.trim();
if (!email.endsWith("@gmail.com")) {
alert("⚠️ Chỉ nhận tài khoản Gmail hợp lệ!");
return;
}
localStorage.setItem("ava_registered", "true");
localStorage.setItem("ava_email", email);
localStorage.setItem("ava_phone", phone);
localStorage.setItem("ava_start", new Date().toISOString());
localStorage.setItem("ava_used", "0");
document.body.removeChild(overlay);
alert("✅ Đăng ký thành công!\\nHãy tham gia nhóm Zalo để được hướng dẫn thêm:\\nhttps://zalo.me/g/skbjyj852");
};
}
// ===== Kiểm tra & Hiển thị Hạn mức =====
function checkQuota(actionType, maxChars=500000, maxDays=3) {
const isRegistered = localStorage.getItem("ava_registered");
if (!isRegistered) {
showRegisterPopup();
return false;
}
const startTime = new Date(localStorage.getItem("ava_start"));
const now = new Date();
const diffDays = Math.floor((now - startTime) / (1000*60*60*24));
const used = parseInt(localStorage.getItem("ava_used") || "0");
const expiredByDay = diffDays >= maxDays;
const expiredByChar = used >= maxChars;
if (expiredByDay || expiredByChar) {
alert("⏳ Hết hạn dùng thử hoặc vượt 200.000 ký tự.\\nVui lòng thanh toán để tiếp tục sử dụng!");
return false;
}
// Nếu còn hạn, cộng ký tự tương ứng với loại thao tác
const add = (actionType === "clone") ? 60000 : 100000;
const newUsed = used + add;
localStorage.setItem("ava_used", newUsed.toString());
const remain = Math.max(0, maxChars - newUsed);
const remainDays = Math.max(0, maxDays - diffDays);
alert(`✅ Thực hiện ${actionType.toUpperCase()} thành công!\\nĐã dùng: ${newUsed}/${maxChars} ký tự.\\nCòn lại: ${remainDays} ngày.`);
return true;
}
// ===== Sự kiện gắn với nút =====
function runClone(){
if (checkQuota("clone")) {
alert("🎧 Đang xử lý Clone... (demo)");
}
}
function runTTS(){
if (checkQuota("tts")) {
alert("🔊 Đang xử lý TTS... (demo)");
}
}
</script>
""")
# =====================
# 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)