refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.

This commit is contained in:
tinix-ai
2026-03-17 14:54:16 +07:00
parent 6699c516cd
commit a40d8ef4a6
28 changed files with 1970 additions and 953 deletions
+400
View File
@@ -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]
)