"""
AI Novel Generator 4.5 - Ứng dụng chính
Tích hợp hệ thống sinh tiểu thuyết, quản lý dự án, xuất file
Bản quyền © 2026 Công ty TNHH Công nghệ An ninh mạng Huyễn Thành Tân Cương (Công nghệ Huyễn Thành)
Tác giả: Huyễn Thành
"""
import gradio as gr
import logging
import threading
import json
from typing import List, Tuple, Optional, Dict, Any
from pathlib import Path
from datetime import datetime
import os
# Import các module hiện có
from api_client import APIClient, get_api_client, reinit_api_client
from config import get_config, ConfigManager, Backend, GenerationConfig, API_PROVIDERS
from novel_generator import (
NovelGenerator, NovelProject, Chapter, OutlineParser,
get_preset_templates, get_generator,
save_generation_cache, load_generation_cache, clear_generation_cache,
list_generation_caches, get_cache_size
)
from project_manager import ProjectManager
from config_api import ConfigAPIManager
from exporter import export_to_docx, export_to_txt, export_to_markdown, export_to_html
from genre_manager import GenreManager
from sub_genre_manager import SubGenreManager
from file_parser import parse_novel_file
from locales.i18n import t
from database import get_db
from logger import get_logger
# ==================== Cấu hình Logging ====================
logger = get_logger("app")
logger.info("=" * 60)
logger.info("AI Novel Generator 4.5 - Hệ thống log đã khởi tạo")
logger.info("=" * 60)
# Cấu hình biến môi trường
WEB_HOST = os.getenv("NOVEL_TOOL_HOST", "0.0.0.0")
WEB_PORT = int(os.getenv("NOVEL_TOOL_PORT", os.getenv("PORT", "7860")))
WEB_SHARE = os.getenv("NOVEL_TOOL_SHARE", "false").lower() in ("1", "true", "yes")
# ==================== Quản lý trạng thái ứng dụng ====================
class AppState:
"""Quản lý trạng thái ứng dụng"""
def __init__(self):
self.is_generating = False
self.stop_requested = False
self.lock = threading.Lock()
self.current_project: Optional[NovelProject] = None
self.generator: Optional[NovelGenerator] = None
def get_generator(self) -> NovelGenerator:
"""Lấy hoặc tạo generator"""
if self.generator is None:
self.generator = get_generator()
return self.generator
# Trạng thái ứng dụng toàn cục
app_state = AppState()
# ==================== Hàm tiện ích ====================
def list_project_titles():
"""Lấy danh sách tiêu đề dự án"""
try:
projects = ProjectManager.list_projects()
return [p["title"] for p in projects]
except Exception:
return []
# ==================== Giao diện chính ====================
def create_main_ui():
"""Tạo giao diện chính"""
# Tải CSS tùy chỉnh
custom_css = ""
css_path = Path("custom.css")
if css_path.exists():
with open(css_path, 'r', encoding='utf-8') as f:
custom_css = f.read()
with gr.Blocks(title=t("app.title")) as app:
# Header
gr.Markdown(f"""
🚀 {t("app.title")}
{t("app.subtitle")}
""")
with gr.Tabs():
# ==================== Tab 1: Sáng tác từ đầu ====================
with gr.Tab(t("tabs.create")):
gr.Markdown(f"### {t('create.header')}")
with gr.Row():
with gr.Column(scale=1):
genre_choices = GenreManager.get_genre_names()
genre_dropdown = gr.Dropdown(
choices=genre_choices,
value=genre_choices[0] if genre_choices else None,
label=t("create.genre_label"),
interactive=True
)
sub_genre_choices = SubGenreManager.get_sub_genre_names()
sub_genre_dropdown = gr.Dropdown(
choices=sub_genre_choices,
label=t("create.sub_genres_label"),
multiselect=True,
interactive=True
)
with gr.Column(scale=2):
title_input = gr.Textbox(
label=t("create.novel_title"),
placeholder=t("create.novel_title_default"),
interactive=True
)
suggested_titles_radio = gr.Radio(
label=t("create.suggested_titles_radio") if t("create.suggested_titles_radio") != "create.suggested_titles_radio" else "Gợi ý tên truyện",
choices=[],
visible=False,
interactive=True
)
with gr.Row():
suggest_title_btn = gr.Button(t("create.suggest_title_btn"), variant="secondary", size="sm")
suggest_title_prompt = gr.Textbox(
label=t("create.custom_prompt_label"),
placeholder=t("create.custom_prompt_placeholder"),
lines=1, interactive=True, scale=3
)
# Thiết lập nhân vật
with gr.Row():
with gr.Column():
character_input = gr.Textbox(
label=t("create.char_setting"),
placeholder=t("create.char_setting_placeholder"),
lines=5, interactive=True
)
with gr.Row():
suggest_char_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm")
num_main_chars = gr.Number(label=t("create.num_main_chars"), value=2, minimum=1, maximum=10, step=1, scale=1)
num_sub_chars = gr.Number(label=t("create.num_sub_chars"), value=3, minimum=0, maximum=20, step=1, scale=1)
suggest_char_prompt = gr.Textbox(label=t("create.custom_prompt_label"), placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=2)
with gr.Column():
world_input = gr.Textbox(
label=t("create.world_setting"),
placeholder=t("create.world_setting_placeholder"),
lines=5, interactive=True
)
with gr.Row():
suggest_world_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm")
suggest_world_prompt = gr.Textbox(label=t("create.custom_prompt_label"), placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=3)
# Ý tưởng cốt truyện
plot_input = gr.Textbox(
label=t("create.plot_idea"),
placeholder=t("create.plot_idea_placeholder"),
lines=3, interactive=True
)
with gr.Row():
suggest_plot_btn = gr.Button(t("create.suggest_plot_btn"), variant="secondary", size="sm")
suggest_plot_prompt = gr.Textbox(label=t("create.custom_prompt_label"), placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=True, scale=3)
# Số chương và tạo dàn ý
with gr.Row():
total_chapters = gr.Number(
label=t("create.chapter_count"), value=20, minimum=1, maximum=200, step=1
)
generate_outline_btn = gr.Button(t("create.gen_outline_btn"), variant="primary", size="lg")
outline_output = gr.Textbox(label=t("create.outline_display"), lines=15, interactive=True)
outline_status = gr.Textbox(label=t("create.gen_status"), interactive=False)
# Tạo toàn bộ tiểu thuyết
gr.Markdown("---")
with gr.Row():
auto_generate_btn = gr.Button(t("create.start_gen_btn"), variant="primary", size="lg")
stop_btn = gr.Button(t("create.pause_gen_btn"), variant="stop", size="lg")
generation_progress = gr.Textbox(label=t("create.gen_status"), lines=10, interactive=False)
# --- Sự kiện Tab Sáng tác ---
def on_suggest_title(genre, sub_genres, custom_prompt):
gen = app_state.get_generator()
content, msg = gen.suggest_title(genre, sub_genres, custom_prompt)
if content:
import json
import re
try:
# Tìm array JSON [...] trực tiếp để tránh rác text bao quanh
match = re.search(r'\[\s*\{[\s\S]*\}\s*\]', content)
if match:
data = {"suggestions": json.loads(match.group(0))}
else:
clean_content = re.sub(r'```(?:json)?\s*', '', content)
clean_content = re.sub(r'```\s*', '', clean_content)
data = json.loads(clean_content)
if "suggestions" in data:
choices = [f"{item.get('title', '')} - {item.get('description', '')}" for item in data["suggestions"]]
return gr.update(choices=choices, visible=True)
except Exception as e:
logger.error(f"Failed to parse title JSON: {e}")
return gr.update(choices=[content], visible=True)
return gr.update(choices=[msg], visible=True)
def on_title_select(selected):
if selected and " - " in selected:
return selected.split(" - ", 1)[0]
return selected
def on_suggest_content(suggest_type, title, genre, sub_genres, char_setting, world_setting, custom_prompt, num_main=2, num_sub=3):
gen = app_state.get_generator()
content, msg = gen.suggest_content(
suggest_type, title, genre, sub_genres,
char_setting, world_setting, custom_prompt,
int(num_main), int(num_sub)
)
return content if content else msg
def on_generate_outline(title, genre, sub_genres, num_chapters, char_setting, world_setting, plot_idea):
gen = app_state.get_generator()
content, msg = gen.generate_outline(
title, genre, sub_genres or [],
int(num_chapters), char_setting, world_setting, plot_idea
)
return content, msg
def on_auto_generate(title, genre, sub_genres, char_setting, world_setting, plot_idea, outline_text, progress=gr.Progress()):
"""Tự động tạo toàn bộ tiểu thuyết"""
gen = app_state.get_generator()
chapters, parse_msg = OutlineParser.parse(outline_text)
if not chapters:
return f"❌ {parse_msg}"
project, create_msg = ProjectManager.create_project(
title, genre, sub_genres or [],
char_setting, world_setting, plot_idea
)
if not project:
return f"❌ {create_msg}"
project.chapters = chapters
app_state.current_project = project
app_state.is_generating = True
app_state.stop_requested = False
results = [f"📋 Đã phân tích {len(chapters)} chương", f"💾 {create_msg}"]
for i, chapter in enumerate(chapters):
if app_state.stop_requested:
results.append("\n⚠️ Đã dừng sinh!")
break
results.append(f"\n✍️ Đang sinh Chương {chapter.num}: {chapter.title}...")
progress((i + 1) / len(chapters))
prev_content = ""
if i > 0 and chapters[i-1].content:
prev_content = chapters[i-1].content[-2000:]
content, msg = gen.generate_chapter(
chapter_num=chapter.num, chapter_title=chapter.title,
chapter_desc=chapter.desc, novel_title=title,
character_setting=char_setting, world_setting=world_setting,
plot_idea=plot_idea, genre=genre, sub_genres=sub_genres,
previous_content=prev_content
)
if content:
chapter.content = content
chapter.word_count = len(content)
chapter.generated_at = datetime.now().isoformat()
results.append(f"✅ Chương {chapter.num}: {len(content)} từ")
ProjectManager.save_project(project)
else:
results.append(f"❌ Chương {chapter.num}: {msg}")
app_state.is_generating = False
total_words = sum(ch.word_count for ch in chapters if ch.content)
results.append(f"\n🎉 Hoàn thành! Tổng: {total_words} từ")
return "\n".join(results)
def on_stop():
app_state.stop_requested = True
return "⏸️ Đang dừng..."
# Bind events
suggest_title_btn.click(
fn=on_suggest_title,
inputs=[genre_dropdown, sub_genre_dropdown, suggest_title_prompt],
outputs=[suggested_titles_radio]
)
suggested_titles_radio.change(
fn=on_title_select,
inputs=[suggested_titles_radio],
outputs=[title_input]
)
suggest_char_btn.click(
fn=lambda ti, g, sg, cp, nm, ns: on_suggest_content("char", ti, g, sg, "", "", cp, nm, ns),
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_char_prompt, num_main_chars, num_sub_chars],
outputs=[character_input]
)
suggest_world_btn.click(
fn=lambda ti, g, sg, cp: on_suggest_content("world", ti, g, sg, "", "", cp),
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_world_prompt],
outputs=[world_input]
)
suggest_plot_btn.click(
fn=lambda ti, g, sg, cs, ws, cp: on_suggest_content("plot", ti, g, sg, cs, ws, cp),
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, suggest_plot_prompt],
outputs=[plot_input]
)
generate_outline_btn.click(
fn=on_generate_outline,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, total_chapters, character_input, world_input, plot_input],
outputs=[outline_output, outline_status]
)
auto_generate_btn.click(
fn=on_auto_generate,
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, plot_input, outline_output],
outputs=[generation_progress]
)
stop_btn.click(fn=on_stop, outputs=[generation_progress])
# ==================== Tab 2: Viết tiếp dự án ====================
with gr.Tab(t("tabs.continue_tab")):
gr.Markdown(f"### {t('continue_tab.header')}")
project_choices = list_project_titles()
continue_project_selector = gr.Dropdown(
choices=project_choices,
label=t("continue_tab.select_project"),
interactive=True
)
refresh_continue_btn = gr.Button(t("continue_tab.load_btn"), size="sm")
continue_project_info = gr.Markdown(t("continue_tab.no_project_loaded"))
gr.Markdown("---")
with gr.Row():
continue_chapter_num = gr.Number(label="Chương số", value=1, minimum=1)
continue_chapter_title = gr.Textbox(label="Tiêu đề chương", placeholder="Nhập tiêu đề chương...", scale=2)
continue_chapter_desc = gr.Textbox(
label=t("continue_tab.outline_label"),
lines=5, placeholder="Nhập mô tả nội dung chương..."
)
with gr.Row():
continue_target_words = gr.Number(label=t("rewrite.target_words"), value=3000, minimum=100, maximum=50000, step=100)
continue_custom_prompt = gr.Textbox(label=t("create.custom_prompt_label"), placeholder=t("create.custom_prompt_placeholder"), scale=2)
continue_generate_btn = gr.Button(t("continue_tab.continue_gen_btn"), variant="primary", size="lg")
continue_status = gr.Textbox(label=t("continue_tab.gen_status"), interactive=False)
continue_output = gr.Textbox(label=t("continue_tab.novel_label"), lines=20, interactive=True)
# Sự kiện
def on_refresh_continue():
titles = list_project_titles()
return gr.update(choices=titles, value=None)
def on_continue_project_select(project_title):
if not project_title:
return t("continue_tab.no_project_loaded"), 1
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"❌ {t('continue_tab.project_not_found')}", 1
project_id = project_data.get("id")
project, msg = ProjectManager.load_project(project_id)
if project:
app_state.current_project = project
completed = project.get_completed_count()
next_ch = completed + 1
info = t("continue_tab.info_template").format(
title=project.title,
genre=project.genre,
completed=completed,
total=len(project.chapters),
words=project.get_total_words()
) if isinstance(t("continue_tab.info_template"), str) and '{' in t("continue_tab.info_template") else f"""### 📖 {project.title}
**Thể loại**: {project.genre}
**Hoàn thành**: {completed}/{len(project.chapters)} chương
**Tổng số từ**: {project.get_total_words()}
💡 Chương tiếp theo: {next_ch}"""
return info, next_ch
return f"❌ {msg}", 1
except Exception as e:
return f"❌ {str(e)}", 1
def on_continue_generate(project_title, ch_num, ch_title, ch_desc, target_words, custom_prompt):
if not app_state.current_project:
return f"❌ {t('continue_tab.no_project_selected')}", ""
gen = app_state.get_generator()
project = app_state.current_project
prev_content = ""
for ch in project.chapters:
if ch.num == int(ch_num) - 1 and ch.content:
prev_content = ch.content[-2000:]
content, msg = gen.generate_chapter(
chapter_num=int(ch_num), chapter_title=ch_title,
chapter_desc=ch_desc, novel_title=project.title,
character_setting=project.character_setting,
world_setting=project.world_setting,
plot_idea=project.plot_idea, genre=project.genre,
sub_genres=project.sub_genres,
previous_content=prev_content, custom_prompt=custom_prompt
)
if content:
new_ch = Chapter(
num=int(ch_num), title=ch_title, desc=ch_desc,
content=content, word_count=len(content),
generated_at=datetime.now().isoformat()
)
found = False
for i, ch in enumerate(project.chapters):
if ch.num == int(ch_num):
project.chapters[i] = new_ch
found = True
break
if not found:
project.chapters.append(new_ch)
project.chapters.sort(key=lambda x: x.num)
ProjectManager.save_project(project)
return f"✅ Chương {int(ch_num)} đã sinh ({len(content)} từ)", content
else:
return f"❌ {msg}", ""
refresh_continue_btn.click(fn=on_refresh_continue, outputs=[continue_project_selector])
continue_project_selector.change(
fn=on_continue_project_select,
inputs=[continue_project_selector],
outputs=[continue_project_info, continue_chapter_num]
)
continue_generate_btn.click(
fn=on_continue_generate,
inputs=[continue_project_selector, continue_chapter_num, continue_chapter_title,
continue_chapter_desc, continue_target_words, continue_custom_prompt],
outputs=[continue_status, continue_output]
)
# ==================== Tab 3: Viết lại ====================
with gr.Tab(t("tabs.rewrite")):
gr.Markdown(f"### {t('rewrite.header')}")
rewrite_file_input = gr.File(label=t("rewrite.upload_file"), file_types=[".txt", ".docx", ".md", ".pdf"])
rewrite_input = gr.Textbox(
label=t("polish.original_text"),
lines=10, placeholder="Dán nội dung cần viết lại..."
)
preset_choices = list(get_preset_templates().keys())
rewrite_style = gr.Dropdown(
choices=preset_choices,
label=t("rewrite.preset_style"),
value=preset_choices[0] if preset_choices else None,
interactive=True
)
rewrite_btn = gr.Button(t("rewrite.start_rewrite"), variant="primary")
rewrite_status = gr.Textbox(label=t("rewrite.parse_status"), interactive=False)
rewrite_output = gr.Textbox(label=t("rewrite.full_rewritten"), lines=15, interactive=True)
def on_file_upload(file):
if file is None:
return ""
try:
content = parse_novel_file(file.name)
return content
except Exception as e:
return f"❌ {str(e)}"
def on_rewrite(text, style):
instructions = get_preset_templates().get(style, "")
gen = app_state.get_generator()
content, msg = gen.rewrite_paragraph(text, instructions)
if content:
return f"✅ {msg}", content
return f"❌ {msg}", ""
rewrite_file_input.change(fn=on_file_upload, inputs=[rewrite_file_input], outputs=[rewrite_input])
rewrite_btn.click(fn=on_rewrite, inputs=[rewrite_input, rewrite_style], outputs=[rewrite_status, rewrite_output])
# ==================== Tab 4: Đánh bóng ====================
with gr.Tab(t("tabs.polish")):
gr.Markdown(f"### {t('polish.header')}")
polish_file_input = gr.File(label=t("polish.upload_file"), file_types=[".txt", ".docx", ".md", ".pdf"])
polish_input = gr.Textbox(
label=t("polish.original_text"),
lines=10, placeholder="Dán nội dung cần đánh bóng..."
)
polish_custom_req = gr.Textbox(
label=t("polish.custom_req"),
placeholder=t("polish.custom_req_placeholder"),
lines=2
)
with gr.Row():
polish_btn = gr.Button(t("polish.start_polish"), variant="primary")
polish_suggest_btn = gr.Button(t("polish.polish_suggest_btn"), variant="secondary")
polish_status = gr.Textbox(label=t("polish.polish_status"), interactive=False)
polish_output = gr.Textbox(label=t("polish.polished_text"), lines=15, interactive=True)
def on_polish_file_upload(file):
if file is None:
return ""
try:
content = parse_novel_file(file.name)
return content
except Exception as e:
return f"❌ {str(e)}"
def on_polish(text):
gen = app_state.get_generator()
content, msg = gen.polish_text(text)
if content:
return f"✅ {msg}", content
return f"❌ {msg}", ""
def on_polish_suggest(text):
gen = app_state.get_generator()
content, msg = gen.polish_and_suggest(text)
if content:
return f"✅ {msg}", content
return f"❌ {msg}", ""
polish_file_input.change(fn=on_polish_file_upload, inputs=[polish_file_input], outputs=[polish_input])
polish_btn.click(fn=on_polish, inputs=[polish_input], outputs=[polish_status, polish_output])
polish_suggest_btn.click(fn=on_polish_suggest, inputs=[polish_input], outputs=[polish_status, polish_output])
# ==================== Tab 5: Xuất file ====================
with gr.Tab(t("tabs.export")):
gr.Markdown(f"### {t('export.header')}")
export_project_choices = list_project_titles()
export_project_selector = gr.Dropdown(
choices=export_project_choices,
label=t("projects.select_project"),
interactive=True
)
refresh_export_btn = gr.Button(t("projects.refresh_btn"), size="sm")
export_format = gr.Radio(
choices=[
t("create.export_format_word"),
t("create.export_format_txt"),
t("create.export_format_md"),
t("create.export_format_html")
],
value=t("create.export_format_txt"),
label=t("projects.export_format"),
interactive=True
)
export_btn = gr.Button(t("projects.export_btn"), variant="primary", size="lg")
export_status = gr.Textbox(label=t("projects.export_status"), interactive=False)
export_download = gr.File(label=t("projects.download_file"), interactive=False)
def on_refresh_export():
titles = list_project_titles()
return gr.update(choices=titles, value=None)
def on_export(project_title, format_type):
if not project_title:
return f"❌ {t('projects.select_project_first')}", None
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"❌ {t('projects.project_not_found')}", None
project_id = project_data.get("id")
project, msg = ProjectManager.load_project(project_id)
if not project:
return f"❌ {msg}", None
full_text = f"# {project.title}\n\n"
for ch in project.chapters:
if ch.content:
full_text += f"## Chương {ch.num}: {ch.title}\n\n"
full_text += ch.content + "\n\n"
if len(full_text.strip()) < 50:
return f"❌ {t('ui.no_content_export')}", None
# Map format
format_map = {
t("create.export_format_word"): "docx",
t("create.export_format_txt"): "txt",
t("create.export_format_md"): "md",
t("create.export_format_html"): "html"
}
fmt = format_map.get(format_type, "txt")
if fmt == "docx":
filepath, exp_msg = export_to_docx(full_text, project.title)
elif fmt == "txt":
filepath, exp_msg = export_to_txt(full_text, project.title)
elif fmt == "md":
filepath, exp_msg = export_to_markdown(full_text, project.title)
elif fmt == "html":
filepath, exp_msg = export_to_html(full_text, project.title)
else:
return f"❌ {t('ui.unsupported_format', format=fmt)}", None
if filepath:
return f"✅ {exp_msg}", filepath
return f"❌ {exp_msg}", None
except Exception as e:
logger.error(f"Export failed: {e}", exc_info=True)
return f"❌ {t('ui.export_failed', error=str(e))}", None
refresh_export_btn.click(fn=on_refresh_export, outputs=[export_project_selector])
export_btn.click(fn=on_export, inputs=[export_project_selector, export_format], outputs=[export_status, export_download])
# ==================== Tab 6: Quản lý dự án ====================
with gr.Tab(t("tabs.projects")):
gr.Markdown(f"### {t('projects.header')}")
projects_table = gr.Dataframe(
headers=["ID", t("ui.col_project_name"), t("ui.col_type"), t("ui.col_created_at"), t("ui.col_chapters")],
interactive=False
)
with gr.Row():
refresh_projects_btn = gr.Button(t("projects.refresh_btn"))
delete_project_btn = gr.Button(t("projects.delete_btn"), variant="stop")
gr.Markdown(f"#### {t('projects.delete_header')}")
delete_project_selector = gr.Dropdown(
choices=list_project_titles(),
label=t("projects.delete_select_project"),
interactive=True
)
project_manage_status = gr.Textbox(label=t("projects.status_label"), interactive=False)
def on_refresh_projects():
try:
projects = ProjectManager.list_projects()
table_data = []
for p in projects:
table_data.append([
p.get("id", ""),
p.get("title", ""),
p.get("genre", ""),
p.get("created_at", "")[:10] if p.get("created_at") else "",
f"{p.get('completed_chapters', 0)}/{p.get('chapter_count', 0)}"
])
titles = [p.get("title", "") for p in projects]
return table_data, gr.update(choices=titles, value=None)
except Exception as e:
logger.error(f"Refresh projects failed: {e}")
return [], gr.update()
def on_delete_project(project_title):
if not project_title or not project_title.strip():
return f"❌ {t('projects.select_project_first')}", gr.update(), gr.update()
try:
project_data = ProjectManager.get_project_by_title(project_title)
if not project_data:
return f"❌ {t('projects.project_not_found')}", gr.update(), gr.update()
project_id = project_data.get("id")
success, msg = ProjectManager.delete_project(project_id)
if success:
new_table, new_dropdown = on_refresh_projects()
return f"✅ {t('projects.delete_success')}", new_table, new_dropdown
return f"❌ {t('projects.delete_failed')}: {msg}", gr.update(), gr.update()
except Exception as e:
return f"❌ {str(e)}", gr.update(), gr.update()
refresh_projects_btn.click(fn=on_refresh_projects, outputs=[projects_table, delete_project_selector])
delete_project_btn.click(
fn=on_delete_project,
inputs=[delete_project_selector],
outputs=[project_manage_status, projects_table, delete_project_selector]
)
# ==================== Tab 7: Cài đặt hệ thống ====================
with gr.Tab(t("tabs.settings")):
gr.Markdown(f"### {t('settings.header')}")
with gr.Tabs():
# Sub-tab: Quản lý giao diện API
with gr.Tab(t("settings.tab_backends")):
gr.Markdown(f"#### {t('settings.backends_header')}")
backends_display = gr.Markdown("...")
def get_backends_display():
try:
result = ConfigAPIManager.list_backends()
if result["success"] and result["data"]:
lines = [f"### 📋 {len(result['data'])} backend(s)\n"]
for b in result["data"]:
enabled = "✅" if b.get("enabled") else "❌"
default = " ⭐" if b.get("is_default") else ""
lines.append(f"{enabled} **{b['name']}**{default}")
lines.append(f" - Model: `{b.get('model', '')}`")
lines.append(f" - URL: `{b.get('base_url', '')[:50]}...`")
lines.append(f" - Timeout: {b.get('timeout', 30)}s")
lines.append("")
return "\n".join(lines)
return t("app.no_backends_warning")
except Exception as e:
return f"❌ {str(e)}"
gr.Markdown("---")
gr.Markdown(f"#### {t('settings.add_backend_header')}")
provider_names = [API_PROVIDERS[k]["name"] for k in API_PROVIDERS]
with gr.Row():
api_provider_dropdown = gr.Dropdown(
choices=provider_names,
label=t("settings.provider_label"),
info=t("settings.provider_info"),
interactive=True
)
with gr.Row():
api_name_input = gr.Textbox(label=t("settings.backend_name"), placeholder=t("settings.backend_name_placeholder"))
api_type_dropdown = gr.Dropdown(
choices=ConfigAPIManager.get_backend_types(),
value="openai", label=t("settings.backend_type"), interactive=True
)
with gr.Row():
api_url_input = gr.Textbox(label=t("settings.base_url"), placeholder=t("settings.base_url_placeholder"))
api_key_input = gr.Textbox(label=t("settings.api_key"), placeholder=t("settings.api_key_placeholder"), type="password")
with gr.Row():
api_model_input = gr.Textbox(label=t("settings.model_name"), placeholder=t("settings.model_name_placeholder"))
api_timeout_input = gr.Slider(minimum=5, maximum=600, value=120, step=5, label=t("settings.timeout"))
with gr.Row():
api_save_btn = gr.Button(t("settings.add_btn"), variant="primary")
api_refresh_btn = gr.Button(t("settings.refresh_list"), size="sm")
api_status = gr.Textbox(label=t("settings.operation_result"), interactive=False)
gr.Markdown("---")
gr.Markdown(f"#### {t('settings.test_manage_header')}")
with gr.Row():
test_name_input = gr.Textbox(label=t("settings.test_backend_name"), placeholder=t("settings.test_backend_placeholder"))
api_test_btn = gr.Button(t("settings.test_btn"), variant="secondary")
with gr.Row():
delete_name_input = gr.Textbox(label=t("settings.delete_backend_name"), placeholder=t("settings.delete_backend_placeholder"))
api_delete_btn = gr.Button(t("settings.delete_btn"), variant="stop")
test_result = gr.Textbox(label=t("settings.test_result"), interactive=False)
def on_provider_select(provider_name):
for key, info in API_PROVIDERS.items():
if info["name"] == provider_name:
return info.get("base_url", ""), info.get("default_model", ""), provider_name
return "", "", provider_name
def on_api_test(name):
result = ConfigAPIManager.test_backend(name)
return result["message"]
def on_api_save(name, btype, url, key, model, timeout):
result = ConfigAPIManager.add_backend(name, btype, url, key, model, int(timeout))
reinit_api_client()
app_state.generator = None
return result["message"], get_backends_display()
def on_api_delete(name):
result = ConfigAPIManager.delete_backend(name)
reinit_api_client()
app_state.generator = None
return result["message"], get_backends_display()
api_provider_dropdown.change(
fn=on_provider_select,
inputs=[api_provider_dropdown],
outputs=[api_url_input, api_model_input, api_name_input]
)
api_save_btn.click(
fn=on_api_save,
inputs=[api_name_input, api_type_dropdown, api_url_input, api_key_input, api_model_input, api_timeout_input],
outputs=[api_status, backends_display]
)
api_test_btn.click(fn=on_api_test, inputs=[test_name_input], outputs=[test_result])
api_delete_btn.click(fn=on_api_delete, inputs=[delete_name_input], outputs=[api_status, backends_display])
api_refresh_btn.click(fn=lambda: get_backends_display(), outputs=[backends_display])
# Sub-tab: Tham số sinh
with gr.Tab(t("settings.tab_params")):
gr.Markdown(f"#### {t('settings.params_header')}")
config = get_config()
gen_config = config.generation
param_temperature = gr.Slider(minimum=0.1, maximum=2.0, value=gen_config.temperature, step=0.1, label=t("settings.temperature_label"), info=t("settings.temperature_info"))
param_top_p = gr.Slider(minimum=0.1, maximum=1.0, value=gen_config.top_p, step=0.05, label="Top P", info=t("settings.top_p_info"))
param_max_tokens = gr.Slider(minimum=100, maximum=100000, value=gen_config.max_tokens, step=100, label="Max Tokens", info=t("settings.max_tokens_info"))
param_chapter_words = gr.Slider(minimum=500, maximum=65536, value=gen_config.chapter_target_words, step=500, label=t("settings.chapter_target_words"))
save_params_btn = gr.Button(t("settings.save_params_btn"), variant="primary")
params_status = gr.Textbox(label=t("settings.save_status"), interactive=False)
def on_save_params(temp, top_p, max_tokens, chapter_words):
cfg = get_config()
success, msg = cfg.update_generation_config(
temperature=temp, top_p=top_p,
max_tokens=int(max_tokens),
chapter_target_words=int(chapter_words)
)
if success:
app_state.generator = None
return f"✅ {msg}"
return f"❌ {msg}"
save_params_btn.click(
fn=on_save_params,
inputs=[param_temperature, param_top_p, param_max_tokens, param_chapter_words],
outputs=[params_status]
)
# Sub-tab: Bộ nhớ đệm
with gr.Tab(t("settings.tab_cache")):
gr.Markdown(f"#### {t('settings.cache_header')}")
cache_info_display = gr.Markdown(t("ui.no_cache"))
def get_cache_info():
try:
api_client = get_api_client()
stats = api_client.get_cache_stats()
gen_caches = list_generation_caches()
gen_size_val = get_cache_size()
return f"""### Thống kê bộ nhớ đệm
- **API Cache**: {stats['total_entries']}/{stats['max_size']} ({stats['usage_rate']:.1f}%)
- **Generation Cache**: {len(gen_caches)} files ({gen_size_val / 1024:.1f} KB)"""
except Exception as e:
return f"❌ {str(e)}"
with gr.Row():
refresh_cache_btn = gr.Button(t("settings.refresh_cache"), size="sm")
clear_all_cache_btn = gr.Button(t("settings.clear_all"), variant="stop")
cache_op_status = gr.Textbox(label=t("settings.cache_op_status"), interactive=False)
def on_clear_all_cache():
try:
api_client = get_api_client()
api_client.clear_cache()
clear_generation_cache()
return "✅ Đã xóa tất cả bộ nhớ đệm", get_cache_info()
except Exception as e:
return f"❌ {str(e)}", get_cache_info()
refresh_cache_btn.click(fn=get_cache_info, outputs=[cache_info_display])
clear_all_cache_btn.click(fn=on_clear_all_cache, outputs=[cache_op_status, cache_info_display])
# Sub-tab: Quản lý thể loại
with gr.Tab(t("settings.tab_genre")):
gr.Markdown(f"#### {t('settings.genre_desc')}")
genre_select = gr.Dropdown(
choices=GenreManager.get_genre_names(),
label=t("settings.genre_select"),
interactive=True
)
genre_name_input = gr.Textbox(label=t("settings.genre_name"), placeholder=t("settings.genre_name_placeholder"))
genre_desc_input = gr.Textbox(label=t("settings.genre_description"), placeholder=t("settings.genre_description_placeholder"), lines=3)
with gr.Row():
genre_add_btn = gr.Button(t("settings.genre_add_btn"), variant="primary")
genre_update_btn = gr.Button(t("settings.genre_update_btn"), variant="secondary")
genre_delete_btn = gr.Button(t("settings.genre_delete_btn"), variant="stop")
genre_op_status = gr.Textbox(label=t("settings.genre_op_status"), interactive=False)
def on_genre_select(name):
if name:
desc = GenreManager.get_genre_description(name)
return name, desc or ""
return "", ""
def on_genre_add(name, desc):
if not name.strip():
return t("settings.genre_err_name_empty"), gr.update()
success = GenreManager.add_genre(name.strip(), desc.strip())
if success:
return t("settings.genre_add_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_exists"), gr.update()
def on_genre_update(old_name, new_name, desc):
if not old_name:
return t("settings.genre_err_none_selected"), gr.update()
success = GenreManager.update_genre(old_name, new_name.strip(), desc.strip())
if success:
return t("settings.genre_update_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_update"), gr.update()
def on_genre_delete(name):
if not name:
return t("settings.genre_err_none_selected"), gr.update()
success = GenreManager.delete_genre(name)
if success:
return t("settings.genre_delete_success"), gr.update(choices=GenreManager.get_genre_names())
return t("settings.genre_err_delete"), gr.update()
genre_select.change(fn=on_genre_select, inputs=[genre_select], outputs=[genre_name_input, genre_desc_input])
genre_add_btn.click(fn=on_genre_add, inputs=[genre_name_input, genre_desc_input], outputs=[genre_op_status, genre_select])
genre_update_btn.click(fn=on_genre_update, inputs=[genre_select, genre_name_input, genre_desc_input], outputs=[genre_op_status, genre_select])
genre_delete_btn.click(fn=on_genre_delete, inputs=[genre_select], outputs=[genre_op_status, genre_select])
# Sub-tab: Quản lý chủ đề con
with gr.Tab(t("settings.tab_sub_genre")):
gr.Markdown(f"#### {t('settings.sub_genre_desc')}")
sub_genre_select = gr.Dropdown(
choices=SubGenreManager.get_sub_genre_names(),
label=t("settings.sub_genre_select"),
interactive=True
)
sub_genre_name_input = gr.Textbox(label=t("settings.sub_genre_name"), placeholder=t("settings.sub_genre_name_placeholder"))
sub_genre_desc_input = gr.Textbox(label=t("settings.sub_genre_description"), placeholder=t("settings.sub_genre_description_placeholder"), lines=3)
with gr.Row():
sub_genre_add_btn = gr.Button(t("settings.sub_genre_add_btn"), variant="primary")
sub_genre_update_btn = gr.Button(t("settings.sub_genre_update_btn"), variant="secondary")
sub_genre_delete_btn = gr.Button(t("settings.sub_genre_delete_btn"), variant="stop")
sub_genre_op_status = gr.Textbox(label=t("settings.sub_genre_op_status"), interactive=False)
def on_sub_genre_select(name):
if name:
desc = SubGenreManager.get_sub_genre_description(name)
return name, desc or ""
return "", ""
def on_sub_genre_add(name, desc):
if not name.strip():
return t("settings.sub_genre_err_name_empty"), gr.update()
success = SubGenreManager.add_sub_genre(name.strip(), desc.strip())
if success:
return t("settings.sub_genre_add_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_exists"), gr.update()
def on_sub_genre_update(old_name, new_name, desc):
if not old_name:
return t("settings.sub_genre_err_none_selected"), gr.update()
success = SubGenreManager.update_sub_genre(old_name, new_name.strip(), desc.strip())
if success:
return t("settings.sub_genre_update_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_update"), gr.update()
def on_sub_genre_delete(name):
if not name:
return t("settings.sub_genre_err_none_selected"), gr.update()
success = SubGenreManager.delete_sub_genre(name)
if success:
return t("settings.sub_genre_delete_success"), gr.update(choices=SubGenreManager.get_sub_genre_names())
return t("settings.sub_genre_err_delete"), gr.update()
sub_genre_select.change(fn=on_sub_genre_select, inputs=[sub_genre_select], outputs=[sub_genre_name_input, sub_genre_desc_input])
sub_genre_add_btn.click(fn=on_sub_genre_add, inputs=[sub_genre_name_input, sub_genre_desc_input], outputs=[sub_genre_op_status, sub_genre_select])
sub_genre_update_btn.click(fn=on_sub_genre_update, inputs=[sub_genre_select, sub_genre_name_input, sub_genre_desc_input], outputs=[sub_genre_op_status, sub_genre_select])
sub_genre_delete_btn.click(fn=on_sub_genre_delete, inputs=[sub_genre_select], outputs=[sub_genre_op_status, sub_genre_select])
# Footer
gr.Markdown("""
AI Novel Generator v4.5.0
Bản quyền © 2026 Công ty TNHH Công nghệ An ninh mạng Huyễn Thành Tân Cương
Made with ❤️ by Huyễn Thành
""")
return app
# ==================== Khởi động ứng dụng ====================
def main():
"""Khởi động ứng dụng"""
logger.info(t("app.startup_log"))
# Tạo UI
app = create_main_ui()
# Tải CSS
custom_css = ""
css_path = Path("custom.css")
if css_path.exists():
with open(css_path, 'r', encoding='utf-8') as f:
custom_css = f.read()
# Khởi động
logger.info(t("app.gradio_start", port=WEB_PORT))
app.launch(
server_name=WEB_HOST,
server_port=WEB_PORT,
share=WEB_SHARE,
show_error=True
)
if __name__ == "__main__":
main()