""" 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()