""" Mô-đun tạo tiểu thuyết - Hỗ trợ tạo dàn ý, tạo chương, viết lại... """ 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 services.api_client import get_api_client from core.config import get_config from locales.i18n import t from core.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 StyleManager""" from services.style_manager import StyleManager styles = StyleManager.load_styles() templates = {} for g in styles: templates[g["name"]] = g["description"] return templates def get_preset_templates(): """Get preset templates (rebuilt each call to respect Active Genres)""" 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*|Chương\s*|Chapter\s*|Hồi\s*|Phần\s*)(\d+)(?:\s*章)?[::\s]+([^\-—–]+)[-—–]\s*(.+)', r'^(\d+)[\).:\s]+([^\-—–]+)[-—–]\s*(.+)', r'(?:第\s*|Chương\s*|Chapter\s*|Hồi\s*|Phần\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 services.genre_manager import GenreManager from services.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, custom_outline_prompt: 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." custom_prompt_str = f"Yêu cầu chuyên biệt của tác giả:\n{custom_outline_prompt}\n\n" if custom_outline_prompt.strip() else "" 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, custom_prompt=custom_prompt_str, 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 = "", use_reflection: bool = False ) -> 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) 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 ) if custom_prompt: prompt += f"\n\n[Yêu cầu bổ sung của tác giả]:\n{custom_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 if use_reflection: logger.info("Applying self-reflection to the generated chapter draft...") # Sử dụng content nháp làm nguyên liệu cho lượt tạo reflection mới reflection_sys = t("prompts.reflection_system") reflection_prompt = t("prompts.reflection_user", chapter_req=prompt, draft_content=content, target_words=target_words) reflection_messages = [ {"role": "system", "content": reflection_sys}, {"role": "user", "content": reflection_prompt} ] success, final_content = self.api_client.generate(reflection_messages, use_cache=False) if not success: logger.error(f"Reflection generation failed: {final_content}. Falling back to draft content.") else: logger.info("Reflection generation success.") content = final_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 = "", use_reflection: bool = False ): """ 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 ) if custom_prompt: prompt += f"\n\n[Yêu cầu bổ sung của tác giả]:\n{custom_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)}") if not use_reflection: for success, chunk in self.api_client.generate_stream(messages=messages): yield success, chunk else: logger.info("Generating draft for reflection in streaming mode...") success, draft_content = self.api_client.generate(messages, use_cache=False) if not success: logger.error(f"Draft generation failed: {draft_content}") yield False, draft_content return logger.info("Draft generated. Applying self-reflection stream...") reflection_sys = t("prompts.reflection_system") reflection_prompt = t("prompts.reflection_user", chapter_req=prompt, draft_content=draft_content, target_words=target_words) reflection_messages = [ {"role": "system", "content": reflection_sys}, {"role": "user", "content": reflection_prompt} ] for success, chunk in self.api_client.generate_stream(messages=reflection_messages): yield success, chunk def rewrite_paragraph( self, text: str, style_template: str = "", use_reflection: bool = False ) -> 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 = "" 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]}") if use_reflection: logger.info("Applying self-reflection to rewritten paragraph draft...") reflection_sys = t("prompts.reflection_system") reflection_prompt = t("prompts.reflection_user", chapter_req=prompt, draft_content=content, target_words=len(content)) reflection_messages = [ {"role": "system", "content": reflection_sys}, {"role": "user", "content": reflection_prompt} ] success, final_content = self.api_client.generate(reflection_messages, use_cache=False) if success: content = final_content 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 = "", use_reflection: bool = False ) -> 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 = "" 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]}") if use_reflection: logger.info("Applying self-reflection to polished paragraph draft...") reflection_sys = t("prompts.reflection_system") reflection_prompt = t("prompts.reflection_user", chapter_req=prompt, draft_content=content, target_words=len(content)) reflection_messages = [ {"role": "system", "content": reflection_sys}, {"role": "user", "content": reflection_prompt} ] success, final_content = self.api_client.generate(reflection_messages, use_cache=False) if success: content = final_content 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 = "", use_reflection: bool = False ) -> 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 if use_reflection: logger.info("Applying self-reflection to polish and suggest paragraph draft...") reflection_sys = t("prompts.reflection_system") reflection_prompt = t("prompts.reflection_user", chapter_req=prompt, draft_content=content, target_words=len(content)) reflection_messages = [ {"role": "system", "content": reflection_sys}, {"role": "user", "content": reflection_prompt} ] success, final_content = self.api_client.generate(reflection_messages, use_cache=False) if success: content = final_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 = "" 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""" from services.style_manager import StyleManager gen = self.config.generation style_name = gen.writing_style style_desc = StyleManager.get_style_description(style_name) full_style = style_name if style_desc: full_style += f" ({style_desc})" return t("prompts.style_description", writing_style=full_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 core.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 core.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