refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
import gradio as gr
|
||||
from datetime import datetime
|
||||
from locales.i18n import t
|
||||
from services.project_manager import ProjectManager, list_project_titles
|
||||
from services.novel_generator import Chapter
|
||||
from core.state import app_state
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_continue_tab():
|
||||
with gr.Tab(t("tabs.continue_tab")):
|
||||
gr.Markdown(f"### {t('continue_tab.header')}")
|
||||
|
||||
with gr.Accordion("📂 1. Quản lý dự án", open=True):
|
||||
with gr.Row():
|
||||
with gr.Column(scale=4):
|
||||
project_choices = list_project_titles()
|
||||
continue_project_selector = gr.Dropdown(
|
||||
choices=project_choices,
|
||||
label=t("continue_tab.select_project"),
|
||||
interactive=True
|
||||
)
|
||||
with gr.Column(scale=1, min_width=100):
|
||||
refresh_continue_btn = gr.Button(t("continue_tab.load_btn"), size="lg")
|
||||
|
||||
with gr.Accordion("⚙️ 2. Thông tin & Cài đặt", open=False):
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
continue_project_info = gr.Markdown(t("continue_tab.no_project_loaded"))
|
||||
with gr.Column(scale=2):
|
||||
continue_outline_display = gr.Textbox(
|
||||
label=t("continue_tab.outline_label"),
|
||||
interactive=False, lines=6, max_lines=12,
|
||||
value="Chưa tải dự án..."
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
memory_type = gr.Radio(label="Cách nhớ ngữ cảnh", choices=["Toàn văn", "Tóm tắt"], value="Toàn văn")
|
||||
with gr.Column(scale=1):
|
||||
memory_chapters = gr.Number(label="Số chương ghi nhớ", value=3, minimum=1, maximum=20, step=1)
|
||||
|
||||
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)
|
||||
|
||||
with gr.Accordion("🚀 3. Sáng tác tiếp", open=False):
|
||||
with gr.Row():
|
||||
continue_chapter_num = gr.Number(label="Chương số", value=1, minimum=1, scale=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="Nội dung/Dàn ý chương này",
|
||||
lines=3, placeholder="Nhập mô tả nội dung chương..."
|
||||
)
|
||||
with gr.Row():
|
||||
continue_generate_btn = gr.Button(t("continue_tab.continue_gen_btn"), variant="primary", size="lg", scale=1)
|
||||
continue_auto_btn = gr.Button("Tự động viết nốt các chương còn lại", variant="primary", size="lg", scale=2)
|
||||
continue_stop_btn = gr.Button("Dừng tự động", variant="stop", size="lg", scale=1)
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
continue_status = gr.Textbox(label=t("continue_tab.gen_status"), interactive=False, lines=15)
|
||||
with gr.Column(scale=2):
|
||||
continue_chapter_selector = gr.Dropdown(label="Danh sách chương đã tạo", choices=[], interactive=True)
|
||||
continue_content_display = gr.Textbox(label="Nội dung chương", lines=15, interactive=False)
|
||||
|
||||
def on_refresh_continue(current_title):
|
||||
titles = list_project_titles()
|
||||
if current_title in titles:
|
||||
info, next_ch, outline_text, chapter_choices = on_continue_project_select(current_title)
|
||||
return gr.update(choices=titles, value=current_title), info, next_ch, outline_text, gr.update(choices=chapter_choices, value=chapter_choices[-1] if chapter_choices else None)
|
||||
else:
|
||||
return gr.update(choices=titles, value=None), t("continue_tab.no_project_loaded"), 1, "Chưa tải dự án...", gr.update(choices=[], value=None)
|
||||
|
||||
def on_continue_project_select(project_title):
|
||||
if not project_title:
|
||||
return t("continue_tab.no_project_loaded"), 1, "Chưa tải dự án...", []
|
||||
try:
|
||||
project_data = ProjectManager.get_project_by_title(project_title)
|
||||
if not project_data:
|
||||
return f"❌ {t('continue_tab.project_not_found')}", 1, "Chưa tải dự án..."
|
||||
|
||||
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()
|
||||
total_chapters = len(project.chapters)
|
||||
next_ch = completed + 1
|
||||
percent = f"{(completed / total_chapters * 100):.1f}" if total_chapters > 0 else "0.0"
|
||||
|
||||
# Handle None values safely
|
||||
char_str = project.character_setting or ""
|
||||
world_str = project.world_setting or ""
|
||||
plot_str = project.plot_idea or ""
|
||||
|
||||
char_fmt = char_str[:100] + "..." if len(char_str) > 100 else char_str
|
||||
world_fmt = world_str[:100] + "..." if len(world_str) > 100 else world_str
|
||||
plot_fmt = plot_str[:100] + "..." if len(plot_str) > 100 else plot_str
|
||||
|
||||
# Logic fallback
|
||||
info_template = t("continue_tab.info_template")
|
||||
if isinstance(info_template, str) and '{' in info_template:
|
||||
info = info_template.format(
|
||||
title=project.title,
|
||||
genre=project.genre,
|
||||
completed=completed,
|
||||
total=total_chapters,
|
||||
words=project.get_total_words(),
|
||||
percent=percent,
|
||||
char=char_fmt,
|
||||
world=world_fmt,
|
||||
plot=plot_fmt
|
||||
)
|
||||
else:
|
||||
info = f"### 📖 {project.title}\n**Thể loại**: {project.genre}\n**Tiến độ**: {percent}% ({completed}/{total_chapters} chương)\n**Tổng số từ**: {project.get_total_words()}\n💡 Chương tiếp theo: {next_ch}"
|
||||
|
||||
# Format the outline list
|
||||
outline_lines = []
|
||||
for ch in project.chapters:
|
||||
status = "✅" if getattr(ch, 'content', None) else "⬜"
|
||||
outline_lines.append(f"{status} Chương {ch.num}: {ch.title} - {ch.desc}")
|
||||
outline_text = "\n".join(outline_lines)
|
||||
if not outline_text:
|
||||
outline_text = "Dự án này chưa có dàn ý chi tiết."
|
||||
|
||||
# Generate chapter choices
|
||||
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
|
||||
|
||||
return info, next_ch, outline_text, chapter_choices
|
||||
return f"❌ {msg}", 1, "Chưa tải dự án...", []
|
||||
except Exception as e:
|
||||
return f"❌ {str(e)}", 1, "Lỗi khi tải dàn ý", []
|
||||
|
||||
def on_continue_chapter_select(chapter_title):
|
||||
if not chapter_title or not app_state.current_project:
|
||||
return ""
|
||||
for ch in app_state.current_project.chapters:
|
||||
if f"Chương {ch.num}: {ch.title}" == chapter_title:
|
||||
return ch.content or ""
|
||||
return ""
|
||||
|
||||
def on_continue_generate(project_title, ch_num, ch_title, ch_desc, target_words, custom_prompt, mem_type, mem_chaps):
|
||||
yield "⏳ Đang chuẩn bị dữ liệu...", "", gr.update(interactive=False), gr.update(), gr.update()
|
||||
if not app_state.current_project:
|
||||
yield f"❌ {t('continue_tab.no_project_selected')}", "", gr.update(interactive=True), gr.update(), gr.update()
|
||||
return
|
||||
|
||||
gen = app_state.get_generator()
|
||||
project = app_state.current_project
|
||||
|
||||
# Check if the requested chapter exists in the outline
|
||||
if not any(int(ch.num) == int(ch_num) for ch in project.chapters):
|
||||
yield f"❌ Lỗi: Chương {int(ch_num)} không có trong dàn ý. Tab này chỉ hỗ trợ hoàn thành nốt nội dung dàn ý đã có, không viết thêm chương mới.", "", gr.update(interactive=True), gr.update(), gr.update()
|
||||
return
|
||||
|
||||
# Get completed past chapters mapped linearly
|
||||
all_past_chapters = [ch for ch in project.chapters if ch.num < int(ch_num) and getattr(ch, 'content', None)]
|
||||
mem_ch = int(mem_chaps) if mem_chaps else 3
|
||||
past_chapters = all_past_chapters[-mem_ch:] if len(all_past_chapters) > mem_ch else all_past_chapters
|
||||
|
||||
prev_content = ""
|
||||
context_summary = ""
|
||||
|
||||
if mem_type == "Toàn văn":
|
||||
prev_texts = [c.content for c in past_chapters]
|
||||
prev_content = "\n\n".join(prev_texts)
|
||||
prev_content = prev_content[-4000:]
|
||||
else:
|
||||
summaries = []
|
||||
for c in past_chapters:
|
||||
if not hasattr(c, 'summary') or not c.summary:
|
||||
yield f"⏳ Đang tạo tóm tắt ngữ cảnh cho chương {c.num}...", ""
|
||||
summ, _ = gen.generate_chapter_summary(c.content, c.title)
|
||||
c.summary = summ
|
||||
if c.summary:
|
||||
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
|
||||
|
||||
if summaries:
|
||||
context_summary = "\n".join(summaries)
|
||||
|
||||
if all_past_chapters:
|
||||
prev_content = all_past_chapters[-1].content[-1500:]
|
||||
|
||||
yield f"⏳ Đang sinh Chương {int(ch_num)}...", "", gr.update(interactive=False), gr.update(), gr.update()
|
||||
|
||||
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, context_summary=context_summary, 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)
|
||||
|
||||
# Format the outline list
|
||||
outline_lines = []
|
||||
for ch in project.chapters:
|
||||
status = "✅" if getattr(ch, 'content', None) else "⬜"
|
||||
outline_lines.append(f"{status} Chương {ch.num}: {ch.title} - {ch.desc}")
|
||||
outline_text = "\n".join(outline_lines)
|
||||
|
||||
# Update choices
|
||||
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
|
||||
selected_choice = f"Chương {int(ch_num)}: {ch_title}"
|
||||
|
||||
yield f"✅ Chương {int(ch_num)} đã sinh ({len(content)} từ)", content, gr.update(interactive=True), gr.update(value=outline_text), gr.update(choices=chapter_choices, value=selected_choice)
|
||||
else:
|
||||
yield f"❌ {msg}", "", gr.update(interactive=True), gr.update(), gr.update()
|
||||
|
||||
def on_continue_auto_generate(project_title, target_words, custom_prompt, mem_type, mem_chaps, progress=gr.Progress()):
|
||||
yield "⏳ Đang chuẩn bị dữ liệu...", "", gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
|
||||
if not app_state.current_project:
|
||||
yield f"❌ {t('continue_tab.no_project_selected')}", "", gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
|
||||
return
|
||||
|
||||
gen = app_state.get_generator()
|
||||
project = app_state.current_project
|
||||
app_state.is_generating = True
|
||||
app_state.stop_requested = False
|
||||
|
||||
blank_chapters = [ch for ch in project.chapters if not getattr(ch, 'content', None)]
|
||||
if not blank_chapters:
|
||||
yield "✅ Đã viết xong tất cả các chương trong dàn ý!", "", gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
|
||||
return
|
||||
|
||||
results = [f"📋 Phát hiện {len(blank_chapters)} chương chưa viết. Bắt đầu tự động tạo..."]
|
||||
yield "\n".join(results), "", gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
|
||||
|
||||
last_content = ""
|
||||
|
||||
for i, ch in enumerate(blank_chapters):
|
||||
if app_state.stop_requested:
|
||||
results.append("\n⚠️ Đã dừng sinh tự động!")
|
||||
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
|
||||
break
|
||||
|
||||
results.append(f"\n✍️ Đang sinh Chương {ch.num}: {ch.title}...")
|
||||
progress((i + 1) / len(blank_chapters))
|
||||
yield "\n".join(results), last_content, gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update()
|
||||
|
||||
all_past_chapters = [past for past in project.chapters if past.num < ch.num and getattr(past, 'content', None)]
|
||||
mem_ch = int(mem_chaps) if mem_chaps else 3
|
||||
past_chapters = all_past_chapters[-mem_ch:] if len(all_past_chapters) > mem_ch else all_past_chapters
|
||||
|
||||
prev_content = ""
|
||||
context_summary = ""
|
||||
|
||||
if mem_type == "Toàn văn":
|
||||
prev_texts = [c.content for c in past_chapters]
|
||||
prev_content = "\n\n".join(prev_texts)
|
||||
prev_content = prev_content[-4000:]
|
||||
else:
|
||||
summaries = []
|
||||
for c in past_chapters:
|
||||
if not hasattr(c, 'summary') or not c.summary:
|
||||
summ, _ = gen.generate_chapter_summary(c.content, c.title)
|
||||
c.summary = summ
|
||||
if c.summary:
|
||||
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
|
||||
|
||||
if summaries:
|
||||
context_summary = "\n".join(summaries)
|
||||
|
||||
if all_past_chapters:
|
||||
prev_content = all_past_chapters[-1].content[-1500:]
|
||||
|
||||
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, context_summary=context_summary, custom_prompt=custom_prompt
|
||||
)
|
||||
|
||||
if content:
|
||||
ch.content = content
|
||||
ch.word_count = len(content)
|
||||
ch.generated_at = datetime.now().isoformat()
|
||||
ProjectManager.save_project(project)
|
||||
last_content = content
|
||||
|
||||
# Format the outline list
|
||||
outline_lines = []
|
||||
for pr_ch in project.chapters:
|
||||
status = "✅" if getattr(pr_ch, 'content', None) else "⬜"
|
||||
outline_lines.append(f"{status} Chương {pr_ch.num}: {pr_ch.title} - {pr_ch.desc}")
|
||||
outline_text = "\n".join(outline_lines)
|
||||
|
||||
# Update choices
|
||||
chapter_choices = [f"Chương {ch.num}: {ch.title}" for ch in project.chapters if getattr(ch, 'content', None)]
|
||||
selected_choice = f"Chương {ch.num}: {ch.title}"
|
||||
|
||||
results.append(f"✅ Chương {ch.num} hoàn tất ({len(content)} từ)")
|
||||
yield "\n".join(results), content, gr.update(interactive=False), gr.update(interactive=False), gr.update(value=outline_text), gr.update(choices=chapter_choices, value=selected_choice)
|
||||
else:
|
||||
results.append(f"❌ Lỗi ở chương {ch.num}: {msg}")
|
||||
app_state.stop_requested = True
|
||||
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
|
||||
break
|
||||
|
||||
app_state.is_generating = False
|
||||
results.append("\n🎉 Hoàn thành chuỗi viết tự động!")
|
||||
yield "\n".join(results), last_content, gr.update(interactive=True), gr.update(interactive=True), gr.update(), gr.update()
|
||||
|
||||
def on_continue_stop():
|
||||
app_state.stop_requested = True
|
||||
return "⏸️ Đang dừng..."
|
||||
|
||||
refresh_continue_btn.click(
|
||||
fn=on_refresh_continue,
|
||||
inputs=[continue_project_selector],
|
||||
outputs=[continue_project_selector, continue_project_info, continue_chapter_num, continue_outline_display, continue_chapter_selector]
|
||||
)
|
||||
continue_project_selector.change(
|
||||
fn=on_continue_project_select,
|
||||
inputs=[continue_project_selector],
|
||||
outputs=[continue_project_info, continue_chapter_num, continue_outline_display, continue_chapter_selector]
|
||||
)
|
||||
continue_chapter_selector.change(
|
||||
fn=on_continue_chapter_select,
|
||||
inputs=[continue_chapter_selector],
|
||||
outputs=[continue_content_display]
|
||||
)
|
||||
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, memory_type, memory_chapters],
|
||||
outputs=[continue_status, continue_content_display, continue_generate_btn, continue_outline_display, continue_chapter_selector]
|
||||
)
|
||||
continue_auto_btn.click(
|
||||
fn=on_continue_auto_generate,
|
||||
inputs=[continue_project_selector, continue_target_words, continue_custom_prompt, memory_type, memory_chapters],
|
||||
outputs=[continue_status, continue_content_display, continue_auto_btn, continue_generate_btn, continue_outline_display, continue_chapter_selector],
|
||||
show_progress="full"
|
||||
)
|
||||
continue_stop_btn.click(
|
||||
fn=on_continue_stop,
|
||||
outputs=[continue_status]
|
||||
)
|
||||
@@ -0,0 +1,400 @@
|
||||
import gradio as gr
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from locales.i18n import t
|
||||
from services.novel_generator import OutlineParser
|
||||
from services.project_manager import ProjectManager
|
||||
from services.genre_manager import GenreManager
|
||||
from services.sub_genre_manager import SubGenreManager
|
||||
from core.state import app_state
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_create_tab():
|
||||
"""Xây dựng giao diện Tab Sáng tác từ đầu"""
|
||||
with gr.Tab(t("tabs.create")):
|
||||
gr.Markdown(f"### 🪄 {t('create.header')}")
|
||||
|
||||
with gr.Accordion("📌 1. Thông tin cơ bản", open=True):
|
||||
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
|
||||
)
|
||||
with gr.Group():
|
||||
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_prompt = gr.Textbox(
|
||||
show_label=False,
|
||||
placeholder=t("create.custom_prompt_placeholder"),
|
||||
lines=1, interactive=True, scale=4
|
||||
)
|
||||
suggest_title_btn = gr.Button(t("create.suggest_title_btn"), variant="secondary", size="sm", scale=1)
|
||||
suggest_title_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||
|
||||
with gr.Accordion("🎭 2. Thiết lập chi tiết", open=False):
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
character_input = gr.Textbox(
|
||||
label=t("create.char_setting"),
|
||||
placeholder=t("create.char_setting_placeholder"),
|
||||
lines=5, interactive=False
|
||||
)
|
||||
with gr.Group():
|
||||
with gr.Row():
|
||||
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)
|
||||
with gr.Row():
|
||||
suggest_char_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
|
||||
suggest_char_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
||||
suggest_char_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||
|
||||
with gr.Column():
|
||||
world_input = gr.Textbox(
|
||||
label=t("create.world_setting"),
|
||||
placeholder=t("create.world_setting_placeholder"),
|
||||
lines=5, interactive=False
|
||||
)
|
||||
with gr.Group():
|
||||
with gr.Row():
|
||||
suggest_world_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
|
||||
suggest_world_btn = gr.Button(t("create.suggest_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
||||
suggest_world_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||
|
||||
plot_input = gr.Textbox(
|
||||
label=t("create.plot_idea"),
|
||||
placeholder=t("create.plot_idea_placeholder"),
|
||||
lines=3, interactive=False
|
||||
)
|
||||
with gr.Group():
|
||||
with gr.Row():
|
||||
suggest_plot_prompt = gr.Textbox(show_label=False, placeholder=t("create.custom_prompt_placeholder"), lines=1, interactive=False, scale=4)
|
||||
suggest_plot_btn = gr.Button(t("create.suggest_plot_btn"), variant="secondary", size="sm", scale=1, interactive=False)
|
||||
suggest_plot_status = gr.Textbox(show_label=False, interactive=False, visible=False)
|
||||
|
||||
with gr.Accordion("📝 3. Dàn ý truyện", open=False):
|
||||
with gr.Row():
|
||||
total_chapters = gr.Number(
|
||||
label=t("create.chapter_count"), value=20, minimum=1, maximum=200, step=1, scale=1, interactive=False
|
||||
)
|
||||
generate_outline_btn = gr.Button(t("create.gen_outline_btn"), variant="primary", size="lg", scale=3, interactive=False)
|
||||
|
||||
outline_output = gr.Textbox(label=t("create.outline_display"), lines=15, interactive=True)
|
||||
outline_status = gr.Textbox(label=t("create.gen_status"), interactive=False)
|
||||
|
||||
with gr.Accordion("🚀 4. Sáng tác tự động", open=False):
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
memory_type = gr.Radio(label="Cách nhớ ngữ cảnh", choices=["Toàn văn", "Tóm tắt"], value="Toàn văn")
|
||||
with gr.Column(scale=1):
|
||||
memory_chapters = gr.Number(label="Số chương ghi nhớ", value=3, minimum=1, maximum=20, step=1)
|
||||
with gr.Row():
|
||||
auto_generate_btn = gr.Button(t("create.start_gen_btn"), variant="primary", size="lg", interactive=False)
|
||||
stop_btn = gr.Button(t("create.pause_gen_btn"), variant="stop", size="lg", interactive=False)
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
generation_progress = gr.Textbox(label=t("create.gen_status"), lines=15, interactive=False)
|
||||
with gr.Column(scale=2):
|
||||
chapter_selector = gr.Dropdown(label="Danh sách chương đã tạo", choices=[], interactive=True)
|
||||
chapter_content_display = gr.Textbox(label="Nội dung chương", lines=15, interactive=False)
|
||||
|
||||
def on_suggest_title(genre, sub_genres, custom_prompt):
|
||||
yield gr.Radio(visible=False), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
|
||||
try:
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.suggest_title(genre, sub_genres, custom_prompt)
|
||||
if content:
|
||||
try:
|
||||
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"]]
|
||||
yield gr.Radio(choices=choices, visible=True), gr.update(visible=False), gr.update(interactive=True)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse title JSON: {e}")
|
||||
yield gr.Radio(choices=[content, f"Parse error: {str(e)}"], visible=True), gr.update(visible=False), gr.update(interactive=True)
|
||||
return
|
||||
yield gr.Radio(choices=[msg], visible=True), gr.update(visible=False), gr.update(interactive=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield gr.Radio(choices=[f"Lỗi: {str(e)}"], visible=True), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||
|
||||
def on_title_select(selected):
|
||||
if selected and " - " in selected:
|
||||
return selected.split(" - ", 1)[0]
|
||||
return selected
|
||||
|
||||
def on_suggest_char(title, genre, sub_genres, custom_prompt, num_main, num_sub):
|
||||
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
|
||||
try:
|
||||
gen = app_state.get_generator()
|
||||
nm = int(num_main) if num_main else 2
|
||||
ns = int(num_sub) if num_sub else 3
|
||||
content, msg = gen.suggest_content("char", title, genre, sub_genres, "", "", custom_prompt, nm, ns)
|
||||
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||
|
||||
def on_suggest_world(title, genre, sub_genres, custom_prompt):
|
||||
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
|
||||
try:
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.suggest_content("world", title, genre, sub_genres, "", "", custom_prompt)
|
||||
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||
|
||||
def on_suggest_plot(title, genre, sub_genres, char_setting, world_setting, custom_prompt):
|
||||
yield gr.update(), gr.update(value="⏳ Đang gọi AI xử lý... Vui lòng chờ.", visible=True), gr.update(interactive=False)
|
||||
try:
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.suggest_content("plot", title, genre, sub_genres, char_setting, world_setting, custom_prompt)
|
||||
yield gr.update(value=content if content else msg), gr.update(visible=False), gr.update(interactive=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield gr.update(), gr.update(value=f"❌ Lỗi: {str(e)}", visible=True), gr.update(interactive=True)
|
||||
|
||||
def on_generate_outline(title, genre, sub_genres, num_chapters, char_setting, world_setting, plot_idea, progress=gr.Progress()):
|
||||
progress(0.1, desc="Đang gọi AI...")
|
||||
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, mem_type, mem_chaps, 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:
|
||||
yield f"❌ {parse_msg}", gr.update()
|
||||
return
|
||||
|
||||
project, create_msg = ProjectManager.create_project(
|
||||
title, genre, sub_genres or [],
|
||||
char_setting, world_setting, plot_idea
|
||||
)
|
||||
if not project:
|
||||
yield f"❌ {create_msg}", gr.update()
|
||||
return
|
||||
|
||||
project.chapters = chapters
|
||||
app_state.current_project = project
|
||||
app_state.is_generating = True
|
||||
app_state.stop_requested = False
|
||||
|
||||
# Save immediately to ensure chapter titles are stored even if generation is stopped early
|
||||
ProjectManager.save_project(project)
|
||||
|
||||
results = [f"📋 Đã phân tích {len(chapters)} chương", f"💾 {create_msg}"]
|
||||
yield "\n".join(results), gr.update(choices=[], value=None)
|
||||
|
||||
generated_chapters = []
|
||||
|
||||
for i, chapter in enumerate(chapters):
|
||||
if app_state.stop_requested:
|
||||
results.append("\n⚠️ Đã dừng sinh!")
|
||||
yield "\n".join(results), gr.update(choices=generated_chapters)
|
||||
break
|
||||
|
||||
results.append(f"\n✍️ Đang sinh Chương {chapter.num}: {chapter.title}...")
|
||||
progress((i + 1) / len(chapters))
|
||||
yield "\n".join(results), gr.update(choices=generated_chapters)
|
||||
|
||||
mem_ch = int(mem_chaps) if mem_chaps else 3
|
||||
start_idx = max(0, i - mem_ch)
|
||||
past_chapters = chapters[start_idx:i]
|
||||
|
||||
prev_content = ""
|
||||
context_summary = ""
|
||||
|
||||
if mem_type == "Toàn văn":
|
||||
prev_texts = [c.content for c in past_chapters if hasattr(c, 'content') and c.content]
|
||||
prev_content = "\n\n".join(prev_texts)
|
||||
prev_content = prev_content[-4000:]
|
||||
else:
|
||||
summaries = []
|
||||
for c in past_chapters:
|
||||
if not hasattr(c, 'content') or not c.content:
|
||||
continue
|
||||
if not hasattr(c, 'summary') or not c.summary:
|
||||
summ, _ = gen.generate_chapter_summary(c.content, c.title)
|
||||
c.summary = summ
|
||||
if c.summary:
|
||||
summaries.append(f"Chương {c.num} - {c.title}: {c.summary}")
|
||||
|
||||
if summaries:
|
||||
context_summary = "\n".join(summaries)
|
||||
|
||||
if i > 0 and chapters[i-1].content:
|
||||
prev_content = chapters[i-1].content[-1500:]
|
||||
|
||||
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, context_summary=context_summary
|
||||
)
|
||||
|
||||
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)
|
||||
chapter_name = f"Chương {chapter.num}: {chapter.title}"
|
||||
generated_chapters.append(chapter_name)
|
||||
yield "\n".join(results), gr.update(choices=generated_chapters, value=chapter_name)
|
||||
else:
|
||||
results.append(f"❌ Chương {chapter.num}: {msg}")
|
||||
yield "\n".join(results), gr.update(choices=generated_chapters)
|
||||
|
||||
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ừ")
|
||||
yield "\n".join(results), gr.update(choices=generated_chapters)
|
||||
|
||||
def on_chapter_select(chapter_title):
|
||||
if not app_state.current_project or not chapter_title:
|
||||
return ""
|
||||
for ch in app_state.current_project.chapters:
|
||||
if f"Chương {ch.num}: {ch.title}" == chapter_title:
|
||||
return ch.content or ""
|
||||
return ""
|
||||
|
||||
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, suggest_title_status, suggest_title_btn]
|
||||
)
|
||||
suggested_titles_radio.change(
|
||||
fn=on_title_select,
|
||||
inputs=[suggested_titles_radio],
|
||||
outputs=[title_input]
|
||||
)
|
||||
suggest_char_btn.click(
|
||||
fn=on_suggest_char,
|
||||
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_char_prompt, num_main_chars, num_sub_chars],
|
||||
outputs=[character_input, suggest_char_status, suggest_char_btn]
|
||||
)
|
||||
suggest_world_btn.click(
|
||||
fn=on_suggest_world,
|
||||
inputs=[title_input, genre_dropdown, sub_genre_dropdown, suggest_world_prompt],
|
||||
outputs=[world_input, suggest_world_status, suggest_world_btn]
|
||||
)
|
||||
suggest_plot_btn.click(
|
||||
fn=on_suggest_plot,
|
||||
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, suggest_plot_prompt],
|
||||
outputs=[plot_input, suggest_plot_status, suggest_plot_btn]
|
||||
)
|
||||
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],
|
||||
show_progress="full"
|
||||
)
|
||||
auto_generate_btn.click(
|
||||
fn=on_auto_generate,
|
||||
inputs=[title_input, genre_dropdown, sub_genre_dropdown, character_input, world_input, plot_input, outline_output, memory_type, memory_chapters],
|
||||
outputs=[generation_progress, chapter_selector]
|
||||
)
|
||||
chapter_selector.change(
|
||||
fn=on_chapter_select,
|
||||
inputs=[chapter_selector],
|
||||
outputs=[chapter_content_display]
|
||||
)
|
||||
stop_btn.click(fn=on_stop, outputs=[generation_progress])
|
||||
|
||||
# Flow Enforcement Events
|
||||
title_input.change(
|
||||
fn=lambda t: [
|
||||
gr.update(interactive=bool(t)),
|
||||
gr.update(interactive=bool(t)),
|
||||
gr.update(interactive=bool(t))
|
||||
],
|
||||
inputs=[title_input],
|
||||
outputs=[character_input, suggest_char_prompt, suggest_char_btn]
|
||||
)
|
||||
|
||||
character_input.change(
|
||||
fn=lambda c: [
|
||||
gr.update(interactive=bool(c)),
|
||||
gr.update(interactive=bool(c)),
|
||||
gr.update(interactive=bool(c))
|
||||
],
|
||||
inputs=[character_input],
|
||||
outputs=[world_input, suggest_world_prompt, suggest_world_btn]
|
||||
)
|
||||
|
||||
world_input.change(
|
||||
fn=lambda w: [
|
||||
gr.update(interactive=bool(w)),
|
||||
gr.update(interactive=bool(w)),
|
||||
gr.update(interactive=bool(w))
|
||||
],
|
||||
inputs=[world_input],
|
||||
outputs=[plot_input, suggest_plot_prompt, suggest_plot_btn]
|
||||
)
|
||||
|
||||
plot_input.change(
|
||||
fn=lambda p: [
|
||||
gr.update(interactive=bool(p)),
|
||||
gr.update(interactive=bool(p))
|
||||
],
|
||||
inputs=[plot_input],
|
||||
outputs=[total_chapters, generate_outline_btn]
|
||||
)
|
||||
|
||||
outline_output.change(
|
||||
fn=lambda o: [
|
||||
gr.update(interactive=bool(o)),
|
||||
gr.update(interactive=bool(o))
|
||||
],
|
||||
inputs=[outline_output],
|
||||
outputs=[auto_generate_btn, stop_btn]
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import gradio as gr
|
||||
from locales.i18n import t
|
||||
from services.project_manager import ProjectManager, list_project_titles
|
||||
from utils.exporter import export_to_docx, export_to_txt, export_to_markdown, export_to_html
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_export_tab():
|
||||
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])
|
||||
@@ -0,0 +1,61 @@
|
||||
import gradio as gr
|
||||
from locales.i18n import t
|
||||
from utils.file_parser import parse_novel_file
|
||||
from core.state import app_state
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_polish_tab():
|
||||
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, custom_req):
|
||||
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.polish_text(text, custom_requirements=custom_req)
|
||||
if content:
|
||||
yield f"✅ {msg}", content, gr.update(interactive=True)
|
||||
else:
|
||||
yield f"❌ {msg}", gr.update(), gr.update(interactive=True)
|
||||
|
||||
def on_polish_suggest(text, custom_req):
|
||||
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.polish_and_suggest(text, custom_requirements=custom_req)
|
||||
if content:
|
||||
yield f"✅ {msg}", content, gr.update(interactive=True)
|
||||
else:
|
||||
yield f"❌ {msg}", gr.update(), gr.update(interactive=True)
|
||||
|
||||
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, polish_custom_req], outputs=[polish_status, polish_output, polish_btn])
|
||||
polish_suggest_btn.click(fn=on_polish_suggest, inputs=[polish_input, polish_custom_req], outputs=[polish_status, polish_output, polish_suggest_btn])
|
||||
@@ -0,0 +1,69 @@
|
||||
import gradio as gr
|
||||
from locales.i18n import t
|
||||
from services.project_manager import ProjectManager, list_project_titles
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_projects_tab():
|
||||
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]
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
import gradio as gr
|
||||
from locales.i18n import t
|
||||
from services.novel_generator import get_preset_templates
|
||||
from utils.file_parser import parse_novel_file
|
||||
from core.state import app_state
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_rewrite_tab():
|
||||
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):
|
||||
yield "⏳ Đang gọi AI xử lý... Vui lòng chờ.", gr.update(), gr.update(interactive=False)
|
||||
instructions = get_preset_templates().get(style, "")
|
||||
gen = app_state.get_generator()
|
||||
content, msg = gen.rewrite_paragraph(text, instructions)
|
||||
if content:
|
||||
yield f"✅ {msg}", content, gr.update(interactive=True)
|
||||
else:
|
||||
yield f"❌ {msg}", gr.update(), gr.update(interactive=True)
|
||||
|
||||
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, rewrite_btn])
|
||||
@@ -0,0 +1,298 @@
|
||||
import gradio as gr
|
||||
from locales.i18n import t
|
||||
from core.config import get_config, API_PROVIDERS
|
||||
from core.config_api import ConfigAPIManager
|
||||
from services.api_client import get_api_client, reinit_api_client
|
||||
from services.novel_generator import get_cache_size, list_generation_caches, clear_generation_cache
|
||||
from services.genre_manager import GenreManager
|
||||
from services.sub_genre_manager import SubGenreManager
|
||||
from core.state import app_state
|
||||
|
||||
def build_settings_tab():
|
||||
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):
|
||||
yield "⏳ Đang kiểm tra kết nối API... Vui lòng chờ."
|
||||
result = ConfigAPIManager.test_backend(name)
|
||||
yield 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\n- **API Cache**: {stats['total_entries']}/{stats['max_size']} ({stats['usage_rate']:.1f}%)\n- **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])
|
||||
Reference in New Issue
Block a user