1371 lines
52 KiB
Python
1371 lines
52 KiB
Python
"""
|
||
Mô-đun tạo tiểu thuyết - Hỗ trợ tạo dàn ý, tạo chương, viết lại...
|
||
|
||
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 re
|
||
import logging
|
||
import json
|
||
import os
|
||
from typing import List, Dict, Optional, Tuple
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from api_client import get_api_client
|
||
from config import get_config
|
||
from locales.i18n import t
|
||
from database import get_db
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Mẫu cài sẵn - lựa chọn kiểu phong phú (các phím được ánh xạ tới ngôn ngữ)
|
||
def _build_preset_templates():
|
||
"""Build preset templates from locale"""
|
||
keys = [
|
||
"default", "xianxia", "romance", "thriller", "scifi", "wuxia",
|
||
"palace", "military", "historical", "supernatural", "campus",
|
||
"business", "cyberpunk", "fantasy", "horror", "humor", "literary", "adventure"
|
||
]
|
||
return {t(f"templates.name_{k}"): t(f"templates.{k}") for k in keys}
|
||
|
||
def get_preset_templates():
|
||
"""Get preset templates (rebuilt each call to respect active language)"""
|
||
return _build_preset_templates()
|
||
|
||
|
||
@dataclass
|
||
class Chapter:
|
||
"""Cấu trúc dữ liệu chương"""
|
||
num: int
|
||
title: str
|
||
desc: str
|
||
content: str = ""
|
||
word_count: int = 0
|
||
generated_at: Optional[str] = None
|
||
|
||
def to_dict(self) -> Dict:
|
||
"""Chuyển đổi sang từ điển"""
|
||
return {
|
||
"num": self.num,
|
||
"title": self.title,
|
||
"desc": self.desc,
|
||
"content": self.content,
|
||
"word_count": self.word_count,
|
||
"generated_at": self.generated_at
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class NovelProject:
|
||
"""Cấu trúc dữ liệu dự án mới"""
|
||
title: str
|
||
genre: str
|
||
character_setting: str
|
||
world_setting: str
|
||
plot_idea: str
|
||
sub_genres: List[str] = field(default_factory=list)
|
||
id: Optional[str] = None
|
||
chapters: List[Chapter] = field(default_factory=list)
|
||
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||
|
||
def get_completed_count(self) -> int:
|
||
"""Lấy số chương đã hoàn thành"""
|
||
return sum(1 for ch in self.chapters if ch.content.strip())
|
||
|
||
def get_total_words(self) -> int:
|
||
"""Nhận tổng số từ"""
|
||
return sum(ch.word_count for ch in self.chapters)
|
||
|
||
|
||
class OutlineParser:
|
||
"""Trình phân tích cú pháp phác thảo"""
|
||
|
||
@staticmethod
|
||
def parse(outline_text: str) -> Tuple[List[Chapter], str]:
|
||
"""
|
||
Phân tích văn bản dàn ý
|
||
|
||
Returns (Trả về):
|
||
(Danh sách chương, Thông báo lỗi)
|
||
"""
|
||
if not outline_text or not outline_text.strip():
|
||
return [], t("generator.outline_empty")
|
||
|
||
chapters = []
|
||
lines = [line.strip() for line in outline_text.split('\n') if line.strip()]
|
||
|
||
chapter_count = 0
|
||
# Hỗ trợ nhiều định dạng phác thảo phổ biến và thử nhiều biểu thức chính quy
|
||
patterns = [
|
||
r'第\s*(\d+)\s*章[::\s]*([^\-—–]+)[-—–]\s*(.+)',
|
||
r'^(\d+)[\).:\s]+([^\-—–]+)[-—–]\s*(.+)',
|
||
r'第\s*(\d+)\s*章[::]\s*(.+)',
|
||
]
|
||
|
||
for line in lines:
|
||
matched = False
|
||
for pat in patterns:
|
||
match = re.match(pat, line)
|
||
if not match:
|
||
continue
|
||
|
||
# Các nhóm chụp mẫu khác nhau có thể có ý nghĩa khác nhau
|
||
if len(match.groups()) >= 3:
|
||
num = int(match.group(1))
|
||
title = match.group(2).strip()
|
||
desc = match.group(3).strip()
|
||
else:
|
||
# Chỉ có hai nhóm, hãy thử sử dụng nhóm thứ hai để chia thành tiêu đề và mô tả
|
||
num = int(match.group(1))
|
||
rest = match.group(2).strip()
|
||
if '-' in rest or '—' in rest or '–' in rest:
|
||
parts = re.split('[-—–]', rest, maxsplit=1)
|
||
title = parts[0].strip()
|
||
desc = parts[1].strip() if len(parts) > 1 else ''
|
||
else:
|
||
# Không thể tách, coi như tiêu đề, mô tả trống
|
||
title = rest
|
||
desc = ''
|
||
|
||
if not title:
|
||
logger.warning(f"Skip invalid chapter (no title): {line}")
|
||
matched = True
|
||
break
|
||
|
||
# Tiêu đề rõ ràng: nếu nó chứa tiền tố "Chương X:", hãy xóa nó
|
||
if re.match(r'^\s*(?:第\s*\d+\s*章|Chương\s*\d+|Chapter\s*\d+|Hồi\s*\d+|Phần\s*\d+)[::\-\s]*', title, re.IGNORECASE):
|
||
title = re.sub(r'^\s*(?:第\s*\d+\s*章|Chương\s*\d+|Chapter\s*\d+|Hồi\s*\d+|Phần\s*\d+)[::\-\s]*', '', title, flags=re.IGNORECASE).strip()
|
||
chapters.append(Chapter(num=num, title=title, desc=desc))
|
||
chapter_count += 1
|
||
matched = True
|
||
break
|
||
|
||
if not matched:
|
||
# Khả năng chịu lỗi bổ sung: cố gắng phân tích cú pháp theo 'mô tả tiêu đề', được tính tự động
|
||
if '-' in line or '—' in line:
|
||
parts = re.split('[-—–]', line, maxsplit=1)
|
||
title = parts[0].strip()
|
||
desc = parts[1].strip() if len(parts) > 1 else ''
|
||
chapter_count += 1
|
||
chapters.append(Chapter(num=chapter_count, title=title, desc=desc))
|
||
|
||
if not chapters:
|
||
return [], t("generator.outline_parse_failed")
|
||
|
||
# Sắp xếp theo Số chương
|
||
chapters.sort(key=lambda x: x.num)
|
||
|
||
# Kiểm tra tính liên tục của Số chương
|
||
for i, chapter in enumerate(chapters, 1):
|
||
if chapter.num != i:
|
||
logger.warning(f"Chapter number not sequential: expected {i}, got {chapter.num}")
|
||
chapter.num = i
|
||
|
||
logger.info(f"Parsed {len(chapters)} chapters")
|
||
return chapters, t("generator.outline_parse_success", count=len(chapters))
|
||
|
||
@staticmethod
|
||
def format_for_display(chapters: List[Chapter]) -> str:
|
||
"""Định dạng danh sách chương dưới dạng có thể chỉnh sửa Văn bản dàn ý"""
|
||
lines = []
|
||
for ch in chapters:
|
||
lines.append(f"Chapter {ch.num}: {ch.title} - {ch.desc}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
from genre_manager import GenreManager
|
||
from sub_genre_manager import SubGenreManager
|
||
|
||
class NovelGenerator:
|
||
"""máy phát điện mới"""
|
||
|
||
def __init__(self):
|
||
self.config = get_config()
|
||
self.api_client = get_api_client()
|
||
|
||
def generate_outline(
|
||
self,
|
||
title: str,
|
||
genre: str,
|
||
sub_genres: List[str],
|
||
total_chapters: int,
|
||
character_setting: str,
|
||
world_setting: str,
|
||
plot_idea: str
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Tạo dàn ý tiểu thuyết
|
||
|
||
Returns (Trả về):
|
||
(Văn bản dàn ý, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not title or not title.strip():
|
||
return "", t("generator.title_empty")
|
||
if not character_setting or not character_setting.strip():
|
||
return "", t("generator.char_empty")
|
||
if not world_setting or not world_setting.strip():
|
||
return "", t("generator.world_empty")
|
||
if not plot_idea or not plot_idea.strip():
|
||
return "", t("generator.plot_empty")
|
||
|
||
if total_chapters <= 0:
|
||
total_chapters = 20
|
||
|
||
style_desc = self._build_style_description()
|
||
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
style_desc += f"\n\nHướng dẫn viết riêng cho thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
style_desc += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nHãy kết hợp chặt chẽ các đặc điểm của những chủ đề này để làm phong phú cấu trúc cốt truyện."
|
||
|
||
prompt = t("prompts.outline_user",
|
||
genre=genre, title=title,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting,
|
||
plot_idea=plot_idea,
|
||
style_desc=style_desc,
|
||
total_chapters=total_chapters
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.outline_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start generating outline: {title}")
|
||
success, content = self.api_client.generate(messages, use_cache=True)
|
||
|
||
if not success:
|
||
logger.error(f"Outline generation failed: {content}")
|
||
return "", content
|
||
|
||
logger.info("Outline generation success")
|
||
return content, t("generator.outline_gen_success")
|
||
|
||
def suggest_title(self, genre: str, sub_genres: List[str] = None, custom_prompt: str = "") -> Tuple[str, str]:
|
||
"""
|
||
Gợi ý Tiêu đề truyện bằng AI.
|
||
"""
|
||
system_prompt = t("prompts.suggest_system")
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
system_prompt += f"\n\nLưu ý bám sát hướng dẫn viết thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
system_prompt += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nVui lòng xem xét các chủ đề này khi đề xuất."
|
||
|
||
# Nếu người dùng có prompt tùy chỉnh, ưu tiên ghép chung, nếu không dùng mặc định
|
||
if custom_prompt.strip():
|
||
user_prompt = f"{t('prompts.suggest_title_user', genre=genre)}\n\nYêu cầu bổ sung của tác giả:\n{custom_prompt}"
|
||
else:
|
||
user_prompt = t("prompts.suggest_title_user", genre=genre)
|
||
|
||
messages = [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt}
|
||
]
|
||
|
||
logger.info(f"Start generating title suggestions for genre: {genre}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Title suggestion generation failed: {content}")
|
||
return "", content
|
||
|
||
logger.info("Title suggestion generation success")
|
||
return content, t("generator.suggest_success")
|
||
|
||
def suggest_content(
|
||
self,
|
||
suggest_type: str,
|
||
title: str,
|
||
genre: str,
|
||
sub_genres: List[str] = None,
|
||
character_setting: str = "",
|
||
world_setting: str = "",
|
||
custom_prompt: str = "",
|
||
num_main_chars: int = 2,
|
||
num_sub_chars: int = 3
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Gợi ý nội dung bằng AI (Nhân vật, Thế giới, Cốt truyện) với Prompt tùy chỉnh
|
||
"""
|
||
if not title or not title.strip():
|
||
return "", t("generator.title_empty")
|
||
|
||
system_prompt = t("prompts.suggest_system")
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
system_prompt += f"\n\nLưu ý bám sát đặc trưng thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
system_prompt += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nVui lòng lồng ghép các yếu tố này vào gợi ý."
|
||
|
||
base_prompt = ""
|
||
if suggest_type == "char":
|
||
base_prompt = t("prompts.suggest_char_user", title=title, genre=genre, num_main_chars=num_main_chars, num_sub_chars=num_sub_chars)
|
||
logger_msg = "character settings"
|
||
elif suggest_type == "world":
|
||
base_prompt = t("prompts.suggest_world_user", title=title, genre=genre)
|
||
logger_msg = "world settings"
|
||
elif suggest_type == "plot":
|
||
base_prompt = t("prompts.suggest_plot_user",
|
||
title=title,
|
||
genre=genre,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting)
|
||
logger_msg = "plot ideas"
|
||
else:
|
||
return "", "Unknown suggestion type"
|
||
|
||
if custom_prompt.strip():
|
||
user_prompt = f"{base_prompt}\n\nYêu cầu bổ sung của tác giả:\n{custom_prompt}"
|
||
else:
|
||
user_prompt = base_prompt
|
||
|
||
messages = [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt}
|
||
]
|
||
|
||
logger.info(f"Start generating {logger_msg} suggestions for: {title}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Suggestion generation failed: {content}")
|
||
return "", content
|
||
|
||
logger.info(f"Suggestion generation success ({logger_msg})")
|
||
return content, t("generator.suggest_success")
|
||
|
||
def generate_chapter(
|
||
self,
|
||
chapter_num: int,
|
||
chapter_title: str,
|
||
chapter_desc: str,
|
||
novel_title: str,
|
||
character_setting: str,
|
||
world_setting: str,
|
||
plot_idea: str,
|
||
genre: str = "",
|
||
sub_genres: List[str] = None,
|
||
previous_content: str = "",
|
||
context_summary: str = "",
|
||
custom_prompt: str = ""
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Tạo một chương đơn lẻ
|
||
|
||
Args (Tham số):
|
||
chapter_num: Số chương
|
||
chapter_title: Tiêu đề chương
|
||
chapter_desc: Mô tả chương
|
||
novel_title: Tiêu đề tiểu thuyết
|
||
character_setting: Thiết lập nhân vật
|
||
world_setting: Thiết lập thế giới quan
|
||
plot_idea: Cốt truyện chính
|
||
genre: Thể loại truyện
|
||
previous_content: Nội dung phần trước (dùng cho tính liền mạch)
|
||
context_summary: Tóm tắt ngữ cảnh (dùng để tăng cường Context)
|
||
|
||
Returns (Trả về):
|
||
(Nội dung chương, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
style_desc = self._build_style_description()
|
||
|
||
if genre:
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
style_desc += f"\n\nHướng dẫn viết riêng cho thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
style_desc += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nHãy kết hợp chặt chẽ các đặc điểm và yếu tố của những chủ đề này vào nội dung chương truyện."
|
||
|
||
target_words = self.config.generation.chapter_target_words
|
||
|
||
# Lời khuyên để xây dựng sự gắn kết
|
||
continuity_prompt = ""
|
||
if previous_content:
|
||
continuity_prompt = t("prompts.continuity_prompt", previous_content=previous_content[-3000:])
|
||
|
||
context_prompt = ""
|
||
if context_summary:
|
||
context_prompt = t("prompts.context_prompt", context_summary=context_summary)
|
||
|
||
# Lấy thông tin thể loại truyện thông qua class properties (cần get từ CSDL dự án hoặc cấu hình, mượn tạm cách try-except)
|
||
# Vì hàm `generate_chapter` ko truyền `genre`, ta sẽ thêm tham số hoặc ngầm hiểu thông qua plot/world_setting.
|
||
# Tạm thời cứ gán thêm nếu có thể, hoặc yêu cầu truyền thêm `genre` ở hàm gọi. Sẽ cập nhật `system_prompt` chung với outline.
|
||
genre_desc_prompt = ""
|
||
# TODO: Sắp tới cần update file `app.py` chỗ gọi hàm `generate_chapter` để truyền thêm Genre vào.
|
||
|
||
prompt = t("prompts.chapter_user",
|
||
novel_title=novel_title, chapter_num=chapter_num,
|
||
chapter_title=chapter_title, chapter_desc=chapter_desc,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting,
|
||
plot_idea=plot_idea,
|
||
style_desc=style_desc,
|
||
target_words=target_words,
|
||
continuity_prompt=continuity_prompt,
|
||
context_prompt=context_prompt
|
||
)
|
||
|
||
sys_prompt = t("prompts.chapter_system")
|
||
|
||
messages = [
|
||
{"role": "system", "content": sys_prompt},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start generating chapter: {chapter_num} - {chapter_title}")
|
||
if context_summary:
|
||
logger.info(f"Using context enhancement, context length: {len(context_summary)}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Chapter generation failed: {content}")
|
||
return "", content
|
||
|
||
logger.info(f"Chapter generation success: {chapter_num}")
|
||
logger.info(f"Chapter content length: {len(content)}")
|
||
return content, t("generator.gen_success")
|
||
|
||
def generate_chapter_stream(
|
||
self,
|
||
chapter_num: int,
|
||
chapter_title: str,
|
||
chapter_desc: str,
|
||
novel_title: str,
|
||
character_setting: str,
|
||
world_setting: str,
|
||
plot_idea: str,
|
||
genre: str = "",
|
||
sub_genres: List[str] = None,
|
||
previous_content: str = "",
|
||
context_summary: str = "",
|
||
custom_prompt: str = ""
|
||
):
|
||
"""
|
||
Tạo một chương đơn lẻ bằng streaming
|
||
|
||
Yields:
|
||
(Chỉ báo thành công, Chunk văn bản / Thông báo lỗi)
|
||
"""
|
||
style_desc = self._build_style_description()
|
||
|
||
if genre:
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
style_desc += f"\n\nHướng dẫn viết riêng cho thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
style_desc += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nHãy kết hợp chặt chẽ các đặc điểm và yếu tố của những chủ đề này vào nội dung chương truyện."
|
||
|
||
target_words = self.config.generation.chapter_target_words
|
||
|
||
continuity_prompt = ""
|
||
if previous_content:
|
||
continuity_prompt = t("prompts.continuity_prompt", previous_content=previous_content[-3000:])
|
||
|
||
context_prompt = ""
|
||
if context_summary:
|
||
context_prompt = t("prompts.context_prompt", context_summary=context_summary)
|
||
|
||
prompt = t("prompts.chapter_user",
|
||
novel_title=novel_title, chapter_num=chapter_num,
|
||
chapter_title=chapter_title, chapter_desc=chapter_desc,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting,
|
||
plot_idea=plot_idea,
|
||
style_desc=style_desc,
|
||
target_words=target_words,
|
||
continuity_prompt=continuity_prompt,
|
||
context_prompt=context_prompt
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.chapter_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start generating chapter (stream): {chapter_num} - {chapter_title}")
|
||
if context_summary:
|
||
logger.info(f"Using context enhancement, context length: {len(context_summary)}")
|
||
|
||
for success, chunk in self.api_client.generate_stream(messages=messages):
|
||
yield success, chunk
|
||
|
||
def rewrite_paragraph(
|
||
self,
|
||
text: str,
|
||
style_template: str = ""
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Viết lại đoạn văn (có cơ chế thử lại)
|
||
|
||
Returns (Trả về):
|
||
(Văn bản sau khi viết lại, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not text or not text.strip():
|
||
return "", t("generator.text_empty")
|
||
|
||
if len(text) > 20000:
|
||
return "", t("generator.text_too_long_rewrite")
|
||
|
||
templates = get_preset_templates()
|
||
default_key = t("templates.name_default")
|
||
style = style_template or templates.get(default_key, t("templates.default"))
|
||
|
||
prompt = t("prompts.rewrite_user", style=style, text=text)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.rewrite_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start rewriting, original length: {len(text)}, style: {style[:50]}")
|
||
|
||
# Cơ chế thử lại: thử lại khi nội dung quá ngắn
|
||
max_retries = 3
|
||
content = ""
|
||
success_msg = ""
|
||
|
||
for attempt in range(max_retries):
|
||
logger.debug(f"Rewrite attempt {attempt + 1}/{max_retries}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Rewrite failed (attempt {attempt + 1}/{max_retries}): {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", content
|
||
|
||
# Xác minh nội dung nghiêm ngặt
|
||
if not content or not content.strip():
|
||
logger.error(f"Rewrite returned empty (attempt {attempt + 1}/{max_retries})")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_empty_content")
|
||
|
||
# Lọc thông báo trạng thái (danh sách mở rộng)
|
||
status_messages = [
|
||
t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success",
|
||
"OK", "ok", "Success", "SUCCESS",
|
||
|
||
|
||
]
|
||
content_stripped = content.strip()
|
||
if content_stripped in status_messages:
|
||
logger.error(f"API returned status msg instead of content: {content} (attempt {attempt + 1}/{max_retries})")
|
||
logger.error(f"Content length: {len(content)}, content: {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_status_msg")
|
||
|
||
# Kiểm tra độ dài nội dung (nghiêm ngặt hơn)
|
||
if len(content_stripped) < 50:
|
||
logger.warning(f"Rewrite content too short: {len(content)} (attempt {attempt + 1}/{max_retries})")
|
||
logger.warning(f"Content: {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.rewrite_too_short", length=len(content))
|
||
|
||
# Xác minh nội dung đã được thông qua
|
||
logger.info(f"Rewrite done, content length: {len(content)}, attempts: {attempt + 1}")
|
||
logger.debug(f"Content first 200: {content[:200]}")
|
||
return content, t("generator.rewrite_success")
|
||
|
||
# Tất cả các lần thử lại đều thất bại
|
||
logger.error(f"Rewrite failed after {max_retries} attempts")
|
||
return "", t("generator.rewrite_failed_retries", max=max_retries)
|
||
|
||
def generate_summary(self, text: str, max_length: int = 200) -> Tuple[str, str]:
|
||
"""
|
||
Tạo tóm tắt văn bản
|
||
|
||
Returns (Trả về):
|
||
(Tóm tắt, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not text or not text.strip():
|
||
return "", t("generator.text_empty")
|
||
|
||
prompt = t("prompts.summary_user", max_length=max_length, text=text)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.summary_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info("Start generating summary")
|
||
success, content = self.api_client.generate(messages, use_cache=True)
|
||
|
||
if not success:
|
||
return "", content
|
||
|
||
logger.info("Summary generation success")
|
||
return content.strip(), t("generator.summary_success")
|
||
|
||
def polish_text(
|
||
self,
|
||
text: str,
|
||
polish_type: str = "general",
|
||
custom_requirements: str = ""
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Văn bản tiếng Ba Lan (với cơ chế thử lại)
|
||
|
||
Args (Tham số):
|
||
text: Văn bản cần trau chuốt
|
||
polish_type: Loại trau chuốt
|
||
custom_requirements: Yêu cầu tùy chỉnh
|
||
|
||
Returns (Trả về):
|
||
(Văn bản sau khi trau chuốt, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not text or not text.strip():
|
||
return "", t("generator.text_empty")
|
||
|
||
if len(text) > 10000:
|
||
return "", t("generator.text_too_long_polish")
|
||
|
||
# Từ nhắc tương ứng với Loại trau dồi
|
||
polish_prompts = {
|
||
"general": t("prompts.polish_general"),
|
||
"find_errors": t("prompts.polish_find_errors"),
|
||
"suggest_improvements": t("prompts.polish_suggest"),
|
||
"direct_modify": t("prompts.polish_direct"),
|
||
"remove_ai_flavor": t("prompts.polish_remove_ai"),
|
||
"enhance_details": t("prompts.polish_enhance"),
|
||
"optimize_dialogue": t("prompts.polish_dialogue"),
|
||
"improve_pacing": t("prompts.polish_pacing"),
|
||
}
|
||
|
||
prompt = polish_prompts.get(polish_type, polish_prompts["general"])
|
||
|
||
if custom_requirements:
|
||
prompt += t("prompts.polish_extra_req", custom_requirements=custom_requirements)
|
||
|
||
prompt += t("prompts.polish_output_only", text=text)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.polish_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start polishing, type: {polish_type}, length: {len(text)}")
|
||
|
||
# Cơ chế thử lại: thử lại khi nội dung quá ngắn
|
||
max_retries = 3
|
||
content = ""
|
||
success_msg = ""
|
||
|
||
for attempt in range(max_retries):
|
||
logger.debug(f"Polish attempt {attempt + 1}/{max_retries}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Polish failed (attempt {attempt + 1}/{max_retries}): {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", content
|
||
|
||
# Xác minh nội dung nghiêm ngặt
|
||
if not content or not content.strip():
|
||
logger.error(f"Polish returned empty (attempt {attempt + 1}/{max_retries})")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_empty_content")
|
||
|
||
# Lọc thông báo trạng thái (danh sách mở rộng)
|
||
status_messages = [
|
||
t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success",
|
||
"OK", "ok", "Success", "SUCCESS",
|
||
|
||
|
||
]
|
||
content_stripped = content.strip()
|
||
if content_stripped in status_messages:
|
||
logger.error(f"API returned status msg: {content} (attempt {attempt + 1}/{max_retries})")
|
||
logger.error(f"Content length: {len(content)}, content: {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_status_msg")
|
||
|
||
# Kiểm tra độ dài nội dung (nghiêm ngặt hơn)
|
||
if len(content_stripped) < 50:
|
||
logger.warning(f"Polish content too short: {len(content)} (attempt {attempt + 1}/{max_retries})")
|
||
logger.warning(f"Content: {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.polish_too_short", length=len(content))
|
||
|
||
# Xác minh nội dung đã được thông qua
|
||
logger.info(f"Polish done, content length: {len(content)}, attempts: {attempt + 1}")
|
||
logger.debug(f"Content first 200: {content[:200]}")
|
||
return content, t("generator.polish_success")
|
||
|
||
# Tất cả các lần thử lại đều thất bại
|
||
logger.error(f"Polish failed after {max_retries} attempts")
|
||
return "", t("generator.polish_failed_retries", max=max_retries)
|
||
|
||
def polish_and_suggest(
|
||
self,
|
||
text: str,
|
||
custom_requirements: str = ""
|
||
) -> Tuple[str, str, str]:
|
||
"""
|
||
Trau chuốt văn bản và đưa ra đề xuất
|
||
|
||
Returns (Trả về):
|
||
(Văn bản sau khi trau chuốt, Đề xuất sửa đổi, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not text or not text.strip():
|
||
return "", "", t("generator.text_empty")
|
||
|
||
if len(text) > 10000:
|
||
return "", "", t("generator.text_too_long_polish")
|
||
|
||
extra_req = t("prompts.polish_extra_req", custom_requirements=custom_requirements) if custom_requirements else ""
|
||
prompt = t("prompts.polish_suggest_user", text=text, extra_req=extra_req)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.polish_suggest_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info("Start polish and suggest")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Polish failed: {content}")
|
||
return "", "", content
|
||
|
||
# Thêm xác minh nội dung
|
||
if len(content) < 10:
|
||
logger.warning(f"Polish content too short: {len(content)}, content: {content}")
|
||
logger.warning("API may have returned status instead of content")
|
||
|
||
# Phân tích nội dung trả về
|
||
import re
|
||
errors = ""
|
||
suggestions = ""
|
||
polished = ""
|
||
|
||
# Cố gắng phân tích đầu ra có cấu trúc
|
||
found_errors_h = t("prompts.found_errors_header")
|
||
suggestions_h = t("prompts.suggestions_header")
|
||
polished_h = t("prompts.polished_text_header")
|
||
|
||
if f"【{found_errors_h}】" in content:
|
||
parts = content.split("【")
|
||
for part in parts:
|
||
if part.startswith(f"{found_errors_h}】"):
|
||
errors = part.replace(f"{found_errors_h}】", "").strip()
|
||
elif part.startswith(f"{suggestions_h}】"):
|
||
suggestions = part.replace(f"{suggestions_h}】", "").strip()
|
||
elif part.startswith(f"{polished_h}】"):
|
||
polished = part.replace(f"{polished_h}】", "").strip()
|
||
else:
|
||
# Nếu đầu ra không được định dạng, toàn bộ nội dung sẽ được coi là kết quả tinh tế
|
||
polished = content.strip()
|
||
suggestions = t("prompts.ai_no_format")
|
||
|
||
logger.info(f"Polish done, polished length: {len(polished)}")
|
||
return polished, suggestions, t("generator.polish_success")
|
||
|
||
def continue_writing(
|
||
self,
|
||
existing_text: str,
|
||
novel_title: str,
|
||
character_setting: str,
|
||
world_setting: str,
|
||
plot_idea: str,
|
||
genre: str = "",
|
||
sub_genres: List[str] = None,
|
||
target_words: int = 2500,
|
||
continue_count: int = 1
|
||
) -> Tuple[str, str]:
|
||
"""
|
||
Viết tiếp nội dung tiểu thuyết (có cơ chế thử lại)
|
||
|
||
Args (Tham số):
|
||
existing_text: Văn bản tiểu thuyết hiện có
|
||
novel_title: Tiêu đề tiểu thuyết
|
||
character_setting: Thiết lập nhân vật
|
||
world_setting: Thiết lập thế giới quan
|
||
plot_idea: Thiết lập cốt truyện
|
||
genre: Thể loại truyện
|
||
sub_genres: Các chủ đề con (Tag)
|
||
target_words: Số chữ mục tiêu
|
||
continue_count: Số chương cần viết tiếp
|
||
|
||
Returns (Trả về):
|
||
(Nội dung viết tiếp, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not existing_text or not existing_text.strip():
|
||
return "", t("generator.existing_text_empty")
|
||
|
||
style_desc = self._build_style_description()
|
||
|
||
# Enrichment thể loại và chủ đề con
|
||
if genre:
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
style_desc += f"\n\nHướng dẫn viết riêng cho thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
style_desc += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nHãy kết hợp chặt chẽ các đặc điểm và yếu tố của những chủ đề này vào nội dung viết tiếp."
|
||
|
||
# Lấy phần cuối cùng của văn bản hiện có làm ngữ cảnh
|
||
previous_content = existing_text[-1500:] if len(existing_text) > 1500 else existing_text
|
||
|
||
prompt = t("prompts.continue_user",
|
||
novel_title=novel_title,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting,
|
||
plot_idea=plot_idea,
|
||
style_desc=style_desc,
|
||
previous_content=previous_content,
|
||
target_words=target_words
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.continue_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start continue writing: {novel_title}, existing: {len(existing_text)}, target: {target_words}")
|
||
|
||
# Cơ chế thử lại
|
||
max_retries = 3
|
||
content = ""
|
||
success_msg = ""
|
||
|
||
for attempt in range(max_retries):
|
||
logger.debug(f"Continue attempt {attempt + 1}/{max_retries}")
|
||
success, content = self.api_client.generate(messages, use_cache=False)
|
||
|
||
if not success:
|
||
logger.error(f"Continue failed (attempt {attempt + 1}/{max_retries}): {content}")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", content
|
||
|
||
if not content or not content.strip():
|
||
logger.error(f"Continue returned empty (attempt {attempt + 1}/{max_retries})")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_empty_content")
|
||
|
||
status_messages = [
|
||
t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success",
|
||
"OK", "ok", "Success", "SUCCESS",
|
||
|
||
|
||
]
|
||
content_stripped = content.strip()
|
||
if content_stripped in status_messages:
|
||
logger.error(f"API returned status msg: {content} (attempt {attempt + 1}/{max_retries})")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.api_status_msg")
|
||
|
||
if len(content_stripped) < 100:
|
||
logger.warning(f"Continue content too short: {len(content)} (attempt {attempt + 1}/{max_retries})")
|
||
if attempt < max_retries - 1:
|
||
continue
|
||
return "", t("generator.continue_too_short", length=len(content))
|
||
|
||
logger.info(f"Continue success, chars: {len(content)}, attempts: {attempt + 1}")
|
||
return content, t("generator.continue_success")
|
||
|
||
logger.error(f"Continue failed after {max_retries} attempts")
|
||
return "", t("generator.continue_failed_retries", max=max_retries)
|
||
|
||
def continue_writing_stream(
|
||
self,
|
||
existing_text: str,
|
||
novel_title: str,
|
||
character_setting: str,
|
||
world_setting: str,
|
||
plot_idea: str,
|
||
genre: str = "",
|
||
sub_genres: List[str] = None,
|
||
target_words: int = 2500,
|
||
continue_count: int = 1
|
||
):
|
||
"""
|
||
Tiếp tục tiểu thuyết dưới dạng stream
|
||
Yields:
|
||
(Success flag, Chunk or error message)
|
||
"""
|
||
if not existing_text or not existing_text.strip():
|
||
yield False, t("generator.existing_text_empty")
|
||
return
|
||
|
||
style_desc = self._build_style_description()
|
||
|
||
# Enrichment thể loại và chủ đề con
|
||
if genre:
|
||
genre_desc = GenreManager.get_genre_description(genre)
|
||
if genre_desc:
|
||
style_desc += f"\n\nHướng dẫn viết riêng cho thể loại {genre}: {genre_desc}"
|
||
|
||
if sub_genres:
|
||
sub_genre_details = []
|
||
for sg in sub_genres:
|
||
desc = SubGenreManager.get_sub_genre_description(sg)
|
||
if desc:
|
||
sub_genre_details.append(f"- {sg}: {desc}")
|
||
else:
|
||
sub_genre_details.append(f"- {sg}")
|
||
style_desc += f"\n\nCác chủ đề con (Tag) bổ sung:\n" + "\n".join(sub_genre_details) + "\n\nHãy kết hợp chặt chẽ các đặc điểm và yếu tố của những chủ đề này vào nội dung viết tiếp."
|
||
|
||
previous_content = existing_text[-1500:] if len(existing_text) > 1500 else existing_text
|
||
|
||
prompt = t("prompts.continue_user",
|
||
novel_title=novel_title,
|
||
character_setting=character_setting,
|
||
world_setting=world_setting,
|
||
plot_idea=plot_idea,
|
||
style_desc=style_desc,
|
||
previous_content=previous_content,
|
||
target_words=target_words
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.continue_system")},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
|
||
logger.info(f"Start continue writing (stream): {novel_title}, existing: {len(existing_text)}")
|
||
for success, chunk in self.api_client.generate_stream(messages=messages):
|
||
yield success, chunk
|
||
|
||
def _build_style_description(self) -> str:
|
||
"""Mô tả phong cách xây dựng"""
|
||
gen = self.config.generation
|
||
return t("prompts.style_description",
|
||
writing_style=gen.writing_style,
|
||
writing_tone=gen.writing_tone,
|
||
character_development=gen.character_development,
|
||
plot_complexity=gen.plot_complexity
|
||
)
|
||
|
||
|
||
def get_generator() -> NovelGenerator:
|
||
"""Nhận phiên bản trình tạo mới"""
|
||
global _GENERATOR
|
||
try:
|
||
_GENERATOR
|
||
except NameError:
|
||
_GENERATOR = NovelGenerator()
|
||
return _GENERATOR
|
||
|
||
|
||
# ===================== Quản lý bộ đệm =======================
|
||
|
||
def save_generation_cache(project_id: str, cache_data: Dict) -> Tuple[bool, str]:
|
||
"""
|
||
Lưu bộ nhớ cache khởi tạo vào SQLite
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
cache_data: Từ điển dữ liệu cache
|
||
|
||
Returns (Trả về):
|
||
(Có thành công không, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return False, t("generator.cache_id_empty")
|
||
|
||
if not cache_data:
|
||
return False, t("generator.cache_data_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
now = datetime.now().isoformat()
|
||
conn.execute(
|
||
"INSERT OR REPLACE INTO generation_cache (project_id, data, updated_at) VALUES (?, ?, ?)",
|
||
(project_id, json.dumps(cache_data, ensure_ascii=False), now)
|
||
)
|
||
conn.commit()
|
||
logger.info(f"Cache saved to database: {project_id}")
|
||
return True, t("generator.cache_save_success")
|
||
except Exception as e:
|
||
logger.error(f"Cache save failed: {e}")
|
||
return False, t("generator.cache_save_failed", error=str(e))
|
||
|
||
|
||
def load_generation_cache(project_id: str) -> Tuple[Optional[Dict], str]:
|
||
"""
|
||
Tải bộ nhớ cache khởi tạo từ SQLite
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
|
||
Returns (Trả về):
|
||
(Dữ liệu cache, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return None, t("generator.cache_id_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
row = conn.execute(
|
||
"SELECT data FROM generation_cache WHERE project_id = ?", (project_id,)
|
||
).fetchone()
|
||
|
||
if not row:
|
||
return None, t("generator.cache_not_found")
|
||
|
||
cache_data = json.loads(row["data"])
|
||
logger.info(f"Cache loaded from database: {project_id}")
|
||
return cache_data, t("generator.cache_load_success")
|
||
except Exception as e:
|
||
logger.error(f"Cache load failed: {e}")
|
||
return None, t("generator.cache_load_failed", error=str(e))
|
||
|
||
|
||
def clear_generation_cache(project_id: str) -> Tuple[bool, str]:
|
||
"""
|
||
Xóa bộ nhớ cache khởi tạo
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
|
||
Returns (Trả về):
|
||
(Có thành công không, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return False, t("generator.cache_id_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
cursor = conn.execute(
|
||
"DELETE FROM generation_cache WHERE project_id = ?", (project_id,)
|
||
)
|
||
conn.commit()
|
||
|
||
if cursor.rowcount == 0:
|
||
return False, t("generator.cache_not_found")
|
||
|
||
logger.info(f"Cache cleared: {project_id}")
|
||
return True, t("generator.cache_clear_success")
|
||
except Exception as e:
|
||
logger.error(f"Cache clear failed: {e}")
|
||
return False, t("generator.cache_clear_failed", error=str(e))
|
||
|
||
|
||
def list_generation_caches() -> List[Dict]:
|
||
"""
|
||
Liệt kê tất cả bộ nhớ cache khởi tạo
|
||
|
||
Returns (Trả về):
|
||
Danh sách thông tin bộ đệm
|
||
"""
|
||
caches = []
|
||
try:
|
||
conn = get_db()
|
||
rows = conn.execute(
|
||
"SELECT project_id, data, LENGTH(data) as size, updated_at FROM generation_cache"
|
||
).fetchall()
|
||
|
||
for row in rows:
|
||
try:
|
||
cache_data = json.loads(row["data"])
|
||
caches.append({
|
||
"project_id": row["project_id"],
|
||
"title": cache_data.get("title", t("generator.unknown_title")),
|
||
"current_chapter": cache_data.get("current_chapter", 0),
|
||
"total_chapters": cache_data.get("total_chapters", 0),
|
||
"status": cache_data.get("generation_status", "unknown"),
|
||
"timestamp": cache_data.get("timestamp", ""),
|
||
"size": row["size"]
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Read cache failed {row['project_id']}: {e}")
|
||
except Exception as e:
|
||
logger.error(f"List generation caches failed: {e}")
|
||
|
||
return caches
|
||
|
||
|
||
def get_cache_size() -> int:
|
||
"""
|
||
Lấy tổng kích thước cache (byte)
|
||
|
||
Returns (Trả về):
|
||
Tổng kích thước bộ đệm
|
||
"""
|
||
try:
|
||
conn = get_db()
|
||
row = conn.execute("SELECT COALESCE(SUM(LENGTH(data)), 0) as total FROM generation_cache").fetchone()
|
||
return row["total"] if row else 0
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
# ===================== Quản lý tóm tắt chương ======================
|
||
|
||
def generate_chapter_summary(chapter_content: str, chapter_title: str) -> Tuple[str, str]:
|
||
"""
|
||
Tạo tóm tắt chương
|
||
|
||
Args (Tham số):
|
||
chapter_content: Nội dung chương
|
||
chapter_title: Tiêu đề chương
|
||
|
||
Returns (Trả về):
|
||
(Nội dung tóm tắt, Thông báo lỗi hoặc tin báo thành công)
|
||
"""
|
||
if not chapter_content or not chapter_content.strip():
|
||
return "", t("generator.summary_content_empty")
|
||
|
||
try:
|
||
api_client = get_api_client()
|
||
messages = [
|
||
{"role": "system", "content": t("prompts.chapter_summary_system")},
|
||
{"role": "user", "content": t("prompts.chapter_summary_user", chapter_title=chapter_title, chapter_content=chapter_content[:3000])}
|
||
]
|
||
success, summary = api_client.generate(messages, use_cache=False)
|
||
if success and summary:
|
||
return summary.strip(), t("generator.summary_gen_success")
|
||
else:
|
||
return "", summary or t("generator.summary_gen_failed")
|
||
except Exception as e:
|
||
logger.error(f"Generate chapter summary failed: {e}")
|
||
return "", t("generator.summary_gen_error", error=str(e))
|
||
|
||
|
||
def save_chapter_summary(project_id: str, chapter_num: int, summary: str) -> Tuple[bool, str]:
|
||
"""
|
||
Lưu tóm tắt chương vào SQLite
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
chapter_num: Số chương
|
||
summary: Nội dung tóm tắt
|
||
|
||
Returns (Trả về):
|
||
(Có thành công không, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return False, t("generator.cache_id_empty")
|
||
|
||
if not summary or not summary.strip():
|
||
return False, t("generator.summary_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
now = datetime.now().isoformat()
|
||
conn.execute("""
|
||
INSERT OR REPLACE INTO chapter_summaries
|
||
(project_id, chapter_num, summary, generated_at)
|
||
VALUES (?, ?, ?, ?)
|
||
""", (project_id, chapter_num, summary, now))
|
||
conn.commit()
|
||
logger.info(f"Chapter summary saved to database: {project_id}/{chapter_num}")
|
||
return True, t("generator.summary_save_success")
|
||
except Exception as e:
|
||
logger.error(f"Save chapter summary failed: {e}")
|
||
return False, t("generator.summary_save_failed", error=str(e))
|
||
|
||
|
||
def load_chapter_summaries(project_id: str) -> Tuple[List[Dict], str]:
|
||
"""
|
||
Tải tất cả tóm tắt chương từ SQLite
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
|
||
Returns (Trả về):
|
||
(Danh sách tóm tắt, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return [], t("generator.cache_id_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
rows = conn.execute("""
|
||
SELECT chapter_num, summary, generated_at
|
||
FROM chapter_summaries
|
||
WHERE project_id = ?
|
||
ORDER BY chapter_num
|
||
""", (project_id,)).fetchall()
|
||
|
||
if not rows:
|
||
return [], t("generator.summary_dir_not_found")
|
||
|
||
summaries = [{"chapter_num": row["chapter_num"], "summary": row["summary"], "generated_at": row["generated_at"]} for row in rows]
|
||
logger.info(f"Loaded {len(summaries)} chapter summaries from database")
|
||
return summaries, t("generator.summary_load_done", count=len(summaries))
|
||
except Exception as e:
|
||
logger.error(f"Load chapter summaries failed: {e}")
|
||
return [], t("generator.summary_load_failed", error=str(e))
|
||
|
||
|
||
def build_context_from_summaries(summaries: List[Dict], max_context_length: int = 1000) -> str:
|
||
"""
|
||
Xây dựng bối cảnh từ tóm tắt
|
||
|
||
Args (Tham số):
|
||
summaries: Danh sách tóm tắt
|
||
max_context_length: Độ dài bối cảnh tối đa (số ký tự)
|
||
|
||
Returns (Trả về):
|
||
Chuỗi bối cảnh
|
||
"""
|
||
if not summaries:
|
||
return ""
|
||
|
||
sorted_summaries = sorted(summaries, key=lambda x: x.get('chapter_num', 0), reverse=True)
|
||
|
||
context_parts = []
|
||
current_length = 0
|
||
|
||
for summary_data in sorted_summaries:
|
||
chapter_num = summary_data.get('chapter_num', 0)
|
||
summary = summary_data.get('summary', '')
|
||
|
||
if not summary:
|
||
continue
|
||
|
||
part = t("prompts.chapter_context_line", chapter_num=chapter_num, summary=summary)
|
||
if current_length + len(part) > max_context_length:
|
||
break
|
||
|
||
context_parts.append(part)
|
||
current_length += len(part)
|
||
|
||
context_parts.reverse()
|
||
|
||
if context_parts:
|
||
context = t("prompts.context_header") + "\n".join(context_parts)
|
||
logger.info(f"Context built, {len(context_parts)} summaries, total length {len(context)}")
|
||
return context
|
||
else:
|
||
logger.warning("No summaries available to build context")
|
||
return ""
|
||
|
||
|
||
def clear_chapter_summaries(project_id: str) -> Tuple[bool, str]:
|
||
"""
|
||
Xóa tóm tắt chương của dự án
|
||
|
||
Args (Tham số):
|
||
project_id: ID dự án
|
||
|
||
Returns (Trả về):
|
||
(Có thành công không, Thông báo)
|
||
"""
|
||
if not project_id or not project_id.strip():
|
||
return False, t("generator.cache_id_empty")
|
||
|
||
try:
|
||
conn = get_db()
|
||
cursor = conn.execute(
|
||
"DELETE FROM chapter_summaries WHERE project_id = ?", (project_id,)
|
||
)
|
||
conn.commit()
|
||
|
||
if cursor.rowcount == 0:
|
||
return False, t("generator.summary_dir_not_found")
|
||
|
||
logger.info(f"Chapter summaries cleared: {project_id}")
|
||
return True, t("generator.summary_clear_success")
|
||
except Exception as e:
|
||
logger.error(f"Clear chapter summaries failed: {e}")
|
||
return False, t("generator.summary_clear_failed", error=str(e))
|
||
|
||
|
||
def list_summary_caches() -> List[Dict]:
|
||
"""
|
||
Liệt kê tất cả cache tóm tắt
|
||
|
||
Returns (Trả về):
|
||
Danh sách thông tin bộ đệm
|
||
"""
|
||
caches = []
|
||
try:
|
||
conn = get_db()
|
||
rows = conn.execute("""
|
||
SELECT project_id, COUNT(*) as chapter_count,
|
||
SUM(LENGTH(summary)) as total_size
|
||
FROM chapter_summaries
|
||
GROUP BY project_id
|
||
""").fetchall()
|
||
|
||
for row in rows:
|
||
caches.append({
|
||
"project_id": row["project_id"],
|
||
"chapter_count": row["chapter_count"],
|
||
"total_size": row["total_size"],
|
||
"size_kb": round(row["total_size"] / 1024, 2)
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"List summary caches failed: {e}")
|
||
|
||
return caches
|
||
|
||
|
||
def get_summary_cache_size() -> int:
|
||
"""
|
||
Lấy tổng kích thước cache tóm tắt (byte)
|
||
|
||
Returns (Trả về):
|
||
Tổng kích thước bộ đệm
|
||
"""
|
||
try:
|
||
conn = get_db()
|
||
row = conn.execute("SELECT COALESCE(SUM(LENGTH(summary)), 0) as total FROM chapter_summaries").fetchone()
|
||
return row["total"] if row else 0
|
||
except Exception:
|
||
return 0
|