TTS_11Labs / app.py
haidnt's picture
Upload 4 files
befb476 verified
import gradio as gr
import os
import requests
import json
from dotenv import load_dotenv
from elevenlabs.client import ElevenLabs
from elevenlabs import save
import pandas as pd
# === Load .env ===
load_dotenv()
DEFAULT_MODEL = os.getenv("ELEVENLABS_MODEL_ID", "eleven_multilingual_v2")
DEFAULT_FORMAT = os.getenv("ELEVENLABS_OUTPUT_FORMAT", "mp3_44100")
# === Data files ===
API_KEY_FILE = "api_keys.json"
VOICE_FILE = "voices.json"
MODELS = ["eleven_monolingual_v1", "eleven_multilingual_v1", "eleven_multilingual_v2"]
# Cấu hình mặc định cho voice mới
DEFAULT_VOICE_SETTINGS = {
"speed": 1.0, # 0.70 - 1.20
"stability": 0.5, # 0.0 - 1.0 (50%)
"similarity_boost": 0.75, # 0.0 - 1.0 (75%)
"style_exaggeration": 0.0, # 0.0 - 1.0 (0%)
"use_speaker_boost": True
}
# Danh sách voice ID mặc định
DEFAULT_VOICES = {
"Josh": "TxGEqnHWrfWFTfGW9XjX",
"Liam": "TX3LPaxmHKxFdv7VOQHJ",
"Antoni": "ErXwobaYiN019PkySvjV",
"Chris": "iP95p4xoKVk53GoZ742B",
"Arnold": "VR6AewLTigWG4xSOukaG",
"Adam": "pNInz6obpgDQGcFmaJgB",
"Brian": "nPczCjzI2devNBz1zQrb",
"Roger": "CwhRBWXzGAHq8TQ4Fs17",
"Paul": "5Q0t7uMcjvnagumLfvZi",
"Drew": "29vD33N1CtxCmqQRPOHJ",
"Michael": "f1q6f7yk4E4fJM5XTYuZ",
"Jessie": "tOjbN1BVZ17f02VDIeMI",
"Bill": "pqH3ZKP75CvO1Qy1NhV4",
"Emily": "LcfcDJNUP1GQjkzn1xUU",
"Glinda": "z9fAnlkpzviPz146aGWa",
"Serena": "pMsXgVXv3BLzUgSXRplE",
"Matilda": "XrExE9yKIg1WjnnlVkGX",
"River": "SAz9YHcvj6GT2YYXdXww",
"Callum": "N21VS1w4EtoT3dr4e0W0",
"Clyde": "2EiwWnXFnvU5JabPnv8n"
}
# === JSON helpers ===
def load_json(file):
if not os.path.exists(file):
return {}
with open(file, 'r') as f:
return json.load(f)
def save_json(file, data):
with open(file, 'w') as f:
json.dump(data, f, indent=2)
# Khởi tạo danh sách voice mặc định khi chưa có file voices.json
def initialize_default_voices():
if not os.path.exists(VOICE_FILE) or os.path.getsize(VOICE_FILE) == 0:
voices = {}
for name, voice_id in DEFAULT_VOICES.items():
voices[name] = {
"voice_id": voice_id,
"settings": DEFAULT_VOICE_SETTINGS.copy()
}
save_json(VOICE_FILE, voices)
return load_json(VOICE_FILE)
# === ElevenLabs API ===
def get_client(api_key):
return ElevenLabs(api_key=api_key)
def get_api_usage(api_key):
try:
headers = {"xi-api-key": api_key}
r = requests.get("https://api.elevenlabs.io/v1/user/subscription", headers=headers)
if r.status_code == 200:
data = r.json()
return {
"status": "✅ OK",
"used": data.get("character_count", 0),
"limit": data.get("character_limit", 0),
"tier": data.get("tier", ""),
"remaining": data.get("character_limit", 0) - data.get("character_count", 0)
}
return {"status": f"❌ {r.status_code}"}
except Exception as e:
return {"status": f"⚠️ {str(e)}"}
def total_credit(api_keys_state):
return sum([v.get("remaining", 0) for v in api_keys_state.values()])
# === TTS ===
def tts_from_text(text, voice_name, model, output_format, api_key_choice, auto_mode, api_keys_state):
if not text.strip():
return "", "Văn bản trống!", f"Tổng credit còn lại: {total_credit(api_keys_state):,}"
# Cập nhật lại api_keys_state từ file để có thông tin mới nhất
api_keys_state = load_json(API_KEY_FILE)
token_count = len(text)
key_to_use = None
if auto_mode:
candidates = [(k, v['remaining']) for k, v in api_keys_state.items() if v.get("remaining", 0) >= token_count]
if not candidates:
return "", "❌ Không có API Key nào đủ credit.", f"Tổng credit còn lại: {total_credit(api_keys_state):,}"
key_to_use = sorted(candidates, key=lambda x: x[1])[0][0]
else:
key_to_use = api_key_choice
client = get_client(key_to_use)
voices = load_json(VOICE_FILE)
voice_info = voices.get(voice_name, {})
voice_id = voice_info.get("voice_id", "")
settings = voice_info.get("settings", {})
if not voice_id:
return "", "❌ Không tìm thấy Voice ID.", f"Tổng credit còn lại: {total_credit(api_keys_state):,}"
audio = client.generate(
text=text,
voice=voice_id,
model=model,
output_format=output_format,
voice_settings=settings if settings else None
)
output_path = f"output.{output_format.split('_')[0]}"
save(audio, output_path)
# Cập nhật thông tin API key sau khi sử dụng
updated_key_info = get_api_usage(key_to_use)
api_keys_state[key_to_use] = updated_key_info
save_json(API_KEY_FILE, api_keys_state)
return output_path, f"✅ Đã tạo giọng nói với {token_count} ký tự.", f"Tổng credit còn lại: {total_credit(api_keys_state):,}"
# === Refresh ===
def refresh_all_dropdowns():
# Đảm bảo rằng đã khởi tạo danh sách voice mặc định
initialize_default_voices()
voices = list(load_json(VOICE_FILE).keys())
# Cập nhật thông tin credit cho tất cả API keys
api_keys = load_json(API_KEY_FILE)
for key in list(api_keys.keys()):
api_keys[key] = get_api_usage(key)
save_json(API_KEY_FILE, api_keys)
keys = list(api_keys.keys())
# Tìm API key có ít credit nhất
default_key = get_lowest_credit_key(api_keys)
return voices, keys, f"Tổng credit còn lại: {total_credit(api_keys):,}", default_key
def get_lowest_credit_key(api_keys):
if not api_keys:
return None
# Sắp xếp các API key theo số credit còn lại (tăng dần)
sorted_keys = sorted(api_keys.items(), key=lambda x: x[1].get('remaining', float('inf')))
# Trả về key có ít credit nhất
return sorted_keys[0][0] if sorted_keys else None
def remove_insufficient_keys(threshold):
keys = load_json(API_KEY_FILE)
filtered = {k: v for k, v in keys.items() if v.get("remaining", 0) >= threshold}
save_json(API_KEY_FILE, filtered)
api_keys = list(filtered.keys())
default_key = get_lowest_credit_key(filtered)
return pd.DataFrame.from_dict(filtered, orient="index").reset_index().rename(columns={"index": "API Key"}), api_keys, default_key
def filter_api_keys_by_credit(threshold):
keys = load_json(API_KEY_FILE)
filtered = {k: v for k, v in keys.items() if v.get("remaining", 0) < threshold}
df = pd.DataFrame.from_dict(filtered, orient="index")
df.reset_index(inplace=True)
df.columns = ["API Key", "Status", "Used", "Limit", "Tier", "Remaining"][:len(df.columns)]
return df
# === Voice Save & Management ===
def save_voice(name, voice_id, current_voice):
if not name or not voice_id:
return "❌ Cần nhập tên Voice và Voice ID!", get_voice_list(), get_voice_list(), current_voice
voices = load_json(VOICE_FILE)
# Kiểm tra xem Voice ID đã tồn tại chưa (trường hợp ghi đè)
for existing_name, voice_data in voices.items():
if voice_data.get("voice_id") == voice_id and existing_name != name:
return f"❌ Voice ID đã tồn tại cho voice '{existing_name}'. Vui lòng sử dụng tên này hoặc chọn Voice ID khác.", get_voice_list(), get_voice_list(), current_voice
# Cấu hình mặc định cho voice mới
voices[name] = {
"voice_id": voice_id,
"settings": DEFAULT_VOICE_SETTINGS.copy()
}
save_json(VOICE_FILE, voices)
# Lấy danh sách voice mới và trả về tên voice vừa tạo
voice_list = get_voice_list()
return f"✅ Đã lưu Voice '{name}'", voice_list, voice_list, name
def load_voice_for_edit(name):
if not name:
return "", "", DEFAULT_VOICE_SETTINGS["speed"], DEFAULT_VOICE_SETTINGS["stability"], \
DEFAULT_VOICE_SETTINGS["similarity_boost"], DEFAULT_VOICE_SETTINGS["style_exaggeration"], \
DEFAULT_VOICE_SETTINGS["use_speaker_boost"]
voices = load_json(VOICE_FILE)
v = voices.get(name, {})
cfg = v.get("settings", DEFAULT_VOICE_SETTINGS.copy())
return (
name,
v.get("voice_id", ""),
cfg.get("speed", DEFAULT_VOICE_SETTINGS["speed"]),
cfg.get("stability", DEFAULT_VOICE_SETTINGS["stability"]),
cfg.get("similarity_boost", DEFAULT_VOICE_SETTINGS["similarity_boost"]),
cfg.get("style_exaggeration", DEFAULT_VOICE_SETTINGS["style_exaggeration"]),
cfg.get("use_speaker_boost", DEFAULT_VOICE_SETTINGS["use_speaker_boost"])
)
def delete_voice(name, current_voice):
if not name:
return "❌ Vui lòng chọn Voice để xóa", get_voice_list(), get_voice_list(), current_voice
voices = load_json(VOICE_FILE)
if name in voices:
del voices[name]
save_json(VOICE_FILE, voices)
voice_list = get_voice_list()
new_current = voice_list[0] if voice_list else None
return f"✅ Đã xóa Voice '{name}'", voice_list, voice_list, new_current
return f"❌ Không tìm thấy Voice '{name}'", get_voice_list(), get_voice_list(), current_voice
def delete_all_voices(confirm_delete):
if not confirm_delete:
return "❌ Vui lòng đánh dấu vào ô xác nhận để xóa tất cả Voice", [], []
voices = load_json(VOICE_FILE)
if not voices:
return "❌ Không có Voice nào để xóa", [], []
# Xóa tất cả voices
save_json(VOICE_FILE, {})
return "✅ Đã xóa tất cả Voice", [], []
def voice_table():
voices = load_json(VOICE_FILE)
rows = [[k, v.get("voice_id", ""), "✅" if v.get("settings") else "❌"] for k, v in voices.items()]
return pd.DataFrame(rows, columns=["Tên Voice", "Voice ID", "Đã cấu hình"])
def reset_voice_settings(name, current_voice):
if not name:
return "❌ Vui lòng chọn Voice để reset cấu hình", get_voice_list(), get_voice_list(), current_voice
voices = load_json(VOICE_FILE)
if name not in voices:
return f"❌ Không tìm thấy Voice '{name}'", get_voice_list(), get_voice_list(), current_voice
# Lấy voice_id hiện tại
voice_id = voices[name].get("voice_id", "")
# Reset cấu hình về mặc định
voices[name] = {
"voice_id": voice_id,
"settings": DEFAULT_VOICE_SETTINGS.copy()
}
save_json(VOICE_FILE, voices)
return f"✅ Đã reset cấu hình Voice '{name}' về mặc định", get_voice_list(), get_voice_list(), name
def update_voice_config(name, speed, stability, similarity, exaggeration, boost, current_voice):
if not name:
return "❌ Vui lòng chọn Voice để cập nhật cấu hình", get_voice_list(), get_voice_list(), current_voice
voices = load_json(VOICE_FILE)
if name not in voices:
return f"❌ Không tìm thấy Voice '{name}'", get_voice_list(), get_voice_list(), current_voice
# Cập nhật cấu hình
voices[name]["settings"] = {
"stability": stability,
"similarity_boost": similarity,
"style_exaggeration": exaggeration,
"use_speaker_boost": boost,
"speed": speed
}
save_json(VOICE_FILE, voices)
return f"✅ Đã cập nhật cấu hình Voice '{name}'", get_voice_list(), get_voice_list(), name
def get_voice_list():
# Đảm bảo đã khởi tạo voice mặc định
initialize_default_voices()
voices = load_json(VOICE_FILE)
voice_list = list(voices.keys())
return voice_list if voice_list else []
def get_default_voice():
# Đảm bảo đã khởi tạo voice mặc định
initialize_default_voices()
voices = load_json(VOICE_FILE)
voice_list = list(voices.keys())
return voice_list[0] if voice_list else None
# === API key management ===
def save_and_show_keys(text):
# Lấy các API key hiện có
existing_keys = load_json(API_KEY_FILE)
# Thêm hoặc cập nhật các API key mới
for key in text.strip().splitlines():
key = key.strip()
if key and key not in existing_keys:
existing_keys[key] = get_api_usage(key)
save_json(API_KEY_FILE, existing_keys)
df = pd.DataFrame.from_dict(existing_keys, orient="index")
df.reset_index(inplace=True)
df.columns = ["API Key", "Status", "Used", "Limit", "Tier", "Remaining"][:len(df.columns)]
api_keys = list(existing_keys.keys())
default_key = get_lowest_credit_key(existing_keys)
return df, api_keys, default_key
def refresh_keys():
# Cập nhật thông tin credit cho tất cả API keys
keys = load_json(API_KEY_FILE)
for key in list(keys.keys()):
keys[key] = get_api_usage(key)
save_json(API_KEY_FILE, keys)
df = pd.DataFrame.from_dict(keys, orient="index")
df.reset_index(inplace=True)
df.columns = ["API Key", "Status", "Used", "Limit", "Tier", "Remaining"][:len(df.columns)]
default_key = get_lowest_credit_key(keys)
return df, list(keys.keys()), f"Tổng credit còn lại: {total_credit(keys):,}", default_key
# === UI ===
with gr.Blocks() as demo:
gr.Markdown("# 🎙️ ElevenLabs TTS Interface")
gr.Markdown("### Tạo giọng nói từ văn bản với ElevenLabs API")
gr.Markdown("Vui lòng nhập API Key của bạn trong tab 'Quản lý API Key'")
api_keys_state = gr.State(load_json(API_KEY_FILE))
with gr.Tabs():
with gr.Tab("1. Xử lý Batch"):
with gr.Row():
voice_dropdown = gr.Dropdown(choices=get_voice_list(), value=get_default_voice(), label="Chọn Voice")
model_dropdown = gr.Dropdown(choices=MODELS, value=DEFAULT_MODEL, label="Chọn Model")
output_format = gr.Dropdown(choices=["mp3_44100", "wav"], value=DEFAULT_FORMAT, label="Output Format")
with gr.Row():
api_keys_list = gr.Dropdown(choices=list(load_json(API_KEY_FILE).keys()), label="Chọn API Key (thủ công)")
api_key_credit = gr.Text(label="Credit của API Key", interactive=False)
total_credit_label = gr.Text(label="Tổng Credit", interactive=False)
def update_api_key_credit(key):
keys = load_json(API_KEY_FILE)
return f"{keys.get(key, {}).get('remaining', 0):,}" if key in keys else "-"
api_keys_list.change(fn=update_api_key_credit, inputs=api_keys_list, outputs=api_key_credit)
refresh_btn = gr.Button("🔄 Refresh")
refresh_btn.click(fn=refresh_all_dropdowns, outputs=[voice_dropdown, api_keys_list, total_credit_label, api_keys_list])
input_text = gr.Textbox(lines=6, label="Nhập văn bản")
token_info = gr.Text(label="Tổng token")
input_text.change(fn=lambda txt: f"{len(txt)} ký tự", inputs=input_text, outputs=token_info)
result_audio = gr.Audio(label="Kết quả", type="filepath")
status = gr.Text(label="Trạng thái")
auto_mode = gr.Checkbox(label="Tự động chọn API Key đủ credit", value=True)
generate_btn = gr.Button("🌀 Tạo giọng nói")
generate_btn.click(
fn=tts_from_text,
inputs=[input_text, voice_dropdown, model_dropdown, output_format, api_keys_list, auto_mode, api_keys_state],
outputs=[result_audio, status, total_credit_label]
)
with gr.Tab("2. Quản lý API Key"):
gr.Markdown("### Thêm API Key của bạn")
gr.Markdown("Nhập API key của ElevenLabs tại đây. Mỗi key trên một dòng.")
multi_api_input = gr.Textbox(lines=5, label="Nhập API Key (mỗi dòng 1 key)")
save_btn = gr.Button("💾 Lưu & Kiểm tra")
key_status_table = gr.Dataframe(headers=["API Key", "Status", "Used", "Limit", "Tier", "Remaining"], label="Danh sách API Key")
refresh_keys_btn = gr.Button("🔄 Refresh danh sách")
filter_input = gr.Number(label="Lọc các API Key dưới bao nhiêu credit")
filter_btn = gr.Button("🔍 Lọc")
remove_low_btn = gr.Button("❌ Xoá các key không đủ credit")
save_btn.click(
fn=save_and_show_keys,
inputs=multi_api_input,
outputs=[key_status_table, api_keys_list, api_keys_list]
)
refresh_keys_btn.click(
fn=refresh_keys,
outputs=[key_status_table, api_keys_list, total_credit_label, api_keys_list]
)
filter_btn.click(fn=filter_api_keys_by_credit, inputs=filter_input, outputs=key_status_table)
remove_low_btn.click(fn=remove_insufficient_keys, inputs=filter_input, outputs=[key_status_table, api_keys_list, api_keys_list])
with gr.Tab("3. Quản lý Voice ID"):
gr.Markdown("### Thêm Voice ID mới")
gr.Markdown("Bạn có thể tìm Voice ID mới trên ElevenLabs và thêm vào đây.")
with gr.Row():
voice_name = gr.Textbox(label="Tên Voice", scale=1)
voice_id_box = gr.Textbox(label="Voice ID", scale=2)
voice_select = gr.Dropdown(choices=get_voice_list(), value=None, label="Chọn Voice để sửa", interactive=True, scale=2)
voice_status = gr.Textbox(label="Trạng thái", interactive=False)
with gr.Row():
save_voice_btn = gr.Button("💾 Lưu Voice", scale=1)
save_config_btn = gr.Button("💾 Lưu cấu hình Voice", scale=1)
with gr.Row():
delete_voice_btn = gr.Button("❌ Xoá Voice đang chọn", variant="secondary", scale=1)
reset_config_btn = gr.Button("↻ Reset cấu hình Voice", variant="primary", scale=1)
gr.Markdown("### Cấu hình Voice")
speed_slider = gr.Slider(0.70, 1.20, DEFAULT_VOICE_SETTINGS["speed"], step=0.01, label="Tốc độ (Speed): Chậm 0.70 - Nhanh 1.20")
stability_slider = gr.Slider(0.0, 1.0, DEFAULT_VOICE_SETTINGS["stability"], label="Độ ổn định (Stability): 0-100% - Cao = Giọng đều đặn, Thấp = Giọng biểu cảm hơn")
similarity_slider = gr.Slider(0.0, 1.0, DEFAULT_VOICE_SETTINGS["similarity_boost"], label="Độ tương đồng (Similarity): 0-100% - Cao = Giống giọng gốc, Thấp = Đa dạng hơn")
exaggeration_slider = gr.Slider(0.0, 1.0, DEFAULT_VOICE_SETTINGS["style_exaggeration"], label="Phóng đại phong cách (Style Exaggeration): 0-100% - Cao = Phóng đại phong cách, Thấp = Tự nhiên")
boost_checkbox = gr.Checkbox(label="Tăng cường giọng nói (Speaker Boost): Giọng rõ và trong hơn", value=DEFAULT_VOICE_SETTINGS["use_speaker_boost"])
gr.Markdown("## 📄 Danh sách Voice đã lưu")
voice_table_refresh = gr.Button("🔄 Làm mới danh sách")
voice_table_view = gr.Dataframe(label="Voice List", interactive=False)
# Phần xóa tất cả voices - chuyển xuống dưới cùng
gr.Markdown("### Xóa tất cả Voice")
with gr.Row():
delete_confirm = gr.Checkbox(label="Xác nhận xóa tất cả Voice", value=False)
delete_all_btn = gr.Button("❌ Xoá tất cả Voice", variant="stop")
# Voice management event handlers
save_voice_btn.click(
fn=save_voice,
inputs=[voice_name, voice_id_box, voice_select],
outputs=[voice_status]
).then(
fn=get_voice_list,
outputs=[voice_select, voice_dropdown]
).then(
fn=voice_table,
outputs=voice_table_view
).then(
fn=lambda name: name,
inputs=[voice_name],
outputs=[voice_select]
)
voice_select.change(
fn=load_voice_for_edit,
inputs=[voice_select],
outputs=[voice_name, voice_id_box, speed_slider, stability_slider, similarity_slider, exaggeration_slider, boost_checkbox]
)
# "Reset cấu hình Voice" button
reset_config_btn.click(
fn=reset_voice_settings,
inputs=[voice_select, voice_select],
outputs=[voice_status, voice_select, voice_dropdown, voice_select]
).then(
fn=voice_table,
outputs=voice_table_view
).then(
fn=lambda name: load_voice_for_edit(name),
inputs=[voice_select],
outputs=[voice_name, voice_id_box, speed_slider, stability_slider, similarity_slider, exaggeration_slider, boost_checkbox]
)
# "Xoá Voice đang chọn" button
delete_voice_btn.click(
fn=delete_voice,
inputs=[voice_select, voice_select],
outputs=[voice_status, voice_select, voice_dropdown, voice_select]
).then(
fn=voice_table,
outputs=voice_table_view
).then(
fn=lambda name: load_voice_for_edit(name) if name else ("", "", DEFAULT_VOICE_SETTINGS["speed"], DEFAULT_VOICE_SETTINGS["stability"], DEFAULT_VOICE_SETTINGS["similarity_boost"], DEFAULT_VOICE_SETTINGS["style_exaggeration"], DEFAULT_VOICE_SETTINGS["use_speaker_boost"]),
inputs=[voice_select],
outputs=[voice_name, voice_id_box, speed_slider, stability_slider, similarity_slider, exaggeration_slider, boost_checkbox]
)
# "Xoá tất cả Voice" button - thêm tham số confirm_delete
delete_all_btn.click(
fn=delete_all_voices,
inputs=[delete_confirm],
outputs=[voice_status, voice_select, voice_dropdown]
).then(
fn=voice_table,
outputs=voice_table_view
).then(
# Xóa giá trị trong các trường nhập
fn=lambda: ("", "", DEFAULT_VOICE_SETTINGS["speed"], DEFAULT_VOICE_SETTINGS["stability"], DEFAULT_VOICE_SETTINGS["similarity_boost"], DEFAULT_VOICE_SETTINGS["style_exaggeration"], DEFAULT_VOICE_SETTINGS["use_speaker_boost"]),
outputs=[voice_name, voice_id_box, speed_slider, stability_slider, similarity_slider, exaggeration_slider, boost_checkbox]
)
# "Lưu cấu hình Voice" button
save_config_btn.click(
fn=update_voice_config,
inputs=[voice_select, speed_slider, stability_slider, similarity_slider, exaggeration_slider, boost_checkbox, voice_select],
outputs=[