385 lines
21 KiB
Python
385 lines
21 KiB
Python
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 services.style_manager import StyleManager
|
|
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.Column(scale=1):
|
|
use_reflection_checkbox = gr.Checkbox(label="Bật chế độ Tự kiểm duyệt (Self-Reflection)", value=False, info="AI tự đọc lại và sửa lỗi nháp. Tốn gấp đôi thời gian & Token.")
|
|
|
|
with gr.Row():
|
|
continue_target_words = gr.Number(label=t("rewrite.target_words"), value=3000, minimum=100, maximum=50000, step=100)
|
|
|
|
style_choices = StyleManager.get_style_names()
|
|
continue_style_dropdown = gr.Dropdown(
|
|
choices=style_choices,
|
|
label="Phong cách viết",
|
|
value=style_choices[0] if style_choices else None,
|
|
interactive=True
|
|
)
|
|
|
|
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, allow_custom_value=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, use_reflection, selected_style):
|
|
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()
|
|
if selected_style:
|
|
gen.config.generation.writing_style = selected_style
|
|
|
|
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,
|
|
use_reflection=use_reflection
|
|
)
|
|
|
|
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, use_reflection, selected_style, 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()
|
|
if selected_style:
|
|
gen.config.generation.writing_style = selected_style
|
|
|
|
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,
|
|
use_reflection=use_reflection
|
|
)
|
|
|
|
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, use_reflection_checkbox, continue_style_dropdown],
|
|
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, use_reflection_checkbox, continue_style_dropdown],
|
|
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]
|
|
)
|