first commit

This commit is contained in:
Mai-Vu Tran
2026-03-16 18:14:44 +07:00
commit 6699c516cd
31 changed files with 9946 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
# Docker 构建忽略文件
# 排除不需要复制到 Docker 容器中的文件和目录
# 版本控制
.git
.gitignore
.gitattributes
# 开发环境文件
.env
.env.local
.env.*.local
# Python 相关
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 测试文件
tests/
tox/
.coverage
.pytest_cache/
htmlcov/
.nox/
# 文档
docs/
*.md
README*
CHANGELOG*
CONTRIBUTING*
# 日志文件
*.log
logs/
log/
# 缓存文件
cache/
*.cache
# 输出文件
output/
generated/
# 备份文件
backups/
*.bak
*.backup
# IDE 和编辑器文件
.vscode/
.idea/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# 临时文件
tmp/
temp/
*.tmp
*.temp
# 虚拟环境
venv/
env/
ENV/
.venv/
# 数据库文件
*.db
*.sqlite
*.sqlite3
# 上传文件
uploads/
static/media/
# 压缩文件
*.tar
*.tar.gz
*.tar.bz2
*.tar.xz
*.zip
*.rar
*.7z
# 其他
*.pid
*.sock
*.prof
# Docker 相关
Dockerfile*
docker-compose*
.dockerignore
# 开发工具配置
.pylintrc
.pylama.ini
.flake8
.coveragerc
.tox.ini
.noxfile.py
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.env.local
.env.*.local
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
# VS Code
.vscode/
# macOS
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
+202
View File
@@ -0,0 +1,202 @@
# ============================================
# AI小说生成工具正式版V3.0
# 版权所有 © 2026 新疆幻城网安科技有限责任公司 (幻城科技)
# 作者:幻城
# ============================================
# ============================================
# 虚拟环境
# ============================================
venv/
env/
ENV/
.venv/
.env/
env.bak/
venv.bak/
# ============================================
# Python缓存
# ============================================
__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
.Python
*.so
# ============================================
# Python包管理
# ============================================
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# ============================================
# 打包文件
# ============================================
*.spec
# ============================================
# 测试覆盖
# ============================================
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
.coverage.*
coverage.xml
*.cover
.hypothesis/
# ============================================
# 项目数据
# ============================================
cache/
exports/
logs/
projects/
config/backups/
# ============================================
# 环境配置(包含敏感信息)
# ============================================
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
config/novel_tool_config.json
# ============================================
# 临时文件
# ============================================
临时区/
*.tmp
*.log
*.temp
*.bak
*.backup
*.old
*.cache
0
1
# ============================================
# IDE配置
# ============================================
.vscode/
.idea/
*.swp
*.swo
*~
# ============================================
# 系统文件
# ============================================
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
.fuse_hidden*
.netrwhist
.n*
# ============================================
# Jupyter Notebook
# ============================================
.ipynb_checkpoints
# ============================================
# pyenv
# ============================================
.python-version
# ============================================
# Celery
# ============================================
celerybeat-schedule
# ============================================
# SageMath
# ============================================
*.sage.py
# ============================================
# Spyder项目设置
# ============================================
.spyderproject
.spyproject
# ============================================
# Rope项目设置
# ============================================
.ropeproject
# ============================================
# MkDocs文档
# ============================================
/site
# ============================================
# MyPy类型检查
# ============================================
.mypy_cache/
.dmypy.json
dmypy.json
# ============================================
# Pyre类型检查
# ============================================
.pyre/
# ============================================
# 数据库文件
# ============================================
*.db
*.sqlite
*.sqlite3
# ============================================
# Node.js(如果有前端组件)
# ============================================
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ============================================
# Gradio缓存
# ============================================
gradio_cached/
# ============================================
# 保留的重要文档
# ============================================
!README.md
!OPEN_SOURCE_CHECKLIST.md
!LICENSE
!API_KEY_SETUP.md
!QUICK_START.md
!HOW_TO_EXPORT.md
!.env.example
!.env.template
+45
View File
@@ -0,0 +1,45 @@
# AI 小说创作工具 Dockerfile
# 基于 Python 3.11 官方镜像
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
build-essential \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
COPY requirements-dev.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements-dev.txt
# 创建必要的目录
RUN mkdir -p logs cache output data backups templates project_templates plugins
# 复制应用代码
COPY . .
# 设置权限
RUN chmod +x start.sh start.bat run.py
# 暴露端口
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 设置启动命令
CMD ["python", "run.py"]
+29
View File
@@ -0,0 +1,29 @@
MIT License
Copyright (c) 2026 新疆幻城网安科技有限责任公司 (幻城科技)
作者:幻城
项目名称:AI小说生成工具正式版V3.0
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
免责声明:
本软件仅供学习和研究使用,使用者应遵守相关法律法规。对于因使用本软件而产生的任何直接或间接损失,开发者不承担任何责任。
+44
View File
@@ -0,0 +1,44 @@
# AI Novel Generator Pro v4.0
## 📖 Documentation / Tài liệu / 文档
| Language | Link |
|----------|------|
| 🇨🇳 中文 (Chinese) | [locales/CN/使用说明.md](locales/CN/使用说明.md) |
| 🇻🇳 Tiếng Việt (Vietnamese) | [locales/VI/使用说明.md](locales/VI/使用说明.md) |
## 🌐 i18n (Internationalization)
The application supports multiple languages. Language files are stored in the `locales/` directory:
```
locales/
├── i18n.py # i18n helper module
├── __init__.py # Package init
├── CN/
│ ├── messages.json # UI strings in Chinese (default)
│ └── 使用说明.md # Documentation in Chinese
└── VI/
├── messages.json # UI strings in Vietnamese
└── 使用说明.md # Documentation in Vietnamese
```
### Switching Language
Set the `APP_LANGUAGE` environment variable before starting the app:
```bash
# Chinese (default)
python app.py
# Vietnamese
set APP_LANGUAGE=VI
python app.py
```
### Adding a New Language
1. Create a new directory under `locales/` (e.g. `locales/EN/`)
2. Copy `locales/CN/messages.json` as a template
3. Translate all values in the JSON file
4. Set `APP_LANGUAGE=EN` and start the app
+727
View File
@@ -0,0 +1,727 @@
"""
Mô-đun Gọi API - Hỗ trợ thử lại, giới hạn tốc độ, bộ nhớ cache, cân bằng tả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 time
import hashlib
import json
import os
import threading
from typing import List, Dict, Any, Optional, Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import wraps
import logging
from openai import OpenAI, RateLimitError, APIError, AuthenticationError, APIConnectionError
import pickle
from config import get_config, Backend
from locales.i18n import t
from database import get_db
logger = logging.getLogger(__name__)
# Số mục được lưu trong bộ nhớ đệm tối đa
MAX_CACHE_SIZE = 100
@dataclass
class CacheEntry:
"""mục bộ nhớ đệm"""
key: str
value: str
timestamp: datetime
ttl: int = 3600 # Mặc định hết hạn sau 1 giờ
class ResponseCache:
"""trình quản lý bộ đệm phản hồi"""
def __init__(self, max_size: int = MAX_CACHE_SIZE):
self.cache: Dict[str, CacheEntry] = {}
self.max_size = max_size
self.lock = threading.Lock()
self._dirty_count = 0 # Đếm số lần set chưa flush
self._disk_loaded = False # Lazy load flag
def _generate_key(self, messages: List[Dict], model: str) -> str:
"""Tạo khóa bộ đệm"""
content = json.dumps(messages, sort_keys=True, ensure_ascii=False) + model
return hashlib.md5(content.encode('utf-8')).hexdigest()
def get(self, messages: List[Dict], model: str) -> Optional[str]:
"""Nhận bộ đệm (lazy load từ DB nếu chưa có trong RAM)"""
key = self._generate_key(messages, model)
with self.lock:
if key in self.cache:
entry = self.cache[key]
# Kiểm tra xem đã hết hạn chưa
if datetime.now() - entry.timestamp < timedelta(seconds=entry.ttl):
logger.debug(f"Cache hit (RAM): {key}")
return entry.value
else:
del self.cache[key]
# Lazy load: thử tìm trong DB nếu không có trong RAM
try:
conn = get_db()
row = conn.execute(
"SELECT value, timestamp, ttl FROM response_cache WHERE key = ?", (key,)
).fetchone()
if row:
try:
ts = datetime.fromisoformat(row["timestamp"])
except Exception:
ts = datetime.now()
ttl = int(row["ttl"])
if datetime.now() - ts < timedelta(seconds=ttl):
# Cache hit từ DB → đưa vào RAM
entry = CacheEntry(key=key, value=row["value"], timestamp=ts, ttl=ttl)
with self.lock:
self.cache[key] = entry
logger.debug(f"Cache hit (DB): {key}")
return row["value"]
except Exception as e:
logger.debug(f"DB cache lookup failed: {e}")
return None
def set(self, messages: List[Dict], model: str, value: str, ttl: int = 3600) -> None:
"""Thiết lập bộ nhớ cache"""
key = self._generate_key(messages, model)
with self.lock:
# Khi bộ đệm đầy, hãy xóa mục cũ nhất
if len(self.cache) >= self.max_size:
oldest_key = min(self.cache.keys(),
key=lambda k: self.cache[k].timestamp)
del self.cache[oldest_key]
self.cache[key] = CacheEntry(
key=key,
value=value,
timestamp=datetime.now(),
ttl=ttl
)
self._dirty_count += 1
logger.debug(f"Cache set: {key}")
# Lưu vào DB ngay lập tức (chỉ entry mới, không flush toàn bộ)
try:
self._save_entry_to_disk(key, value, ttl)
except Exception:
logger.debug("Cache save to disk error (ignored)")
def clear(self) -> None:
"""Xóa bộ nhớ đệm"""
with self.lock:
self.cache.clear()
logger.info("Cache cleared")
def _save_entry_to_disk(self, key: str, value: str, ttl: int) -> None:
"""Lưu một entry vào SQLite (thay vì flush toàn bộ cache)"""
try:
conn = get_db()
conn.execute(
"INSERT OR REPLACE INTO response_cache (key, value, timestamp, ttl) VALUES (?, ?, ?, ?)",
(key, value, datetime.now().isoformat(), ttl)
)
conn.commit()
except Exception as e:
logger.warning(f"Save cache entry to database failed: {e}")
def _cleanup_expired_db(self) -> None:
"""Xóa các entry hết hạn trong DB (gọi định kỳ)"""
try:
conn = get_db()
conn.execute("DELETE FROM response_cache WHERE datetime(timestamp, '+' || ttl || ' seconds') < datetime('now')")
conn.commit()
logger.debug("Expired cache entries cleaned from DB")
except Exception as e:
logger.debug(f"Cache cleanup failed: {e}")
class RateLimiter:
"""Giới hạn tỷ lệ - Thuật toán nhóm mã thông báo"""
def __init__(self, rate: float = 10, window: int = 60):
"""
Args:
rate: số lượng yêu cầu trên mỗi giây của cửa sổ
cửa sổ: cửa sổ thời gian (giây)
"""
self.rate = rate
self.window = window
self.tokens = rate
self.last_update = time.time()
self.lock = threading.Lock()
def acquire(self, tokens: int = 1, blocking: bool = True) -> bool:
"""Nhận mã thông báo"""
with self.lock:
now = time.time()
elapsed = now - self.last_update
# Mã thông báo bổ sung
self.tokens = min(self.rate, self.tokens + elapsed * self.rate / self.window)
self.last_update = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
if blocking:
wait_time = (tokens - self.tokens) * self.window / self.rate
time.sleep(wait_time)
self.tokens = 0
return True
return False
class APIClient:
"""Ứng dụng khách API - hỗ trợ thử lại, giới hạn tốc độ, lưu vào bộ đệm, cân bằng tải"""
def __init__(self):
self.config = get_config()
self.cache = ResponseCache()
self.clients: List[tuple[Backend, OpenAI]] = []
self.rate_limiters: Dict[str, RateLimiter] = {}
self.current_client_index = 0
self.lock = threading.Lock()
self._init_clients()
def _init_clients(self) -> None:
"""Khởi tạo tất cả client"""
self.clients = []
enabled_backends = self.config.get_enabled_backends()
if not enabled_backends:
logger.error("No enabled backends")
return
for backend in enabled_backends:
try:
client = OpenAI(
base_url=backend.base_url.rstrip("/"),
api_key=backend.api_key,
timeout=backend.timeout
)
self.clients.append((backend, client))
# Tạo bộ giới hạn tốc độ cho mỗi chương trình phụ trợ
limiter_key = f"{backend.name}_{backend.model}"
if limiter_key not in self.rate_limiters:
# Giả sử tối đa 10 yêu cầu/phút đồng thời cho mỗi chương trình phụ trợ
self.rate_limiters[limiter_key] = RateLimiter(rate=10, window=60)
logger.info(f"Backend init success: {backend.name}")
except Exception as e:
logger.error(f"Backend init failed {backend.name}: {e}")
if not self.clients:
logger.error("All backends init failed")
def _strip_reasoning(self, text: str) -> str:
"""Loại bỏ phần suy nghĩ (reasoning/thinking) khỏi nội dung"""
if not text:
return ""
import re
# 1. Loại bỏ các thẻ <thought>...</thought> hoặc <reasoning>...</reasoning>
text = re.sub(r'<(thought|reasoning)>[\s\S]*?</\1>', '', text)
# 2. Loại bỏ các đoạn văn bắt đầu bằng "Thinking Process:", "Thought:", v.v.
# Thường các đoạn này nằm ở đầu và phân tách bởi xuống dòng kép
patterns = [
r'^Thinking Process:[\s\S]*?(\n\n|$)',
r'^Thought:[\s\S]*?(\n\n|$)',
r'^Suy nghĩ:[\s\S]*?(\n\n|$)',
r'^Phân tích:[\s\S]*?(\n\n|$)'
]
for pattern in patterns:
text = re.sub(pattern, '', text, flags=re.IGNORECASE)
return text.strip()
def _get_next_client(self, retry_count: int = 0) -> Optional[tuple[Backend, OpenAI]]:
"""Nhận ứng dụng khách có sẵn tiếp theo (cân bằng tải)"""
if not self.clients:
return None
with self.lock:
# Nếu là lần thử đầu tiên, ưu tiên tìm backend mặc định
if retry_count == 0:
for client_tuple in self.clients:
backend, client = client_tuple
if getattr(backend, 'is_default', False):
return client_tuple
idx = self.current_client_index
client_tuple = self.clients[idx]
# Con trỏ tiến lên và cuộc gọi tiếp theo trả về cuộc gọi tiếp theo
self.current_client_index = (idx + 1) % len(self.clients)
return client_tuple
def generate(
self,
messages: List[Dict[str, str]],
use_cache: bool = True,
max_retries: int = 3,
backoff_factor: float = 1.5
) -> tuple[bool, str]:
"""
Tạo văn bản (kèm bộ nhớ cache, thử lại, giới hạn tốc độ)
Args:
messages: Danh sách thông báo (messages)
use_cache: Có sử dụng bộ nhớ cache không
max_retries: Số lần thử lại tối đa
backoff_factor: Hệ số lùi lại (backoff factor)
Returns:
(Cờ thành công, Nội dung khởi tạo/Thông báo lỗi)
"""
enabled_backends = self.config.get_enabled_backends()
if not enabled_backends:
return False, t("api_client.no_backends")
# Xác minh thông số
if not isinstance(messages, list) or len(messages) == 0:
return False, t("api_client.invalid_messages")
# Thử lại logic (thăm dò các chương trình phụ trợ khác nhau)
retry_count = 0
base_wait = 1.0
import random
while retry_count < max_retries:
client_info = self._get_next_client(retry_count)
if not client_info:
return False, t("api_client.no_api_client")
backend, client = client_info
model = getattr(backend, "model", None)
limiter_key = f"{backend.name}_{model}"
# Đảm bảo có giới hạn tỷ lệ
if limiter_key not in self.rate_limiters:
self.rate_limiters[limiter_key] = RateLimiter(rate=10, window=60)
# Cố gắng sử dụng bộ nhớ đệm (tùy theo mô hình phụ trợ đã chọn)
if use_cache and model:
cached = self.cache.get(messages, model)
if cached:
return True, cached
try:
# Yêu cầu mã thông báo (chặn cho đến khi có sẵn)
self.rate_limiters[limiter_key].acquire(blocking=True)
logger.debug(f"API call: {backend.name} model={model}")
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=getattr(self.config.generation, "temperature", 0.8),
top_p=getattr(self.config.generation, "top_p", 1.0),
max_tokens=getattr(self.config.generation, "max_tokens", 4096)
)
# Logic phân tích phản hồi nâng cao - hỗ trợ nhiều định dạng, lọc thông báo trạng thái
logger.debug(f"API response type: {type(response)}")
logger.debug(f"API response object: {response}")
content = ""
try:
# Hãy thử định dạng OpenAI tiêu chuẩn
if hasattr(response, 'choices') and len(response.choices) > 0:
choice = response.choices[0]
logger.debug(f"Choice type: {type(choice)}")
logger.debug(f"Choice attrs: {dir(choice)}")
if hasattr(choice, 'message'):
# Ưu tiên content, nếu không có thử lấy từ reasoning (ví dụ DeepSeek R1)
content = getattr(choice.message, 'content', None) or ""
reasoning = getattr(choice.message, 'reasoning', None)
if not content and reasoning:
logger.info(f"[{backend.name}] Content is empty but reasoning is found, using reasoning as content")
content = reasoning
if not (not content or len(content.strip()) < 10):
logger.debug(f"Got content from message, len: {len(content)}")
elif hasattr(choice, 'text'):
content = choice.text
logger.debug(f"Got content from choice.text, len: {len(content) if content else 0}")
else:
logger.warning(f"Cannot get content from choice, type: {type(choice)}")
else:
logger.warning(f"Response has no choices, type: {type(response)}")
# Nếu phân tích cú pháp tiêu chuẩn không thành công, hãy thử các định dạng có thể khác
if not content or len(content.strip()) < 10:
logger.warning("Standard parse failed, trying alternatives")
# Hãy thử truy cập trực tiếp vào thuộc tính nội dung của phản hồi
if hasattr(response, 'content'):
content = response.content
logger.debug(f"Got content from response.content, len: {len(content) if content else 0}")
# Cố gắng trích xuất từ biểu diễn chính tả của phản hồi
if not content or len(content.strip()) < 10:
try:
response_dict = response.model_dump() if hasattr(response, 'model_dump') else response.dict() if hasattr(response, 'dict') else {}
if 'choices' in response_dict and response_dict['choices']:
msg = response_dict['choices'][0].get('message', {})
content = msg.get('content', '') or msg.get('reasoning', '')
logger.debug(f"Extracted from dict, len: {len(content) if content else 0}")
except Exception as e:
logger.debug(f"Dict conversion failed: {e}")
# Dự phòng cuối cùng: chuyển đổi thành chuỗi và dùng regex (Xử lý trường hợp đối tượng thô quá lớn)
if not content or len(content.strip()) < 10:
logger.warning("All primary parse methods failed, using regex fallback on str(response)")
response_str = str(response)
# Thử tìm content='...'
import re
content_match = re.search(r"content=(?:'|\")((?:.|\n)*?)(?:'|\"),\s*refusal", response_str)
if content_match:
content = content_match.group(1).replace("\\n", "\n").replace("\\'", "'")
logger.info(f"Regex extracted content, len: {len(content)}")
# Nếu vẫn không có, thử tìm reasoning='...'
if not content or len(content.strip()) < 10:
reasoning_match = re.search(r"reasoning=(?:'|\")((?:.|\n)*?)(?:'|\"),\s*role", response_str)
if reasoning_match:
content = reasoning_match.group(1).replace("\\n", "\n").replace("\\'", "'")
logger.info(f"Regex extracted reasoning, len: {len(content)}")
if not content or len(content.strip()) < 10:
# Lọc các thông báo trạng thái phổ biến
status_messages = [t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success"]
if response_str.strip() in status_messages or len(response_str.strip()) < 10:
content = None
else:
content = response_str
if not content or len(content.strip()) < 10:
logger.error("Failed to extract content even with fallback methods")
# Xác thực cuối cùng - lọc nghiêm ngặt các thông báo trạng thái
if content:
content = content.strip()
# Xác định trạng thái cần lọc Danh sách thông báo (tin nhắn)
status_messages = [
t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success",
"OK", "ok", "Success", "SUCCESS",
]
# Kiểm tra xem nội dung có phải là thông báo trạng thái không
if content in status_messages:
logger.error(f"Status msg detected, rejecting: {content}")
content = ""
# Kiểm tra độ dài nội dung
elif len(content) < 10:
logger.warning(f"Content too short ({len(content)} chars), may be status msg")
logger.warning(f"Content: {content}")
content = ""
else:
# Loại bỏ reasoning trước khi trả về
content = self._strip_reasoning(content)
logger.info(f"Got content successfully, final len: {len(content)}")
logger.debug(f"Content first 200: {content[:200]}")
else:
logger.error("Failed to get any content")
except Exception as e:
logger.exception(f"API response parse exception: {e}")
# Đồng thời cố gắng lấy nội dung trong những trường hợp bất thường
try:
response_str = str(response)
# Lọc thông báo trạng thái
status_messages = [t("generator.continue_success"), t("generator.rewrite_success"), t("generator.polish_success"), t("generator.gen_success"), "done", "success"]
if response_str.strip() not in status_messages and len(response_str.strip()) >= 10:
content = response_str
logger.warning(f"Exception fallback str(response), len: {len(content)}")
else:
logger.error(f"Exception fallback: API returned status msg: {response_str}")
content = ""
except Exception as e2:
logger.exception(f"Exception fallback also failed: {e2}")
content = ""
# Kết quả được lưu vào bộ nhớ đệm - chỉ lưu nội dung hợp lệ vào bộ nhớ đệm
if use_cache and model and content and len(content) >= 10:
self.cache.set(messages, model, content)
elif use_cache and model and (not content or len(content) < 10):
logger.warning("Invalid content, not caching")
# Xác minh cuối cùng: Đảm bảo nội dung không trống và hợp lệ
if not content or not content.strip() or len(content.strip()) < 10:
logger.error(f"Invalid/short content, rejecting: len={len(content) if content else 0}")
return False, t("api_client.invalid_content", length=len(content) if content else 0)
logger.info(f"API call success: {backend.name}")
return True, content
except RateLimitError as e:
retry_count += 1
jitter = random.random() * 0.5
wait_time = base_wait * (backoff_factor ** retry_count) + jitter
logger.warning(f"API rate limit ({backend.name}), waiting {wait_time:.2f}s... (retry {retry_count})")
if retry_count >= max_retries:
return False, t("api_client.rate_limit_error", error=str(e))
time.sleep(wait_time)
except AuthenticationError as e:
logger.error(f"API authentication error ({backend.name}): {e}")
return False, t("api_client.auth_error", error=str(e))
except APIConnectionError as e:
retry_count += 1
jitter = random.random() * 0.5
wait_time = base_wait * (backoff_factor ** retry_count) + jitter
logger.warning(f"API connection error ({backend.name}), waiting {wait_time:.2f}s... (retry {retry_count})")
if retry_count >= max_retries:
return False, t("api_client.connection_error", error=str(e))
time.sleep(wait_time)
except APIError as e:
retry_count += 1
jitter = random.random() * 0.5
wait_time = base_wait * (backoff_factor ** retry_count) + jitter
logger.warning(f"API error ({backend.name}): {e}, waiting {wait_time:.2f}s... (retry {retry_count})")
if retry_count >= max_retries:
return False, t("api_client.api_error", error=str(e))
time.sleep(wait_time)
except Exception as e:
# Lỗi không xác định được trả về trực tiếp nhưng có ngữ cảnh
logger.exception(f"Unexpected error ({getattr(backend,'name', 'unknown')}): {e}")
return False, t("api_client.error_prefix", error=str(e))
return False, t("api_client.retry_failed", max=max_retries)
def generate_stream(
self,
messages: List[Dict[str, str]],
max_retries: int = 3,
backoff_factor: float = 1.5
):
"""
Tạo văn bản theo luồng (Streaming)
Args:
messages: Danh sách thông báo (messages)
max_retries: Số lần thử lại tối đa
backoff_factor: Hệ số lùi lại (backoff factor)
Yields:
(Cờ thành công, Nội dung chunk/Thông báo lỗi)
"""
enabled_backends = self.config.get_enabled_backends()
if not enabled_backends:
yield False, t("api_client.no_backends")
return
if not isinstance(messages, list) or len(messages) == 0:
yield False, t("api_client.invalid_messages")
return
retry_count = 0
base_wait = 1.0
import random
while retry_count < max_retries:
client_info = self._get_next_client(retry_count)
if not client_info:
yield False, t("api_client.no_api_client")
return
backend, client = client_info
model = getattr(backend, "model", None)
limiter_key = f"{backend.name}_{model}"
if limiter_key not in self.rate_limiters:
self.rate_limiters[limiter_key] = RateLimiter(rate=10, window=60)
try:
self.rate_limiters[limiter_key].acquire(blocking=True)
logger.debug(f"API call (stream): {backend.name} model={model}")
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=getattr(self.config.generation, "temperature", 0.8),
top_p=getattr(self.config.generation, "top_p", 1.0),
max_tokens=getattr(self.config.generation, "max_tokens", 4096),
stream=True
)
chunk_count = 0
for chunk in response:
if hasattr(chunk, 'choices') and len(chunk.choices) > 0:
delta = chunk.choices[0].delta
# Thử lấy từ content hoặc reasoning/reasoning_content (hỗ trợ DeepSeek R1 và các mô hình tương tự)
content_chunk = getattr(delta, 'content', None)
reasoning_chunk = getattr(delta, 'reasoning', None) or getattr(delta, 'reasoning_content', None)
if content_chunk:
chunk_count += 1
yield True, content_chunk
logger.info(f"API call stream success: {backend.name}, received {chunk_count} chunks")
return
except RateLimitError as e:
retry_count += 1
wait_time = base_wait * (backoff_factor ** retry_count) + random.random() * 0.5
logger.warning(f"API rate limit (stream), waiting {wait_time:.2f}s...")
if retry_count >= max_retries:
yield False, t("api_client.rate_limit_error", error=str(e))
return
time.sleep(wait_time)
except Exception as e:
logger.exception(f"Unexpected error in stream ({getattr(backend,'name', 'unknown')}): {e}")
yield False, t("api_client.error_prefix", error=str(e))
return
yield False, t("api_client.retry_failed", max=max_retries)
def test_backends(self) -> Dict[str, bool]:
"""Kiểm tra tính khả dụng của tất cả các phụ trợ"""
results = {}
test_messages = [
{"role": "system", "content": t("api_client.test_prompt")},
{"role": "user", "content": t("api_client.test_hello")}
]
for backend in self.config.get_enabled_backends():
try:
client = OpenAI(
base_url=backend.base_url.rstrip("/"),
api_key=backend.api_key,
timeout=5
)
response = client.chat.completions.create(
model=backend.model,
messages=test_messages,
max_tokens=10
)
results[backend.name] = True
logger.info(f"Backend test success: {backend.name}")
except Exception as e:
results[backend.name] = False
logger.error(f"Backend test failed {backend.name}: {e}")
return results
def test_connection(self, base_url: str, api_key: str, model: str) -> bool:
"""Kiểm tra kết nối cho một phụ trợ duy nhất"""
test_messages = [
{"role": "system", "content": t("api_client.test_prompt")},
{"role": "user", "content": t("api_client.test_hello")}
]
try:
client = OpenAI(
base_url=base_url.rstrip("/"),
api_key=api_key,
timeout=10
)
response = client.chat.completions.create(
model=model,
messages=test_messages,
max_tokens=10
)
return True
except Exception as e:
logger.error(f"Test connection failed: {e}")
raise e
def clear_cache(self) -> None:
"""Xóa bộ nhớ đệm"""
self.cache.clear()
def get_cache_stats(self) -> Dict[str, Any]:
"""Nhận số liệu thống kê bộ đệm"""
return {
"total_entries": len(self.cache.cache),
"max_size": self.cache.max_size,
"usage_rate": len(self.cache.cache) / self.cache.max_size * 100
}
def generate_image(
self,
prompt: str,
size: str = "1024x1024",
quality: str = "standard",
n: int = 1
) -> tuple[bool, str]:
"""
Tạo hình ảnh (DALL-E) qua API OpenAI
Args:
prompt: Nội dung mô tả hình ảnh
size: Kích thước hình ảnh (1024x1024, v.v.)
quality: Chất lượng (standard/hd)
n: Số lượng hình ảnh
Returns:
(Cờ thành công, URL hình ảnh hoặc thông báo lỗi)
"""
client_info = self._get_next_client(0)
if not client_info:
return False, t("api_client.no_api_client")
backend, client = client_info
try:
logger.info(f"Generating image with prompt: {prompt[:100]}... using {backend.name}")
# Thử gọi API tạo hình ảnh (chỉ OpenAI chính thức mới hỗ trợ tốt nhất)
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
quality=quality,
n=n,
)
image_url = response.data[0].url
return True, image_url
except Exception as e:
error_msg = str(e)
if "<!DOCTYPE html>" in error_msg or "<html" in error_msg.lower():
user_msg = t("api_client.image_gen_unsupported")
logger.error(f"Image generation not supported by backend {backend.name}")
return False, user_msg
logger.error(f"Image generation failed: {error_msg}")
return False, t("api_client.error_prefix", error=error_msg)
# Phiên bản ứng dụng khách API toàn cầu
_api_client: Optional[APIClient] = None
def get_api_client() -> APIClient:
"""Nhận phiên bản máy khách API toàn cầu"""
global _api_client
if _api_client is None:
_api_client = APIClient()
return _api_client
def reinit_api_client() -> None:
"""Re-Khởi tạo API client (được gọi sau khi thay đổi cấu hình)"""
global _api_client
if _api_client is not None:
_api_client._init_clients()
+1035
View File
File diff suppressed because it is too large Load Diff
+646
View File
@@ -0,0 +1,646 @@
"""
Mô-đun Quản lý cấu hình - Hỗ trợ mã hóa thông tin nhạy cảm, quản lý phiên bản, xác thực
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 json
import os
from typing import List, Dict, Any, Optional, Union
from dataclasses import dataclass, asdict
from datetime import datetime
import hashlib
import logging
from locales.i18n import t
from database import get_db
logger = logging.getLogger(__name__)
# Giới hạn kích thước tệp tối đa (50MB)
MAX_FILE_SIZE = 50 * 1024 * 1024
# Các định dạng tệp cấu hình được hỗ trợ
SUPPORTED_CONFIG_FORMATS = [".json", ".yaml", ".yml"]
# Cấu hình nhà cung cấp API
API_PROVIDERS = {
"openai": {
"name": "OpenAI",
"default_model": "gpt-4o-mini",
"base_url": "https://api.openai.com/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "OpenAI API chính thức"
},
"openai_compatible": {
"name": "OpenAI (Giao diện tương thích)",
"default_model": "gpt-3.5-turbo",
"base_url": "",
"api_key_field": "api_key",
"requires_custom_url": True,
"description": "Dịch vụ bên thứ ba tương thích với định dạng OpenAI API"
},
"anthropic": {
"name": "Anthropic",
"default_model": "claude-3-5-sonnet-20241022",
"base_url": "https://api.anthropic.com",
"api_key_field": "x-api-key",
"requires_custom_url": False,
"description": "Mô hình Claude"
},
"google": {
"name": "Google",
"default_model": "gemini-1.5-pro",
"base_url": "https://generativelanguage.googleapis.com",
"api_key_field": "key",
"requires_custom_url": False,
"description": "Mô hình Gemini"
},
"alibaba": {
"name": "Alibaba DashScope",
"default_model": "qwen-turbo",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Qwen"
},
"deepseek": {
"name": "DeepSeek",
"default_model": "deepseek-chat",
"base_url": "https://api.deepseek.com/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "DeepSeek-V3"
},
"zhipu": {
"name": "Zhipu AI",
"default_model": "glm-4",
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng GLM"
},
"groq": {
"name": "Groq",
"default_model": "llama3-70b-8192",
"base_url": "https://api.groq.com/openai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Llama3, Mixtral"
},
"together": {
"name": "Together AI",
"default_model": "meta-llama/Llama-3-70b-chat-hf",
"base_url": "https://api.together.xyz/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Llama, Qwen"
},
"fireworks": {
"name": "Fireworks AI",
"default_model": "accounts/fireworks/models/llama-v3-70b-instruct",
"base_url": "https://api.fireworks.ai/inference/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Llama, Mixtral"
},
"mistral": {
"name": "Mistral AI",
"default_model": "mistral-large-latest",
"base_url": "https://api.mistral.ai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Mistral Large, Pixtral"
},
"openrouter": {
"name": "OpenRouter",
"default_model": "anthropic/claude-3.5-sonnet",
"base_url": "https://openrouter.ai/api/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Tổng hợp đa mô hình (GPT, Claude, v.v.)"
},
"deepinfra": {
"name": "DeepInfra",
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
"base_url": "https://api.deepinfra.com/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Lưu trữ mô hình mã nguồn mở"
},
"anyscale": {
"name": "Anyscale Endpoints",
"default_model": "meta-llama/Llama-3-70b-chat-hf",
"base_url": "https://api.endpoints.anyscale.com/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Llama, Mistral"
},
"perplexity": {
"name": "Perplexity AI",
"default_model": "llama-3.1-sonar-small-128k-online",
"base_url": "https://api.perplexity.ai",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Sonar, Llama"
},
"hyperbolic": {
"name": "Hyperbolic",
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
"base_url": "https://api.hyperbolic.xyz/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Mô hình mã nguồn mở"
},
"siliconflow": {
"name": "SiliconFlow",
"default_model": "Qwen/Qwen2.5-72B-Instruct",
"base_url": "https://api.siliconflow.cn/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Qwen, Llama"
},
"moonshot": {
"name": "Moonshot AI (Kimi)",
"default_model": "moonshot-v1-8k",
"base_url": "https://api.moonshot.ai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Kimi"
},
"novita": {
"name": "Novita AI",
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
"base_url": "https://api.novita.ai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Lưu trữ mô hình mã nguồn mở"
},
"baichuan": {
"name": "Baichuan AI",
"default_model": "Baichuan4",
"base_url": "https://api.baichuan-ai.com/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Baichuan"
},
"cerebras": {
"name": "Cerebras",
"default_model": "llama3.1-70b",
"base_url": "https://api.cerebras.ai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Llama"
},
"sambanova": {
"name": "SambaNova",
"default_model": "Meta-Llama-3.1-70B-Instruct",
"base_url": "https://api.sambanova.ai/v1",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Llama"
},
"volcengine": {
"name": "Volcengine (Doubao)",
"default_model": "doubao-pro-4k",
"base_url": "https://ark.volcengine.com/api/v3",
"api_key_field": "api_key",
"requires_custom_url": False,
"description": "Dòng Doubao"
}
}
@dataclass
class Backend:
"""Lớp dữ liệu cấu hình phụ trợ"""
name: str
type: str
base_url: str
api_key: str
model: str
enabled: bool = True
timeout: int = 120
retry_times: int = 3
is_default: bool = False
def validate(self) -> tuple[bool, str]:
"""Xác minh tính hợp lệ của cấu hình"""
if not self.name or not self.name.strip():
return False, t("config.backend_name_empty")
if self.type not in ["ollama", "openai", "claude", "other"]:
return False, t("config.unsupported_type", type=self.type)
if not self.base_url or not self.base_url.strip().startswith(("http://", "https://")):
return False, t("config.base_url_invalid")
# loại ollama cho phép api_key trống
if self.type != "ollama" and (not self.api_key or not self.api_key.strip()):
return False, t("config.api_key_empty")
if not self.model or not self.model.strip():
return False, t("config.model_empty")
if self.timeout < 5 or self.timeout > 10000:
return False, t("config.timeout_range")
if self.retry_times < 1 or self.retry_times > 10:
return False, t("config.retry_range")
return True, "OK"
@dataclass
class GenerationConfig:
"""Cấu hình tham số tạo"""
temperature: float = 0.7
top_p: float = 0.9
top_k: int = 40
max_tokens: int = 16384
chapter_target_words: int = 4000
writing_style: str = "Trôi chảy tự nhiên, tình tiết chặt chẽ, miêu tả nhân vật tinh tế"
writing_tone: str = "Trung lập"
character_development: str = "Chi tiết"
plot_complexity: str = "Trung bình"
def validate(self) -> tuple[bool, str]:
"""Xác minh tính hợp lệ của các tham số"""
if not 0.1 <= self.temperature <= 2.0:
return False, t("config.temp_range")
if not 0.1 <= self.top_p <= 1.0:
return False, t("config.top_p_range")
if self.max_tokens < 100 or self.max_tokens > 100000:
return False, t("config.max_tokens_range")
if self.chapter_target_words < 500 or self.chapter_target_words > 65536:
return False, t("config.chapter_words_range")
return True, "OK"
class ConfigManager:
"""Trình quản lý cấu hình - Chế độ đơn"""
_instance: Optional["ConfigManager"] = None
def __new__(cls) -> "ConfigManager":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.backends: List[Backend] = []
self.generation: GenerationConfig = GenerationConfig()
self.version: str = "4.0.0"
self.last_modified: str = datetime.now().isoformat()
self._load()
self._initialized = True
def _load(self) -> None:
"""Tải cấu hình từ SQLite"""
try:
conn = get_db()
# Tải backends
rows = conn.execute("SELECT * FROM backends ORDER BY id").fetchall()
if rows:
for row in rows:
try:
backend = Backend(
name=row["name"],
type=row["type"],
base_url=row["base_url"],
api_key=row["api_key"],
model=row["model"],
enabled=bool(row["enabled"]),
timeout=row["timeout"],
retry_times=row["retry_times"],
is_default=bool(row["is_default"])
)
valid, msg = backend.validate()
if valid:
self.backends.append(backend)
else:
logger.warning(f"Skip invalid backend {backend.name}: {msg}")
except Exception as e:
logger.warning(f"Load backend config failed: {e}")
# Tải generation config
gen_row = conn.execute("SELECT value FROM config WHERE key = 'generation'").fetchone()
if gen_row:
try:
gen_data = json.loads(gen_row["value"])
gen_data = {k: v for k, v in gen_data.items()
if k in GenerationConfig.__dataclass_fields__}
self.generation = GenerationConfig(**gen_data)
except Exception as e:
logger.warning(f"Load generation config failed: {e}")
# Tải version
ver_row = conn.execute("SELECT value FROM config WHERE key = 'version'").fetchone()
if ver_row:
self.version = ver_row["value"]
mod_row = conn.execute("SELECT value FROM config WHERE key = 'last_modified'").fetchone()
if mod_row:
self.last_modified = mod_row["value"]
if rows or gen_row:
logger.info("Config loaded from database")
else:
logger.info("No config in database, using defaults")
self._init_default()
except Exception as e:
logger.error(f"Config load failed: {e}")
self._init_default()
def _init_default(self) -> None:
"""Khởi tạo cấu hình mặc định"""
self.backends = [
Backend(
name=t("config.default_backend_name"),
type="ollama",
base_url="http://localhost:11434/v1",
api_key="ollama",
model="llama3.1:latest"
)
]
self.generation = GenerationConfig()
self.save()
def save(self) -> tuple[bool, str]:
"""Lưu cấu hình vào SQLite"""
try:
conn = get_db()
now = datetime.now().isoformat()
# Tạo bản sao lưu
backup_data = {
"version": self.version,
"last_modified": self.last_modified,
"backends": [asdict(b) for b in self.backends],
"generation": asdict(self.generation),
}
conn.execute(
"INSERT INTO config_backups (data, created_at) VALUES (?, ?)",
(json.dumps(backup_data, ensure_ascii=False), now)
)
# Xóa backends cũ và insert lại
conn.execute("DELETE FROM backends")
for b in self.backends:
conn.execute("""
INSERT INTO backends
(name, type, base_url, api_key, model, enabled, timeout, retry_times, is_default, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
b.name, b.type, b.base_url, b.api_key, b.model,
1 if b.enabled else 0, b.timeout, b.retry_times,
1 if b.is_default else 0, now, now
))
# Lưu generation config
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
("generation", json.dumps(asdict(self.generation), ensure_ascii=False), now)
)
# Lưu version + last_modified
self.last_modified = now
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
("version", self.version, now)
)
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
("last_modified", now, now)
)
conn.commit()
logger.info("Config saved to database")
return True, t("config.config_save_success")
except Exception as e:
logger.error(f"Config save failed: {e}")
return False, t("config.config_save_failed", error=str(e))
def add_backend(self, backend: Backend) -> tuple[bool, str]:
"""Thêm backend"""
valid, msg = backend.validate()
if not valid:
return False, msg
# Kiểm tra sự trùng lặp
if any(b.name == backend.name for b in self.backends):
return False, t("config.backend_exists", name=backend.name)
self.backends.append(backend)
success, msg = self.save()
return success, msg if not success else t("config.backend_add_success")
def update_backend(self, name: str, **kwargs) -> tuple[bool, str]:
"""Cập nhật cấu hình phụ trợ"""
for backend in self.backends:
if backend.name == name:
for key, value in kwargs.items():
if hasattr(backend, key):
setattr(backend, key, value)
valid, msg = backend.validate()
if not valid:
return False, msg
success, msg = self.save()
return success, msg if not success else t("config.backend_update_success")
return False, t("config.backend_not_found", name=name)
def delete_backend(self, name: str) -> tuple[bool, str]:
"""Xóa backend"""
self.backends = [b for b in self.backends if b.name != name]
success, msg = self.save()
return success, msg if not success else t("config.backend_delete_success", name=name)
def set_default_backend(self, name: str) -> tuple[bool, str]:
"""Đặt giao diện làm mặc định"""
found = False
for backend in self.backends:
if backend.name == name:
backend.is_default = True
found = True
else:
backend.is_default = False
if found:
success, msg = self.save()
return success, msg if not success else "Success"
return False, t("config.backend_not_found", name=name)
def get_enabled_backends(self) -> List[Backend]:
"""Nhận tất cả các phụ trợ được kích hoạt"""
return [b for b in self.backends if b.enabled]
def update_generation_config(self, **kwargs) -> tuple[bool, str]:
"""Cập nhật cấu hình bản dựng"""
for key, value in kwargs.items():
if hasattr(self.generation, key):
setattr(self.generation, key, value)
valid, msg = self.generation.validate()
if not valid:
return False, msg
success, msg = self.save()
return success, msg if not success else t("config.gen_params_update_success")
def export_config(self, filepath: str) -> tuple[bool, str]:
"""Xuất cấu hình (không chứa thông tin nhạy cảm)"""
try:
data = {
"version": self.version,
"backends": [{"name": b.name, "type": b.type, "model": b.model}
for b in self.backends],
"generation": asdict(self.generation),
}
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
return True, t("config.config_export_success", filepath=filepath)
except Exception as e:
return False, t("config.config_export_failed", error=str(e))
def to_dict(self) -> Dict[str, Any]:
"""Chuyển đổi cấu hình sang định dạng từ điển"""
return {
"backends": [asdict(b) for b in self.backends],
"generation": asdict(self.generation),
"system": {
"logging": {
"level": "INFO",
"file": "logs/novel_generator.log",
"console_output": True
},
"concurrency": {
"max_workers": 4,
"request_timeout": 30
},
"cache": {
"enabled": True,
"type": "file",
"location": "cache",
"ttl": 3600
}
},
"export": {
"default_format": "markdown",
"output_directory": "output",
"supported_formats": ["markdown", "pdf", "docx", "txt", "epub"]
},
"ui": {
"theme": "light",
"language": "zh-CN",
"editor": {
"font_size": 14,
"font_family": "Microsoft YaHei, sans-serif",
"tab_size": 2,
"word_wrap": True
}
},
"project": {
"auto_save": {
"enabled": True,
"interval": 300,
"backup_count": 5
},
"backup": {
"enabled": True,
"location": "backups",
"schedule": "daily",
"keep_days": 30
},
"templates": {
"enabled": True,
"location": "project_templates",
"default_template": "standard_novel"
}
},
"plugins": {
"enabled": True,
"directory": "plugins",
"auto_load": True,
"enabled_plugins": [
"style_analyzer",
"grammar_checker",
"character_tracker",
"plot_generator"
]
},
"advanced": {
"performance": {
"enable_profiling": False,
"memory_limit": "1GB",
"cpu_limit": 80
},
"debug": {
"show_errors": False,
"debug_mode": False,
"trace_requests": False
},
"monitoring": {
"enabled": False,
"metrics_port": 8080,
"health_check_interval": 30
}
}
}
@staticmethod
def get_api_providers() -> Dict[str, Dict[str, Any]]:
"""Nhận tất cả cấu hình của nhà cung cấp API"""
return API_PROVIDERS
@staticmethod
def get_api_provider_choices() -> List[str]:
"""Nhận danh sách lựa chọn nhà cung cấp API"""
return [provider["name"] for provider in API_PROVIDERS.values()]
@staticmethod
def get_api_provider_info(provider_key: str) -> Optional[Dict[str, Any]]:
"""Nhận thông tin nhà cung cấp dựa trên khóa nhà cung cấp"""
return API_PROVIDERS.get(provider_key)
@staticmethod
def get_api_provider_key_by_name(provider_name: str) -> Optional[str]:
"""Nhận khóa nhà cung cấp dựa trên tên nhà cung cấp"""
for key, provider in API_PROVIDERS.items():
if provider["name"] == provider_name:
return key
return None
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"""
Tải cấu hình từ database hoặc file chỉ định
Args:
config_path: đường dẫn file cấu hình (hỗ trợ import từ file)
Returns:
Từ điển cấu hình
"""
if config_path:
# Import từ file chỉ định
if not os.path.exists(config_path):
raise FileNotFoundError(t("config.config_file_missing", path=config_path))
file_ext = os.path.splitext(config_path)[1].lower()
if file_ext == ".json":
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
else:
raise ValueError(t("config.config_format_unsupported", ext=file_ext))
else:
return get_config().to_dict()
def get_config() -> ConfigManager:
"""Nhận phiên bản cấu hình toàn cầu"""
return ConfigManager()
def get_config_manager() -> ConfigManager:
"""Nhận phiên bản trình quản lý cấu hình toàn cầu (bí danh)"""
return get_config()
+300
View File
@@ -0,0 +1,300 @@
"""
-đun Quản cấu hình Web API
Hỗ trợ thêm, sửa, xóa, kiểm tra giao diện API qua Web UI
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 json
from typing import Dict, List, Tuple, Any
from dataclasses import asdict
from config import Backend, get_config
from api_client import get_api_client, reinit_api_client
from logger import get_logger
from locales.i18n import t
logger = get_logger("ConfigAPI")
class ConfigAPIManager:
"""API quản lý cấu hình"""
@staticmethod
def list_backends() -> Dict[str, Any]:
"""Nhận danh sách tất cả các phụ trợ"""
try:
config = get_config()
backends_data = []
for backend in config.backends:
backend_dict = asdict(backend)
backends_data.append(backend_dict)
return {
"success": True,
"data": backends_data,
"message": t("app.backends_loaded", count=len(backends_data))
}
except Exception as e:
logger.error(f"List backends failed: {e}")
return {
"success": False,
"data": [],
"message": f"{str(e)}"
}
@staticmethod
def add_backend(name: str, type: str, base_url: str, api_key: str,
model: str, timeout: int = 30, retry_times: int = 3,
enabled: bool = True) -> Dict[str, Any]:
"""Thêm cấu hình phụ trợ mới"""
try:
config = get_config()
# Kiểm tra xem tên có trùng lặp không
for backend in config.backends:
if backend.name == name:
return {
"success": False,
"message": t("config_api.name_exists", name=name)
}
# Tạo phụ trợ mới
new_backend = Backend(
name=name,
type=type,
base_url=base_url,
api_key=api_key,
model=model,
timeout=timeout,
retry_times=retry_times,
enabled=enabled
)
# Xác minh cấu hình phụ trợ
valid, msg = new_backend.validate()
if not valid:
return {
"success": False,
"message": f"{msg}"
}
# Thêm phụ trợ
config.backends.append(new_backend)
success, save_msg = config.save()
if success:
logger.info(f"Backend added: {name}")
return {
"success": True,
"message": t("config_api.add_success", name=name),
"backend": asdict(new_backend)
}
else:
return {
"success": False,
"message": t("config_api.add_failed", error=save_msg)
}
except Exception as e:
logger.error(f"Add backend failed: {e}")
return {
"success": False,
"message": t("config_api.add_failed", error=str(e))
}
@staticmethod
def update_backend(name: str, **kwargs) -> Dict[str, Any]:
"""Cập nhật cấu hình backend"""
try:
config = get_config()
success, msg = config.update_backend(name, **kwargs)
if success:
logger.info(f"Backend updated: {name}")
return {
"success": True,
"message": msg
}
else:
return {
"success": False,
"message": msg
}
except Exception as e:
logger.error(f"Update backend failed: {e}")
return {
"success": False,
"message": t("config_api.update_failed", error=str(e))
}
@staticmethod
def delete_backend(name: str) -> Dict[str, Any]:
"""Xóa cấu hình phụ trợ"""
try:
config = get_config()
success, msg = config.delete_backend(name)
if success:
logger.info(f"Backend deleted: {name}")
return {
"success": True,
"message": msg
}
else:
return {
"success": False,
"message": msg
}
except Exception as e:
logger.error(f"Delete backend failed: {e}")
return {
"success": False,
"message": t("config_api.delete_failed", error=str(e))
}
@staticmethod
def toggle_backend(name: str, enabled: bool) -> Dict[str, Any]:
"""Bật/tắt phụ trợ"""
try:
config = get_config()
success, msg = config.update_backend(name, enabled=enabled)
if success:
status = t("config_api.toggle_enabled") if enabled else t("config_api.toggle_disabled")
logger.info(f"Backend {name}: {status}")
return {
"success": True,
"message": t("config_api.toggle_success", name=name, status=status)
}
else:
return {
"success": False,
"message": msg
}
except Exception as e:
logger.error(f"Toggle backend failed: {e}")
return {
"success": False,
"message": t("config_api.toggle_failed", error=str(e))
}
@staticmethod
def set_default_backend(name: str) -> Dict[str, Any]:
"""Đặt giao diện làm mặc định"""
try:
config = get_config()
success, msg = config.set_default_backend(name)
if success:
logger.info(f"Backend set to default: {name}")
return {
"success": True,
"message": t("config_api.default_success", name=name)
}
else:
return {
"success": False,
"message": msg
}
except Exception as e:
logger.error(f"Set default backend failed: {e}")
return {
"success": False,
"message": t("config_api.default_failed", error=str(e))
}
@staticmethod
def test_backend(name: str) -> Dict[str, Any]:
"""Kiểm tra kết nối backend"""
try:
config = get_config()
backend = None
# Tìm phần phụ trợ được chỉ định
for b in config.backends:
if b.name == name:
backend = b
break
if not backend:
return {
"success": False,
"message": t("config_api.test_not_found", name=name)
}
# kết nối thử nghiệm
if not backend.enabled:
return {
"success": False,
"message": f"{name} disabled"
}
# Kiểm tra bằng ứng dụng khách API
try:
api_client = get_api_client()
# Hãy thử lấy thông tin model để kiểm tra kết nối
test_response = api_client.test_connection(backend.base_url, backend.api_key, backend.model)
if test_response:
logger.info(f"Backend test passed: {name}")
return {
"success": True,
"message": t("config_api.test_success", name=name),
"backend": name,
"model": backend.model
}
else:
return {
"success": False,
"message": t("config_api.test_failed", name=name, error="No response")
}
except Exception as test_error:
logger.error(f"Backend test error: {test_error}")
return {
"success": False,
"message": t("config_api.test_failed", name=name, error=str(test_error))
}
except Exception as e:
logger.error(f"Test backend failed: {e}")
return {
"success": False,
"message": t("config_api.test_failed", name="", error=str(e))
}
@staticmethod
def get_backend_types() -> List[str]:
"""Nhận danh sách các loại phụ trợ được hỗ trợ"""
return ["ollama", "openai", "claude", "other"]
@staticmethod
def export_config(filepath: str) -> Dict[str, Any]:
"""Xuất file cấu hình"""
try:
config = get_config()
success, msg = config.export_config(filepath)
if success:
return {
"success": True,
"message": msg
}
else:
return {
"success": False,
"message": msg
}
except Exception as e:
logger.error(f"Export config failed: {e}")
return {
"success": False,
"message": t("config_api.export_failed", error=str(e))
}
# Phiên bản trình quản lý API toàn cầu
config_api = ConfigAPIManager()
+160
View File
@@ -0,0 +1,160 @@
/* Modern & Aesthetic UI for Tinix Story (tinix-novels) */
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap');
:root {
--primary-color: #6366f1;
--primary-hover: #4f46e5;
--secondary-color: #64748b;
--bg-gradient: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
--card-bg: rgba(255, 255, 255, 0.85);
--border-radius-lg: 16px;
--border-radius-md: 12px;
--shadow-soft: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
--shadow-hover: 0 20px 30px -10px rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
background: var(--bg-gradient) !important;
min-height: 100vh;
}
h1,
h2,
h3 {
font-family: 'Outfit', sans-serif !important;
font-weight: 700 !important;
letter-spacing: -0.02em !important;
}
/* Header Styling */
.app-header {
text-align: center;
margin-bottom: 2rem;
padding: 2rem 0;
}
/* Containers & Cards */
.gradio-container {
max-width: 1200px !important;
}
.gr-group,
.gr-box,
.gr-form {
background-color: var(--card-bg) !important;
backdrop-filter: blur(12px) !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
border-radius: var(--border-radius-lg) !important;
box-shadow: var(--shadow-soft) !important;
transition: all 0.3s ease !important;
padding: 1.5rem !important;
margin-bottom: 1.5rem !important;
}
.gr-group:hover {
box-shadow: var(--shadow-hover) !important;
transform: translateY(-2px);
}
/* Tab Styling */
.tabs {
background: transparent !important;
border: none !important;
margin-bottom: 2rem !important;
}
.tab-nav {
display: flex !important;
justify-content: center !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important;
margin-bottom: 1.5rem !important;
}
.tabitem {
padding: 2rem !important;
background: transparent !important;
}
button.selected {
color: var(--primary-color) !important;
border-bottom: 2px solid var(--primary-color) !important;
}
/* Button Improvements */
button.gr-button-primary {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
border: none !important;
border-radius: var(--border-radius-md) !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3) !important;
font-weight: 600 !important;
transition: all 0.2s ease !important;
color: white !important;
}
button.gr-button-primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4) !important;
filter: brightness(1.1) !important;
}
button.gr-button-stop {
background: linear-gradient(135deg, #f43f5e, #e11d48) !important;
border-radius: var(--border-radius-md) !important;
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.3) !important;
font-weight: 600 !important;
transition: all 0.2s ease !important;
color: white !important;
}
/* Inputs & Textboxes */
textarea,
input[type="text"],
input[type="number"] {
border-radius: var(--border-radius-md) !important;
border: 1px solid rgba(0, 0, 0, 0.1) !important;
padding: 0.75rem !important;
background-color: rgba(255, 255, 255, 0.9) !important;
transition: border-color 0.2s ease !important;
}
textarea:focus,
input[type="text"]:focus {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important;
}
/* Micro-animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gradio-container {
animation: fadeIn 0.8s ease-out;
}
/* Custom classes we'll add to app.py */
.main-title h1 {
font-size: 3rem !important;
background: linear-gradient(to right, #6366f1, #ec4899);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.25rem !important;
}
.subtitle p {
color: var(--secondary-color) !important;
font-size: 1.1rem !important;
opacity: 0.8;
}
+150
View File
@@ -0,0 +1,150 @@
[
{
"name": "Huyền huyễn tiên hiệp",
"description": "Thế giới tu tiên rộng lớn, sức mạnh siêu phàm, phân chia nhiều cảnh giới rõ rệt. Cốt truyện thường xoay quanh hành trình thăng cấp, cướp đoạt cơ duyên, phi thăng tiên giới. Văn phong cần kỳ ảo, chú trọng miêu tả công pháp, pháp bảo, linh thú."
},
{
"name": "Đô thị ngôn tình",
"description": "Bối cảnh hiện đại, đời sống thành thị, xoay quanh các mối quan hệ tình cảm, gia đình, công sở. Tập trung vào tâm lý nhân vật, tình huống đời thường, lãng mạn hoặc ngược luyến. Lời thoại tự nhiên, gần gũi thực tế."
},
{
"name": "Khoa học viễn tưởng",
"description": "Lấy bối cảnh tương lai, vũ trụ, hoặc các thế giới với trình độ công nghệ/khoa học vượt bậc (AI, robot, du hành thời gian). Đòi hỏi sự logic, xây dựng hệ thống quy tắc công nghệ/vũ trụ chặt chẽ mang tính thuyết phục cao."
},
{
"name": "Võ hiệp",
"description": "Thế giới giang hồ, ân oán tình thù, võ công cái thế. Nhấn mạnh vào tinh thần trượng nghĩa, môn phái, các chiêu thức võ thuật võ lâm. Văn phong cổ trang, miêu tả chiêu thức hoa mỹ, tiết tấu nhanh."
},
{
"name": "Trinh thám",
"description": "Xoay quanh các vụ án bí ẩn, tội phạm và quá trình đi tìm lời giải, phá án. Yêu cầu tính logic cực cao, chuỗi manh mối đan xen, gây cấn, hồi hộp, tạo bất ngờ ở phút chót (plot twist)."
},
{
"name": "Lịch sử",
"description": "Bối cảnh dựa trên các triều đại lịch sử có thật hoặc hư cấu dựa trên bối cảnh lịch sử. Xoay quanh quyền mưu, tranh đoạt thiên hạ, chiến tranh giữa các quốc gia, xây dựng thế lực. Đòi hỏi kiến thức lịch sử, chính trị, văn phong trang trọng, mang đậm tính sử thi."
},
{
"name": "Quân sự",
"description": "Tập trung vào các đề tài chiến tranh, quân đội, vũ khí, và các chiến dịch quân sự. Nhân vật chính thường là quân nhân, nhà chiến lược. Yêu cầu tính logic, am hiểu về chiến thuật, vũ khí thực tế, miêu tả các trận đánh hoành tráng, khốc liệt."
},
{
"name": "Game",
"description": "Bối cảnh trong môi trường game thực tế ảo hoặc thế giới game kết hợp đời thực (Võng du). Nhân vật chính đánh quái, thăng cấp, cày đồ, lập guild, tham gia e-sports hoặc tranh bá. Cần hệ thống chỉ số, kỹ năng, trang bị rõ ràng, nhịp độ giải trí nhanh."
},
{
"name": "Kinh dị",
"description": "Cốt truyện rùng rợn, khai thác các yếu tố siêu nhiên, tâm linh, quái vật hoặc tâm lý học vặn vẹo. Bầu không khí tăm tối, u ám, miêu tả cảm giác sợ hãi tột độ của nhân vật để gây rùng mình cho người đọc."
},
{
"name": "Xuyên không - Trọng sinh",
"description": "Nhân vật chính du hành thời gian, không gian đến một thế giới khác hoặc sống lại kiếp trước. Thường mang theo kiến thức hiện đại hoặc bám sát ký ức kiếp trước để thay đổi số phận, vả mặt kẻ thù, xây dựng lại cuộc đời."
},
{
"name": "Hệ thống",
"description": "Nhân vật chính sở hữu một 'Hệ thống' (như một trí tuệ nhân tạo trong não) giao nhiệm vụ, thưởng phạt, cung cấp cửa hàng đổi vật phẩm, kỹ năng. Văn phong mang tính giải trí cao, nhịp độ nhanh, tập trung thăng cấp."
},
{
"name": "Đồng nhân",
"description": "Truyện dựa trên bối cảnh, nhân vật của một tác phẩm gốc có sẵn (như Naruto, Harry Potter, v.v.). Nhân vật chính thường xuyên không vào thế giới gốc, thay đổi cốt truyện hoặc tương tác với nhân vật gốc."
},
{
"name": "Mạt thế",
"description": "Bối cảnh tận thế, thảm họa zombie, thiên tai, hoặc biến dị sinh học. Con người đấu tranh sinh tồn, thế giới xuất hiện các dị năng giả. Nhấn mạnh sự tàn khốc của nhân tính, thiếu thốn vật tư và xây dựng căn cứ."
},
{
"name": "Điền văn - Hài hước",
"description": "Tập trung vào cuộc sống thường nhật, trồng trọt, chăn nuôi, làm giàu hoặc gia đình êm ấm. Nhịp độ chậm rãi (slow-burn), nhẹ nhàng, thư giãn, pha trộn nhiều tình huống hài hước, dở khóc dở cười."
},
{
"name": "Cổ đại ngôn tình",
"description": "Bối cảnh phong kiến, xoay quanh tình yêu nam nữ. Khai thác gia đấu (những mâu thuẫn gia tộc), cung đấu (tranh giành quyền lực chốn hậu cung) hoặc quyền mưu quyền thần. Lời thoại cổ kính, trang nhã."
},
{
"name": "Kỳ ảo phương Tây",
"description": "Bối cảnh phương Tây thời Trung Cổ, có hiệp sĩ, phép thuật, elf, rồng, ma cà rồng... Hệ thống ma pháp và thế giới quan mang đậm nét thần thoại hoặc fantasy (như D&D, Warcraft)."
},
{
"name": "Nữ cường",
"description": "Nữ chính có tính cách kiên cường, thông minh, độc lập, hoặc sở hữu sức mạnh vượt trội. Truyện thường tập trung vào quá trình tự vươn lên, phá bỏ định kiến, đối mặt kẻ thù mà không dựa dẫm vào nam chính."
},
{
"name": "Tổng tài",
"description": "Xa hoa, tập trung vào nam chính là những chủ tịch (tổng tài) giàu có, lạnh lùng, quyền lực, cùng nữ chính thường có xuất thân thấp hơn hoặc có vướng mắc tình cảm phức tạp. Yếu tố sủng ngọt hoặc ngược luyến tình thâm thường được đẩy mạnh."
},
{
"name": "Thanh xuân vườn trường",
"description": "Bối cảnh trường học, thanh xuân rực rỡ. Khai thác tình yêu tuổi học trò trong sáng, nhiệt huyết thanh xuân, tình bạn, vượt qua áp lực thi cử và những rung động đầu đời."
},
{
"name": "Cung đấu",
"description": "Bối cảnh mưu mô xảo quyệt chốn hậu cung phong kiến. Các phi tần, hoàng hậu, cung nữ dùng trí tuệ, mưu kế triệt hạ lẫn nhau để tranh giành sủng ái và quyền lực. Bầu không khí căng thẳng, máu lạnh."
},
{
"name": "Gia đấu",
"description": "Bối cảnh trong những gia tộc lớn thời phong kiến. Đấu tranh, kèn cựa giữa mẹ chồng nàng dâu, các phòng, các chị em gái để bảo vệ lợi ích và vị thế trong gia đình. Đòi hỏi logic cao trị gia."
},
{
"name": "Hồng hoang",
"description": "Dựa trên hệ thống thần thoại Trung Hoa cổ đại (Bàn Cổ khai thiên, Nữ Oa tạo nhân,...). Hệ thống sức mạnh cực kỳ khổng lồ, bối cảnh cấp bậc thần thánh vô lượng kiếp quy mô vũ trụ."
},
{
"name": "Ngôn tình võng du",
"description": "Kết hợp game online và tình cảm đời thực. Tuyến tình cảm phát triển song song trong thế giới ảo và đời thực, có sự kiện offline, PK, đấu giải giữa các bang phái đầy thú vị."
},
{
"name": "Đô thị dị năng",
"description": "Bối cảnh xã hội hiện đại nhưng đan xen những con người sở hữu năng lực đặc biệt (dị năng), tổ chức ngầm, hoặc yêu quái ẩn mình. Đòi hỏi sự kết hợp cân bằng giữa đời sống thực và thế giới huyền bí."
},
{
"name": "Linh dị - Bí ẩn",
"description": "Xoay quanh tà ma, phong thủy, đạo sĩ trừ tà, hoặc những hiện tượng tâm linh không thể lý giải bằng khoa học. Không quá kinh dị tột độ mà chú trọng vào yếu tố huyền bí, hồi hộp khám phá sự thật."
},
{
"name": "Đam mỹ",
"description": "Khai thác câu chuyện tình cảm sâu sắc, tinh tế hoặc ngang trái giữa hai nhân vật nam. Văn phong trau chuốt, chú trọng tâm lý, có thể lồng ghép mọi bối cảnh (cổ đại, hiện đại, mạt thế, tinh tế, v.v.)."
},
{
"name": "Bách hợp",
"description": "Tập trung miêu tả tuyến tình cảm nhẹ nhàng, gắn bó hoặc mãnh liệt giữa hai nhân vật nữ. Duyên dáng, thiên về khai phá cảm xúc tinh tế, đồng cảm nội tâm, kết hợp nhiều bối cảnh khác nhau."
},
{
"name": "Thám hiểm lăng mộ",
"description": "Hành trình trộm mộ, săn bảo vật ở các di tích cổ xưa chứa đầy cạm bẫy, cương thi, quái vật (như Đạo Mộ Bút Ký, Ma Thổi Đèn). Các chi tiết về đạo cụ, phong thủy, địa lý phải cực kỳ sống động và hấp dẫn."
},
{
"name": "Dị giới đại lục",
"description": "Thế giới hoàn toàn hư cấu với bản đồ lục địa rộng lớn, có thể bao gồm kiếm thuật, ma pháp hoặc đấu khí. Tôn trọng luật rừng kẻ mạnh làm vua, mô phỏng các vương quốc, chủng tộc đa dạng tranh đấu."
},
{
"name": "Cổ đại làm ruộng",
"description": "Một nhánh phụ của Điền văn nhưng nhấn mạnh vào bối cảnh cổ đại nghèo khó. Quá trình làm giàu chậm rãi từng bước từ hai bàn tay trắng, kinh doanh buôn bán, xây dựng gia đình no ấm."
},
{
"name": "Không gian Tùy thân",
"description": "Nhân vật chính sở hữu một không gian bí mật (vòng tay, ngọc bội) có thể vào đó trồng trọt linh dược, chứa đồ, trữ nước thần, hoặc trốn tránh kẻ thù. Là bàn đạp lớn cho quá trình thăng cấp."
},
{
"name": "ABO",
"description": "Bối cảnh Omegaverse (Alpha, Beta, Omega) với các đặc điểm sinh học và chất dẫn dụ đặc thù, thường lấy bối cảnh Tinh Tế (vũ trụ). Nhấn mạnh bản năng, sự kiểm soát, đánh dấu và các mối quan hệ tình cảm gai góc."
},
{
"name": "Ma cà rồng",
"description": "Truyện xoay quanh sinh vật Ma cà rồng (Vampire), ma lang (Người sói), thợ săn. Thể hiện sự đấu tranh giữa bản năng khát máu và nhân tính, thường mang sắc thái lãng mạn tăm tối (Dark Romance)."
},
{
"name": "Cạnh kỹ - Thể thao",
"description": "Nhiệt huyết thanh xuân, thi đấu e-sports hoặc các môn thể thao truyền thống (bóng rổ, điền kinh). Tôn vinh tinh thần đồng đội, nỗ lực luyện tập, vinh quang thi đấu, các chiến thuật đối kháng kịch tính."
},
{
"name": "Đồng nhân Anime",
"description": "Viết dựa theo thế giới của các bộ Manga/Anime đình đám (One Piece, Pokemon, Bleach,...). Tương tác với các nhân vật được yêu thích, bổ sung những cái kết luyến tiếc hoặc tạo cuộc phiêu lưu hoàn toàn mới."
},
{
"name": "Vô hạn lưu",
"description": "Nhân vật chính bị kéo vào một 'Không gian Chủ Thần', buộc phải xuyên qua nhiều thế giới (phim ảnh, game, ác mộng) để làm nhiệm vụ sinh tử, kiếm điểm nâng cấp. Nhịp độ dồn dập, hack não và nguy hiểm."
},
{
"name": "Khác",
"description": "Các thể loại không nằm trong các phân loại chính, hoặc pha trộn nhiều yếu tố khác nhau. AI cần linh hoạt kết hợp các yếu tố trong bối cảnh và yêu cầu riêng của tác giả để sáng tác cho phù hợp."
}
]
+402
View File
@@ -0,0 +1,402 @@
[
{
"name": "Hậu cung",
"description": "Nhân vật chính (thường là nam) có mối quan hệ tình cảm hoặc chung sống với nhiều nhân vật khác giới. Tập trung vào việc quản lý các mối quan hệ phức tạp và sự cạnh tranh giữa các thành viên."
},
{
"name": "1v1",
"description": "Mối quan hệ chính chỉ gồm một cặp đôi duy nhất xuyên suốt tác phẩm, cam kết chung thủy và tập trung sâu vào sự phát triển tình cảm của hai người."
},
{
"name": "NP",
"description": "Nhiều người yêu (N-Persons), nhân vật chính có mối quan hệ tình cảm đồng thời với nhiều người khác, thường không theo mô hình một vợ một chồng truyền thống."
},
{
"name": "Nữ phẫn nam trang",
"description": "Nhân vật nữ cải trang thành nam giới vì lý do bối cảnh, bảo vệ bản thân hoặc nhiệm vụ. Tạo ra các tình huống hiểu lầm và kịch tính trong tương tác xã hội."
},
{
"name": "Nam phẫn nữ trang",
"description": "Nhân vật nam cải trang thành nữ giới. Thường xuất hiện trong các bối cảnh hài hước, thâm nhập hoặc sở thích cá nhân, tạo ra các tình huống dở khóc dở cười."
},
{
"name": "Xuyên không",
"description": "Nhân vật từ thế giới hoặc thời đại này vượt qua không gian/thời gian để đến một thế giới hoặc thời đại khác. Thường tận dụng kiến thức hiện đại để thay đổi vận mệnh."
},
{
"name": "Xuyên sách",
"description": "Nhân vật xuyên vào một cuốn tiểu thuyết đã biết trước cốt truyện, thường đóng vai phản diện hoặc nhân vật phụ và cố gắng thay đổi kết cục bi thảm của mình."
},
{
"name": "Trọng sinh",
"description": "Nhân vật chết đi và được sống lại ở một thời điểm trong quá quá khứ của chính mình, mang theo ký ức và kinh nghiệm từ kiếp trước để sửa chữa sai lầm."
},
{
"name": "Hệ thống",
"description": "Nhân vật sở hữu một thực thể trí tuệ hoặc giao diện điện tử cung cấp nhiệm vụ, phần thưởng và khả năng đặc biệt để thăng tiến sức mạnh nhanh chóng."
},
{
"name": "Bàn tay vàng",
"description": "Chỉ những khả năng, vật phẩm hoặc may mắn cực lớn mà tác giả ban cho nhân vật chính, giúp họ vượt qua mọi nghịch cảnh một cách dễ dàng và áp đảo đối thủ."
},
{
"name": "Không gian tùy thân",
"description": "Nhân vật sở hữu một không gian bí mật có thể lưu trữ vật phẩm, trồng trọt hoặc tu luyện, thường liên kết trực tiếp với cơ thể hoặc vật dụng mang theo."
},
{
"name": "Linh tuyền",
"description": "Một nguồn nước thần kỳ có khả năng chữa lành vết thương, tăng cường sức khỏe hoặc nâng cao tư chất tu luyện, thường nằm trong không gian tùy thân."
},
{
"name": "Đọc tâm thuật",
"description": "Khả năng nghe được suy nghĩ thầm kín của người khác, tạo ra lợi thế tuyệt đối trong giao tiếp, đàm phán và phát hiện âm mưu."
},
{
"name": "Vô địch lưu",
"description": "Nhân vật chính ngay từ đầu đã có sức mạnh tuyệt đối, không có đối thủ xứng tầm, tập trung vào việc thể hiện sức mạnh và giải quyết vấn đề theo cách áp đảo."
},
{
"name": "Cẩu đạo",
"description": "Nhân vật chính cực kỳ cẩn trọng, ẩn mình, tránh xa rắc rối và chỉ hành động khi nắm chắc phần thắng, ưu tiên sự an toàn và trường thọ lên hàng đầu."
},
{
"name": "Nhiệt huyết",
"description": "Tập trung vào sự nỗ lực không ngừng, tình bạn, lòng dũng cảm và các cuộc chiến đấu đầy cảm xúc để đạt được mục tiêu cao cả."
},
{
"name": "Hài hước",
"description": "Sử dụng các tình huống trớ trêu, lời thoại hóm hỉnh và các nhân vật kỳ lạ để tạo ra tiếng cười và không khí thư giãn cho độc giả."
},
{
"name": "Sảng văn",
"description": "Loại truyện nhấn mạnh vào sự thỏa mãn của độc giả thông qua việc nhân vật chính liên tục thành công, tát mặt đối thủ và nhận được phần thưởng hậu hĩnh."
},
{
"name": "Ngọt sủng",
"description": "Tập trung vào mối quan hệ tình cảm vô cùng ngọt ngào, ít mâu thuẫn, nhân vật chính luôn được che chở và yêu thương hết mực."
},
{
"name": "Ngược luyến",
"description": "Khai thác những đau khổ, dằn vặt và hiểu lầm trong tình yêu, lấy nước mắt của độc giả thông qua những tình tiết bi kịch."
},
{
"name": "Gương vỡ lại lành",
"description": "Hai người yêu nhau vì lý do nào đó mà chia tay, sau một thời gian xa cách lại gặp nhau và quyết định hàn gắn mối quan hệ cũ."
},
{
"name": "Cưới trước yêu sau",
"description": "Cặp đôi kết hôn vì thỏa thuận, nhiệm vụ hoặc ép buộc, sau đó trong quá trình chung sống mới dần hiểu nhau và phát sinh tình cảm chân thành."
},
{
"name": "Oan gia ngõ hẹp",
"description": "Hai nhân vật ban đầu có ác cảm hoặc thường xuyên tranh cãi, nhưng qua các biến cố lại trở nên gắn bó và yêu nhau."
},
{
"name": "Thanh mai trúc mã",
"description": "Đề tài về những cặp bạn thân từ thuở nhỏ, cùng nhau lớn lên và dần phát triển tình cảm từ tình bạn thành tình yêu nam nữ."
},
{
"name": "Hào môn thế gia",
"description": "Bối cảnh giới thượng lưu, các gia tộc giàu có với những cuộc chiến tranh giành quyền lực, tài sản và các mối quan hệ xã hội phức tạp."
},
{
"name": "Tổng tài",
"description": "Nhân vật chính là lãnh đạo cấp cao của một tập đoàn lớn, giàu có, quyền lực và thường mang phong thái lạnh lùng hoặc bá đạo."
},
{
"name": "Minh tinh",
"description": "Khai thác cuộc sống và sự nghiệp của những người nổi tiếng trong giới nghệ thuật, từ sự rực rỡ trên sân khấu đến những góc khuất đời tư."
},
{
"name": "Giới giải trí",
"description": "Bối cảnh xoay quanh showbiz, các quy tắc ngầm, sự cạnh tranh giữa các nghệ sĩ và quá trình vươn tới đỉnh cao sự nghiệp."
},
{
"name": "Vườn trường",
"description": "Bối cảnh học đường, xoay quanh cuộc sống của học sinh, sinh viên với những kỷ niệm trong sáng, tình bạn và tình khôi ngây ngô."
},
{
"name": "Học bá",
"description": "Nhân vật có thành tích học tập xuất sắc, trí tuệ siêu việt, thường giải quyết các vấn đề bằng kiến thức và sự thông minh."
},
{
"name": "Võng du",
"description": "Bối cảnh trong các trò chơi trực tuyến, kết hợp giữa đời thực và thế giới ảo, tập trung vào việc cày cấp, lập đội và các giải đấu game."
},
{
"name": "E-sports",
"description": "Thể thao điện tử chuyên nghiệp, tập trung vào tinh thần đồng đội, sự khổ luyện và hành trình chinh phục các chức vô địch thế giới."
},
{
"name": "Livestream",
"description": "Nhân vật chính là người phát trực tiếp trên mạng, tương tác với khán giả và thực hiện các nội dung sáng tạo để kiếm tiền và danh tiếng."
},
{
"name": "Mỹ thực",
"description": "Tập trung vào ẩm thực, quá trình nấu nướng, các món ăn ngon và cảm xúc chia sẻ thông qua những bữa cơm."
},
{
"name": "Nông trại",
"description": "Bối cảnh vùng quê hoặc không gian riêng biệt nơi nhân vật chính thực hiện việc trồng trọt, chăn nuôi và xây dựng cuộc sống thanh bình."
},
{
"name": "Điền văn",
"description": "Truyện có nhịp chậm, tập trung vào cuộc sống đời thường, làm ruộng, gia đình và những chuyện nhỏ nhặt bình dị nhưng ấm áp."
},
{
"name": "Nuôi con",
"description": "Tập trung vào quá trình chăm sóc, giáo dục và chứng kiến sự trưởng thành của những đứa trẻ, thường mang màu sắc gia đình và chữa lành."
},
{
"name": "Làm giàu",
"description": "Ghi lại hành trình khởi nghiệp, kinh doanh từ bàn tay trắng đến khi trở nên giàu có nhờ sự thông minh và nỗ lực của nhân vật."
},
{
"name": "Cung đấu",
"description": "Những cuộc tranh đoạt sủng ái và quyền lực chốn hậu cung của các phi tần, sử dụng mưu kế và sự sắc sảo để tồn tại."
},
{
"name": "Gia đấu",
"description": "Mâu thuẫn và tranh chấp quyền lợi giữa các thành viên trong một gia đình hoặc gia tộc lớn, thường liên quan đến vợ lẽ, con thứ."
},
{
"name": "Quyền mưu",
"description": "Tập trung vào các chiến lược chính trị, mưu đồ chính trị và các cuộc đấu trí đỉnh cao tại triều đình hoặc các tổ chức quyền lực."
},
{
"name": "Nữ cường",
"description": "Nhân vật chính là nữ giới có tính cách mạnh mẽ, độc lập, có năng lực vượt trội và không phụ thuộc vào nam giới."
},
{
"name": "Nam cường",
"description": "Nhân vật chính là nam giới cực kỳ bản lĩnh, có khí chất lãnh đạo và sức mạnh nội tại lớn lao."
},
{
"name": "Song khiết",
"description": "Cả hai nhân vật chính đều chưa từng có quan hệ tình cảm hay thể xác với bất kỳ ai khác trước khi gặp nhau, đề cao sự thuần khiết."
},
{
"name": "Phế Sài",
"description": "Nhân vật ban đầu bị coi là vô dụng, kém cỏi, bị coi thường nhưng sau đó gặp kỳ ngộ và nỗ lực để vươn lên trở thành kẻ mạnh."
},
{
"name": "Thiên tài",
"description": "Sở hữu tiềm năng bẩm sinh cực lớn, học một hiểu mười, thường vượt xa những người cùng trang lứa về mọi mặt."
},
{
"name": "Từ hôn",
"description": "Tình tiết nhân vật bị đối phương hủy bỏ hôn ước công khai, tạo ra động lực để nhân vật nỗ lực thay đổi và quay lại chứng minh giá trị bản thân."
},
{
"name": "Linh khí khôi phục",
"description": "Bối cảnh thế giới hiện đại hoặc cổ đại đột ngột xuất hiện nguồn năng lượng siêu nhiên, khiến thực vật, động vật và con người bắt đầu tiến hóa."
},
{
"name": "Mạt thế",
"description": "Bối cảnh thế giới bên bờ vực diệt vong do thảm họa, dịch bệnh hoặc thây ma, tập trung vào sự sinh tồn và bản chất con người trong nghịch cảnh."
},
{
"name": "Cơ giáp",
"description": "Sử dụng các robot khổng lồ điều khiển bởi con người để chiến đấu trong bối cảnh khoa học viễn tưởng hoặc tương lai."
},
{
"name": "Tinh tế",
"description": "Bối cảnh ngoài không gian, du hành giữa các vì sao, các nền văn minh thiên hà và công nghệ vượt bậc của tương lai."
},
{
"name": "ABO",
"description": "Phân loại con người dựa trên thuộc tính sinh học Alpha (mạnh mẽ, thống trị), Beta (bình thường), và Omega (nhu mì, sinh sản), tạo ra các quy tắc xã hội đặc biệt."
},
{
"name": "Người sói",
"description": "Tập trung vào bộ tộc người sói với các phân cấp về sức mạnh, bản năng hoang dã và mối quan hệ giữa người sói với con người hoặc ma cà rồng."
},
{
"name": "Ma cà rồng",
"description": "Khai thác thế giới của những sinh vật bất tử khát máu, vẻ đẹp bí ẩn, sự quý tộc và những lời nguyền lâu đời."
},
{
"name": "Tây huyễn",
"description": "Huyền huyễn phương Tây với rồng, yêu tinh, pháp sư, hiệp sĩ và bối cảnh trung cổ huyền ảo."
},
{
"name": "Ma pháp",
"description": "Hệ thống sức mạnh dựa trên việc sử dụng các vòng chú pháp, các nguyên tố thiên nhiên hoặc quyền năng phép thuật bí ẩn."
},
{
"name": "Kiếm ma",
"description": "Sự kết hợp giữa kỹ năng sử dụng kiếm thuật điêu luyện và sức mạnh phép thuật, tạo ra những chiến binh đa năng và mạnh mẽ."
},
{
"name": "Pháp sư",
"description": "Nhân vật chuyên về việc sử dụng gậy phép, đọc thần chú và điều khiển các nguồn năng lượng siêu nhiên từ xa."
},
{
"name": "Giả heo ăn hổ",
"description": "Nhân vật chính cố tình che giấu thực lực, giả vờ yếu đuối để lừa đối thủ chủ quan, sau đó mới thể hiện sức mạnh thật sự để lật ngược tình thế."
},
{
"name": "Bi kịch",
"description": "Truyện có kết thúc không có hậu hoặc quá trình diễn biến đầy đau thương, mất mát, để lại ấn tượng sâu sắc và buồn bã."
},
{
"name": "Chữa lành",
"description": "Những câu chuyện nhẹ nhàng, ấm áp giúp xoa dịu tâm hồn độc giả, khơi dậy niềm tin vào cuộc sống và những điều tốt đẹp."
},
{
"name": "Hắc ám",
"description": "Khai thác những góc khuất tăm tối, sự tàn độc, không khoan nhượng của nhân vật và xã hội, thường có nhịp độ căng thẳng và lạnh lùng."
},
{
"name": "Gothic",
"description": "Phong cách mang hơi hướng cổ điển, u ám, bí ẩn với bối cảnh lâu đài cũ, hầm mộ và những bí mật gia tộc rùng rợn."
},
{
"name": "Tâm lý",
"description": "Tập trung sâu vào diễn biến tâm trạng, sự biến đổi tính cách và những đấu tranh nội tâm phức tạp của nhân vật."
},
{
"name": "Tâm thần phân liệt",
"description": "Nhân vật có sự chia tách về tâm trí, tạo ra những ảo giác hoặc hành vi không kiểm soát, dẫn đến các tình huống kịch tính và khó đoán."
},
{
"name": "Đa nhân cách",
"description": "Một cơ thể tồn tại nhiều danh tính khác nhau với ký ức và tính cách riêng biệt, tạo ra những xung đột nội tại gay gắt."
},
{
"name": "Trinh thám",
"description": "Quá trình điều tra, phá án, giải mã các câu đố hóc búa để tìm ra sự thật đằng sau những tội ác bí ẩn."
},
{
"name": "Phá án",
"description": "Tập trung vào các vụ án cụ thể, kỹ thuật khám nghiệm và suy luận logic để vạch trần thủ phạm."
},
{
"name": "Đạo mộ",
"description": "Khám phá các ngôi mộ cổ, đối mặt với các bẫy rập, sinh vật kỳ bí và tìm kiếm những báu vật dân gian bị lãng quên."
},
{
"name": "Cương thi",
"description": "Khai thác hình tượng xác sống truyền thống của Á Đông, các phương pháp trấn yểm, đạo sĩ và những câu chuyện tâm linh huyền bí."
},
{
"name": "Quy tắc quái đàm",
"description": "Bối cảnh nơi mọi người phải tuân thủ nghiêm ngặt các quy tắc kỳ lạ để sống sót, thường mang yếu tố kinh dị và căng thẳng tột độ."
},
{
"name": "Vô hạn lưu",
"description": "Nhân vật chính bị đưa vào chuỗi các thế giới hoặc trò chơi nhiệm vụ khác nhau, phải hoàn thành để tích điểm và sống sót."
},
{
"name": "Mau xuyên",
"description": "Nhân vật xuyên qua rất nhiều thế giới nhỏ trong thời gian ngắn để thực hiện các nhiệm vụ chuyên biệt như trả thù, tìm tình yêu."
},
{
"name": "Thực dân",
"description": "Bối cảnh khai phá miền đất mới, xây dựng thuộc địa hoặc các cuộc đấu tranh giành độc lập khỏi ách áp bức."
},
{
"name": "Chế tạo",
"description": "Nhân vật tập trung vào việc nghiên cứu, phát minh và sản xuất các thiết bị, vũ khí hoặc vật phẩm công nghệ cao để thay đổi thế giới."
},
{
"name": "Lĩnh chúa",
"description": "Quản lý một vùng lãnh thổ, từ việc xây dựng cơ sở hạ tầng đến việc tuyển quân, phát triển kinh tế và bảo vệ thần dân."
},
{
"name": "Trồng trọt",
"description": "Nhấn mạnh vào kỹ thuật canh tác, lai tạo giống cây trồng và tận hưởng thành quả từ việc chăm sóc đất đai."
},
{
"name": "Câu cá",
"description": "Một chủ đề mang tính giải trí cao, miêu tả chi tiết kỹ thuật câu và những kỳ ngộ khi thu được những \"chiến lợi phẩm\" bất ngờ."
},
{
"name": "Nuôi thú",
"description": "Xoay quanh việc thuần hóa và chăm sóc các loại linh thú, sủng vật, coi chúng như những người bạn đồng hành trung thành."
},
{
"name": "Pokemon",
"description": "Bối cảnh đồng nhân hoặc lấy cảm hứng từ thế giới Pokemon, tập trung vào việc thu phục và huấn luyện các quái thú túi để thi đấu."
},
{
"name": "Anime",
"description": "Lấy bối cảnh hoặc phong cách từ các bộ phim hoạt hình Nhật Bản nổi tiếng, thường có hệ thống sức mạnh và logic đặc trưng."
},
{
"name": "Harry Potter",
"description": "Đồng nhân về thế giới phù thủy của J.K. Rowling, với các ngôi nhà phép thuật, đũa phép và cuộc chiến chống lại hắc ám."
},
{
"name": "Marvel",
"description": "Khai thác thế giới của các siêu anh hùng với sức mạnh phi thường, công nghệ tương lai và những cuộc chiến bảo vệ Trái Đất."
},
{
"name": "Biển sao",
"description": "Bối cảnh vũ trụ bao la với những hành trình khám phá, các thiên hà xa xôi và sự nhỏ bé của con người trước không gian."
},
{
"name": "Truyền thuyết đô thị",
"description": "Những câu chuyện kinh dị hoặc bí ẩn xảy ra ngay trong lòng các thành phố hiện đại, gắn liền với các tin đồn và địa danh cụ thể."
},
{
"name": "Cổ đại",
"description": "Bối cảnh lịch sử xa xưa với những phong tục tập quán, lễ nghi và các cuộc sống của con người thời kỳ trước công nghiệp."
},
{
"name": "Dân quốc",
"description": "Giai đoạn lịch sử đầu thế kỷ 20 với sự giao thoa giữa cũ và mới, các cuộc cách mạng và không khí thời đại đặc trưng."
},
{
"name": "Thập niên 70",
"description": "Tập trung vào cuộc sống khó khăn nhưng chân thành của những năm 1970, thường gắn liền với thanh niên tri thức về nông thôn."
},
{
"name": "Thập niên 80",
"description": "Giai đoạn cải cách mở cửa, khởi đầu của sự năng động kinh tế và những thay đổi lớn lao trong đời sống xã hội."
},
{
"name": "Thập niên 90",
"description": "Sự bùng nổ của công nghệ sơ khai, nhạc pop và tinh thần khao khát khẳng định bản thân của thế hệ mới."
},
{
"name": "Dị thế",
"description": "Thế giới khác hoàn toàn so với địa cầu, có luật lệ, sinh vật và hệ thống xã hội hoàn toàn mới lạ."
},
{
"name": "Đại lục",
"description": "Tập trung vào bối cảnh vùng đất rộng lớn với nhiều quốc gia, chủng tộc và những cuộc chiến tranh giành cương thổ."
},
{
"name": "Bộ lạc",
"description": "Cuộc sống cộng đồng nguyên thủy, gắn liền với săn bắn, hái lượm và những tín ngưỡng thần linh sơ khai."
},
{
"name": "Nguyên thủy",
"description": "Xoay quanh giai đoạn khai sinh của loài người, việc tìm ra lửa, công cụ đá và quá trình phát triển sơ khai nhất."
},
{
"name": "Tu tiên",
"description": "Hành trình tu luyện rèn luyện cơ thể và linh hồn để đạt tới sự trường thọ và sức mạnh của thần tiên."
},
{
"name": "Trả thù",
"description": "Động lực chính của nhân vật là tìm cách bắt những kẻ đã gây ra đau khổ cho mình phải trả giá, thường có những mưu kế thâm sâu."
},
{
"name": "Pháo hôi",
"description": "Nhân vật ban đầu chỉ là vật hy sinh làm nền cho nhân vật chính trong cốt truyện, nhưng sau đó đã tự mình vươn lên thay đổi số phận."
},
{
"name": "Vai ác",
"description": "Nhân vật đóng vai phản diện trong cốt truyện, thường có góc nhìn mới lạ về công lý và cái ác."
},
{
"name": "Tiên tri",
"description": "Khả năng nhìn thấy trước tương lai, giúp nhân vật chuẩn bị cho các biến cố hoặc cố gắng thay đổi dòng thời gian."
},
{
"name": "Phép thuật",
"description": "Hệ thống quyền năng kỳ bí dựa trên ý chí, các nguyên tố hoặc các thực thể siêu nhiên để tác động vào thực tại."
}
]
Binary file not shown.
Binary file not shown.
+350
View File
@@ -0,0 +1,350 @@
"""
Module quản sở dữ liệu SQLite - Lưu trữ tập trung
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 sqlite3
import json
import os
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
DB_DIR = "data"
DB_FILE = os.path.join(DB_DIR, "tinix_story.db")
os.makedirs(DB_DIR, exist_ok=True)
_connection: Optional[sqlite3.Connection] = None
def get_db() -> sqlite3.Connection:
"""Lấy kết nối DB singleton (WAL mode, foreign keys ON)"""
global _connection
if _connection is None:
_connection = sqlite3.connect(DB_FILE, check_same_thread=False)
_connection.execute("PRAGMA journal_mode=WAL")
_connection.execute("PRAGMA foreign_keys=ON")
_connection.row_factory = sqlite3.Row
init_db(_connection)
logger.info(f"Database connected: {DB_FILE}")
return _connection
def init_db(conn: Optional[sqlite3.Connection] = None) -> None:
"""Tạo tất cả bảng nếu chưa có"""
if conn is None:
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS backends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
base_url TEXT NOT NULL,
api_key TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
timeout INTEGER NOT NULL DEFAULT 30,
retry_times INTEGER NOT NULL DEFAULT 3,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS config_backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS response_cache (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
timestamp TEXT NOT NULL,
ttl INTEGER NOT NULL DEFAULT 3600
);
CREATE TABLE IF NOT EXISTS generation_cache (
project_id TEXT PRIMARY KEY,
data TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS chapter_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL,
chapter_num INTEGER NOT NULL,
summary TEXT NOT NULL,
generated_at TEXT NOT NULL,
UNIQUE(project_id, chapter_num)
);
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
genre TEXT NOT NULL DEFAULT '',
sub_genres TEXT NOT NULL DEFAULT '[]',
character_setting TEXT NOT NULL DEFAULT '',
world_setting TEXT NOT NULL DEFAULT '',
plot_idea TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS chapters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL,
num INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT '',
desc TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
word_count INTEGER NOT NULL DEFAULT 0,
generated_at TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE(project_id, num)
);
""")
# Đảm bảo schema cũ được cập nhật
try:
conn.execute("ALTER TABLE projects ADD COLUMN sub_genres TEXT NOT NULL DEFAULT '[]'")
except sqlite3.OperationalError:
pass # Đã có cột
conn.commit()
logger.info("Database tables initialized")
def migrate_from_files() -> str:
"""
Đọc dữ liệu từ file JSON insert vào DB.
Không xóa file (giữ lại để phòng lỗi).
Returns:
Báo cáo migration
"""
conn = get_db()
report = []
now = datetime.now().isoformat()
# 1. Migrate config
config_file = os.path.join("config", "novel_tool_config.json")
if os.path.exists(config_file):
try:
with open(config_file, "r", encoding="utf-8") as f:
data = json.load(f)
# Migrate backends
backends = data.get("backends", [])
migrated_backends = 0
for b in backends:
try:
conn.execute("""
INSERT OR IGNORE INTO backends
(name, type, base_url, api_key, model, enabled, timeout, retry_times, is_default, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
b.get("name", ""),
b.get("type", "openai"),
b.get("base_url", ""),
b.get("api_key", ""),
b.get("model", ""),
1 if b.get("enabled", True) else 0,
b.get("timeout", 30),
b.get("retry_times", 3),
1 if b.get("is_default", False) else 0,
now, now
))
migrated_backends += 1
except Exception as e:
logger.warning(f"Migrate backend failed: {e}")
# Migrate generation config
gen = data.get("generation", {})
if gen:
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
("generation", json.dumps(gen, ensure_ascii=False), now)
)
# Migrate version
version = data.get("version", "4.0.0")
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
("version", version, now)
)
conn.commit()
report.append(f"✅ Config: {migrated_backends} backends migrated")
except Exception as e:
report.append(f"❌ Config migration failed: {e}")
else:
report.append("⏭ Config file not found, skipped")
# 2. Migrate config backups
backup_dir = os.path.join("config", "backups")
if os.path.exists(backup_dir):
migrated_backups = 0
for fname in os.listdir(backup_dir):
fpath = os.path.join(backup_dir, fname)
if fname.endswith(".json") and os.path.isfile(fpath):
try:
with open(fpath, "r", encoding="utf-8") as f:
backup_data = f.read()
# Extract timestamp from filename if possible
created = now
if fname.startswith("backup_"):
parts = fname.replace("backup_", "").replace(".json", "")
try:
created = datetime.strptime(parts, "%Y%m%d_%H%M%S").isoformat()
except ValueError:
pass
conn.execute(
"INSERT INTO config_backups (data, created_at) VALUES (?, ?)",
(backup_data, created)
)
migrated_backups += 1
except Exception as e:
logger.warning(f"Migrate backup {fname} failed: {e}")
conn.commit()
report.append(f"✅ Config backups: {migrated_backups} backups migrated")
# 3. Migrate response cache
cache_file = os.path.join("cache", "response_cache.json")
if os.path.exists(cache_file):
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache_data = json.load(f)
migrated_cache = 0
for k, v in cache_data.items():
try:
conn.execute(
"INSERT OR IGNORE INTO response_cache (key, value, timestamp, ttl) VALUES (?, ?, ?, ?)",
(k, v.get("value", ""), v.get("timestamp", now), int(v.get("ttl", 3600)))
)
migrated_cache += 1
except Exception as e:
logger.warning(f"Migrate cache entry failed: {e}")
conn.commit()
report.append(f"✅ Response cache: {migrated_cache} entries migrated")
except Exception as e:
report.append(f"❌ Response cache migration failed: {e}")
else:
report.append("⏭ Response cache not found, skipped")
# 4. Migrate generation cache
gen_cache_dir = Path("cache/generation")
if gen_cache_dir.exists():
migrated_gen = 0
for cache_file in gen_cache_dir.glob("*.json"):
try:
with open(cache_file, "r", encoding="utf-8") as f:
gen_data = f.read()
conn.execute(
"INSERT OR IGNORE INTO generation_cache (project_id, data, updated_at) VALUES (?, ?, ?)",
(cache_file.stem, gen_data, now)
)
migrated_gen += 1
except Exception as e:
logger.warning(f"Migrate generation cache {cache_file.name} failed: {e}")
conn.commit()
report.append(f"✅ Generation cache: {migrated_gen} entries migrated")
# 5. Migrate chapter summaries
summary_dir = Path("cache/summaries")
if summary_dir.exists():
migrated_summaries = 0
for project_dir in summary_dir.iterdir():
if not project_dir.is_dir():
continue
for summary_file in project_dir.glob("*.json"):
try:
with open(summary_file, "r", encoding="utf-8") as f:
summary_data = json.load(f)
conn.execute("""
INSERT OR IGNORE INTO chapter_summaries
(project_id, chapter_num, summary, generated_at)
VALUES (?, ?, ?, ?)
""", (
project_dir.name,
summary_data.get("chapter_num", int(summary_file.stem)),
summary_data.get("summary", ""),
summary_data.get("generated_at", now)
))
migrated_summaries += 1
except Exception as e:
logger.warning(f"Migrate summary {summary_file} failed: {e}")
conn.commit()
report.append(f"✅ Chapter summaries: {migrated_summaries} entries migrated")
# 6. Migrate projects
projects_dir = "projects"
if os.path.exists(projects_dir):
migrated_projects = 0
for project_id in os.listdir(projects_dir):
project_path = os.path.join(projects_dir, project_id)
if not os.path.isdir(project_path):
continue
metadata_file = os.path.join(project_path, "metadata.json")
if not os.path.exists(metadata_file):
continue
try:
with open(metadata_file, "r", encoding="utf-8") as f:
metadata = json.load(f)
conn.execute("""
INSERT OR IGNORE INTO projects
(id, title, genre, character_setting, world_setting, plot_idea, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
metadata.get("id", project_id),
metadata.get("title", ""),
metadata.get("genre", ""),
metadata.get("character_setting", ""),
metadata.get("world_setting", ""),
metadata.get("plot_idea", ""),
metadata.get("created_at", now),
metadata.get("updated_at", now)
))
# Migrate chapters
for ch in metadata.get("chapters", []):
try:
conn.execute("""
INSERT OR IGNORE INTO chapters
(project_id, num, title, desc, content, word_count, generated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
metadata.get("id", project_id),
ch.get("num", 0),
ch.get("title", ""),
ch.get("desc", ""),
ch.get("content", ""),
ch.get("word_count", 0),
ch.get("generated_at")
))
except Exception as e:
logger.warning(f"Migrate chapter {ch.get('num')} failed: {e}")
migrated_projects += 1
except Exception as e:
logger.warning(f"Migrate project {project_id} failed: {e}")
conn.commit()
report.append(f"✅ Projects: {migrated_projects} projects migrated")
else:
report.append("⏭ Projects directory not found, skipped")
result = "\n".join(report)
logger.info(f"Migration complete:\n{result}")
return result
+140
View File
@@ -0,0 +1,140 @@
version: '3.8'
services:
# AI 小说创作工具主服务
novel-generator:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: ai-novel-generator
restart: unless-stopped
ports:
- "8000:8000"
environment:
# 基础配置
- MODE=production
- HOST=0.0.0.0
- PORT=8000
- CONCURRENCY=4
# 日志配置
- LOG_LEVEL=INFO
- LOG_FILE=logs/novel_generator.log
- CONSOLE_OUTPUT=true
# 数据库配置
- DB_TYPE=sqlite
- DB_PATH=data/novel_generator.db
# 缓存配置
- CACHE_ENABLED=true
- CACHE_TYPE=file
- CACHE_LOCATION=cache
- CACHE_TTL=3600
# 安全配置
- ENABLE_CORS=true
- ALLOWED_ORIGINS=*
# API 配置
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GLM_API_KEY=${GLM_API_KEY}
- CLAUDE_API_KEY=${CLAUDE_API_KEY}
# 生成参数
- TEMPERATURE=0.7
- TOP_P=0.9
- MAX_TOKENS=4000
volumes:
# 数据持久化
- ./data:/app/data
- ./cache:/app/cache
- ./logs:/app/logs
- ./output:/app/output
- ./backups:/app/backups
- ./templates:/app/templates
- ./project_templates:/app/project_templates
- ./plugins:/app/plugins
# 健康检查
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# 资源限制
deploy:
resources:
limits:
memory: 1G
cpus: '0.5'
reservations:
memory: 512M
cpus: '0.25'
# Redis 缓存服务(可选)
redis:
image: redis:7-alpine
container_name: ai-novel-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Nginx 反向代理(可选)
nginx:
image: nginx:alpine
container_name: ai-novel-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- novel-generator
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/nginx_status"]
interval: 30s
timeout: 10s
retries: 3
# 数据库备份服务(可选)
db-backup:
image: alpine:latest
container_name: ai-novel-db-backup
restart: "no"
volumes:
- ./backups:/backups
- ./data:/data
depends_on:
- novel-generator
command: |
apk add --no-cache curl
while true; do
echo "Starting database backup..."
curl -X POST http://novel-generator:8000/api/backup
sleep 86400 # 每天备份一次
done
volumes:
redis_data:
driver: local
networks:
default:
name: ai-novel-network
+407
View File
@@ -0,0 +1,407 @@
"""
-đun Xuất - Hỗ trợ Word (DOCX), TXT, Markdown
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 os
import re
import logging
import tempfile
from typing import Tuple, Optional
from datetime import datetime
from pathlib import Path
logger = logging.getLogger(__name__)
from locales.i18n import t
MODULE_ROOT = os.path.dirname(os.path.abspath(__file__))
EXPORT_DIR = os.path.join(MODULE_ROOT, "exports")
os.makedirs(EXPORT_DIR, exist_ok=True)
def _sanitize_filename(name: str, max_len: int = 120) -> str:
"""Làm sạch các ký tự không hợp lệ trong tên tệp và giới hạn độ dài"""
if not name or not name.strip():
name = "novel"
safe = re.sub(r'[<>:"/\\|?*]', '_', name).strip()
if len(safe) > max_len:
safe = safe[:max_len]
return safe
def _extract_chapters_from_markdown(text: str) -> list:
"""
Trích xuất thông tin chương từ văn bản tiểu thuyết định dạng Markdown
Returns:
[{"title": "...", "content": "..."}, ...]
"""
# Loại bỏ các thẻ HTML phụ trợ (details/summary/b/i) để parse Regex tiêu đề Markdown chính xác
text = re.sub(r'</?(details|summary|b|i|br|u|strong|em)[^>]*>', '', text)
chapters = []
current_chapter = None
content_lines = []
# Phát hiện tiêu đề chương tổng quát hơn, hỗ trợ '#', '##', '###' và các cấp độ khác, hỗ trợ các biến thể tiếng Trung và không gian
header_re = re.compile(r'^(?:# {1,6}\s*)?(Chương\s*\d+\s*[\s\S]*|Chương\s*\d+[\s\S]*|Chương\s*\d+\s*[::\s\--]?.*)$', re.IGNORECASE)
for line in text.splitlines():
if not line:
# Dòng trống có tác dụng ngăn cách đoạn văn nhưng không kết thúc chương
if current_chapter:
content_lines.append('')
continue
# Phát hiện tiêu đề chương
if header_re.match(line.strip()):
# Lưu chương trước
if current_chapter:
current_chapter['content'] = '\n'.join([l for l in content_lines]).strip()
chapters.append(current_chapter)
# Trích xuất văn bản tiêu đề
title_match = re.search(r'(第\s*\d+\s*章[\s\S]*)', line)
title = title_match.group(1).strip() if title_match else line.strip()
current_chapter = {'title': title, 'content': ''}
content_lines = []
continue
# Bỏ qua tiêu đề cấp tệp
if line.strip().startswith('# '):
continue
if current_chapter is None:
# Nếu bạn chưa gặp tiêu đề chương, hãy đặt nội dung của chương đầu tiên
current_chapter = {'title': t("exporter.first_chapter"), 'content': ''}
content_lines = [line]
else:
content_lines.append(line)
# lưu chương cuối
if current_chapter:
current_chapter['content'] = '\n'.join([l for l in content_lines]).strip()
chapters.append(current_chapter)
return chapters
def export_to_txt(novel_text: str, title: str) -> Tuple[Optional[str], str]:
"""
Xuất ra định dạng TXT
Args:
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
title: Tiêu đề tiểu thuyết
Returns:
(Đường dẫn tệp, thông tin trạng thái)
"""
try:
if not novel_text.strip():
return None, t("exporter.no_content")
# Trích xuất chương
chapters = _extract_chapters_from_markdown(novel_text)
if not chapters:
return None, t("exporter.no_chapters")
# Tạo nội dung TXT
txt_content = f"{title}\n\n"
for chapter in chapters:
txt_content += f"{chapter['title']}\n\n"
txt_content += f"{chapter['content']}\n\n"
txt_content += "-" * 80 + "\n\n"
# Lưu tập tin (ghi nguyên tử)
safe_title = _sanitize_filename(title)
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
filepath = os.path.join(EXPORT_DIR, filename)
try:
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
tmp.write(txt_content)
tmp_path = tmp.name
os.replace(tmp_path, filepath)
except Exception as e:
logger.error(f"TXT write failed: {e}")
return None, t("exporter.export_failed", error=str(e))
logger.info(f"TXT export success: {filename}")
return filepath, t("exporter.export_success", filename=filename)
except Exception as e:
logger.error(f"TXT export failed: {e}")
return None, t("exporter.export_failed", error=str(e))
def export_to_markdown(novel_text: str, title: str) -> Tuple[Optional[str], str]:
"""
Xuất ra định dạng Markdown
Args:
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
title: Tiêu đề tiểu thuyết
Returns:
(Đường dẫn tệp, thông tin trạng thái)
"""
try:
if not novel_text.strip():
return None, t("exporter.no_content")
# Thêm siêu dữ liệu
md_content = f"# {title}\n\n"
md_content += f"*{t('exporter.generated_at', datetime=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}*\n\n"
md_content += "---\n\n"
md_content += novel_text
# Lưu tập tin (ghi nguyên tử)
safe_title = _sanitize_filename(title)
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
filepath = os.path.join(EXPORT_DIR, filename)
try:
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
tmp.write(md_content)
tmp_path = tmp.name
os.replace(tmp_path, filepath)
except Exception as e:
logger.error(f"Markdown write failed: {e}")
return None, t("exporter.export_failed", error=str(e))
logger.info(f"Markdown export success: {filename}")
return filepath, t("exporter.export_success", filename=filename)
except Exception as e:
logger.error(f"Markdown export failed: {e}")
return None, t("exporter.export_failed", error=str(e))
def export_to_docx(novel_text: str, title: str) -> Tuple[Optional[str], str]:
"""
Xuất ra định dạng Word (DOCX) - Dàn trang chuyên nghiệp
Args:
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
title: Tiêu đề tiểu thuyết
Returns:
(Đường dẫn tệp, thông tin trạng thái)
"""
try:
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
except ImportError:
return None, t("exporter.missing_docx")
try:
if not novel_text.strip():
return None, t("exporter.no_content")
# Trích xuất chương
chapters = _extract_chapters_from_markdown(novel_text)
if not chapters:
return None, t("exporter.no_chapters")
doc = Document()
# Kiểu cấu hình
style = doc.styles['Normal']
font = style.font
font.name = t("exporter.body_font")
font.size = Pt(12)
# phông chữ tiếng trung
rPr = style.element.get_or_add_rPr()
rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.body_font"))
# định dạng đoạn văn
style.paragraph_format.first_line_indent = Pt(24)
style.paragraph_format.space_after = Pt(0)
style.paragraph_format.line_spacing = 1.5
# Thêm tên sách
title_para = doc.add_paragraph(title)
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_run = title_para.runs[0]
title_run.font.name = t("exporter.title_font")
title_run.font.size = Pt(26)
title_run.font.bold = True
title_run.font.color.rgb = RGBColor(0, 0, 0)
# Cài đặt phông chữ tiếng Trung
title_rPr = title_run._element.get_or_add_rPr()
title_rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.title_font"))
# Thêm thông tin tác giả và ngày tháng
info_para = doc.add_paragraph()
info_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
info_run = info_para.add_run(t("exporter.generated_date", date=datetime.now().strftime('%Y-%m-%d')))
info_run.font.size = Pt(10)
doc.add_paragraph() # Dòng trống
# Thêm chương
for chapter in chapters:
# Tiêu đề chương
chapter_title_para = doc.add_paragraph(chapter['title'])
chapter_title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in chapter_title_para.runs:
run.font.name = t('exporter.title_font')
run.font.size = Pt(16)
run.font.bold = True
run_rPr = run._element.get_or_add_rPr()
run_rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.title_font"))
doc.add_paragraph() # Dòng trống
# Nội dung chương - thêm theo đoạn
paragraphs = chapter['content'].split('\n\n')
for para_text in paragraphs:
if para_text.strip():
p = doc.add_paragraph(para_text.strip(), style='Normal')
doc.add_paragraph() # Dòng trống giữa các chương
# Lưu tập tin (ghi nguyên tử)
safe_title = _sanitize_filename(title)
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
filepath = os.path.join(EXPORT_DIR, filename)
try:
tmp_fd, tmp_path = tempfile.mkstemp(suffix='.docx', dir=EXPORT_DIR)
os.close(tmp_fd)
doc.save(tmp_path)
os.replace(tmp_path, filepath)
except Exception as e:
logger.error(f"DOCX write failed: {e}")
return None, t("exporter.export_failed", error=str(e))
logger.info(f"DOCX export success: {filename}")
return filepath, t("exporter.export_success", filename=filename)
except Exception as e:
logger.error(f"DOCX export failed: {e}")
return None, t("exporter.export_failed", error=str(e))
def export_to_html(novel_text: str, title: str) -> Tuple[Optional[str], str]:
"""
Xuất ra định dạng HTML - thể đọc trên trình duyệt
Args:
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
title: Tiêu đề tiểu thuyết
Returns:
(Đường dẫn tệp, thông tin trạng thái)
"""
try:
import markdown
except ImportError:
return None, t("exporter.missing_markdown")
try:
if not novel_text.strip():
return None, t("exporter.no_content")
# Chuyển đổi Markdown sang HTML
html_content = markdown.markdown(novel_text)
# Được gói gọn dưới dạng tài liệu HTML hoàn chỉnh
full_html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body {{
font-family: 'Arial', 'Times New Roman', serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.8;
background-color: #f5f5f5;
color: #333;
}}
h1 {{
text-align: center;
font-size: 2.5em;
margin-bottom: 0.5em;
}}
h2 {{
text-align: center;
font-size: 1.5em;
margin-top: 1.5em;
margin-bottom: 0.5em;
border-bottom: 2px solid #ddd;
padding-bottom: 0.3em;
}}
p {{
text-align: justify;
text-indent: 2em;
margin: 1em 0;
}}
.info {{
text-align: center;
color: #999;
font-size: 0.9em;
}}
</style>
</head>
<body>
<h1>{title}</h1>
<p class="info">{t('exporter.generated_at', datetime=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}</p>
<hr>
{html_content}
</body>
</html>"""
# Lưu tập tin (ghi nguyên tử)
safe_title = _sanitize_filename(title)
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = os.path.join(EXPORT_DIR, filename)
try:
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
tmp.write(full_html)
tmp_path = tmp.name
os.replace(tmp_path, filepath)
except Exception as e:
logger.error(f"HTML write failed: {e}")
return None, t("exporter.export_failed", error=str(e))
logger.info(f"HTML export success: {filename}")
return filepath, t("exporter.export_success", filename=filename)
except Exception as e:
logger.error(f"HTML export failed: {e}")
return None, t("exporter.export_failed", error=str(e))
def list_export_files() -> list:
"""Liệt kê tất cả các tập tin xuất"""
try:
files = []
for filename in os.listdir(EXPORT_DIR):
filepath = os.path.join(EXPORT_DIR, filename)
if os.path.isfile(filepath):
file_size = os.path.getsize(filepath)
file_time = datetime.fromtimestamp(os.path.getmtime(filepath))
files.append({
'name': filename,
'path': filepath,
'size': file_size,
'time': file_time.strftime('%Y-%m-%d %H:%M:%S')
})
return sorted(files, key=lambda x: x['time'], reverse=True)
except Exception as e:
logger.error(f"List export files failed: {e}")
return []
+779
View File
@@ -0,0 +1,779 @@
"""
-đun phân tích tệp - Hỗ trợ txt/pdf/epub, theo dõi tiến trình xử lỗi, hỗ trợ mẫu chương tùy chỉnh
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 os
import re
import logging
import tempfile
from typing import Tuple, List, Optional, IO, Dict
from enum import Enum
from dataclasses import dataclass
from locales.i18n import t
logger = logging.getLogger(__name__)
# không thay đổi
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
MIN_PARAGRAPH_LENGTH = 20 # Độ dài đoạn văn tối thiểu
# Mẫu chương mặc định
CHAPTER_PATTERNS = {
"default": [
r'\s*\d+\s*章[:\s]*.*',
r'\s*\d+\s*章',
r'Chapter\s*\d+',
],
"compact": [
r'^\d+\.',
r'^\d+、',
r'^\d+\s',
],
"brackets": [
r'《第\d+章》',
r'「第\d+章」',
],
"english": [
r'Chapter\s+\d+[:\s]*.*',
r'CHAPTER\s+\d+[:\s]*.*',
r'Part\s+\d+',
],
"special": [
r'【.*第\d+章.*】',
r'≮.*第\d+章.*≯',
r'◆.*第\d+章.*◆',
],
}
@dataclass
class ChapterInfo:
"""Thông tin chương"""
num: int
title: str
content: str
start_pos: int = 0
end_pos: int = 0
class FileType(Enum):
"""Loại tệp"""
TXT = "txt"
PDF = "pdf"
EPUB = "epub"
MD = "md"
DOCX = "docx"
UNKNOWN = "unknown"
def get_file_type(file_path: str) -> FileType:
"""Nhận loại tập tin"""
if not file_path:
return FileType.UNKNOWN
ext = os.path.splitext(file_path)[1].lower()
if ext == ".txt":
return FileType.TXT
elif ext == ".pdf":
return FileType.PDF
elif ext == ".epub":
return FileType.EPUB
elif ext == ".md":
return FileType.MD
elif ext == ".docx":
return FileType.DOCX
else:
return FileType.UNKNOWN
def parse_txt_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp TXT
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
try:
# Hỗ trợ truyền vào các đối tượng hoặc đường dẫn tệp
if hasattr(file_path, 'read'):
fobj: IO = file_path
# Cố gắng lấy thuộc tính kích thước
try:
fobj.seek(0, os.SEEK_END)
file_size = fobj.tell()
fobj.seek(0)
except Exception:
file_size = 0
else:
file_size = os.path.getsize(file_path)
if file_size and file_size > MAX_FILE_SIZE:
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
paragraphs: List[str] = []
buf_lines: List[str] = []
total_chars = 0
# Đọc từng dòng để giảm áp lực bộ nhớ
if hasattr(file_path, 'read'):
stream = file_path
else:
stream = open(file_path, 'r', encoding='utf-8', errors='ignore')
try:
for line in stream:
stripped = line.rstrip('\n')
total_chars += len(stripped)
if stripped.strip() == '':
# Dòng trống -> cuối đoạn
if buf_lines:
para = '\n'.join(buf_lines).strip()
if len(para) >= MIN_PARAGRAPH_LENGTH:
paragraphs.append(para)
buf_lines = []
continue
# hàng thông thường
buf_lines.append(stripped)
# đoạn cuối
if buf_lines:
para = '\n'.join(buf_lines).strip()
if len(para) >= MIN_PARAGRAPH_LENGTH:
paragraphs.append(para)
finally:
if not hasattr(file_path, 'read'):
stream.close()
logger.info(f"TXT parse done: {len(paragraphs)} paragraphs")
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
except Exception as e:
logger.error(f"TXT parse failed: {e}")
return [], t("file_parser.read_failed", error=str(e))
def parse_pdf_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp PDF
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
try:
import fitz
except ImportError:
return [], t("file_parser.missing_pymupdf")
try:
file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE:
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
text_parts = []
doc = fitz.open(file_path)
for page_num, page in enumerate(doc):
try:
page_text = page.get_text("text")
text_parts.append(page_text)
except Exception as e:
logger.warning(f"PDF page {page_num} parse failed: {e}")
doc.close()
text = "\n".join(text_parts)
paragraphs = _split_paragraphs(text)
logger.info(f"PDF parse done: {len(paragraphs)} paragraphs")
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=len(text))
except Exception as e:
logger.error(f"PDF parse failed: {e}")
return [], t("file_parser.read_failed", error=str(e))
def parse_epub_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp EPUB
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
try:
from ebooklib import epub
from bs4 import BeautifulSoup
except ImportError:
return [], t("file_parser.missing_ebooklib")
try:
file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE:
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
text_parts = []
book = epub.read_epub(file_path)
for item in book.get_items():
if item.get_type() == epub.ITEM_DOCUMENT:
try:
soup = BeautifulSoup(item.get_content(), 'html.parser')
text_parts.append(soup.get_text(separator="\n"))
except Exception as e:
logger.warning(f"EPUB chapter parse failed: {e}")
text = "\n".join(text_parts)
paragraphs = _split_paragraphs(text)
logger.info(f"EPUB parse done: {len(paragraphs)} paragraphs")
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=len(text))
except Exception as e:
logger.error(f"EPUB parse failed: {e}")
return [], t("file_parser.read_failed", error=str(e))
def parse_md_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp Markdown
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
try:
# Hỗ trợ truyền vào các đối tượng hoặc đường dẫn tệp
if hasattr(file_path, 'read'):
fobj: IO = file_path
# Cố gắng lấy thuộc tính kích thước
try:
fobj.seek(0, os.SEEK_END)
file_size = fobj.tell()
fobj.seek(0)
except Exception:
file_size = 0
else:
file_size = os.path.getsize(file_path)
if file_size and file_size > MAX_FILE_SIZE:
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
paragraphs: List[str] = []
buf_lines: List[str] = []
total_chars = 0
# Đọc từng dòng để giảm áp lực bộ nhớ
if hasattr(file_path, 'read'):
stream = file_path
else:
stream = open(file_path, 'r', encoding='utf-8', errors='ignore')
try:
for line in stream:
stripped = line.rstrip('\n')
total_chars += len(stripped)
if stripped.strip() == '':
# Dòng trống -> cuối đoạn
if buf_lines:
para = '\n'.join(buf_lines).strip()
if len(para) >= MIN_PARAGRAPH_LENGTH:
paragraphs.append(para)
buf_lines = []
continue
# hàng thông thường
buf_lines.append(stripped)
# đoạn cuối
if buf_lines:
para = '\n'.join(buf_lines).strip()
if len(para) >= MIN_PARAGRAPH_LENGTH:
paragraphs.append(para)
finally:
if not hasattr(file_path, 'read'):
stream.close()
logger.info(f"Markdown parse done: {len(paragraphs)} paragraphs")
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
except Exception as e:
logger.error(f"Markdown parse failed: {e}")
return [], t("file_parser.read_failed", error=str(e))
def parse_docx_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp tài liệu Word
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
try:
from docx import Document
except ImportError:
return [], t("file_parser.missing_docx")
try:
file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE:
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
doc = Document(file_path)
paragraphs: List[str] = []
total_chars = 0
for para in doc.paragraphs:
text = para.text.strip()
if text and len(text) >= MIN_PARAGRAPH_LENGTH:
paragraphs.append(text)
total_chars += len(text)
logger.info(f"Word parse done: {len(paragraphs)} paragraphs")
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
except Exception as e:
logger.error(f"Word parse failed: {e}")
return [], t("file_parser.read_failed", error=str(e))
def parse_novel_file(file_path: str) -> Tuple[List[str], str]:
"""
Phân tích tệp tiểu thuyết (tự động nhận dạng định dạng)
Args:
file_path: Đường dẫn tệp
Returns:
(Danh sách đoạn văn, Thông tin trạng thái)
"""
if not file_path:
return [], t("file_parser.no_file")
# Xử lý các đối tượng tệp hoặc luồng tệp được tải lên bởi Gradio
temp_path = None
if hasattr(file_path, 'name') and isinstance(file_path.name, str) and os.path.exists(file_path.name):
file_path = file_path.name
elif hasattr(file_path, 'read'):
# Ghi luồng đã tải lên vào một tệp tạm thời để các thư viện xuôi dòng xử lý (PDF/EPUB/DOCX yêu cầu Đường dẫn tệp)
try:
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp')
chunk = file_path.read(8192)
while chunk:
if isinstance(chunk, str):
tmp.write(chunk.encode('utf-8'))
else:
tmp.write(chunk)
chunk = file_path.read(8192)
tmp.close()
temp_path = tmp.name
file_path = temp_path
except Exception as e:
logger.error(f"Upload file processing failed: {e}")
return [], t("file_parser.upload_read_failed", error=str(e))
if not os.path.exists(file_path):
return [], t("file_parser.file_not_exist", path=file_path)
file_type = get_file_type(file_path)
if file_type == FileType.TXT:
try:
return parse_txt_file(file_path)
finally:
if temp_path:
try:
os.remove(temp_path)
except Exception:
pass
elif file_type == FileType.PDF:
try:
return parse_pdf_file(file_path)
finally:
if temp_path:
try:
os.remove(temp_path)
except Exception:
pass
elif file_type == FileType.EPUB:
try:
return parse_epub_file(file_path)
finally:
if temp_path:
try:
os.remove(temp_path)
except Exception:
pass
elif file_type == FileType.MD:
try:
return parse_md_file(file_path)
finally:
if temp_path:
try:
os.remove(temp_path)
except Exception:
pass
elif file_type == FileType.DOCX:
try:
return parse_docx_file(file_path)
finally:
if temp_path:
try:
os.remove(temp_path)
except Exception:
pass
else:
return [], t("file_parser.unsupported_format")
def _split_paragraphs(text: str, min_length: int = MIN_PARAGRAPH_LENGTH) -> List[str]:
"""
Chia văn bản thành các đoạn văn
Args:
text: Văn bản gốc
min_length: Độ dài đoạn văn tối thiểu
Returns:
Danh sách đoạn văn
"""
# Chia theo nhiều dòng mới
raw_paragraphs = re.split(r'\n\s*\n+', text)
# Làm sạch và lọc
paragraphs = []
for para in raw_paragraphs:
para = para.strip()
# Xóa các điểm đánh dấu đặc biệt như tiêu đề chương
para = re.sub(r'^(第\d+章|Chapter \d+|第 \d+ 章)[:]?\s*', '', para)
para = re.sub(r'^\s*\*+\s*|\s*\*+\s*$', '', para)
if len(para) >= min_length:
paragraphs.append(para)
return paragraphs
def estimate_word_count(text: str) -> int:
"""Số lượng ký tự tiếng Trung ước tính (ước tính sơ bộ)"""
chinese_count = len(re.findall(r'[\u4e00-\u9fff]', text))
english_count = len(re.findall(r'\b[a-zA-Z]+\b', text))
# Tiếng Trung được tính là 1 ký tự, tiếng Anh được tính là 0,5 ký tự
return chinese_count + int(english_count * 0.5)
def parse_novel_by_chapters(
file_path: str,
pattern_name: str = "default",
custom_pattern: str = ""
) -> Tuple[List[ChapterInfo], str]:
"""
Phân tích tệp tiểu thuyết theo chương
Args:
file_path: Đường dẫn tệp
pattern_name: Tên mẫu định sẵn
custom_pattern: Biểu thức chính quy tùy chỉnh (nếu được cung cấp, sẽ được ưu tiên sử dụng)
Returns:
(Danh sách chương, Thông tin trạng thái)
"""
try:
# đọc văn bản
file_type = get_file_type(file_path)
if file_type == FileType.TXT:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
text = f.read()
elif file_type == FileType.PDF:
import fitz
text_parts = []
doc = fitz.open(file_path)
for page in doc:
text_parts.append(page.get_text("text"))
doc.close()
text = "\n".join(text_parts)
elif file_type == FileType.EPUB:
from ebooklib import epub
from bs4 import BeautifulSoup
text_parts = []
book = epub.read_epub(file_path)
for item in book.get_items():
if item.get_type() == epub.ITEM_DOCUMENT:
soup = BeautifulSoup(item.get_content(), 'html.parser')
text_parts.append(soup.get_text(separator="\n"))
text = "\n".join(text_parts)
elif file_type == FileType.MD:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
text = f.read()
elif file_type == FileType.DOCX:
from docx import Document
doc = Document(file_path)
text = ""
for para in doc.paragraphs:
text += para.text + "\n"
else:
return [], t("file_parser.unsupported_chapter_format")
# Xác định biểu thức chính quy để sử dụng
if custom_pattern and custom_pattern.strip():
patterns = [custom_pattern.strip()]
elif pattern_name in CHAPTER_PATTERNS:
patterns = CHAPTER_PATTERNS[pattern_name]
else:
patterns = CHAPTER_PATTERNS.get("default", list(CHAPTER_PATTERNS.values())[0])
# Tìm tất cả các tiêu đề chương
chapters = []
lines = text.split('\n')
current_chapter_num = 0
current_chapter_title = ""
current_chapter_content = []
chapter_start_pos = 0
for i, line in enumerate(lines):
line_stripped = line.strip()
is_chapter_header = False
# Kiểm tra xem có mẫu chương nào khớp không
for pattern in patterns:
if re.match(pattern, line_stripped, re.IGNORECASE):
is_chapter_header = True
break
if is_chapter_header:
# Lưu chương trước
if current_chapter_num > 0:
content = '\n'.join(current_chapter_content).strip()
if content:
chapters.append(ChapterInfo(
num=current_chapter_num,
title=current_chapter_title,
content=content,
start_pos=chapter_start_pos,
end_pos=i
))
# Trích xuất số chương và tiêu đề
current_chapter_num += 1
current_chapter_title = line_stripped
current_chapter_content = []
chapter_start_pos = i
else:
# Bỏ qua dòng trống nhưng giữ nguyên nội dung
if line_stripped or current_chapter_content:
current_chapter_content.append(line)
# lưu chương cuối
if current_chapter_num > 0 and current_chapter_content:
content = '\n'.join(current_chapter_content).strip()
if content:
chapters.append(ChapterInfo(
num=current_chapter_num,
title=current_chapter_title,
content=content,
start_pos=chapter_start_pos,
end_pos=len(lines)
))
logger.info(f"Chapter parse done: {len(chapters)} chapters")
return chapters, t("file_parser.chapter_parse_complete", count=len(chapters))
except Exception as e:
logger.error(f"Chapter parse failed: {e}")
return [], t("file_parser.chapter_parse_failed", error=str(e))
def parse_novel_with_custom_template(
file_path: str,
custom_template: str
) -> Tuple[List[ChapterInfo], str]:
"""
Sử dụng mẫu tùy chỉnh để phân tích tiểu thuyết
Args:
file_path: Đường dẫn tệp
custom_template: Mẫu chương tùy chỉnh (hỗ trợ chỗ dành sẵn placeholders)
dụ: "Chương {n} {title}" hoặc "Chapter {n}: {title}"
Returns:
(Danh sách chương, Thông tin trạng thái)
"""
if not custom_template or not custom_template.strip():
return parse_novel_by_chapters(file_path, "default", "")
# Chuyển đổi mẫu thành biểu thức chính quy
# {n} hoặc {num} -> (\d+)
# {title} -> (.*)
pattern = custom_template.strip()
pattern = re.escape(pattern)
pattern = pattern.replace(r'\{n\}', r'(\d+)')
pattern = pattern.replace(r'\{num\}', r'(\d+)')
pattern = pattern.replace(r'\{title\}', r'(.*)')
pattern = pattern.replace(r'\{.*?\}', r'.*') # Các placeholder khác
# Đảm bảo khớp với đầu dòng
if not pattern.startswith('^'):
pattern = '^' + pattern
return parse_novel_by_chapters(file_path, custom_pattern=pattern)
def split_by_word_count(text: str, word_count: int) -> List[str]:
"""
Chia đoạn theo số chữ
Args:
text: Văn bản gốc
word_count: Số chữ mỗi đoạn
Returns:
Danh sách văn bản sau khi chia đoạn
"""
if not text or not text.strip():
return []
if word_count <= 0:
raise ValueError(t("file_parser.word_count_positive"))
# Chia đều cho số từ
segments = []
total_length = len(text)
start = 0
while start < total_length:
end = start + word_count
if end > total_length:
end = total_length
segment = text[start:end].strip()
if segment:
segments.append(segment)
start = end
logger.info(f"Word count split done: {len(segments)} segments, ~{word_count} each")
return segments
def split_by_pattern(text: str, pattern: str, keep_marker: bool = True) -> List[str]:
"""
Chia đoạn theo văn bản/biến cố định
Args:
text: Văn bản gốc
mẫu: Đánh dấu đoạn (Biến hỗ trợ: % Chương (Chương), % Phần (Tiết), % Quay lại (Hồi), hoặc văn bản tùy chỉnh)
keep_marker: giữ lại đánh dấu chia đoạn không
Returns:
Danh sách văn bản sau khi chia đoạn
"""
if not text or not text.strip():
return []
if not pattern or not pattern.strip():
raise ValueError(t("file_parser.split_pattern_empty"))
# Nhận dạng thông minh: Nếu người dùng nhập "Chương x", "Chương X", v.v., nó sẽ tự động được chuyển đổi thành biểu thức chính quy
# Kiểm tra xem nó có chứa sự kết hợp của "chương" và "chương", "phần" và "trở lại" không
pattern_lower = pattern.strip().lower()
# Kiểm tra xem đó có phải là chế độ đơn giản hóa hay không (chẳng hạn như "Chương x", "Chương X")
if pattern_lower in ['第x章', '第x章', '第x章', '第x章']:
# Hỗ trợ cả chữ số Trung Quốc và chữ số Ả Rập, sử dụng + để đảm bảo khớp ít nhất một chữ số
# Sử dụng cái nhìn phủ định để đảm bảo rằng "Chương x" không thể được theo sau bởi các ký tự tiếng Trung (ngoại trừ dấu cách và dấu chấm câu)
# Định dạng phù hợp: Chương x, Chương x:, Chương x:, Chương x (dấu cách), Chương x (ngắt dòng sau dấu cách)
# Hỗ trợ định dạng Markdown: ## Chương x
# Nhưng nó không khớp: đây là chương đầu tiên, nội dung chương đầu tiên, v.v. (có chữ Hán sau đó)
regex_pattern = r'^[\s# )'
logger.info("Detected chapter pattern, auto-converting to regex")
elif pattern_lower in ['第x节', '第x节', '第x节', '第x节']:
regex_pattern = r'^\s*第\s*[一二三四五六七八九十百千万零〇0123456789]+\s*节\s*[:\s]*(?![\u4e00-\u9fff])'
logger.info("Detected section pattern, auto-converting to regex")
elif pattern_lower in ['第x回', '第x回', '第x回', '第x回']:
regex_pattern = r'^\s*第\s*[一二三四五六七八九十百千万零〇0123456789]+\s*回\s*[:\s]*(?![\u4e00-\u9fff])'
logger.info("Detected episode pattern, auto-converting to regex")
elif '%' in pattern_lower or '%' in pattern_lower or '%' in pattern_lower:
# Sử dụng thay thế biến
regex_pattern = pattern.strip()
# %Chương -> Khớp "Chương X", "Chương x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
regex_pattern = regex_pattern.replace('%', r'[一二三四五六七八九十百千万零〇0123456789]+\s*章')
# %Phần -> Khớp "Phần X", "Phần x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
regex_pattern = regex_pattern.replace('%', r'[一二三四五六七八九十百千万零〇0123456789]+\s*节')
# %chapter -> Khớp "chương X", "chương x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
regex_pattern = regex_pattern.replace('%', r'[一二三四五六七八九十百千万零〇0123456789]+\s*回')
# Đảm bảo biểu thức chính quy bắt đầu bằng ^ (khớp với đầu dòng)
if not regex_pattern.startswith('^'):
regex_pattern = '^' + regex_pattern
else:
# Không chứa đánh dấu chương, sử dụng trực tiếp chế độ gốc
regex_pattern = pattern.strip()
# Hãy thử chia theo mẫu
try:
# Nếu mã thông báo được giữ lại, hãy sử dụng biểu thức chính quy để tìm tất cả các vị trí phù hợp
if keep_marker:
# Tìm tất cả các vị trí phù hợp
matches = list(re.finditer(regex_pattern, text, flags=re.MULTILINE | re.IGNORECASE))
if not matches:
# Không khớp, trả lại toàn bộ văn bản
logger.warning(f"No pattern match: {regex_pattern}, returning full text")
return [text.strip()] if text.strip() else []
segments = []
prev_end = 0
for match in matches:
# Nhận thẻ phù hợp
marker = match.group(0)
# Lấy nội dung trước dấu (nếu có)
if prev_end < match.start():
prev_content = text[prev_end:match.start()].strip()
if prev_content:
segments.append(prev_content)
# Thêm thẻ
segments.append(marker.strip())
prev_end = match.end()
# Thêm đoạn cuối
if prev_end < len(text):
last_content = text[prev_end:].strip()
if last_content:
segments.append(last_content)
# Hợp nhất đánh dấu và nội dung
result = []
i = 0
while i < len(segments):
# Nếu nó hiện là một nhãn hiệu và có nội dung đằng sau nó
if i + 1 < len(segments):
result.append((segments[i] + segments[i + 1]).strip())
i += 2
else:
# chỉ đánh dấu hoặc nội dung
if segments[i].strip():
result.append(segments[i].strip())
i += 1
segments = result
else:
# Không giữ lại điểm đánh dấu và chia trực tiếp
segments = re.split(regex_pattern, text, flags=re.MULTILINE | re.IGNORECASE)
# Dọn dẹp các đoạn văn trống
segments = [seg.strip() for seg in segments if seg.strip()]
logger.info(f"Pattern split done: {len(segments)} segments")
return segments
except re.error as e:
raise ValueError(t("file_parser.invalid_regex", error=str(e)))
+202
View File
@@ -0,0 +1,202 @@
import os
import json
import logging
from typing import Dict, List, Optional
from locales.i18n import t
logger = logging.getLogger(__name__)
GENRES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "genres.json")
class GenreManager:
"""Quản lý các thể loại truyện và mô tả hướng dẫn viết"""
_cached_genres = None
_cached_mtime = 0
@classmethod
def get_default_genres(cls) -> List[Dict[str, str]]:
"""Lấy danh sách thể loại mặc định nếu chưa có file"""
# Mặc định sử dụng danh sách cũ từ file ngôn ngữ
default_names = t("create.genres")
if isinstance(default_names, str):
default_names = ["Huyền huyễn tiên hiệp", "Đô thị ngôn tình", "Khoa học viễn tưởng", "Võ hiệp", "Trinh thám", "Lịch sử", "Quân sự", "Game", "Kinh dị", "Xuyên không - Trọng sinh", "Hệ thống", "Đồng nhân", "Mạt thế", "Điền văn - Hài hước", "Cổ đại ngôn tình", "Kỳ ảo phương Tây", "Nữ cường", "Tổng tài", "Thanh xuân vườn trường", "Cung đấu", "Gia đấu", "Hồng hoang", "Ngôn tình võng du", "Đô thị dị năng", "Linh dị - Bí ẩn", "Đam mỹ", "Bách hợp", "Thám hiểm lăng mộ", "Dị giới đại lục", "Cổ đại làm ruộng", "Không gian Tùy thân", "ABO", "Ma cà rồng", "Cạnh kỹ - Thể thao", "Đồng nhân Anime", "Vô hạn lưu", "Khác"]
default_genres = []
for name in default_names:
desc = ""
if name == "Huyền huyễn tiên hiệp":
desc = "Thế giới tu tiên rộng lớn, sức mạnh siêu phàm, phân chia nhiều cảnh giới rõ rệt. Cốt truyện thường xoay quanh hành trình thăng cấp, cướp đoạt cơ duyên, phi thăng tiên giới. Văn phong cần kỳ ảo, chú trọng miêu tả công pháp, pháp bảo, linh thú."
elif name == "Đô thị ngôn tình":
desc = "Bối cảnh hiện đại, đời sống thành thị, xoay quanh các mối quan hệ tình cảm, gia đình, công sở. Tập trung vào tâm lý nhân vật, tình huống đời thường, lãng mạn hoặc ngược luyến. Lời thoại tự nhiên, gần gũi thực tế."
elif name == "Khoa học viễn tưởng":
desc = "Lấy bối cảnh tương lai, vũ trụ, hoặc các thế giới với trình độ công nghệ/khoa học vượt bậc (AI, robot, du hành thời gian). Đòi hỏi sự logic, xây dựng hệ thống quy tắc công nghệ/vũ trụ chặt chẽ mang tính thuyết phục cao."
elif name == "Võ hiệp":
desc = "Thế giới giang hồ, ân oán tình thù, võ công cái thế. Nhấn mạnh vào tinh thần trượng nghĩa, môn phái, các chiêu thức võ thuật võ lâm. Văn phong cổ trang, miêu tả chiêu thức hoa mỹ, tiết tấu nhanh."
elif name == "Trinh thám":
desc = "Xoay quanh các vụ án bí ẩn, tội phạm và quá trình đi tìm lời giải, phá án. Yêu cầu tính logic cực cao, chuỗi manh mối đan xen, gây cấn, hồi hộp, tạo bất ngờ ở phút chót (plot twist)."
elif name == "Lịch sử":
desc = "Bối cảnh dựa trên các triều đại lịch sử có thật hoặc hư cấu dựa trên bối cảnh lịch sử. Xoay quanh quyền mưu, tranh đoạt thiên hạ, chiến tranh giữa các quốc gia, xây dựng thế lực. Đòi hỏi kiến thức lịch sử, chính trị, văn phong trang trọng, mang đậm tính sử thi."
elif name == "Quân sự":
desc = "Tập trung vào các đề tài chiến tranh, quân đội, vũ khí, và các chiến dịch quân sự. Nhân vật chính thường là quân nhân, nhà chiến lược. Yêu cầu tính logic, am hiểu về chiến thuật, vũ khí thực tế, miêu tả các trận đánh hoành tráng, khốc liệt."
elif name == "Game":
desc = "Bối cảnh trong môi trường game thực tế ảo hoặc thế giới game kết hợp đời thực (Võng du). Nhân vật chính đánh quái, thăng cấp, cày đồ, lập guild, tham gia e-sports hoặc tranh bá. Cần hệ thống chỉ số, kỹ năng, trang bị rõ ràng, nhịp độ giải trí nhanh."
elif name == "Kinh dị":
desc = "Cốt truyện rùng rợn, khai thác các yếu tố siêu nhiên, tâm linh, quái vật hoặc tâm lý học vặn vẹo. Bầu không khí tăm tối, u ám, miêu tả cảm giác sợ hãi tột độ của nhân vật để gây rùng mình cho người đọc."
elif name == "Xuyên không - Trọng sinh":
desc = "Nhân vật chính du hành thời gian, không gian đến một thế giới khác hoặc sống lại kiếp trước. Thường mang theo kiến thức hiện đại hoặc bám sát ký ức kiếp trước để thay đổi số phận, vả mặt kẻ thù, xây dựng lại cuộc đời."
elif name == "Hệ thống":
desc = "Nhân vật chính sở hữu một 'Hệ thống' (như một trí tuệ nhân tạo trong não) giao nhiệm vụ, thưởng phạt, cung cấp cửa hàng đổi vật phẩm, kỹ năng. Văn phong mang tính giải trí cao, nhịp độ nhanh, tập trung thăng cấp."
elif name == "Đồng nhân":
desc = "Truyện dựa trên bối cảnh, nhân vật của một tác phẩm gốc có sẵn (như Naruto, Harry Potter, v.v.). Nhân vật chính thường xuyên không vào thế giới gốc, thay đổi cốt truyện hoặc tương tác với nhân vật gốc."
elif name == "Mạt thế":
desc = "Bối cảnh tận thế, thảm họa zombie, thiên tai, hoặc biến dị sinh học. Con người đấu tranh sinh tồn, thế giới xuất hiện các dị năng giả. Nhấn mạnh sự tàn khốc của nhân tính, thiếu thốn vật tư và xây dựng căn cứ."
elif name == "Điền văn - Hài hước":
desc = "Tập trung vào cuộc sống thường nhật, trồng trọt, chăn nuôi, làm giàu hoặc gia đình êm ấm. Nhịp độ chậm rãi (slow-burn), nhẹ nhàng, thư giãn, pha trộn nhiều tình huống hài hước, dở khóc dở cười."
elif name == "Cổ đại ngôn tình":
desc = "Bối cảnh phong kiến, xoay quanh tình yêu nam nữ. Khai thác gia đấu (những mâu thuẫn gia tộc), cung đấu (tranh giành quyền lực chốn hậu cung) hoặc quyền mưu quyền thần. Lời thoại cổ kính, trang nhã."
elif name == "Kỳ ảo phương Tây":
desc = "Bối cảnh phương Tây thời Trung Cổ, có hiệp sĩ, phép thuật, elf, rồng, ma cà rồng... Hệ thống ma pháp và thế giới quan mang đậm nét thần thoại hoặc fantasy (như D&D, Warcraft)."
elif name == "Nữ cường":
desc = "Nữ chính có tính cách kiên cường, thông minh, độc lập, hoặc sở hữu sức mạnh vượt trội. Truyện thường tập trung vào quá trình tự vươn lên, phá bỏ định kiến, đối mặt kẻ thù mà không dựa dẫm vào nam chính."
elif name == "Tổng tài":
desc = "Xa hoa, tập trung vào nam chính là những chủ tịch (tổng tài) giàu có, lạnh lùng, quyền lực, cùng nữ chính thường có xuất thân thấp hơn hoặc có vướng mắc tình cảm phức tạp. Yếu tố sủng ngọt hoặc ngược luyến tình thâm thường được đẩy mạnh."
elif name == "Thanh xuân vườn trường":
desc = "Bối cảnh trường học, thanh xuân rực rỡ. Khai thác tình yêu tuổi học trò trong sáng, nhiệt huyết thanh xuân, tình bạn, vượt qua áp lực thi cử và những rung động đầu đời."
elif name == "Cung đấu":
desc = "Bối cảnh mưu mô xảo quyệt chốn hậu cung phong kiến. Các phi tần, hoàng hậu, cung nữ dùng trí tuệ, mưu kế triệt hạ lẫn nhau để tranh giành sủng ái và quyền lực. Bầu không khí căng thẳng, máu lạnh."
elif name == "Gia đấu":
desc = "Bối cảnh trong những gia tộc lớn thời phong kiến. Đấu tranh, kèn cựa giữa mẹ chồng nàng dâu, các phòng, các chị em gái để bảo vệ lợi ích và vị thế trong gia đình. Đòi hỏi logic cao trị gia."
elif name == "Hồng hoang":
desc = "Dựa trên hệ thống thần thoại Trung Hoa cổ đại (Bàn Cổ khai thiên, Nữ Oa tạo nhân,...). Hệ thống sức mạnh cực kỳ khổng lồ, bối cảnh cấp bậc thần thánh vô lượng kiếp quy mô vũ trụ."
elif name == "Ngôn tình võng du":
desc = "Kết hợp game online và tình cảm đời thực. Tuyến tình cảm phát triển song song trong thế giới ảo và đời thực, có sự kiện offline, PK, đấu giải giữa các bang phái đầy thú vị."
elif name == "Đô thị dị năng":
desc = "Bối cảnh xã hội hiện đại nhưng đan xen những con người sở hữu năng lực đặc biệt (dị năng), tổ chức ngầm, hoặc yêu quái ẩn mình. Đòi hỏi sự kết hợp cân bằng giữa đời sống thực và thế giới huyền bí."
elif name == "Linh dị - Bí ẩn":
desc = "Xoay quanh tà ma, phong thủy, đạo sĩ trừ tà, hoặc những hiện tượng tâm linh không thể lý giải bằng khoa học. Không quá kinh dị tột độ mà chú trọng vào yếu tố huyền bí, hồi hộp khám phá sự thật."
elif name == "Đam mỹ":
desc = "Khai thác câu chuyện tình cảm sâu sắc, tinh tế hoặc ngang trái giữa hai nhân vật nam. Văn phong trau chuốt, chú trọng tâm lý, có thể lồng ghép mọi bối cảnh (cổ đại, hiện đại, mạt thế, tinh tế, v.v.)."
elif name == "Bách hợp":
desc = "Tập trung miêu tả tuyến tình cảm nhẹ nhàng, gắn bó hoặc mãnh liệt giữa hai nhân vật nữ. Duyên dáng, thiên về khai phá cảm xúc tinh tế, đồng cảm nội tâm, kết hợp nhiều bối cảnh khác nhau."
elif name == "Thám hiểm lăng mộ":
desc = "Hành trình trộm mộ, săn bảo vật ở các di tích cổ xưa chứa đầy cạm bẫy, cương thi, quái vật (như Đạo Mộ Bút Ký, Ma Thổi Đèn). Các chi tiết về đạo cụ, phong thủy, địa lý phải cực kỳ sống động và hấp dẫn."
elif name == "Dị giới đại lục":
desc = "Thế giới hoàn toàn hư cấu với bản đồ lục địa rộng lớn, có thể bao gồm kiếm thuật, ma pháp hoặc đấu khí. Tôn trọng luật rừng kẻ mạnh làm vua, mô phỏng các vương quốc, chủng tộc đa dạng tranh đấu."
elif name == "Cổ đại làm ruộng":
desc = "Một nhánh phụ của Điền văn nhưng nhấn mạnh vào bối cảnh cổ đại nghèo khó. Quá trình làm giàu chậm rãi từng bước từ hai bàn tay trắng, kinh doanh buôn bán, xây dựng gia đình no ấm."
elif name == "Không gian Tùy thân":
desc = "Nhân vật chính sở hữu một không gian bí mật (vòng tay, ngọc bội) có thể vào đó trồng trọt linh dược, chứa đồ, trữ nước thần, hoặc trốn tránh kẻ thù. Là bàn đạp lớn cho quá trình thăng cấp."
elif name == "ABO":
desc = "Bối cảnh Omegaverse (Alpha, Beta, Omega) với các đặc điểm sinh học và chất dẫn dụ đặc thù, thường lấy bối cảnh Tinh Tế (vũ trụ). Nhấn mạnh bản năng, sự kiểm soát, đánh dấu và các mối quan hệ tình cảm gai góc."
elif name == "Ma cà rồng":
desc = "Truyện xoay quanh sinh vật Ma cà rồng (Vampire), ma lang (Người sói), thợ săn. Thể hiện sự đấu tranh giữa bản năng khát máu và nhân tính, thường mang sắc thái lãng mạn tăm tối (Dark Romance)."
elif name == "Cạnh kỹ - Thể thao":
desc = "Nhiệt huyết thanh xuân, thi đấu e-sports hoặc các môn thể thao truyền thống (bóng rổ, điền kinh). Tôn vinh tinh thần đồng đội, nỗ lực luyện tập, vinh quang thi đấu, các chiến thuật đối kháng kịch tính."
elif name == "Đồng nhân Anime":
desc = "Viết dựa theo thế giới của các bộ Manga/Anime đình đám (One Piece, Pokemon, Bleach,...). Tương tác với các nhân vật được yêu thích, bổ sung những cái kết luyến tiếc hoặc tạo cuộc phiêu lưu hoàn toàn mới."
elif name == "Vô hạn lưu":
desc = "Nhân vật chính bị kéo vào một 'Không gian Chủ Thần', buộc phải xuyên qua nhiều thế giới (phim ảnh, game, ác mộng) để làm nhiệm vụ sinh tử, kiếm điểm nâng cấp. Nhịp độ dồn dập, hack não và nguy hiểm."
elif name == "Khác":
desc = "Các thể loại không nằm trong các phân loại chính, hoặc pha trộn nhiều yếu tố khác nhau (như Đồng nhân, Kỳ ảo phương Tây, Đam mỹ, Bách hợp, v.v.). AI cần linh hoạt kết hợp các yếu tố trong bối cảnh và yêu cầu riêng của tác giả để sáng tác cho phù hợp."
default_genres.append({
"name": name,
"description": desc
})
return default_genres
@classmethod
def ensure_data_dir(cls):
"""Đảm bảo thư mục data tồn tại"""
os.makedirs(os.path.dirname(GENRES_FILE), exist_ok=True)
@classmethod
def load_genres(cls) -> List[Dict[str, str]]:
"""Tải danh sách thể loại từ file (có cache theo mtime)"""
cls.ensure_data_dir()
if not os.path.exists(GENRES_FILE):
default_genres = cls.get_default_genres()
cls.save_genres(default_genres)
return default_genres
try:
current_mtime = os.path.getmtime(GENRES_FILE)
if cls._cached_genres is not None and cls._cached_mtime == current_mtime:
return cls._cached_genres
with open(GENRES_FILE, 'r', encoding='utf-8') as f:
genres = json.load(f)
cls._cached_genres = genres
cls._cached_mtime = current_mtime
return genres
except Exception as e:
logger.error(f"Error loading genres: {e}")
return cls.get_default_genres()
@classmethod
def save_genres(cls, genres: List[Dict[str, str]]) -> bool:
"""Lưu danh sách thể loại xuống file"""
cls.ensure_data_dir()
try:
with open(GENRES_FILE, 'w', encoding='utf-8') as f:
json.dump(genres, f, ensure_ascii=False, indent=4)
# Invalidate cache
cls._cached_genres = genres
cls._cached_mtime = os.path.getmtime(GENRES_FILE)
return True
except Exception as e:
logger.error(f"Error saving genres: {e}")
return False
@classmethod
def add_genre(cls, name: str, description: str = "") -> bool:
"""Thêm một thể loại mới"""
genres = cls.load_genres()
# Kiểm tra trùng tên
if any(g["name"] == name for g in genres):
return False
genres.append({"name": name, "description": description})
return cls.save_genres(genres)
@classmethod
def update_genre(cls, old_name: str, new_name: str, description: str) -> bool:
"""Cập nhật thông tin thể loại"""
genres = cls.load_genres()
for i, g in enumerate(genres):
if g["name"] == old_name:
# Nếu đổi tên, kiểm tra trùng tên mới
if old_name != new_name and any(x["name"] == new_name for x in genres):
return False
genres[i] = {"name": new_name, "description": description}
return cls.save_genres(genres)
return False
@classmethod
def delete_genre(cls, name: str) -> bool:
"""Xóa thể loại"""
genres = cls.load_genres()
initial_length = len(genres)
genres = [g for g in genres if g["name"] != name]
if len(genres) < initial_length:
return cls.save_genres(genres)
return False
@classmethod
def get_genre_names(cls) -> List[str]:
"""Lấy danh sách tên các thể loại để hiển thị UI"""
genres = cls.load_genres()
return [g["name"] for g in genres]
@classmethod
def get_genre_description(cls, name: str) -> str:
"""Lấy mô tả hướng dẫn của một thể loại"""
genres = cls.load_genres()
for g in genres:
if g["name"] == name:
return g["description"]
return ""
+893
View File
@@ -0,0 +1,893 @@
{
"app": {
"title": "AI小说创作工具 Pro",
"window_title": "AI小说创作工具 Pro - 生产级版本",
"subtitle": "_生产级别的智能小说创作系统 v4.0 正式版_",
"startup_log": "AI小说创作工具 Pro v4.0 正式版启动",
"port_in_use": "端口 {port} 被占用,使用端口 {new_port}",
"backends_loaded": "已加载 {count} 个后端",
"no_backends_warning": "⚠️ 没有启用的后端!请在设置中添加 API 配置",
"init_failed": "初始化失败: {error}",
"gradio_start": "启动Gradio应用... (端口: {port})",
"gradio_failed": "Gradio 启动失败: {error}"
},
"tabs": {
"rewrite": "📝 小说重写",
"polish": "✨ 小说润色",
"create": "✍️ 从零开始创作",
"continue_tab": "📖 续写项目",
"export": "💾 导出与分享",
"projects": "📂 项目管理",
"settings": "⚙️ 系统设置",
"about": "️ 关于"
},
"rewrite": {
"header": "### 上传小说 → 选择模式 → 智能处理",
"mode_rewrite": "重写模式",
"mode_continue": "续写模式",
"mode_label": "功能模式",
"rewrite_header": "#### 🔄 重写模式:上传小说 → 选择风格 → 智能重写",
"upload_file": "📤 上传文件 (txt/pdf/epub/md/docx)",
"split_method_label": "分段方式",
"split_auto": "自动分段",
"split_by_words": "按字数分段",
"split_by_pattern": "按固定文本分段",
"words_per_segment": "每段字数",
"words_per_segment_info": "按字数均匀分段",
"pattern_label": "分段标记",
"pattern_placeholder": "支持变量:%章、%节、%回,或自定义文本如---、***等",
"pattern_info": "使用%章匹配'第X章'%节匹配'第X节'%回匹配'第X回'",
"keep_marker": "保留分段标记",
"keep_marker_info": "是否在分段中保留标记文本",
"parse_file_btn": "解析文件",
"preset_style": "预设风格",
"parse_status": "解析状态",
"start_rewrite": "开始重写",
"stop_rewrite": "停止重写",
"live_preview": "实时预览",
"full_rewritten": "完整重写文本(可编辑)",
"progress_stats": "进度统计",
"continue_header": "#### ✍️ 续写模式:上传已有小说 → AI智能续写",
"upload_existing": "📤 上传已有小说 (txt/pdf/epub/md/docx)",
"novel_title": "📖 小说标题",
"novel_title_placeholder": "输入小说标题",
"target_words": "📊 目标字数",
"char_setting": "👥 人物设定",
"char_setting_placeholder": "主角姓名、性格、背景等(可选)",
"world_setting": "🌍 世界观设定",
"world_setting_placeholder": "时代背景、世界规则等(可选)",
"plot_idea": "📖 主线剧情想法",
"plot_idea_placeholder": "核心冲突、发展方向、结局走向等(可选)",
"existing_content": "已有内容(可编辑)",
"continue_result": "续写结果",
"continue_status": "续写状态",
"start_continue": "开始续写"
},
"polish": {
"header": "### 上传小说 → 选择润色类型 → 智能优化",
"upload_file": "📤 上传文件 (txt/pdf/epub/md/docx)",
"polish_type_label": "润色类型",
"polish_general": "全面润色",
"polish_find_errors": "查找错误",
"polish_suggestions": "改进建议",
"polish_direct": "直接修改",
"polish_remove_ai": "去除AI味",
"polish_enhance": "增强细节",
"polish_dialogue": "优化对话",
"polish_pacing": "改善节奏",
"custom_req": "自定义要求(可选)",
"custom_req_placeholder": "例如:加强人物性格描写、增加环境氛围、优化对话流畅度等",
"parse_status": "解析状态",
"start_polish": "开始润色",
"polish_suggest_btn": "润色并提供建议",
"original_text": "原文(可编辑)",
"polished_text": "润色结果",
"suggestions_label": "改进建议",
"polish_status": "润色状态"
},
"create": {
"header": "### 填写设定 → 生成大纲 → 自动创作(支持续写、暂停)",
"novel_title": "📖 小说标题",
"novel_title_default": "未命名小说",
"genre_label": "📚 小说类型",
"sub_genres_label": "🏷️ 子主题 / 标签",
"suggested_titles_list": "推荐列表(可在此编辑)",
"suggested_titles_radio": "选择一个标题",
"suggested_titles_help": "您可以自由编辑上方的文本,下方的选项将自动更新。",
"sub_genres": [
"后宫",
"1v1",
"NP",
"女扮男装",
"男扮女装",
"穿越",
"穿书",
"重生",
"系统",
"金手指",
"随身空间",
"灵泉",
"读心术",
"无敌流",
"苟道",
"热血",
"搞笑",
"爽文",
"甜宠",
"虐恋",
"破镜重圆",
"先婚后爱",
"欢喜冤家",
"青梅竹马",
"豪门世家",
"总裁",
"明星",
"娱乐圈",
"校园",
"学霸",
"网游",
"电竞",
"直播",
"美食",
"农场",
"种田",
"养娃",
"致富",
"宫斗",
"宅斗",
"权谋",
"女强",
"男强",
"双洁",
"废柴流",
"天才流",
"退婚流",
"灵气复苏",
"末世",
"机甲",
"星际",
"ABO",
"狼人",
"吸血鬼",
"西幻",
"魔法",
"剑魔",
"法师",
"扮猪吃虎",
"悲剧",
"治愈",
"黑暗",
"哥特",
"心理学",
"精神分裂",
"多重人格",
"悬疑",
"破案",
"盗墓",
"僵尸",
"规则怪谈",
"无限流",
"快穿",
"殖民",
"制造",
"领主",
"种植",
"钓鱼",
"御兽",
"宝可梦",
"动漫",
"哈利波特",
"漫威",
"星海",
"都市传说",
"古代",
"民国",
"七零年代",
"八零年代",
"九零年代",
"异世",
"大陆",
"部落",
"原始",
"修仙",
"复仇",
"炮灰",
"反派",
"先知",
"法术"
],
"genres": [
"玄幻仙侠",
"都市言情",
"科幻",
"武侠",
"悬疑",
"历史",
"军事",
"游戏",
"恐怖",
"穿越重生",
"系统",
"同人",
"末世",
"种田搞笑",
"古代言情",
"西方奇幻",
"女强",
"总裁",
"青春校园",
"宫斗",
"宅斗",
"洪荒",
"网游言情",
"都市异能",
"灵异悬疑",
"耽美",
"百合",
"盗墓探险",
"异界大陆",
"古代种田",
"随身空间",
"ABO",
"吸血鬼",
"体育竞技",
"动漫同人",
"无限流",
"其他"
],
"char_setting": "👥 人物设定",
"char_setting_placeholder": "主角姓名、性格、背景等",
"world_setting": "🌍 世界观设定",
"world_setting_placeholder": "时代背景、世界规则、特色设置等",
"plot_idea": "📖 主线剧情想法",
"plot_idea_placeholder": "核心冲突、发展方向、结局走向等",
"num_main_chars": "主要角色数量",
"num_sub_chars": "配角/反派数量",
"custom_prompt_label": "附加要求 (可选)",
"custom_prompt_placeholder": "输入额外要求 (例如: 都市异能风格...)",
"suggest_title_btn": "✨ 生成书名",
"suggest_btn": "✨ AI获取灵感",
"suggest_plot_btn": "✨ AI构思主线",
"suggesting_status": "正在思考灵感...",
"chapter_count": "📊 章节数目(留空为20",
"gen_outline_btn": "生成大纲",
"outline_display": "📋 大纲(可手动编辑)",
"context_header": "### 🔄 上下文增强设置(可选)",
"enable_context": "启用上下文增强",
"enable_context_info": "开启后,将使用前面章节的摘要/全文作为生成新章节的上下文",
"context_mode_label": "上下文模式",
"context_summary_mode": "摘要模式",
"context_full_mode": "全文模式",
"context_mode_info": "摘要模式:使用前面章节的摘要;全文模式:使用前面所有章节的完整内容",
"context_chapters": "上下文章节数",
"context_chapters_info": "使用前面多少章的摘要作为上下文",
"context_max_length": "上下文最大长度",
"context_max_length_info": "上下文的最大字符数",
"outline_format_help": "#### 📝 大纲格式说明\n请按以下格式编写大纲(任选其一):\n\n**格式1** (推荐):\n- 第1章: 开篇 - 介绍主人公和世界观\n- 第2章: 冲突 - 主人公遇到第一个重大挑战\n\n**格式2**:\n- 1. 开篇 - 介绍主人公和世界观\n- 2. 冲突 - 主人公遇到第一个重大挑战\n\n**注意**: 标题和描述用英文破折号 `-` 分隔,每行一章",
"start_gen_btn": "开始生成 / 续写",
"pause_gen_btn": "暂停生成",
"cache_status": "💾 缓存状态",
"cache_status_default": "无缓存",
"cache_timestamp": "⏰ 缓存时间",
"export_format_label": "📄 导出格式",
"export_format_word": "Word (.docx)",
"export_format_txt": "文本 (.txt)",
"export_format_md": "Markdown (.md)",
"export_format_html": "HTML (.html)",
"export_progress_btn": "📥 导出当前进度",
"download_file": "下载文件",
"export_status": "导出状态",
"novel_display": "📚 小说正文(实时更新)",
"gen_status": "生成状态",
"gen_status_default": "就绪"
},
"export": {
"header": "### 将创作导出为多种格式",
"paste_content": "粘贴小说内容",
"paste_placeholder": "从创作页面复制完整小说文本",
"novel_title": "小说标题",
"novel_title_placeholder": "用于文件名",
"export_word": "导出为 Word (.docx)",
"export_txt": "导出为纯文本 (.txt)",
"export_md": "导出为 Markdown (.md)",
"export_html": "导出为网页 (.html)",
"download_file": "下载文件",
"export_status": "导出状态",
"recent_exports": "最近导出的文件",
"refresh_files": "刷新文件列表",
"empty_content": "内容为空"
},
"continue_tab": {
"header": "### 选择项目 → 查看进度 → 继续创作",
"select_project": "📖 选择要续写的项目",
"load_btn": "📥 加载项目",
"project_info": "项目信息",
"no_project_loaded": "_尚未选择项目。请从列表中选择项目并点击加载项目。_",
"info_template": "**📖 {title}** | 📚 {genre}\n\n✅ 完成: **{completed}/{total} 章** ({percent}%)\n\n👤 人物: {char}\n\n🌍 世界观: {world}\n\n📖 剧情: {plot}",
"outline_label": "📋 项目大纲 (✅ = 已完成, ⬜ = 未写)",
"novel_label": "📚 已写内容",
"context_header": "### 🔄 上下文设置",
"continue_gen_btn": "▶️ 继续生成章节",
"pause_btn": "⏸️ 暂停",
"gen_status": "状态",
"gen_status_default": "就绪 - 选择项目以继续写作",
"select_first": "❌ 请先选择并加载项目",
"loaded_ok": "✅ 项目'{title}'已加载 - {completed}/{total} 章已完成",
"all_complete": "✅ 项目'{title}'已完成所有章节!",
"no_project_selected": "❌ 请选择一个项目",
"project_not_found": "❌ 未找到项目",
"load_failed": "❌ 加载项目失败: {error}",
"no_outline": "❌ 项目没有大纲。请使用创作标签。",
"continue_starting": "▶️ 从第 {chapter} 章开始继续生成..."
},
"projects": {
"header": "### 管理所有创作项目",
"refresh_btn": "🔄 刷新项目列表",
"projects_table": "我的项目",
"status_label": "状态",
"export_header": "### 📥 导出项目",
"select_project": "📖 选择要导出的项目",
"export_format": "📄 导出格式",
"export_btn": "📤 准备下载文件",
"download_file": "下载文件",
"export_status": "状态",
"recent_exports": "最近导出的文件",
"refresh_files": "刷新文件列表",
"delete_header": "### 🗑️ 删除项目",
"delete_select_project": "选择要删除的项目",
"delete_btn": "🗑️ 永久删除项目",
"delete_confirm": "⚠️ 确定要删除该项目吗?此操作不可逆。",
"delete_success": "✅ 成功删除项目",
"delete_failed": "❌ 删除项目失败:{error}",
"actions_header": "### ✍️ 项目操作",
"select_project_action": "📖 选择项目",
"continue_btn": "✍️ 续写",
"rewrite_btn": "🔄 重写",
"polish_btn": "✨ 润色",
"select_project_first": "❌ 请先选择一个项目",
"project_not_found": "❌ 未找到项目",
"loaded_to_create": "✅ 项目'{title}'已加载到创作标签",
"loaded_to_rewrite": "✅ 项目'{title}'内容已加载到重写标签",
"loaded_to_polish": "✅ 项目'{title}'内容已加载到润色标签",
"action_status": "状态",
"ai_illustration_header": "AI 章节插画生成",
"gen_illustration_btn": "生成插画",
"illustration_label": "章节插画",
"select_chapter": "请选择一个章节",
"invalid_chapter": "章节无效",
"chapter_not_found": "未找到章节"
},
"settings": {
"header": "### 🔧 API 接口配置与写作参数",
"tab_backends": "🌐 接口管理",
"tab_params": "📝 生成参数",
"tab_cache": "💾 缓存管理",
"backends_header": "#### 📋 配置后端接口",
"refresh_list": "🔄 刷新列表",
"test_all": "✅ 测试所有接口",
"backends_table": "已配置的后端列表",
"add_backend_header": "#### 添加新接口",
"provider_label": "API提供商",
"provider_info": "选择API提供商后自动填充默认配置",
"backend_name": "接口名称*",
"backend_name_placeholder": "例如: 我的OpenAI",
"backend_type": "接口类型*",
"base_url": "Base URL",
"base_url_placeholder": "例如: https://api.example.com/v1(仅OpenAI兼容接口需要填写)",
"model_name": "模型名称*",
"model_name_placeholder": "选择提供商后自动填充,可修改",
"api_key": "API Key (Ollama可留空)*",
"api_key_placeholder": "输入您的API密钥(不会被明文保存)",
"timeout": "超时时间(秒)",
"retry_count": "重试次数",
"enable_backend": "启用此接口",
"add_btn": " 添加接口",
"operation_result": "操作结果",
"test_manage_header": "#### 🔍 测试与管理",
"test_backend_name": "要测试的接口名称",
"test_backend_placeholder": "输入接口名称来测试连接",
"test_btn": "🧪 测试连接",
"test_result": "测试结果",
"delete_backend_name": "要删除的接口名称",
"delete_backend_placeholder": "输入接口名称来删除",
"delete_btn": "🗑️ 删除接口",
"delete_result": "删除结果",
"params_header": "#### 调整小说生成的各项参数",
"temperature_label": "Temperature (创意度)",
"temperature_info": "越高越有创意,越低越保守",
"top_p_info": "控制输出的多样性",
"top_k_info": "从最可能的K个token中选择",
"max_tokens_info": "每次生成的最大token数",
"chapter_target_words": "每章目标字数",
"writing_style": "写作风格",
"writing_styles": [
"流畅自然,情节紧凑,人物刻画细腻",
"文笔优美,意境深远",
"快节奏,情节跌宕起伏",
"细腻描写,情感丰富",
"诙谐趣味,轻松活泼"
],
"tone_label": "语调",
"tones": [
"中立",
"严肃",
"轻松",
"怀疑",
"温和",
"激情"
],
"char_dev_label": "人物塑造",
"char_dev_options": [
"详细",
"中等",
"简洁"
],
"plot_complexity_label": "情节复杂度",
"plot_complexity_options": [
"简单",
"中等",
"复杂"
],
"save_params_btn": "💾 保存生成参数",
"save_status": "保存状态",
"cache_header": "#### 📋 管理生成缓存",
"cache_size": "缓存大小",
"refresh_cache": "🔄 刷新缓存列表",
"get_cache_size": "📊 获取缓存大小",
"cache_table": "缓存列表",
"clear_selected": "🗑️ 清理选中缓存",
"clear_all": "🗑️ 清理所有缓存",
"cache_op_status": "操作状态",
"summary_cache_header": "#### 📋 管理上下文摘要缓存",
"summary_cache_size": "摘要缓存大小",
"refresh_summary": "🔄 刷新摘要列表",
"get_summary_size": "📊 获取摘要大小",
"summary_cache_table": "摘要缓存列表",
"clear_summary_cache": "🗑️ 清理所有摘要缓存",
"tab_genre": "📚 类型管理",
"genre_desc": "添加、修改、删除小说类型并编写详细描述/指南。AI在此类型的写作/灵感生成时将其作为系统提示。",
"genre_select": "选择要修改的类型",
"genre_name": "类型名称",
"genre_name_placeholder": "玄幻仙侠、科幻...",
"genre_description": "描述 / 写作指南",
"genre_description_placeholder": "输入此类型的写作风格、语气和特征,供AI学习...",
"genre_add_btn": " 新增",
"genre_update_btn": "💾 更新",
"genre_delete_btn": "🗑️ 删除",
"genre_op_status": "操作状态",
"genre_err_name_empty": "请输入类型名称!",
"genre_err_exists": "该类型名称已存在!",
"genre_add_success": "成功添加新类型!",
"genre_err_none_selected": "请选择一个类型!",
"genre_update_success": "成功更新该类型!",
"genre_err_update": "更新错误或名称冲突!",
"genre_delete_success": "已成功删除该类型!",
"genre_err_delete": "删除失败!",
"tab_sub_genre": "🏷️ 子主题管理(Tag)",
"sub_genre_desc": "添加、修改、删除子主题(标签)并编写描述/指南。AI在此子主题的写作生成时将其合并作为系统提示。",
"sub_genre_select": "选择要修改的子主题",
"sub_genre_name": "子主题名称",
"sub_genre_name_placeholder": "系统、穿越、爽文...",
"sub_genre_description": "描述 / 写作指南",
"sub_genre_description_placeholder": "输入此主题的细节,如人物塑造或背景设定,供AI学习...",
"sub_genre_add_btn": " 新增",
"sub_genre_update_btn": "💾 更新",
"sub_genre_delete_btn": "🗑️ 删除",
"sub_genre_op_status": "操作状态",
"sub_genre_err_name_empty": "请输入子主题名称!",
"sub_genre_err_exists": "该子主题名已存在!",
"sub_genre_add_success": "成功添加新子主题!",
"sub_genre_err_none_selected": "请选择一个子主题!",
"sub_genre_update_success": "成功更新该子主题!",
"sub_genre_err_update": "更新错误或名称冲突!",
"sub_genre_delete_success": "已成功删除该子主题!",
"sub_genre_err_delete": "删除失败!"
},
"about": {
"content": "# AI小说创作工具 Pro v4.0 正式版\n## 生产级别的智能小说创作系统\n\n### 🌟 主要功能\n- **智能创作**: 从零开始创作长篇小说,支持自定义大纲\n- **断点续传**: 支持暂停后续写,每章自动保存,生成中途也能在项目管理中看到\n- **智能重写**: 上传已有小说文本,用17种预设风格进行高质量重写\n- **智能续写**: 新增续写模式,上传别人写一半的小说,AI自动续写后续内容\n- **小说润色**: 全新的润色功能,支持全面润色、查找错误、改进建议、去除AI味等8种润色类型\n- **灵活文件解析**: 支持自定义章节模板,可识别各种格式的小说文件\n- **多格式导出**: 支持 Word、TXT、Markdown、HTML 等多种格式,导出可直接下载\n- **项目管理**: 管理多个创作项目,支持断点续写,自动保存进度\n- **灵活配置**: 支持多个 API 后端,Ollama密钥可选配置,细粒度的创作参数调整\n- **错误重试**: 生成章节时字数为0会自动重试,确保每章都有内容\n- **端口自动查找**: 默认端口被占用时自动查找可用端口\n\n### 🔧 技术特性\n- **错误恢复**: 完整的错误处理和日志系统\n- **缓存机制**: 智能缓存避免重复调用 API\n- **速率限制**: 内置令牌桶算法防止 API 限流\n- **负载均衡**: 多后端自动轮询\n- **性能监控**: 实时性能统计和分析\n- **线程安全**: 完全的并发安全设计\n\n### 🆕 v4.0 新增功能\n1. **断点续传**: 小说生成支持暂停后续写,每章自动保存\n2. **字数为0重试**: 章节生成失败或字数为0时自动重试3次\n3. **17种重写风格**: 新增古代宫斗、现代军事、历史演义等多种风格\n4. **智能续写模式**: 上传别人写一半的小说,AI自动续写后续内容\n5. **小说润色模块**: 全新润色功能,支持8种润色类型\n6. **灵活章节解析**: 支持5种预设模板和自定义章节格式\n7. **Ollama优化**: Ollama接口的API Key现在可以留空\n8. **直接下载**: 项目导出现在支持直接下载,无需到文件夹查找\n9. **端口自动查找**: 默认端口被占用时自动查找可用端口\n10. **打包支持**: 支持打包成exe,无需Python环境也可运行\n\n### 📋 系统要求\n- Python 3.8+\n- 依赖: gradio, pandas, openai, python-docx\n- 可选: PyMuPDF (PDF支持), ebooklib+beautifulsoup4 (EPUB支持)\n\n### 📦 打包成exe\n使用 `python build_exe.py` 命令可将应用打包成Windows可执行文件\n详见 `打包说明.md`\n\n### 📞 技术支持\n- 查看日志文件: `logs/` 目录\n- 项目保存位置: `projects/` 目录\n- 导出文件位置: `exports/` 目录\n- 缓存位置: `cache/` 目录\n- GitHub: [GitHub](https://github.com/yangqi1309134997-coder/ai-novel-generator)\n- 幻城云笔记: [幻城云笔记](https://hcnote.cn/)\n\n### ⚖️ 许可证\nMIT License"
},
"messages": {
"no_content_polish": "无内容可润色",
"no_content_rewrite": "无内容可重写",
"no_content_continue": "无内容可续写",
"no_title": "请填写小说标题",
"no_file": "无文件",
"polish_success": "润色成功",
"polish_complete": "润色完成",
"polish_complete_saved": "润色完成 | 已保存至项目管理",
"polish_failed": "润色失败: {error}",
"polish_invalid": "润色返回了无效内容(长度: {length}字)",
"polish_status_msg": "润色返回了状态消息而非实际内容: '{content}'",
"polish_segment_failed": "第 {num} 段润色失败: {error}",
"polish_segment_invalid": "第 {num} 段润色返回了无效内容(长度: {length}字)",
"polish_segment_progress": "润色第 {current}/{total} 段",
"polish_auto_split": "文本过长({length}字),启用自动分段处理",
"polish_split_done": "已分为 {count} 段,开始逐段润色",
"polish_all_done": "分段润色完成,共 {count} 段,总字数: {total}",
"polish_save_success": "润色结果已保存到项目: {id}",
"polish_save_failed": "润色结果保存失败: {msg}",
"rewrite_success": "重写成功",
"rewrite_complete": "重写完成",
"rewrite_paused": "已暂停 - 完成 {done}/{total} 段",
"rewrite_progress": "进度 {done}/{total} | 约 {words} 字",
"rewrite_segment_failed": "第 {num} 段重写失败: {error}",
"rewrite_invalid": "第 {num} 段重写返回了无效内容(长度: {length}字)",
"rewrite_status_msg": "第 {num} 段重写返回了状态消息而非实际内容: '{content}'",
"rewrite_segment_progress": "重写第 {current}/{total} 段",
"rewrite_save_success": "重写结果已保存到项目: {id}",
"rewrite_save_failed": "重写结果保存失败: {msg}",
"continue_success": "续写成功",
"continue_complete": "续写完成",
"continue_complete_saved": "续写完成 | 已保存至项目管理",
"continue_failed": "续写失败: {error}",
"continue_invalid": "续写返回了无效内容(长度: {length}字)",
"continue_status_msg": "续写返回了状态消息而非实际内容: '{content}'",
"continue_save_success": "续写结果已保存到项目: {id}",
"continue_save_failed": "续写结果保存失败: {msg}",
"gen_success": "生成成功",
"gen_outline_empty": "错误:大纲为空",
"gen_outline_failed": "大纲解析失败: {msg}",
"gen_paused": "已暂停 - 已完成 {done}/{total} 章,项目已保存",
"gen_chapter_progress": "正在生成第 {current}/{total} 章:{title}",
"gen_chapter_skip": "已完成 {num}/{total} 章(从缓存恢复)",
"gen_chapter_retry": "第 {num} 章生成失败或字数为0(尝试 {attempt}/{max}),正在重试...",
"gen_summarizing": "正在总结前几章以获取上下文...",
"gen_constructing_prompt": "正在构建第 {num} 章的提示词...",
"gen_waiting_ai": "正在等待 AI 响应第 {num} 章...",
"gen_saving_db": "正在将第 {num} 章保存到数据库...",
"gen_chapter_failed": "生成失败:第 {num} 章生成失败(已重试{max}次)",
"gen_complete": "生成完成 | 共 {total} 章 | 约 {words} 字 | 项目已保存",
"gen_save_success": "项目已保存: {id}",
"gen_save_failed": "项目保存失败: {msg}",
"cache_found": "发现缓存: {msg}",
"cache_chapter_restore": "从缓存恢复章节 {num}: {words} 字",
"stop_requested": "已请求暂停(当前章节完成后停止)",
"user_stop_requested": "用户请求停止生成",
"split_words_done": "按字数分段完成,共 {count} 段,每段约 {words} 字",
"split_pattern_done": "按固定文本分段完成,共 {count} 段",
"split_failed": "分段失败: {error}",
"split_no_pattern": "请输入分段标记",
"export_complete": "导出完成",
"export_failed": "导出失败: {error}",
"image_cover_base": "为小说《{title}》设计的高质量专业书封,题材为“{genre}”。电影级光效,细节丰富,史诗风格。",
"image_illustration_base": "为小说章节《{title}》设计的精美插画。场景描述:{summary}。数字艺术,高度精细。"
},
"templates": {
"default": "用更生动、细腻的笔触重写,语言优美,保留原意和情节,但加入更多细节描写和人物内心活动。",
"xianxia": "以古典仙侠风格重写,语言古风优雅,增加仙术法宝描写、灵气意境、人物心境修炼与道心感悟,保留原情节。",
"romance": "现代都市言情风格重写,语言轻松甜宠或虐心,增加浪漫互动、细腻心理描写、日常生活细节,人物情感更丰富。",
"thriller": "悬疑惊悚风格重写,语言营造紧张氛围,增加心理惊悚描写、线索铺垫、环境渲染与反转元素。",
"scifi": "硬科幻风格重写,语言严谨专业,增加科学原理解释、技术细节、世界观构建,逻辑自洽。",
"wuxia": "金庸古龙式武侠风格重写,语言潇洒豪气,增加武功招式描写、江湖恩怨、侠义精神。",
"palace": "古代宫廷风格重写,语言典雅华丽,增加宫廷礼仪、权谋算计、勾心斗角,人物关系复杂微妙。",
"military": "现代军事风格重写,语言硬朗刚毅,增加战术描写、武器装备、军营生活,突出军人的血性与担当。",
"historical": "历史演义风格重写,语言古朴庄重,增加历史背景、时代氛围、人物传记感,宏大的历史视角。",
"supernatural": "灵异玄幻风格重写,语言神秘诡异,增加超自然元素、灵异现象、阴阳五行,营造玄幻氛围。",
"campus": "青春校园风格重写,语言清新活泼,增加校园生活细节、青春悸动、成长感悟,纯真美好。",
"business": "职场商战风格重写,语言干练务实,增加商业策略、职场博弈、心理博弈,突出商业智慧。",
"cyberpunk": "赛博朋克风格重写,语言科技感十足,增加高科技元素、虚拟现实、人工智能,反乌托邦色彩。",
"fantasy": "西方奇幻风格重写,语言史诗感强,增加魔法体系、种族设定、神话元素,中世纪氛围。",
"horror": "恐怖悬疑风格重写,语言阴森压抑,增加恐怖氛围、心理暗示、超自然现象,让人毛骨悚然。",
"humor": "幽默搞笑风格重写,语言诙谐机智,增加搞笑元素、夸张描写、喜剧效果,轻松有趣。",
"literary": "文艺清新风格重写,语言优美清新,增加情感细腻、意境深远、文字诗意,如诗如画。",
"adventure": "热血冒险风格重写,语言激昂澎湃,增加冒险元素、战斗场景、友情羁绊,充满正能量。",
"name_default": "重写风格 - 默认",
"name_xianxia": "重写风格 - 玄幻仙侠",
"name_romance": "重写风格 - 都市言情",
"name_thriller": "重写风格 - 悬疑惊悚",
"name_scifi": "重写风格 - 科幻硬核",
"name_wuxia": "重写风格 - 武侠江湖",
"name_palace": "重写风格 - 古代宫斗",
"name_military": "重写风格 - 现代军事",
"name_historical": "重写风格 - 历史演义",
"name_supernatural": "重写风格 - 灵异玄幻",
"name_campus": "重写风格 - 青春校园",
"name_business": "重写风格 - 职场商战",
"name_cyberpunk": "重写风格 - 赛博朋克",
"name_fantasy": "重写风格 - 西幻魔法",
"name_horror": "重写风格 - 恐怖悬疑",
"name_humor": "重写风格 - 幽默搞笑",
"name_literary": "重写风格 - 文艺清新",
"name_adventure": "重写风格 - 热血冒险"
},
"prompts": {
"outline_system": "你是专业的小说大纲策划师,擅长创作吸引人的故事框架。",
"outline_user": "请生成一篇{genre}小说的完整大纲,标题:《{title}》。\n\n人物设定:{character_setting}\n\n世界观:{world_setting}\n\n主线剧情:{plot_idea}\n\n风格要求:{style_desc}\n\n要求:\n1. 总章节数约 {total_chapters} 章\n2. 每章格式严格:第X章: 章节标题 - 简要剧情描述(50-100字)\n3. 情节连贯,有起承转合,人物发展合理\n4. 只输出大纲列表,不要其他内容\n5. 大纲要精彩、引人入胜、有悬念\n6. 按三幕结构分配章节:\n - 第一幕(25%章节):介绍背景、建立世界观、设定冲突\n - 第二幕(50%章节):发展、升级、中期转折\n - 第三幕(25%章节):高潮、解决、结局\n7. 每章必须有小冲突或明确的进展,不能有‘水章’",
"chapter_system": "你是优秀的长篇小说作家,创作深入人心的故事。请用自然的人类写作风格。避免AI常见的写作模式,如:频繁以‘然而’、‘此外’、‘与此同时’开头;结尾过于圆满或说教;过多使用‘地+动词’结构;连续列举三个并列内容(如‘感到X、Y和Z’)。",
"chapter_user": "请撰写小说《{novel_title}》的第{chapter_num}章。\n\n章节标题:{chapter_title}\n本章大纲:{chapter_desc}\n\n整体设定:\n人物:{character_setting}\n世界观:{world_setting}\n主线剧情:{plot_idea}\n\n风格要求:{style_desc}\n\n具体要求:\n1. 正文约 {target_words} 字(中文字符)\n2. 情节严格符合本章大纲,与全书连贯\n3. 对话自然且具有每个角色的独特个性,心理描写细腻,环境描写生动\n4. 结尾留下适当悬念或铺垫下一章\n5. 使用‘展示而非叙述’技巧:通过行动和细节体现情感,而不是直接描述\n6. 平衡行动、对话和内心描写\n7. 只输出正文,不要章节标题、说明或其他内容{continuity_prompt}{context_prompt}",
"continuity_prompt": "\n\n【前文回顾】\n{previous_content}\n\n【连贯性检查清单】\n✓ 确保情节走向与前文保持一致\n✓ 人物状态和位置与前文对应\n✓ 已有的悬念在本章得到呼应或推进\n✓ 人物对话风格保持一致\n✓ 避免重复已有的信息或情节\n✓ 新的悬念为后续章节铺路\n✓ 检查与总体大纲的进度,确保不偏离主线\n✓ 发展副线剧情与主线并行推进\n\n请严格按照以上检查清单确保内容与前文连贯流畅。",
"context_prompt": "\n\n{context_summary}\n\n请根据以上摘要了解前文的主要情节,确保本章与前文连贯。",
"rewrite_system": "你是优秀的小说编辑,擅长用生动细腻的笔触改进文本。",
"rewrite_user": "请按照以下风格重写原文,保留原意和情节,但加入更多细节:\n\n风格要求:{style}\n\n原文:\n{text}\n\n【重要要求】\n1. 必须输出完整的重写后的小说内容,字数应该与原文相当\n2. 绝对不能只输出\"重写成功\"、\"润色成功\"、\"生成成功\"等状态消息\n3. 必须输出实际的重写文本,包含丰富的细节描写和情节展开\n4. 如果原文有1000字,重写后也应该有1000字左右\n5. 不要输出任何说明性文字或状态确认消息\n6. 增强多感官描写:嗅觉、味觉、触觉,不仅仅是视觉和听觉\n7. 添加环境细节反映角色心情\n8. 使用创意的比喻和类比,避免陈词滥调\n\n请严格按照以上要求输出完整的重写内容。",
"summary_system": "你是专业的内容编辑,擅长提炼文本的核心内容。",
"summary_user": "请为以下文本生成一个简洁的摘要,不超过{max_length}字。\n\n文本:\n{text}\n\n摘要:",
"polish_system": "你是专业的文学编辑和润色专家,擅长提升文本质量和文笔水平。",
"polish_general": "请对以下文本进行全面的润色优化,提升文笔质量,使语言更流畅、更生动、更有感染力。",
"polish_find_errors": "请仔细检查以下文本,找出其中的错误(包括错别字、语法错误、逻辑错误、用词不当等),并提出修改建议。",
"polish_suggest": "请阅读以下文本,提出具体的改进建议,包括情节、人物、对话、描写等方面的优化方向。",
"polish_direct": "请直接修改并优化以下文本,提升文笔质量,使其更加专业和完善。",
"polish_remove_ai": "请去除以下文本中的AI生成痕迹,使其更加自然、更像人工创作,增加人味和情感深度。",
"polish_enhance": "请对以下文本进行细节增强,增加环境描写、心理描写、感官描写等,使内容更加丰富立体。",
"polish_dialogue": "请优化以下文本中的对话部分,使对话更自然、更符合人物性格、更有个性。",
"polish_pacing": "请调整以下文本的节奏,优化情节推进速度,使故事更加引人入胜。",
"polish_extra_req": "\n\n额外要求:{custom_requirements}",
"polish_output_only": "\n\n原文:\n{text}\n\n请只输出润色后的文本或建议,不要其他内容。",
"polish_suggest_system": "你是专业的文学编辑,擅长文本分析、错误查找和润色优化。",
"polish_suggest_user": "请对以下文本进行全面分析和优化:\n\n1. **找出错误**:检查错别字、语法错误、逻辑错误、用词不当等\n2. **提出建议**:给出具体的改进建议,包括情节、人物、对话、描写等\n3. **直接修改**:提供润色后的优化版本\n\n原文:\n{text}\n\n{extra_req}\n\n请按以下格式输出:\n---\n【发现的错误】\n(列出发现的错误)\n\n【改进建议】\n(列出改进建议)\n\n【润色后的文本】\n(直接修改后的文本)\n---",
"suggest_system": "你是擅长为创意小说构思剧情、背景与世界观的专业策划专家,给出的创意要深邃有趣且非常具体。",
"suggest_title_user": "请为一部小说提供约10个书名建议。\n\n类型:{genre}\n\n要求:\n- 书名必须简洁、令人印象深刻、引起好奇心,并准确反映该类型的特点。\n- 每个书名需附带一句简短的故事初步内容描述。\n- 必须且只能返回原生 JSON 格式的结果,不要包含 Markdown 标记 (```json),也不要任何解释或多余的对话。\n- 强制 JSON 结构:{\"suggestions\": [{\"title\": \"书名 1\", \"description\": \"描述 1\"}, {\"title\": \"书名 2\", \"description\": \"描述 2\"}]}",
"suggest_char_user": "请为一本小说构思详细的人物设定。\n\n暂定书名:{title}\n类型:{genre}\n数量要求:\n- 主要角色:{num_main_chars} 人\n- 配角/反派:{num_sub_chars} 人\n\n对每个角色的要求:\n1. 明确角色定位(主角/配角/反派)\n2. 姓名、外貌及突出的性格特点\n3. 能力、技能或特殊力量\n4. 背景身世及核心动机\n\n请进行深入且富有层次的描写。仅返回设定内容,不作多余解释。",
"suggest_world_user": "请为一本小说构思世界观设定(150-200字)。\n\n暂定书名:{title}\n类型:{genre}\n\n要求:指出独特的法则、力量体系、社会结构或历史背景。仅返回设定内容。",
"suggest_plot_user": "请为该小说构思核心主线剧情(200-250字)作为发展基础。\n\n书名:{title}\n类型:{genre}\n\n(如有) 人物设定:{character_setting}\n(如有) 世界观:{world_setting}\n\n要求:\n- 剧情要有吸睛的开局、中期的转折以及明确的核心冲突\n- 外部冲突(对手、势力、任务)和内心冲突(矛盾、难以抉择)\n- 至少2-3个意想不到但合理的转折\n- 结局留有余味(圆满结局或开放式结局视类型而定)\n- 仅返回剧情大纲。",
"continue_system": "你是优秀的长篇小说作家,擅长创作引人入胜的故事和自然的情节衔接。",
"continue_user": "请续写小说《{novel_title}》的下一章内容。\n\n【已有设定】\n人物设定:{character_setting}\n世界观:{world_setting}\n主线剧情:{plot_idea}\n\n【风格要求】\n{style_desc}\n\n【前文回顾】(最近1500字)\n{previous_content}\n\n【续写要求】\n1. 根据前文内容自然续写下一章\n2. 保持与前文的连贯性,包括人物性格、情节发展、对话风格等\n3. 字数约 {target_words} 字\n4. 不要重复前文已有的内容\n5. 结尾留下适当的悬念或铺垫\n6. 只输出续写的正文,不要章节标题、说明或其他内容",
"chapter_summary_system": "你是专业的内容编辑,擅长提炼章节的核心情节和关键信息。",
"chapter_summary_user": "请为以下章节生成一个简洁的摘要(100-200字)。\n\n章节标题:{chapter_title}\n\n章节内容:\n{chapter_content}\n\n要求:\n1. 保留关键情节和人物信息\n2. 突出章节的核心冲突和转折\n3. 语言简洁明了\n4. 只输出摘要内容,不要其他说明",
"style_description": "写作风格:{writing_style}\n语调:{writing_tone}\n人物塑造:{character_development}\n情节复杂度:{plot_complexity}",
"context_header": "【前文摘要】\n",
"chapter_context_line": "第{chapter_num}章:{summary}\n",
"found_errors_header": "发现的错误",
"suggestions_header": "改进建议",
"polished_text_header": "润色后的文本",
"ai_no_format": "AI未按格式输出,请查看润色结果"
},
"generator": {
"title_empty": "小说标题不能为空",
"char_empty": "人物设定不能为空",
"world_empty": "世界观设定不能为空",
"plot_empty": "主线剧情不能为空",
"outline_empty": "大纲为空",
"outline_parse_failed": "无法从大纲中解析任何章节,请检查格式",
"outline_parse_success": "解析成功,共 {count} 章",
"outline_gen_success": "大纲生成成功",
"gen_success": "生成成功",
"suggest_success": "生成建议成功",
"text_empty": "文本为空",
"text_too_long_rewrite": "文本过长(>20000字),请分段处理",
"text_too_long_polish": "文本过长(>10000字),请分段处理",
"existing_text_empty": "已有文本为空",
"chapter_content_empty": "章节内容为空",
"api_empty_content": "API返回空内容,请检查API配置",
"api_status_msg": "API返回了状态消息,请检查API配置",
"rewrite_success": "重写成功",
"rewrite_too_short": "重写内容过短({length}字),可能是API问题",
"rewrite_failed_retries": "重写失败:在{max}次尝试后仍然失败",
"polish_success": "润色成功",
"polish_too_short": "润色内容过短({length}字),可能是API问题",
"polish_failed_retries": "润色失败:在{max}次尝试后仍然失败",
"continue_success": "续写成功",
"continue_too_short": "续写内容过短({length}字),可能是API问题",
"continue_failed_retries": "续写失败:在{max}次尝试后仍然失败",
"summary_success": "成功",
"summary_gen_success": "摘要生成成功",
"summary_gen_failed": "摘要生成失败",
"summary_gen_error": "生成摘要出错: {error}",
"cache_id_empty": "项目ID不能为空",
"cache_data_empty": "缓存数据不能为空",
"cache_save_success": "缓存保存成功",
"cache_save_failed": "保存缓存失败: {error}",
"cache_not_found": "缓存不存在",
"cache_load_success": "缓存加载成功",
"cache_load_failed": "加载缓存失败: {error}",
"cache_clear_success": "缓存清理成功",
"cache_clear_failed": "清理缓存失败: {error}",
"summary_empty": "摘要内容不能为空",
"summary_save_success": "摘要保存成功",
"summary_save_failed": "保存摘要失败: {error}",
"summary_dir_not_found": "摘要目录不存在",
"summary_load_done": "加载了 {count} 个章节摘要",
"summary_load_failed": "加载摘要失败: {error}",
"summary_clear_success": "摘要清理成功",
"summary_clear_failed": "清理摘要失败: {error}",
"unknown_title": "未知"
},
"exporter": {
"no_content": "无内容可导出",
"no_chapters": "无法从文本中提取章节",
"export_success": "导出成功: {filename}",
"export_failed": "导出失败: {error}",
"missing_docx": "错误:缺少python-docx依赖,请运行: pip install python-docx",
"missing_markdown": "错误:缺少markdown依赖,请运行: pip install markdown",
"generated_at": "生成于: {datetime}",
"generated_date": "生成日期:{date}",
"first_chapter": "第一章",
"body_font": "宋体",
"title_font": "黑体"
},
"config_api": {
"name_required": "接口名称不能为空",
"type_required": "接口类型不能为空",
"model_required": "模型名称不能为空",
"name_exists": "接口 '{name}' 已存在",
"add_success": "接口 '{name}' 添加成功",
"add_failed": "添加接口失败: {error}",
"update_success": "接口 '{name}' 更新成功",
"update_not_found": "未找到接口: {name}",
"update_failed": "更新接口失败: {error}",
"delete_success": "接口 '{name}' 已删除",
"delete_not_found": "未找到接口: {name}",
"delete_failed": "删除接口失败: {error}",
"toggle_success": "接口 '{name}' 已{status}",
"toggle_enabled": "启用",
"toggle_disabled": "禁用",
"toggle_not_found": "未找到接口: {name}",
"toggle_failed": "切换接口状态失败: {error}",
"test_success": "接口 '{name}' 测试成功",
"test_not_found": "未找到接口: {name}",
"test_failed": "接口 '{name}' 测试失败: {error}",
"test_prompt": "请回复'OK'来确认API连接正常。",
"export_success": "配置已导出到: {filepath}",
"export_failed": "导出配置失败: {error}",
"default_success": "已将接口 '{name}' 设为默认\n所有请求将优先尝试使用此接口",
"default_failed": "设置默认失败: {error}"
},
"project_manager": {
"create_success": "项目 '{title}' 创建成功",
"create_failed": "创建项目失败: {error}",
"save_success": "项目 '{title}' 保存成功",
"save_failed": "保存项目失败: {error}",
"load_success": "项目加载成功",
"load_not_found": "项目不存在: {id}",
"load_failed": "加载项目失败: {error}",
"delete_success": "项目已删除",
"delete_not_found": "项目不存在: {id}",
"delete_failed": "删除项目失败: {error}",
"export_success": "项目导出成功: {filepath}",
"export_failed": "导出项目失败: {error}"
},
"ui": {
"col_project_name": "项目名",
"col_type": "类型",
"col_created_at": "创建时间",
"col_updated_at": "更新时间",
"col_chapters": "章节数",
"col_completion": "完成度",
"col_current_chapter": "当前章节",
"col_total_chapters": "总章节",
"col_status": "状态",
"col_cache_time": "缓存时间",
"col_size_kb": "大小(KB)",
"col_project_id": "项目ID",
"col_chapter_count": "章节数",
"col_name": "名称",
"col_backend_type": "类型",
"col_model": "模型",
"col_enabled": "启用",
"col_timeout": "超时(秒)",
"col_retry_times": "重试次数",
"col_test": "🧪 测试",
"col_default": "⭐ 默认",
"no_projects": "暂无项目",
"no_cache": "暂无缓存",
"no_summary_cache": "暂无摘要缓存",
"found_projects": "找到 {count} 个项目",
"found_caches": "找到 {count} 个缓存",
"found_summary_caches": "找到 {count} 个摘要缓存",
"cache_total_size_kb": "缓存总大小: {size} KB",
"cache_total_size_mb": "缓存总大小: {size} MB",
"summary_total_size_kb": "摘要缓存总大小: {size} KB",
"summary_total_size_mb": "摘要缓存总大小: {size} MB",
"get_cache_size_failed": "获取缓存大小失败",
"get_summary_size_failed": "获取摘要缓存大小失败",
"cleared_caches": "✅ 已清理 {cleared}/{total} 个缓存",
"cleared_summary_caches": "✅ 已清理 {cleared}/{total} 个摘要缓存",
"no_cache_to_clear": "❌ 没有缓存可清理",
"no_summary_to_clear": "❌ 没有摘要缓存可清理",
"select_cache_to_clear": "❌ 请选择要清理的缓存",
"cache_found_info": "发现缓存:已完成 {current}/{total} 章",
"cache_timestamp_info": "缓存时间: {time}",
"cache_check_failed": "检查失败",
"no_content_export": "❌ 没有内容可导出",
"no_title_export": "❌ 请填写小说标题",
"unsupported_format": "❌ 不支持的导出格式: {format}",
"export_success": "✅ 导出成功!",
"export_failed": "❌ 导出失败: {error}",
"export_error": "❌ 导出出错: {error}",
"select_project": "❌ 请选择一个项目",
"project_not_exist": "❌ 项目'{title}'不存在",
"metadata_not_exist": "❌ 项目元数据文件不存在: {file}",
"no_exportable_content": "❌ 项目没有可导出的内容,请先生成章节内容",
"file_not_exist": "❌ 导出文件不存在: {file}",
"export_no_filepath": "❌ 导出失败: 未返回文件路径",
"fill_required_fields": "❌ 请填写所有必填字段(名称、URL、模型)",
"api_key_required": "❌ 该类型接口必须填写API密钥",
"enter_backend_name": "❌ 请输入后端名称",
"enter_delete_name": "❌ 请输入要删除的后端名称",
"refreshed_backends": "✅ 已刷新后端列表",
"backend_loaded_for_edit": "✅ 已加载配置 '{name}' 以供编辑",
"backend_test_header": "后端连接测试结果:\n",
"backend_available": "✓ 可用",
"backend_unavailable": "✗ 不可用",
"save_success": "保存成功",
"save_failed": "保存失败: {error}",
"no_file": "无文件",
"perf_no_data": "暂无性能数据",
"perf_report_header": "=== 性能监控报告 ===\n",
"perf_stat_line": "{name}: 平均={avg:.2f}ms, 最大={max:.2f}ms, 最小={min:.2f}ms, 次数={count}\n",
"validation_api_key_empty": "API密钥不能为空",
"validation_unknown_provider": "未知的API提供商",
"validation_openai_key_format": "OpenAI API密钥应该以'sk-'开头",
"validation_anthropic_key_format": "Anthropic API密钥应该以'sk-ant-'开头",
"validation_google_key_format": "Google API密钥格式不正确",
"validation_key_format_invalid": "API密钥格式不正确",
"validation_key_passed": "密钥格式验证通过",
"validation_model_empty": "模型名称不能为空",
"validation_model_invalid": "模型名称只能包含字母、数字、下划线、点和连字符",
"validation_model_passed": "模型名称验证通过",
"outline_chapter_format": "<details>\n<summary><b>第{num}章: {title}</b> <i>(点击展开/折叠)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"outline_chapter_format_open": "<details open>\n<summary><b>第{num}章: {title}</b> <i>(点击展开/折叠)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"segment_format": "<details open>\n<summary><b>第{num}段</b> <i>(点击展开/折叠)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"content_format": "<details open>\n<summary><b>内容</b> <i>(点击展开/折叠)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"rewritten_segments_summary": "所有段落",
"view_all": "🔍 查看全部",
"chapter": "第",
"segment": "段落",
"select_chapter_to_view": "📑 选择显示的章节",
"select_segment_to_view": "📑 选择显示的段落"
},
"api_client": {
"no_backends": "错误:无有效后端,请检查设置",
"invalid_messages": "错误:messages 必须是非空列表",
"no_api_client": "错误:无可用的API客户端",
"invalid_content": "API返回了无效内容(长度: {length}字)",
"error_prefix": "错误:{error}",
"retry_failed": "错误:在 {max} 次重试后仍然失败",
"rate_limit_error": "错误:已超过API调用频率限制(Rate Limit):{error}",
"auth_error": "认证错误:请检查API密钥或后端配置:{error}",
"connection_error": "连接错误:无法连接到API服务器:{error}",
"api_error": "API错误:API服务器返回错误:{error}",
"test_prompt": "你是一个有帮助的助手",
"test_hello": "你好",
"gen_image_success": "图像生成成功",
"gen_image_failed": "图像生成失败: {error}",
"image_gen_unsupported": "当前API提供商不支持图像生成功能。请使用OpenAI、Together AI或SiliconFlow等提供商的付费API。"
},
"file_parser": {
"no_file": "无文件",
"file_too_large": "错误:文件过大 ({size}MB > 50MB)",
"parse_complete": "解析完成,共 {count} 段,约 {chars} 字",
"read_failed": "读取失败:{error}",
"missing_pymupdf": "错误:缺少PyMuPDF依赖,请运行: pip install PyMuPDF",
"missing_ebooklib": "错误:缺少ebooklib或beautifulsoup4依赖,请运行: pip install ebooklib beautifulsoup4",
"missing_docx": "错误:缺少python-docx依赖,请运行: pip install python-docx",
"unsupported_format": "不支持的文件格式(支持 txt/pdf/epub/md/docx",
"unsupported_chapter_format": "不支持的文件格式",
"chapter_parse_complete": "解析完成,共 {count} 章",
"chapter_parse_failed": "解析失败: {error}",
"split_word_done": "按字数分段完成,共 {count} 段,每段约 {words} 字",
"split_pattern_empty": "分段标记不能为空",
"word_count_positive": "字数必须大于0",
"invalid_regex": "无效的正则表达式: {error}",
"file_not_exist": "文件不存在: {path}",
"upload_read_failed": "读取上传文件失败: {error}"
},
"config": {
"backend_name_empty": "后端名称不能为空",
"unsupported_type": "不支持的类型: {type}",
"base_url_invalid": "Base URL必须以http或https开头",
"api_key_empty": "API Key不能为空",
"model_empty": "模型名称不能为空",
"timeout_range": "超时时间必须在5-10000秒之间",
"retry_range": "重试次数必须在1-10之间",
"temp_range": "温度值必须在0.1-2.0之间",
"top_p_range": "top_p必须在0.1-1.0之间",
"max_tokens_range": "max_tokens必须在100-100000之间",
"chapter_words_range": "章节目标字数必须在500-65536之间",
"backend_exists": "后端'{name}'已存在",
"backend_add_success": "后端添加成功",
"backend_update_success": "后端更新成功",
"backend_not_found": "后端'{name}'不存在",
"backend_delete_success": "后端'{name}'已删除",
"gen_params_update_success": "生成参数更新成功",
"config_export_success": "配置已导出至 {filepath}",
"config_export_failed": "导出配置失败: {error}",
"config_save_success": "配置保存成功",
"config_save_failed": "保存配置失败: {error}",
"config_load_success": "配置加载成功",
"config_file_not_found": "配置文件不存在,使用默认配置",
"config_load_failed": "加载配置失败: {error}",
"config_file_missing": "配置文件不存在: {path}",
"config_format_unsupported": "不支持的配置文件格式: {ext}",
"default_backend_name": "本地Ollama"
}
}
+115
View File
@@ -0,0 +1,115 @@
# AI小说创作工具 Pro v4.0 - 使用说明
## 📦 快速开始
### 一键启动(推荐)
1. 确保已安装Python 3.8+
2. 在项目根目录运行:
```bash
python start_venv.py
```
3. 脚本会自动:
- 检查Python版本
- 创建虚拟环境(如需要)
- 安装/更新依赖
- 启动应用
### 手动启动
1. 创建虚拟环境:
```bash
python -m venv venv
```
2. 激活虚拟环境:
- Windows: `venv\Scripts\activate`
- Linux/Mac: `source venv/bin/activate`
3. 安装依赖:
```bash
pip install -r requirements.txt
```
4. 启动应用:
```bash
python app.py
```
## 📚 版权信息
版权所有 © 2026 新疆幻城网安科技有限责任公司 (幻城科技)
作者:幻城
## 🔗 相关链接
- GitHub: <https://github.com/yangqi1309134997-coder/ai-novel-generator>
- 幻城云笔记: <https://hcnote.cn/>
## ❓ 常见问题
### 1. 端口被占用
**问题**:启动时提示端口7860被占用
**解决**:脚本会自动查找可用端口,无需手动处理
### 2. 依赖安装失败
**问题**pip安装依赖时失败
**解决**
- 升级pip`python -m pip install --upgrade pip`
- 使用国内镜像:`pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt`
### 3. 虚拟环境创建失败
**问题**venv模块不可用
**解决**:使用virtualenv
```bash
pip install virtualenv
virtualenv venv
```
### 4. 模块导入错误
**问题**:启动时提示模块不存在
**解决**:确保在虚拟环境中安装了所有依赖
### 5. API配置错误
**问题**:无法连接API
**解决**
1. 检查API密钥是否正确
2. 在"系统设置"中测试后端连接
3. 查看日志文件:`logs/novel_tool_*.log`
## 📁 项目结构
```
ai-novel-generator-4.0/
├── app.py # 主程序
├── start_venv.py # 一键启动脚本
├── requirements.txt # 依赖列表
├── config/ # 配置目录
├── logs/ # 日志目录
├── projects/ # 项目数据
├── exports/ # 导出文件
└── cache/ # 缓存目录
```
## 📖 功能特性
- ✅ 智能创作:从零开始创作长篇小说
- ✅ 智能重写:17种预设风格重写
- ✅ 智能续写:自动续写已有小说
- ✅ 小说润色:8种润色类型
- ✅ 灵活分段:支持自动分段、按字数分段、按固定文本分段
- ✅ 项目管理:多项目管理,支持断点续传
- ✅ 多格式导出:Word、TXT、Markdown、HTML
- ✅ 错误重试:自动重试失败的操作
+891
View File
@@ -0,0 +1,891 @@
{
"app": {
"title": "Công cụ sáng tác tiểu thuyết AI Pro",
"window_title": "Công cụ sáng tác tiểu thuyết AI Pro - Phiên bản chuyên nghiệp",
"subtitle": "_Hệ thống sáng tác tiểu thuyết thông minh cấp chuyên nghiệp v4.0 chính thức_",
"startup_log": "Công cụ sáng tác tiểu thuyết AI Pro v4.0 chính thức khởi động",
"port_in_use": "Cổng {port} đã bị chiếm, sử dụng cổng {new_port}",
"backends_loaded": "Đã tải {count} backend",
"no_backends_warning": "⚠️ Không có backend nào được bật! Vui lòng thêm cấu hình API trong phần cài đặt",
"init_failed": "Khởi tạo thất bại: {error}",
"gradio_start": "Đang khởi động Gradio... (cổng: {port})",
"gradio_failed": "Khởi động Gradio thất bại: {error}"
},
"tabs": {
"rewrite": "📝 Viết lại tiểu thuyết",
"polish": "✨ Trau chuốt tiểu thuyết",
"create": "✍️ Sáng tác từ đầu",
"continue_tab": "📖 Viết tiếp dự án",
"export": "💾 Xuất & Chia sẻ",
"projects": "📂 Quản lý dự án",
"settings": "⚙️ Cài đặt hệ thống",
"about": "️ Giới thiệu"
},
"rewrite": {
"header": "### Tải lên tiểu thuyết → Chọn chế độ → Xử lý thông minh",
"mode_rewrite": "Chế độ viết lại",
"mode_continue": "Chế độ viết tiếp",
"mode_label": "Chế độ chức năng",
"rewrite_header": "#### 🔄 Chế độ viết lại: Tải lên tiểu thuyết → Chọn phong cách → Viết lại thông minh",
"upload_file": "📤 Tải lên tệp (txt/pdf/epub/md/docx)",
"split_method_label": "Phương thức phân đoạn",
"split_auto": "Phân đoạn tự động",
"split_by_words": "Phân đoạn theo số chữ",
"split_by_pattern": "Phân đoạn theo văn bản cố định",
"words_per_segment": "Số chữ mỗi đoạn",
"words_per_segment_info": "Phân đoạn đều theo số chữ",
"pattern_label": "Đánh dấu phân đoạn",
"pattern_placeholder": "Hỗ trợ biến: %章、%节、%回, hoặc văn bản tùy chỉnh như ---、*** v.v.",
"pattern_info": "Sử dụng %章 để khớp 'Chương X', %节 khớp 'Phần X', %回 khớp 'Hồi X'",
"keep_marker": "Giữ lại đánh dấu phân đoạn",
"keep_marker_info": "Có giữ lại văn bản đánh dấu trong phân đoạn không",
"parse_file_btn": "Phân tích tệp",
"preset_style": "Phong cách thiết lập sẵn",
"parse_status": "Trạng thái phân tích",
"start_rewrite": "Bắt đầu viết lại",
"stop_rewrite": "Dừng viết lại",
"live_preview": "Xem trước trực tiếp",
"full_rewritten": "Văn bản viết lại đầy đủ (có thể chỉnh sửa)",
"progress_stats": "Thống kê tiến độ",
"continue_header": "#### ✍️ Chế độ viết tiếp: Tải lên tiểu thuyết hiện có → AI viết tiếp thông minh",
"upload_existing": "📤 Tải lên tiểu thuyết hiện có (txt/pdf/epub/md/docx)",
"novel_title": "📖 Tên tiểu thuyết",
"novel_title_placeholder": "Nhập tên tiểu thuyết",
"target_words": "📊 Số chữ mục tiêu",
"char_setting": "👥 Thiết lập nhân vật",
"char_setting_placeholder": "Tên nhân vật chính, tính cách, bối cảnh v.v. (tùy chọn)",
"world_setting": "🌍 Thiết lập thế giới quan",
"world_setting_placeholder": "Bối cảnh thời đại, quy tắc thế giới v.v. (tùy chọn)",
"plot_idea": "📖 Ý tưởng cốt truyện chính",
"plot_idea_placeholder": "Xung đột cốt lõi, hướng phát triển, kết thúc v.v. (tùy chọn)",
"existing_content": "Nội dung hiện có (có thể chỉnh sửa)",
"continue_result": "Kết quả viết tiếp",
"continue_status": "Trạng thái viết tiếp",
"start_continue": "Bắt đầu viết tiếp"
},
"polish": {
"header": "### Tải lên tiểu thuyết → Chọn loại trau chuốt → Tối ưu thông minh",
"upload_file": "📤 Tải lên tệp (txt/pdf/epub/md/docx)",
"polish_type_label": "Loại trau chuốt",
"polish_general": "Trau chuốt toàn diện",
"polish_find_errors": "Tìm lỗi",
"polish_suggestions": "Gợi ý cải thiện",
"polish_direct": "Sửa trực tiếp",
"polish_remove_ai": "Loại bỏ phong cách AI",
"polish_enhance": "Tăng cường chi tiết",
"polish_dialogue": "Tối ưu đối thoại",
"polish_pacing": "Cải thiện nhịp điệu",
"custom_req": "Yêu cầu tùy chỉnh (tùy chọn)",
"custom_req_placeholder": "Ví dụ: Tăng cường mô tả tính cách nhân vật, thêm bầu không khí, tối ưu lưu loát đối thoại v.v.",
"parse_status": "Trạng thái phân tích",
"start_polish": "Bắt đầu trau chuốt",
"polish_suggest_btn": "Trau chuốt & Gợi ý",
"original_text": "Văn bản gốc (có thể chỉnh sửa)",
"polished_text": "Kết quả trau chuốt",
"suggestions_label": "Gợi ý cải thiện",
"polish_status": "Trạng thái trau chuốt"
},
"create": {
"header": "### Điền thiết lập → Tạo dàn ý → Sáng tác tự động (hỗ trợ viết tiếp, tạm dừng)",
"novel_title": "📖 Tên tiểu thuyết",
"novel_title_default": "Tiểu thuyết chưa đặt tên",
"genre_label": "📚 Thể loại tiểu thuyết",
"sub_genres_label": "🏷️ Chủ đề con / Hashtag",
"suggested_titles_list": "Danh sách gợi ý (Sửa tên tại đây)",
"suggested_titles_radio": "Chọn một tiêu đề",
"suggested_titles_help": "Bạn có thể tự do chỉnh sửa văn bản ở trên, các lựa chọn bên dưới sẽ tự động cập nhật.",
"sub_genres": [
"Hậu cung",
"1v1",
"NP",
"Nữ phẫn nam trang",
"Nam phẫn nữ trang",
"Xuyên không",
"Xuyên sách",
"Trọng sinh",
"Hệ thống",
"Bàn tay vàng",
"Không gian tùy thân",
"Linh tuyền",
"Đọc tâm thuật",
"Vô địch lưu",
"Cẩu đạo",
"Nhiệt huyết",
"Hài hước",
"Sảng văn",
"Ngọt sủng",
"Ngược luyến",
"Gương vỡ lại lành",
"Cưới trước yêu sau",
"Oan gia ngõ hẹp",
"Thanh mai trúc mã",
"Hào môn thế gia",
"Tổng tài",
"Minh tinh",
"Giới giải trí",
"Vườn trường",
"Học bá",
"Võng du",
"E-sports",
"Livestream",
"Mỹ thực",
"Nông trại",
"Điền văn",
"Nuôi con",
"Làm giàu",
"Cung đấu",
"Gia đấu",
"Quyền mưu",
"Nữ cường",
"Nam cường",
"Song khiết",
"Phế Sài",
"Thiên tài",
"Từ hôn",
"Linh khí khôi phục",
"Mạt thế",
"Cơ giáp",
"Tinh tế",
"ABO",
"Người sói",
"Ma cà rồng",
"Tây huyễn",
"Ma pháp",
"Kiếm ma",
"Pháp sư",
"Giả heo ăn hổ",
"Bi kịch",
"Chữa lành",
"Hắc ám",
"Gothic",
"Tâm lý",
"Tâm thần phân liệt",
"Đa nhân cách",
"Trinh thám",
"Phá án",
"Đạo mộ",
"Cương thi",
"Quy tắc quái đàm",
"Vô hạn lưu",
"Mau xuyên",
"Thực dân",
"Chế tạo",
"Lĩnh chúa",
"Trồng trọt",
"Câu cá",
"Nuôi thú",
"Pokemon",
"Anime",
"Harry Potter",
"Marvel",
"Biển sao",
"Truyền thuyết đô thị",
"Cổ đại",
"Dân quốc",
"Thập niên 70",
"Thập niên 80",
"Thập niên 90",
"Dị thế",
"Đại lục",
"Bộ lạc",
"Nguyên thủy",
"Tu tiên",
"Trả thù",
"Pháo hôi",
"Vai ác",
"Tiên tri",
"Phép thuật"
],
"genres": [
"Huyền huyễn tiên hiệp",
"Đô thị ngôn tình",
"Khoa học viễn tưởng",
"Võ hiệp",
"Trinh thám",
"Lịch sử",
"Quân sự",
"Game",
"Kinh dị",
"Xuyên không - Trọng sinh",
"Hệ thống",
"Đồng nhân",
"Mạt thế",
"Điền văn - Hài hước",
"Cổ đại ngôn tình",
"Kỳ ảo phương Tây",
"Nữ cường",
"Tổng tài",
"Thanh xuân vườn trường",
"Cung đấu",
"Gia đấu",
"Hồng hoang",
"Ngôn tình võng du",
"Đô thị dị năng",
"Linh dị - Bí ẩn",
"Đam mỹ",
"Bách hợp",
"Thám hiểm lăng mộ",
"Dị giới đại lục",
"Cổ đại làm ruộng",
"Không gian Tùy thân",
"ABO",
"Ma cà rồng",
"Cạnh kỹ - Thể thao",
"Đồng nhân Anime",
"Vô hạn lưu",
"Khác"
],
"char_setting": "👥 Thiết lập nhân vật",
"char_setting_placeholder": "Tên nhân vật chính, tính cách, bối cảnh v.v.",
"num_main_chars": "Số lượng nhân vật chính",
"num_sub_chars": "Số lượng nhân vật phụ",
"world_setting": "🌍 Thiết lập thế giới quan",
"world_setting_placeholder": "Bối cảnh thời đại, quy tắc thế giới, thiết lập đặc biệt v.v.",
"plot_idea": "📖 Ý tưởng cốt truyện chính",
"plot_idea_placeholder": "Xung đột cốt lõi, hướng phát triển, kết thúc v.v.",
"custom_prompt_label": "📝 Yêu cầu riêng (Tùy chọn)",
"custom_prompt_placeholder": "Nhập thêm yêu cầu riêng cho AI (Ví dụ: Thể loại đô thị dị năng)",
"suggest_title_btn": "✨ Gợi ý Tên truyện",
"suggest_btn": "✨ Gợi ý bằng AI",
"suggest_plot_btn": "✨ Gợi ý cốt truyện bằng AI",
"suggesting_status": "Đang suy nghĩ gợi ý...",
"chapter_count": "📊 Số chương (mặc định 20)",
"gen_outline_btn": "Tạo dàn ý",
"outline_display": "📋 Dàn ý (có thể chỉnh sửa thủ công)",
"context_header": "### 🔄 Cài đặt tăng cường ngữ cảnh (tùy chọn)",
"enable_context": "Bật tăng cường ngữ cảnh",
"enable_context_info": "Khi bật, sẽ sử dụng tóm tắt/toàn văn các chương trước làm ngữ cảnh cho chương mới",
"context_mode_label": "Chế độ ngữ cảnh",
"context_summary_mode": "Chế độ tóm tắt",
"context_full_mode": "Chế độ toàn văn",
"context_mode_info": "Chế độ tóm tắt: sử dụng tóm tắt các chương trước; Chế độ toàn văn: sử dụng toàn bộ nội dung các chương trước",
"context_chapters": "Số chương ngữ cảnh",
"context_chapters_info": "Sử dụng tóm tắt bao nhiêu chương trước làm ngữ cảnh",
"context_max_length": "Độ dài tối đa ngữ cảnh",
"context_max_length_info": "Số ký tự tối đa của ngữ cảnh",
"outline_format_help": "#### 📝 Hướng dẫn định dạng dàn ý\nVui lòng viết dàn ý theo định dạng sau (chọn một):\n\n**Định dạng 1** (khuyến nghị):\n- Chương 1: Mở đầu - Giới thiệu nhân vật chính và thế giới quan\n- Chương 2: Xung đột - Nhân vật chính gặp thử thách lớn đầu tiên\n\n**Định dạng 2**:\n- 1. Mở đầu - Giới thiệu nhân vật chính và thế giới quan\n- 2. Xung đột - Nhân vật chính gặp thử thách lớn đầu tiên\n\n**Lưu ý**: Tiêu đề và mô tả được phân tách bằng dấu gạch ngang `-`, mỗi dòng một chương",
"start_gen_btn": "Bắt đầu tạo / Viết tiếp",
"pause_gen_btn": "Tạm dừng tạo",
"cache_status": "💾 Trạng thái bộ nhớ đệm",
"cache_status_default": "Không có bộ nhớ đệm",
"cache_timestamp": "⏰ Thời gian bộ nhớ đệm",
"export_format_label": "📄 Định dạng xuất",
"export_format_word": "Word (.docx)",
"export_format_txt": "Văn bản (.txt)",
"export_format_md": "Markdown (.md)",
"export_format_html": "HTML (.html)",
"export_progress_btn": "📥 Xuất tiến độ hiện tại",
"download_file": "Tải xuống tệp",
"export_status": "Trạng thái xuất",
"novel_display": "📚 Nội dung tiểu thuyết (cập nhật trực tiếp)",
"gen_status": "Trạng thái tạo",
"gen_status_default": "Sẵn sàng"
},
"export": {
"header": "### Xuất sáng tác ra nhiều định dạng",
"paste_content": "Dán nội dung tiểu thuyết",
"paste_placeholder": "Sao chép toàn bộ văn bản tiểu thuyết từ trang sáng tác",
"novel_title": "Tên tiểu thuyết",
"novel_title_placeholder": "Dùng làm tên tệp",
"export_word": "Xuất ra Word (.docx)",
"export_txt": "Xuất ra văn bản thuần (.txt)",
"export_md": "Xuất ra Markdown (.md)",
"export_html": "Xuất ra trang web (.html)",
"download_file": "Tải xuống tệp",
"export_status": "Trạng thái xuất",
"recent_exports": "Các tệp xuất gần đây",
"refresh_files": "Làm mới danh sách tệp",
"empty_content": "Nội dung trống"
},
"continue_tab": {
"header": "### Chọn dự án → Xem tiến độ → Tiếp tục sáng tác",
"select_project": "📖 Chọn dự án để viết tiếp",
"load_btn": "📥 Tải dự án",
"project_info": "Thông tin dự án",
"no_project_loaded": "_Chưa chọn dự án nào. Vui lòng chọn dự án từ danh sách và bấm Tải dự án._",
"info_template": "**📖 {title}** | 📚 {genre}\n\n✅ Hoàn thành: **{completed}/{total} chương** ({percent}%)\n\n👤 Nhân vật: {char}\n\n🌍 Thế giới: {world}\n\n📖 Cốt truyện: {plot}",
"outline_label": "📋 Dàn ý dự án (✅ = đã xong, ⬜ = chưa viết)",
"novel_label": "📚 Nội dung đã viết",
"context_header": "### 🔄 Cài đặt ngữ cảnh",
"continue_gen_btn": "▶️ Tiếp tục tạo chương",
"pause_btn": "⏸️ Tạm dừng",
"gen_status": "Trạng thái",
"gen_status_default": "Sẵn sàng - Chọn dự án để tiếp tục viết",
"select_first": "❌ Vui lòng chọn và tải dự án trước",
"loaded_ok": "✅ Đã tải dự án '{title}' - {completed}/{total} chương hoàn thành",
"all_complete": "✅ Dự án '{title}' đã hoàn thành tất cả chương!",
"no_project_selected": "❌ Vui lòng chọn một dự án",
"project_not_found": "❌ Không tìm thấy dự án",
"load_failed": "❌ Tải dự án thất bại: {error}",
"no_outline": "❌ Dự án không có dàn ý. Vui lòng dùng tab Sáng tác từ đầu.",
"continue_starting": "▶️ Bắt đầu tiếp tục tạo từ chương {chapter}..."
},
"projects": {
"header": "### Quản lý tất cả dự án sáng tác",
"refresh_btn": "🔄 Làm mới danh sách dự án",
"projects_table": "Dự án của tôi",
"status_label": "Trạng thái",
"export_header": "### 📥 Xuất dự án",
"select_project": "📖 Chọn dự án để xuất",
"export_format": "📄 Định dạng xuất",
"export_btn": "📤 Chuẩn bị tệp tải xuống",
"download_file": "Tệp tải xuống",
"export_status": "Trạng thái",
"delete_header": "### 🗑️ Xóa dự án",
"delete_select_project": "Chọn dự án để xóa",
"delete_btn": "🗑️ Xóa dự án vĩnh viễn",
"delete_confirm": "⚠️ Bạn có chắc chắn muốn xóa dự án này? Thao tác này KHÔNG THỂ hoàn tác.",
"delete_success": "✅ Đã xóa dự án thành công",
"delete_failed": "❌ Xóa dự án thất bại: {error}",
"actions_header": "### ✍️ Thao tác với dự án",
"select_project_action": "📖 Chọn dự án",
"continue_btn": "✍️ Viết tiếp",
"rewrite_btn": "🔄 Viết lại",
"polish_btn": "✨ Trau chuốt",
"select_project_first": "❌ Vui lòng chọn một dự án",
"project_not_found": "❌ Không tìm thấy dự án",
"loaded_to_create": "✅ Đã tải dự án '{title}' sang tab Sáng tác",
"loaded_to_rewrite": "✅ Đã tải nội dung dự án '{title}' sang tab Viết lại",
"loaded_to_polish": "✅ Đã tải nội dung dự án '{title}' sang tab Trau chuốt",
"action_status": "Trạng thái",
"ai_illustration_header": "Tạo minh họa chương AI",
"gen_illustration_btn": "Tạo minh họa",
"illustration_label": "Minh họa chương",
"select_chapter": "Vui lòng chọn một chương",
"invalid_chapter": "Chương không hợp lệ",
"chapter_not_found": "Không tìm thấy chương"
},
"settings": {
"header": "### 🔧 Cấu hình API & Tham số viết",
"tab_backends": "🌐 Quản lý giao diện",
"tab_params": "📝 Tham số tạo",
"tab_cache": "💾 Quản lý bộ nhớ đệm",
"backends_header": "#### 📋 Cấu hình backend API",
"refresh_list": "🔄 Làm mới danh sách",
"test_all": "✅ Kiểm tra tất cả giao diện",
"backends_table": "Danh sách backend đã cấu hình",
"add_backend_header": "#### Thêm giao diện mới",
"provider_label": "Nhà cung cấp API",
"provider_info": "Chọn nhà cung cấp API để tự động điền cấu hình mặc định",
"backend_name": "Tên giao diện*",
"backend_name_placeholder": "Ví dụ: OpenAI của tôi",
"backend_type": "Loại giao diện*",
"base_url": "Base URL",
"base_url_placeholder": "Ví dụ: https://api.example.com/v1 (chỉ cần điền cho giao diện tương thích OpenAI)",
"model_name": "Tên mô hình*",
"model_name_placeholder": "Tự động điền sau khi chọn nhà cung cấp, có thể sửa",
"api_key": "API Key (Ollama có thể bỏ trống)*",
"api_key_placeholder": "Nhập API Key của bạn (sẽ không được lưu dạng văn bản thuần)",
"timeout": "Thời gian chờ (giây)",
"retry_count": "Số lần thử lại",
"enable_backend": "Bật giao diện này",
"add_btn": " Thêm giao diện",
"operation_result": "Kết quả thao tác",
"test_manage_header": "#### 🔍 Kiểm tra & Quản lý",
"test_backend_name": "Tên giao diện cần kiểm tra",
"test_backend_placeholder": "Nhập tên giao diện để kiểm tra kết nối",
"test_btn": "🧪 Kiểm tra kết nối",
"test_result": "Kết quả kiểm tra",
"delete_backend_name": "Tên giao diện cần xóa",
"delete_backend_placeholder": "Nhập tên giao diện để xóa",
"delete_btn": "🗑️ Xóa giao diện",
"delete_result": "Kết quả xóa",
"params_header": "#### Điều chỉnh các tham số tạo tiểu thuyết",
"temperature_label": "Temperature (Độ sáng tạo)",
"temperature_info": "Càng cao càng sáng tạo, càng thấp càng bảo thủ",
"top_p_info": "Kiểm soát tính đa dạng của đầu ra",
"top_k_info": "Chọn từ K token có khả năng cao nhất",
"max_tokens_info": "Số token tối đa mỗi lần tạo",
"chapter_target_words": "Số chữ mục tiêu mỗi chương",
"writing_style": "Phong cách viết",
"writing_styles": [
"Mượt mà tự nhiên, cốt truyện chặt chẽ, nhân vật tinh tế",
"Văn phong đẹp, ý cảnh sâu xa",
"Nhịp nhanh, cốt truyện kịch tính",
"Mô tả tinh tế, cảm xúc phong phú",
"Hài hước thú vị, nhẹ nhàng vui vẻ"
],
"tone_label": "Giọng điệu",
"tones": [
"Trung lập",
"Nghiêm túc",
"Nhẹ nhàng",
"Hoài nghi",
"Ôn hòa",
"Đam mê"
],
"char_dev_label": "Xây dựng nhân vật",
"char_dev_options": [
"Chi tiết",
"Trung bình",
"Ngắn gọn"
],
"plot_complexity_label": "Độ phức tạp cốt truyện",
"plot_complexity_options": [
"Đơn giản",
"Trung bình",
"Phức tạp"
],
"save_params_btn": "💾 Lưu tham số tạo",
"save_status": "Trạng thái lưu",
"cache_header": "#### 📋 Quản lý bộ nhớ đệm tạo",
"cache_size": "Kích thước bộ nhớ đệm",
"refresh_cache": "🔄 Làm mới danh sách bộ nhớ đệm",
"get_cache_size": "📊 Lấy kích thước bộ nhớ đệm",
"cache_table": "Danh sách bộ nhớ đệm",
"clear_selected": "🗑️ Xóa bộ nhớ đệm đã chọn",
"clear_all": "🗑️ Xóa tất cả bộ nhớ đệm",
"cache_op_status": "Trạng thái thao tác",
"summary_cache_header": "#### 📋 Quản lý bộ nhớ đệm tóm tắt ngữ cảnh",
"summary_cache_size": "Kích thước bộ nhớ đệm tóm tắt",
"refresh_summary": "🔄 Làm mới danh sách tóm tắt",
"get_summary_size": "📊 Lấy kích thước tóm tắt",
"summary_cache_table": "Danh sách bộ nhớ đệm tóm tắt",
"clear_summary_cache": "🗑️ Xóa tất cả bộ nhớ đệm tóm tắt",
"tab_genre": "📚 Quản lý thể loại",
"genre_desc": "Thêm, sửa, xóa các thể loại truyện và viết mô tả/hướng dẫn chi tiết. AI sẽ dùng mô tả này làm Prompt hệ thống khi sáng tác/gợi ý truyện thuộc thể loại tương ứng.",
"genre_select": "Chọn thể loại cần sửa",
"genre_name": "Tên thể loại",
"genre_name_placeholder": "Huyền huyễn tiên hiệp, Khoa học viễn tưởng...",
"genre_description": "Mô tả / Hướng dẫn cách viết",
"genre_description_placeholder": "Nhập phong cách viết, văn phong, yếu tố đặc trưng của thể loại để AI học theo...",
"genre_add_btn": " Thêm mới",
"genre_update_btn": "💾 Cập nhật",
"genre_delete_btn": "🗑️ Xóa",
"genre_op_status": "Trạng thái",
"genre_err_name_empty": "Vui lòng nhập tên thể loại!",
"genre_err_exists": "Tên thể loại đã tồn tại!",
"genre_add_success": "Thêm thể loại mới thành công!",
"genre_err_none_selected": "Vui lòng chọn một thể loại!",
"genre_update_success": "Cập nhật thể loại thành công!",
"genre_err_update": "Cập nhật lỗi hoặc trùng tên!",
"genre_delete_success": "Đã xóa thể loại!",
"genre_err_delete": "Xóa thất bại!",
"tab_sub_genre": "🏷️ Quản lý chủ đề con (Tag)",
"sub_genre_desc": "Thêm, sửa, xóa các chủ đề con (hashtags) và mô tả. AI sẽ gộp mô tả này khi sáng tác nếu bạn chọn tag này.",
"sub_genre_select": "Chọn chủ đề con cần sửa",
"sub_genre_name": "Tên chủ đề con",
"sub_genre_name_placeholder": "Hệ thống, Xuyên không, Sảng văn...",
"sub_genre_description": "Mô tả / Hướng dẫn cách viết",
"sub_genre_description_placeholder": "Nhập chi tiết về motif này, cách xây dựng nhân vật hoặc bối cảnh cho AI...",
"sub_genre_add_btn": " Thêm mới",
"sub_genre_update_btn": "💾 Cập nhật",
"sub_genre_delete_btn": "🗑️ Xóa",
"sub_genre_op_status": "Trạng thái",
"sub_genre_err_name_empty": "Vui lòng nhập tên chủ đề con!",
"sub_genre_err_exists": "Tên chủ đề con đã tồn tại!",
"sub_genre_add_success": "Thêm chủ đề con mới thành công!",
"sub_genre_err_none_selected": "Vui lòng chọn một chủ đề con!",
"sub_genre_update_success": "Cập nhật chủ đề con thành công!",
"sub_genre_err_update": "Cập nhật lỗi hoặc trùng tên!",
"sub_genre_delete_success": "Đã xóa chủ đề con!",
"sub_genre_err_delete": "Xóa thất bại!"
},
"about": {
"content": "# Công cụ sáng tác tiểu thuyết AI Pro v4.0 chính thức\n## Hệ thống sáng tác tiểu thuyết thông minh cấp chuyên nghiệp\n\n### 🌟 Tính năng chính\n- **Sáng tác thông minh**: Sáng tác tiểu thuyết dài kỳ từ đầu, hỗ trợ tùy chỉnh dàn ý\n- **Tiếp tục từ điểm dừng**: Hỗ trợ tạm dừng rồi viết tiếp, mỗi chương tự động lưu\n- **Viết lại thông minh**: Tải lên văn bản tiểu thuyết, viết lại chất lượng cao với 17 phong cách\n- **Viết tiếp thông minh**: Tải lên tiểu thuyết viết dở, AI tự động viết tiếp\n- **Trau chuốt tiểu thuyết**: Hỗ trợ 8 loại trau chuốt bao gồm toàn diện, tìm lỗi, gợi ý cải thiện, loại bỏ phong cách AI\n- **Phân tích tệp linh hoạt**: Hỗ trợ tùy chỉnh mẫu chương, nhận dạng nhiều định dạng tệp tiểu thuyết\n- **Xuất đa định dạng**: Word, TXT, Markdown, HTML - tải trực tiếp\n- **Quản lý dự án**: Quản lý nhiều dự án sáng tác, hỗ trợ viết tiếp từ điểm dừng\n- **Cấu hình linh hoạt**: Hỗ trợ nhiều API backend, Ollama key tùy chọn\n- **Tự động thử lại**: Khi tạo chương bị lỗi sẽ tự động thử lại\n- **Tự động tìm cổng**: Khi cổng mặc định bị chiếm sẽ tự động tìm cổng khả dụng\n\n### 🔧 Đặc điểm kỹ thuật\n- **Phục hồi lỗi**: Hệ thống xử lý lỗi và ghi log hoàn chỉnh\n- **Cơ chế bộ nhớ đệm**: Bộ nhớ đệm thông minh tránh gọi API trùng lặp\n- **Giới hạn tốc độ**: Thuật toán token bucket tích hợp chống giới hạn API\n- **Cân bằng tải**: Tự động luân chuyển nhiều backend\n- **Giám sát hiệu suất**: Thống kê và phân tích hiệu suất thời gian thực\n- **An toàn luồng**: Thiết kế an toàn đồng thời hoàn toàn\n\n### 📋 Yêu cầu hệ thống\n- Python 3.8+\n- Phụ thuộc: gradio, pandas, openai, python-docx\n- Tùy chọn: PyMuPDF (hỗ trợ PDF), ebooklib+beautifulsoup4 (hỗ trợ EPUB)\n\n### 📞 Hỗ trợ kỹ thuật\n- Xem tệp log: thư mục `logs/`\n- Vị trí lưu dự án: thư mục `projects/`\n- Vị trí tệp xuất: thư mục `exports/`\n- Vị trí bộ nhớ đệm: thư mục `cache/`\n- GitHub: [GitHub](https://github.com/yangqi1309134997-coder/ai-novel-generator)\n\n### ⚖️ Giấy phép\nMIT License"
},
"messages": {
"no_content_polish": "Không có nội dung để trau chuốt",
"no_content_rewrite": "Không có nội dung để viết lại",
"no_content_continue": "Không có nội dung để viết tiếp",
"no_title": "Vui lòng điền tên tiểu thuyết",
"no_file": "Không có tệp",
"polish_success": "Trau chuốt thành công",
"polish_complete": "Trau chuốt hoàn tất",
"polish_complete_saved": "Trau chuốt hoàn tất | Đã lưu vào quản lý dự án",
"polish_failed": "Trau chuốt thất bại: {error}",
"polish_invalid": "Trau chuốt trả về nội dung không hợp lệ (độ dài: {length} chữ)",
"polish_status_msg": "Trau chuốt trả về thông báo trạng thái thay vì nội dung thực: '{content}'",
"polish_segment_failed": "Đoạn {num} trau chuốt thất bại: {error}",
"polish_segment_invalid": "Đoạn {num} trau chuốt trả về nội dung không hợp lệ (độ dài: {length} chữ)",
"polish_segment_progress": "Trau chuốt đoạn {current}/{total}",
"polish_auto_split": "Văn bản quá dài ({length} chữ), bật xử lý phân đoạn tự động",
"polish_split_done": "Đã chia thành {count} đoạn, bắt đầu trau chuốt từng đoạn",
"polish_all_done": "Hoàn tất trau chuốt phân đoạn, tổng cộng {count} đoạn, tổng số chữ: {total}",
"polish_save_success": "Kết quả trau chuốt đã lưu vào dự án: {id}",
"polish_save_failed": "Lưu kết quả trau chuốt thất bại: {msg}",
"rewrite_success": "Viết lại thành công",
"rewrite_complete": "Viết lại hoàn tất",
"rewrite_paused": "Đã tạm dừng - Hoàn thành {done}/{total} đoạn",
"rewrite_progress": "Tiến độ {done}/{total} | Khoảng {words} chữ",
"rewrite_segment_failed": "Đoạn {num} viết lại thất bại: {error}",
"rewrite_invalid": "Đoạn {num} viết lại trả về nội dung không hợp lệ (độ dài: {length} chữ)",
"rewrite_status_msg": "Đoạn {num} viết lại trả về thông báo trạng thái thay vì nội dung: '{content}'",
"rewrite_segment_progress": "Viết lại đoạn {current}/{total}",
"rewrite_save_success": "Kết quả viết lại đã lưu vào dự án: {id}",
"rewrite_save_failed": "Lưu kết quả viết lại thất bại: {msg}",
"continue_success": "Viết tiếp thành công",
"continue_complete": "Viết tiếp hoàn tất",
"continue_complete_saved": "Viết tiếp hoàn tất | Đã lưu vào quản lý dự án",
"continue_failed": "Viết tiếp thất bại: {error}",
"continue_invalid": "Viết tiếp trả về nội dung không hợp lệ (độ dài: {length} chữ)",
"continue_status_msg": "Viết tiếp trả về thông báo trạng thái thay vì nội dung: '{content}'",
"continue_save_success": "Kết quả viết tiếp đã lưu vào dự án: {id}",
"continue_save_failed": "Lưu kết quả viết tiếp thất bại: {msg}",
"gen_success": "Tạo thành công",
"gen_outline_empty": "Lỗi: Dàn ý trống",
"gen_outline_failed": "Phân tích dàn ý thất bại: {msg}",
"gen_paused": "Đã tạm dừng - Hoàn thành {done}/{total} chương, dự án đã lưu",
"gen_chapter_progress": "Đang tạo chương {current}/{total}: {title}",
"gen_chapter_skip": "Hoàn thành {num}/{total} chương (phục hồi từ bộ nhớ đệm)",
"gen_chapter_retry": "Chương {num} tạo thất bại hoặc 0 chữ (thử lần {attempt}/{max}), đang thử lại...",
"gen_summarizing": "Đang tóm tắt các chương trước để lấy ngữ cảnh...",
"gen_constructing_prompt": "Đang xây dựng prompt cho Chương {num}...",
"gen_waiting_ai": "Đang chờ phản hồi từ AI cho Chương {num}...",
"gen_saving_db": "Đang lưu Chương {num} vào cơ sở dữ liệu...",
"gen_chapter_failed": "Tạo thất bại: Chương {num} tạo thất bại (đã thử lại {max} lần)",
"gen_complete": "Tạo hoàn tất | Tổng cộng {total} chương | Khoảng {words} chữ | Dự án đã lưu",
"gen_save_success": "Dự án đã lưu: {id}",
"gen_save_failed": "Lưu dự án thất bại: {msg}",
"cache_found": "Tìm thấy bộ nhớ đệm: {msg}",
"cache_chapter_restore": "Phục hồi chương {num} từ bộ nhớ đệm: {words} chữ",
"stop_requested": "Đã yêu cầu tạm dừng (sẽ dừng sau khi hoàn thành chương hiện tại)",
"user_stop_requested": "Người dùng yêu cầu dừng tạo",
"split_words_done": "Phân đoạn theo số chữ hoàn tất, tổng cộng {count} đoạn, mỗi đoạn khoảng {words} chữ",
"split_pattern_done": "Phân đoạn theo văn bản cố định hoàn tất, tổng cộng {count} đoạn",
"split_failed": "Phân đoạn thất bại: {error}",
"split_no_pattern": "Vui lòng nhập đánh dấu phân đoạn",
"export_complete": "Xuất hoàn tất",
"export_failed": "Xuất thất bại: {error}"
},
"templates": {
"default": "Viết lại bằng lối viết sinh động, tinh tế hơn, ngôn ngữ đẹp, giữ nguyên ý nghĩa và cốt truyện, nhưng thêm nhiều chi tiết miêu tả và nội tâm nhân vật.",
"xianxia": "Viết lại theo phong cách tiên hiệp cổ điển, ngôn ngữ cổ phong trang nhã, thêm miêu tả tiên thuật pháp bảo, linh khí ý cảnh, tâm cảnh tu luyện nhân vật, giữ nguyên cốt truyện.",
"romance": "Viết lại theo phong cách ngôn tình đô thị hiện đại, ngôn ngữ nhẹ nhàng ngọt ngào hoặc ngược tâm, thêm tương tác lãng mạn, miêu tả tâm lý tinh tế, chi tiết đời sống.",
"thriller": "Viết lại theo phong cách trinh thám kinh dị, ngôn ngữ tạo bầu không khí căng thẳng, thêm miêu tả kinh dị tâm lý, manh mối, dựng cảnh và yếu tố đảo ngược.",
"scifi": "Viết lại theo phong cách khoa học viễn tưởng cứng, ngôn ngữ chặt chẽ chuyên nghiệp, thêm giải thích nguyên lý khoa học, chi tiết kỹ thuật, xây dựng thế giới quan.",
"wuxia": "Viết lại theo phong cách võ hiệp Kim Dung - Cổ Long, ngôn ngữ hào sảng, thêm miêu tả võ công chiêu thức, ân oán giang hồ, tinh thần hiệp nghĩa.",
"palace": "Viết lại theo phong cách cung đình cổ, ngôn ngữ trang nhã hoa lệ, thêm lễ nghi cung đình, mưu kế quyền lực, quan hệ nhân vật phức tạp tinh tế.",
"military": "Viết lại theo phong cách quân sự hiện đại, ngôn ngữ cứng cỏi mạnh mẽ, thêm miêu tả chiến thuật, vũ khí trang bị, đời sống quân ngũ.",
"historical": "Viết lại theo phong cách diễn nghĩa lịch sử, ngôn ngữ cổ kính trang trọng, thêm bối cảnh lịch sử, não bộ thời đại, góc nhìn lịch sử vĩ đại.",
"supernatural": "Viết lại theo phong cách linh dị huyền ảo, ngôn ngữ bí ẩn kỳ lạ, thêm yếu tố siêu nhiên, hiện tượng kỳ bí, âm dương ngũ hành.",
"campus": "Viết lại theo phong cách thanh xuân học đường, ngôn ngữ tươi mới vui vẻ, thêm chi tiết đời sống học đường, rung động tuổi trẻ, ngộ về trưởng thành.",
"business": "Viết lại theo phong cách thương chiến, ngôn ngữ gọn gàng thực tế, thêm chiến lược kinh doanh, đấu trí nơi công sở, trí tuệ thương mại.",
"cyberpunk": "Viết lại theo phong cách cyberpunk, ngôn ngữ đầy cảm giác công nghệ, thêm yếu tố công nghệ cao, thực tế ảo, trí tuệ nhân tạo, sắc thái phản không tưởng.",
"fantasy": "Viết lại theo phong cách kỳ ảo phương Tây, ngôn ngữ sử thi hùng tráng, thêm hệ thống phép thuật, thiết lập chủng tộc, yếu tố thần thoại, bầu không khí trung cổ.",
"horror": "Viết lại theo phong cách kinh dị trinh thám, ngôn ngữ u ám đè nén, thêm bầu không khí kinh dị, gợi ý tâm lý, hiện tượng siêu nhiên.",
"humor": "Viết lại theo phong cách hài hước, ngôn ngữ dí dỏm sắc sảo, thêm yếu tố gây cười, miêu tả phóng đại, hiệu ứng hài kịch, nhẹ nhàng thú vị.",
"literary": "Viết lại theo phong cách văn nghệ thanh tân, ngôn ngữ đẹp thanh thoát, thêm cảm xúc tinh tế, ý cảnh sâu xa, chữ nghĩa thơ mộng, như thi như họa.",
"adventure": "Viết lại theo phong cách phiêu lưu nhiệt huyết, ngôn ngữ sôi động mãnh liệt, thêm yếu tố phiêu lưu, cảnh chiến đấu, tình bạn gắn bó, tràn đầy năng lượng tích cực.",
"name_default": "Phong cách - Mặc định",
"name_xianxia": "Phong cách - Tiên hiệp huyền ảo",
"name_romance": "Phong cách - Ngôn tình đô thị",
"name_thriller": "Phong cách - Trinh thám kinh dị",
"name_scifi": "Phong cách - Khoa học viễn tưởng",
"name_wuxia": "Phong cách - Võ hiệp giang hồ",
"name_palace": "Phong cách - Cung đấu cổ đại",
"name_military": "Phong cách - Quân sự hiện đại",
"name_historical": "Phong cách - Diễn nghĩa lịch sử",
"name_supernatural": "Phong cách - Linh dị huyền ảo",
"name_campus": "Phong cách - Thanh xuân học đường",
"name_business": "Phong cách - Thương chiến",
"name_cyberpunk": "Phong cách - Cyberpunk",
"name_fantasy": "Phong cách - Kỳ ảo phương Tây",
"name_horror": "Phong cách - Kinh dị trinh thám",
"name_humor": "Phong cách - Hài hước",
"name_literary": "Phong cách - Văn nghệ thanh tân",
"name_adventure": "Phong cách - Phiêu lưu nhiệt huyết"
},
"prompts": {
"outline_system": "Bạn là nhà hoạch định dàn ý tiểu thuyết chuyên nghiệp, giỏi xây dựng khung truyện hấp dẫn.",
"outline_user": "Hãy tạo dàn ý đầy đủ cho một tiểu thuyết thể loại {genre}, tiêu đề: «{title}».\n\nThiết lập nhân vật: {character_setting}\n\nThế giới quan: {world_setting}\n\nCốt truyện chính: {plot_idea}\n\nYêu cầu phong cách: {style_desc}\n\nYêu cầu:\n1. Tổng cộng khoảng {total_chapters} chương\n2. Mỗi chương theo định dạng nghiêm ngặt: Chương X: Tiêu đề chương - Mô tả cốt truyện ngắn gọn (50-100 từ)\n3. Cốt truyện mạch lạc, có mở đầu - phát triển - cao trào - kết thúc, nhân vật phát triển hợp lý\n4. Chỉ xuất ra danh sách dàn ý, không có nội dung khác\n5. Dàn ý phải hấp dẫn, lôi cuốn, có yếu tố hồi hộp\n6. Phân bổ chương theo cấu trúc 3 hồi:\n - Hồi 1 (25% số chương): Giới thiệu, xây dựng thế giới, thiết lập xung đột\n - Hồi 2 (50% số chương): Phát triển, leo thang, bước ngoặt giữa truyện\n - Hồi 3 (25% số chương): Cao trào, giải quyết, kết thúc\n7. Mỗi chương phải có xung đột nhỏ hoặc tiến triển rõ ràng, không có chương 'chữ lót'",
"chapter_system": "Bạn là nhà văn tiểu thuyết dài kỳ xuất sắc, sáng tác những câu chuyện chạm đến trái tim. Hãy viết với phong cách tự nhiên của con người. Tránh các mẫu câu AI phổ biến như: bắt đầu đoạn bằng 'Tuy nhiên', 'Ngoài ra', 'Hơn nữa' liên tục; kết thúc chương quá gọn gàng hoặc giáo điều; sử dụng cụm từ 'một cách + tính từ' quá nhiều; liệt kê ba thứ liền nhau theo kiểu 'cảm thấy X, Y, và Z'.",
"chapter_user": "Hãy viết Chương {chapter_num} của tiểu thuyết «{novel_title}».\n\nTiêu đề chương: {chapter_title}\nDàn ý chương này: {chapter_desc}\n\nThiết lập tổng thể:\nNhân vật: {character_setting}\nThế giới quan: {world_setting}\nCốt truyện chính: {plot_idea}\n\nYêu cầu phong cách: {style_desc}\n\nYêu cầu cụ thể:\n1. Nội dung khoảng {target_words} từ\n2. Cốt truyện tuân thủ nghiêm ngặt dàn ý chương, mạch lạc với toàn bộ sách\n3. Đối thoại tự nhiên mang cá tính riêng của từng nhân vật, miêu tả tâm lý tinh tế, miêu tả cảnh vật sinh động\n4. Kết thúc để lại yếu tố hồi hộp hoặc gợi mở cho chương tiếp\n5. Sử dụng kỹ thuật 'Show don't Tell': thể hiện cảm xúc qua hành động và chi tiết, không chỉ mô tả trực tiếp\n6. Cân bằng giữa hành động, đối thoại và miêu tả nội tâm\n7. Chỉ xuất ra nội dung chính, KHÔNG có tiêu đề chương, KHÔNG giải thích, KHÔNG có phần suy nghĩ/phân tích, KHÔNG có meta-talk.\n8. TUYỆT ĐỐI KHÔNG lặp lại bất kỳ đoạn văn hay lời dẫn nào đã xuất hiện ở phần trước.{continuity_prompt}{context_prompt}",
"continuity_prompt": "\n\n【Ôn lại trước đó】\n{previous_content}\n\n【Danh sách kiểm tra tính mạch lạc】\n✓ Đảm bảo hướng cốt truyện nhất quán với phần trước\n✓ Trạng thái và vị trí nhân vật tương ứng với phần trước\n✓ Các yếu tố hồi hộp đã có được hưởng ứng hoặc đẩy lên trong chương này\n✓ Phong cách đối thoại nhân vật giữ nhất quán\n✓ TUYỆT ĐỐI KHÔNG lặp lại nội dung đã có. Không bắt đầu bằng việc nhắc lại cảnh cũ.\n✓ Bắt đầu viết thẳng vào diễn biến mới từ điểm kết thúc của phần trước.\n✓ Yếu tố hồi hộp mới mở đường cho các chương sau\n✓ Kiểm tra tiến độ so với dàn ý tổng thể, đảm bảo không đi lệch hướng\n✓ Phát triển các tuyến truyện phụ song song với tuyến chính\n\nHãy tuân thủ nghiêm ngặt danh sách kiểm tra trên để đảm bảo nội dung mạch lạc và KHÔNG TRÙNG LẶP.",
"context_prompt": "\n\n{context_summary}\n\nHãy dựa vào tóm tắt trên để nắm bắt cốt truyện chính phần trước, đảm bảo chương này mạch lạc nhưng KHÔNG nhắc lại những gì đã tóm tắt.",
"rewrite_system": "Bạn là biên tập viên tiểu thuyết xuất sắc, giỏi cải thiện văn bản bằng lối viết sinh động và tinh tế.",
"rewrite_user": "Hãy viết lại văn bản gốc theo phong cách sau, giữ nguyên ý nghĩa và cốt truyện, nhưng thêm nhiều chi tiết:\n\nYêu cầu phong cách: {style}\n\nVăn bản gốc:\n{text}\n\n【Yêu cầu quan trọng】\n1. Phải xuất ra toàn bộ nội dung tiểu thuyết đã viết lại, số từ tương đương với bản gốc\n2. Tuyệt đối không chỉ xuất ra \"viết lại thành công\", \"trau chuốt thành công\", \"tạo thành công\" v.v.\n3. Phải xuất ra văn bản viết lại thực sự, bao gồm chi tiết miêu tả phong phú và triển khai cốt truyện\n4. Nếu bản gốc có 1000 từ, bản viết lại cũng nên có khoảng 1000 từ\n5. Không xuất ra bất kỳ văn bản giải thích hoặc thông báo xác nhận nào\n6. Tăng cường các giác quan: mùi, vị, xúc giác, không chỉ thị giác và thính giác\n7. Thêm chi tiết môi trường phản ánh tâm trạng nhân vật\n8. Sử dụng ẩn dụ và so sánh sáng tạo, tránh sáo rỗng\n\nHãy tuân thủ nghiêm ngặt các yêu cầu trên để xuất ra nội dung viết lại đầy đủ.",
"summary_system": "Bạn là biên tập nội dung chuyên nghiệp, giỏi chiết xuất nội dung cốt lõi của văn bản.",
"summary_user": "Hãy tạo một bản tóm tắt ngắn gọn cho văn bản sau, không quá {max_length} từ.\n\nVăn bản:\n{text}\n\nTóm tắt:",
"polish_system": "Bạn là biên tập văn học và chuyên gia trau chuốt chuyên nghiệp, giỏi nâng cao chất lượng văn bản.",
"polish_general": "Hãy trau chuốt toàn diện văn bản sau, nâng cao chất lượng văn phong, làm cho ngôn ngữ mượt mà hơn, sinh động hơn, có sức truyền cảm hơn.",
"polish_find_errors": "Hãy kiểm tra kỹ văn bản sau, tìm ra các lỗi (bao gồm lỗi chính tả, lỗi ngữ pháp, lỗi logic, dùng từ không phù hợp v.v.), và đề xuất sửa đổi.",
"polish_suggest": "Hãy đọc văn bản sau, đưa ra gợi ý cải thiện cụ thể, bao gồm hướng tối ưu về cốt truyện, nhân vật, đối thoại, miêu tả v.v.",
"polish_direct": "Hãy trực tiếp sửa đổi và tối ưu văn bản sau, nâng cao chất lượng văn phong, làm cho nó chuyên nghiệp và hoàn thiện hơn.",
"polish_remove_ai": "Hãy loại bỏ dấu vết AI trong văn bản sau, làm cho nó tự nhiên hơn, giống sáng tác của con người hơn, thêm chiều sâu cảm xúc.",
"polish_enhance": "Hãy tăng cường chi tiết cho văn bản sau, thêm miêu tả môi trường, miêu tả tâm lý, miêu tả giác quan v.v., làm cho nội dung phong phú lập thể hơn.",
"polish_dialogue": "Hãy tối ưu phần đối thoại trong văn bản sau, làm cho đối thoại tự nhiên hơn, phù hợp với tính cách nhân vật hơn, có cá tính hơn.",
"polish_pacing": "Hãy điều chỉnh nhịp điệu của văn bản sau, tối ưu tốc độ triển khai cốt truyện, làm cho câu chuyện hấp dẫn hơn.",
"polish_extra_req": "\n\nYêu cầu bổ sung: {custom_requirements}",
"polish_output_only": "\n\nVăn bản gốc:\n{text}\n\nChỉ xuất ra văn bản đã trau chuốt hoặc gợi ý, không có nội dung khác.",
"polish_suggest_system": "Bạn là biên tập văn học chuyên nghiệp, giỏi phân tích văn bản, tìm lỗi và trau chuốt tối ưu.",
"polish_suggest_user": "Hãy phân tích và tối ưu toàn diện văn bản sau:\n\n1. **Tìm lỗi**: Kiểm tra lỗi chính tả, lỗi ngữ pháp, lỗi logic, dùng từ không phù hợp v.v.\n2. **Đưa gợi ý**: Đưa ra gợi ý cải thiện cụ thể, bao gồm cốt truyện, nhân vật, đối thoại, miêu tả v.v.\n3. **Sửa trực tiếp**: Cung cấp phiên bản đã trau chuốt tối ưu\n\nVăn bản gốc:\n{text}\n\n{extra_req}\n\nHãy xuất ra theo định dạng sau:\n---\n【Lỗi phát hiện】\nLiệt kê các lỗi phát hiện\n\n【Gợi ý cải thiện】\nLiệt kê gợi ý cải thiện\n\n【Văn bản đã trau chuốt】\nVăn bản đã sửa trực tiếp\n---",
"suggest_system": "Bạn là chuyên gia thiết kế cốt truyện, phát triển nhân vật và xây dựng thế giới quan cho tiểu thuyết sáng tạo. Những ý tưởng bạn đưa ra phải vô cùng sáng tạo, hấp dẫn, chi tiết và có chiều sâu.",
"suggest_title_user": "Hãy đưa ra khoảng 10 gợi ý tên cho một tiểu thuyết.\n\nThể loại: {genre}\n\nYêu cầu:\n- Tên truyện phải thật súc tích, ấn tượng, gây tò mò, phản ánh đúng đặc trưng thể loại.\n- Đi kèm mỗi tên truyện là một câu mô tả ngắn gọn nội dung sơ bộ.\n- BẮT BUỘC chỉ trả về kết quả dưới định dạng JSON nguyên bản, không kèm bất kỳ ký tự markdown (```json), không giải thích hay hội thoại thừa.\n- Cấu trúc JSON bắt buộc: {\"suggestions\": [{\"title\": \"Tên 1\", \"description\": \"Mô tả 1\"}, {\"title\": \"Tên 2\", \"description\": \"Mô tả 2\"}]}",
"suggest_char_user": "Hãy đưa ra ý tưởng thiết lập chi tiết về các nhân vật cho một tiểu thuyết.\n\nTiêu đề dự kiến: {title}\nThể loại: {genre}\nSố lượng yêu cầu:\n- Nhân vật chính: {num_main_chars} người\n- Nhân vật phụ/phản diện: {num_sub_chars} người\n\nYêu cầu đối với mỗi nhân vật:\n1. Nêu rõ vai trò (Chính/Phụ/Phản diện)\n2. Tên, ngoại hình và đặc điểm tính cách nổi bật\n3. Trình độ, kỹ năng hoặc sức mạnh đặc biệt\n4. Bối cảnh xuất thân và động cơ cốt lõi\n\nHãy mô tả sâu sắc và có chiều sâu. Chỉ trả về nội dung ý tưởng, không giải thích dài dòng.",
"suggest_world_user": "Hãy đưa ra ý tưởng thiết lập thế giới quan (khoảng 150-200 từ) cho một tiểu thuyết.\n\nTiêu đề dự kiến: {title}\nThể loại: {genre}\n\nYêu cầu: Chỉ ra các quy luật độc đáo, sức mạnh, cấu trúc xã hội hoặc bối cảnh lịch sử. Chỉ trả về nội dung ý tưởng.",
"suggest_plot_user": "Hãy đưa ra ý tưởng cốt truyện chính (khoảng 200-250 từ) cho tiểu thuyết này để làm nền tảng phát triển.\n\nTiêu đề: {title}\nThể loại: {genre}\n\n(Nếu có) Thiết lập nhân vật: {character_setting}\n(Nếu có) Thế giới quan: {world_setting}\n\nYêu cầu:\n- Cốt truyện cần có điểm nhấn đầu truyện, bước ngoặt giữa truyện và xung đột cốt lõi rõ ràng\n- Xung đột bên ngoài (đối thủ, thế lực, nhiệm vụ) và xung đột nội tâm (mâu thuẫn, lựa chọn khó khăn)\n- Ít nhất 2-3 bước ngoặt bất ngờ nhưng hợp logic\n- Kết thúc để lại dư vị (happy ending hoặc open ending tùy thể loại)\n- Chỉ trả về nội dung ý tưởng.",
"continue_system": "Bạn là nhà văn tiểu thuyết dài kỳ xuất sắc, giỏi sáng tác câu chuyện hấp dẫn và kết nối cốt truyện tự nhiên.",
"continue_user": "Hãy viết tiếp chương tiếp theo của tiểu thuyết «{novel_title}».\n\n【Thiết lập hiện có】\nThiết lập nhân vật: {character_setting}\nThế giới quan: {world_setting}\nCốt truyện chính: {plot_idea}\n\n【Yêu cầu phong cách】\n{style_desc}\n\n【Ôn lại trước đó】(1500 từ gần nhất\n{previous_content}\n\n【Yêu cầu viết tiếp】\n1. Viết tiếp chương mới tự nhiên dựa trên nội dung trước\n2. Giữ tính mạch lạc với phần trước, bao gồm tính cách nhân vật, phát triển cốt truyện, phong cách đối thoại v.v.\n3. Số từ khoảng {target_words}\n4. TUYỆT ĐỐI KHÔNG lặp lại nội dung đã có. Không bắt đầu bằng việc tóm tắt hoặc nhắc lại cảnh cuối cùng.\n5. Kết thúc để lại yếu tố hồi hộp hoặc gợi mở phù hợp\n6. Chỉ xuất ra nội dung viết tiếp, KHÔNG có tiêu đề chương, KHÔNG giải thích, KHÔNG có phần suy nghĩ/phân tích.",
"chapter_summary_system": "Bạn là biên tập nội dung chuyên nghiệp, giỏi chiết xuất cốt truyện cốt lõi và thông tin quan trọng của chương.",
"chapter_summary_user": "Hãy tạo tóm tắt ngắn gọn cho chương sau (100-200 từ).\n\nTiêu đề chương: {chapter_title}\n\nNội dung chương:\n{chapter_content}\n\nYêu cầu:\n1. Giữ lại cốt truyện chính và thông tin nhân vật\n2. Nêu bật xung đột cốt lõi và bước ngoặt của chương\n3. Ngôn ngữ ngắn gọn rõ ràng\n4. Chỉ xuất ra nội dung tóm tắt, không có giải thích khác",
"style_description": "Phong cách viết: {writing_style}\nGiọng điệu: {writing_tone}\nXây dựng nhân vật: {character_development}\nĐộ phức tạp cốt truyện: {plot_complexity}",
"context_header": "【Tóm tắt trước đó】\n",
"chapter_context_line": "Chương {chapter_num}: {summary}\n",
"found_errors_header": "Lỗi phát hiện",
"suggestions_header": "Gợi ý cải thiện",
"polished_text_header": "Văn bản đã trau chuốt",
"ai_no_format": "AI không xuất theo định dạng, vui lòng xem kết quả trau chuốt",
"image_cover_base": "Một bìa sách chuyên nghiệp chất lượng cao cho tiểu thuyết tiêu đề '{title}', thể loại '{genre}'. Ánh sáng điện ảnh, chi tiết, phong cách sử thi.",
"image_illustration_base": "Một minh họa đẹp cho chương truyện tiêu đề '{title}'. Mô tả cảnh: {summary}. Nghệ thuật kỹ thuật số, độ chi tiết cao."
},
"generator": {
"title_empty": "Tên tiểu thuyết không được để trống",
"char_empty": "Thiết lập nhân vật không được để trống",
"world_empty": "Thiết lập thế giới quan không được để trống",
"plot_empty": "Cốt truyện chính không được để trống",
"outline_empty": "Dàn ý trống",
"outline_parse_failed": "Không thể phân tích chương nào từ dàn ý, vui lòng kiểm tra định dạng",
"outline_parse_success": "Phân tích thành công, tổng cộng {count} chương",
"outline_gen_success": "Tạo dàn ý thành công",
"gen_success": "Tạo thành công",
"suggest_success": "Gợi ý thành công",
"text_empty": "Văn bản trống",
"text_too_long_rewrite": "Văn bản quá dài (>20000 từ), vui lòng xử lý phân đoạn",
"text_too_long_polish": "Văn bản quá dài (>10000 từ), vui lòng xử lý phân đoạn",
"existing_text_empty": "Văn bản hiện có trống",
"chapter_content_empty": "Nội dung chương trống",
"api_empty_content": "API trả về nội dung trống, vui lòng kiểm tra cấu hình API",
"api_status_msg": "API trả về thông báo trạng thái, vui lòng kiểm tra cấu hình API",
"rewrite_success": "Viết lại thành công",
"rewrite_too_short": "Nội dung viết lại quá ngắn ({length} từ), có thể là vấn đề API",
"rewrite_failed_retries": "Viết lại thất bại: vẫn thất bại sau {max} lần thử",
"polish_success": "Trau chuốt thành công",
"polish_too_short": "Nội dung trau chuốt quá ngắn ({length} từ), có thể là vấn đề API",
"polish_failed_retries": "Trau chuốt thất bại: vẫn thất bại sau {max} lần thử",
"continue_success": "Viết tiếp thành công",
"continue_too_short": "Nội dung viết tiếp quá ngắn ({length} từ), có thể là vấn đề API",
"continue_failed_retries": "Viết tiếp thất bại: vẫn thất bại sau {max} lần thử",
"summary_success": "Thành công",
"summary_gen_success": "Tạo tóm tắt thành công",
"summary_gen_failed": "Tạo tóm tắt thất bại",
"summary_gen_error": "Lỗi tạo tóm tắt: {error}",
"cache_id_empty": "ID dự án không được để trống",
"cache_data_empty": "Dữ liệu bộ nhớ đệm không được để trống",
"cache_save_success": "Lưu bộ nhớ đệm thành công",
"cache_save_failed": "Lưu bộ nhớ đệm thất bại: {error}",
"cache_not_found": "Bộ nhớ đệm không tồn tại",
"cache_load_success": "Tải bộ nhớ đệm thành công",
"cache_load_failed": "Tải bộ nhớ đệm thất bại: {error}",
"cache_clear_success": "Xóa bộ nhớ đệm thành công",
"cache_clear_failed": "Xóa bộ nhớ đệm thất bại: {error}",
"summary_empty": "Nội dung tóm tắt không được để trống",
"summary_save_success": "Lưu tóm tắt thành công",
"summary_save_failed": "Lưu tóm tắt thất bại: {error}",
"summary_dir_not_found": "Thư mục tóm tắt không tồn tại",
"summary_load_done": "Đã tải {count} tóm tắt chương",
"summary_load_failed": "Tải tóm tắt thất bại: {error}",
"summary_clear_success": "Xóa tóm tắt thành công",
"summary_clear_failed": "Xóa tóm tắt thất bại: {error}",
"unknown_title": "Không rõ"
},
"exporter": {
"no_content": "Không có nội dung để xuất",
"no_chapters": "Không thể trích xuất chương từ văn bản",
"export_success": "Xuất thành công: {filename}",
"export_failed": "Xuất thất bại: {error}",
"missing_docx": "Lỗi: Thiếu thư viện python-docx, vui lòng chạy: pip install python-docx",
"missing_markdown": "Lỗi: Thiếu thư viện markdown, vui lòng chạy: pip install markdown",
"generated_at": "Tạo lúc: {datetime}",
"generated_date": "Ngày tạo: {date}",
"first_chapter": "Chương một",
"body_font": "Times New Roman",
"title_font": "Arial"
},
"config_api": {
"name_required": "Tên giao diện không được để trống",
"type_required": "Loại giao diện không được để trống",
"model_required": "Tên mô hình không được để trống",
"name_exists": "Giao diện '{name}' đã tồn tại",
"add_success": "Thêm giao diện '{name}' thành công",
"add_failed": "Thêm giao diện thất bại: {error}",
"update_success": "Cập nhật giao diện '{name}' thành công",
"update_not_found": "Không tìm thấy giao diện: {name}",
"update_failed": "Cập nhật giao diện thất bại: {error}",
"delete_success": "Đã xóa giao diện '{name}'",
"delete_not_found": "Không tìm thấy giao diện: {name}",
"delete_failed": "Xóa giao diện thất bại: {error}",
"toggle_success": "Giao diện '{name}' đã {status}",
"toggle_enabled": "bật",
"toggle_disabled": "tắt",
"toggle_not_found": "Không tìm thấy giao diện: {name}",
"toggle_failed": "Chuyển đổi trạng thái giao diện thất bại: {error}",
"test_success": "Kiểm tra giao diện '{name}' thành công",
"test_not_found": "Không tìm thấy giao diện: {name}",
"test_failed": "Kiểm tra giao diện '{name}' thất bại: {error}",
"test_prompt": "Vui lòng trả lời 'OK' để xác nhận kết nối API bình thường.",
"export_success": "Đã xuất cấu hình đến: {filepath}",
"export_failed": "Xuất cấu hình thất bại: {error}",
"default_success": "Đã đặt giao diện '{name}' làm mặc định\nCác yêu cầu sẽ ưu tiên thử giao diện này trước tiên",
"default_failed": "Đặt mặc định thất bại: {error}"
},
"project_manager": {
"create_success": "Tạo dự án '{title}' thành công",
"create_failed": "Tạo dự án thất bại: {error}",
"save_success": "Lưu dự án '{title}' thành công",
"save_failed": "Lưu dự án thất bại: {error}",
"load_success": "Tải dự án thành công",
"load_not_found": "Dự án không tồn tại: {id}",
"load_failed": "Tải dự án thất bại: {error}",
"delete_success": "Đã xóa dự án",
"delete_not_found": "Dự án không tồn tại: {id}",
"delete_failed": "Xóa dự án thất bại: {error}",
"export_success": "Xuất dự án thành công: {filepath}",
"export_failed": "Xuất dự án thất bại: {error}"
},
"ui": {
"col_project_name": "Tên dự án",
"col_type": "Loại",
"col_created_at": "Ngày tạo",
"col_updated_at": "Ngày cập nhật",
"col_chapters": "Số chương",
"col_completion": "Hoàn thành",
"col_current_chapter": "Chương hiện tại",
"col_total_chapters": "Tổng chương",
"col_status": "Trạng thái",
"col_cache_time": "Thời gian cache",
"col_size_kb": "Kích thước(KB)",
"col_project_id": "ID dự án",
"col_chapter_count": "Số chương",
"col_name": "Tên",
"col_backend_type": "Loại",
"col_model": "Mô hình",
"col_enabled": "Bật",
"col_timeout": "Thời gian chờ(s)",
"col_retry_times": "Số lần thử lại",
"col_test": "🧪 Thử nghiệm",
"col_default": "⭐ Mặc định",
"no_projects": "Chưa có dự án",
"no_cache": "Chưa có bộ nhớ đệm",
"no_summary_cache": "Chưa có bộ nhớ đệm tóm tắt",
"found_projects": "Tìm thấy {count} dự án",
"found_caches": "Tìm thấy {count} bộ nhớ đệm",
"found_summary_caches": "Tìm thấy {count} bộ nhớ đệm tóm tắt",
"cache_total_size_kb": "Tổng kích thước bộ nhớ đệm: {size} KB",
"cache_total_size_mb": "Tổng kích thước bộ nhớ đệm: {size} MB",
"summary_total_size_kb": "Tổng kích thước bộ nhớ đệm tóm tắt: {size} KB",
"summary_total_size_mb": "Tổng kích thước bộ nhớ đệm tóm tắt: {size} MB",
"get_cache_size_failed": "Lấy kích thước bộ nhớ đệm thất bại",
"get_summary_size_failed": "Lấy kích thước bộ nhớ đệm tóm tắt thất bại",
"cleared_caches": "✅ Đã xóa {cleared}/{total} bộ nhớ đệm",
"cleared_summary_caches": "✅ Đã xóa {cleared}/{total} bộ nhớ đệm tóm tắt",
"no_cache_to_clear": "❌ Không có bộ nhớ đệm để xóa",
"no_summary_to_clear": "❌ Không có bộ nhớ đệm tóm tắt để xóa",
"select_cache_to_clear": "❌ Vui lòng chọn bộ nhớ đệm cần xóa",
"cache_found_info": "Tìm thấy bộ nhớ đệm: Đã hoàn thành {current}/{total} chương",
"cache_timestamp_info": "Thời gian cache: {time}",
"cache_check_failed": "Kiểm tra thất bại",
"no_content_export": "❌ Không có nội dung để xuất",
"no_title_export": "❌ Vui lòng điền tên tiểu thuyết",
"unsupported_format": "❌ Định dạng không được hỗ trợ: {format}",
"export_success": "✅ Xuất thành công!",
"export_failed": "❌ Xuất thất bại: {error}",
"export_error": "❌ Lỗi xuất: {error}",
"select_project": "❌ Vui lòng chọn một dự án",
"project_not_exist": "❌ Dự án '{title}' không tồn tại",
"metadata_not_exist": "❌ File metadata dự án không tồn tại: {file}",
"no_exportable_content": "❌ Dự án không có nội dung để xuất, vui lòng tạo chương trước",
"file_not_exist": "❌ File xuất không tồn tại: {file}",
"export_no_filepath": "❌ Xuất thất bại: Không trả về đường dẫn file",
"fill_required_fields": "❌ Vui lòng điền đầy đủ các trường bắt buộc (tên, URL, mô hình)",
"api_key_required": "❌ Loại giao diện này phải điền API key",
"enter_backend_name": "❌ Vui lòng nhập tên giao diện",
"enter_delete_name": "❌ Vui lòng nhập tên giao diện cần xóa",
"refreshed_backends": "✅ Đã làm mới danh sách giao diện",
"backend_loaded_for_edit": "✅ Đã tải cấu hình '{name}' để chỉnh sửa",
"backend_test_header": "Kết quả kiểm tra kết nối backend:\n",
"backend_available": "✓ Khả dụng",
"backend_unavailable": "✗ Không khả dụng",
"save_success": "Lưu thành công",
"save_failed": "Lưu thất bại: {error}",
"no_file": "Không có tệp",
"perf_no_data": "Chưa có dữ liệu hiệu suất",
"perf_report_header": "=== Báo cáo giám sát hiệu suất ===\n",
"perf_stat_line": "{name}: TB={avg:.2f}ms, Max={max:.2f}ms, Min={min:.2f}ms, Số lần={count}\n",
"validation_api_key_empty": "API key không được để trống",
"validation_unknown_provider": "Nhà cung cấp API không xác định",
"validation_openai_key_format": "OpenAI API key phải bắt đầu bằng 'sk-'",
"validation_anthropic_key_format": "Anthropic API key phải bắt đầu bằng 'sk-ant-'",
"validation_google_key_format": "Google API key không đúng định dạng",
"validation_key_format_invalid": "API key không đúng định dạng",
"validation_key_passed": "Kiểm tra định dạng key thành công",
"validation_model_empty": "Tên mô hình không được để trống",
"validation_model_invalid": "Tên mô hình chỉ chứa chữ cái, số, dấu gạch dưới, dấu chấm, dấu gạch ngang, dấu gạch chéo và dấu hai chấm",
"validation_model_passed": "Kiểm tra tên mô hình thành công",
"outline_chapter_format": "<details>\n<summary><b>Chương {num}: {title}</b> <i>(Click để mở/thu gọn)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"outline_chapter_format_open": "<details open>\n<summary><b>Chương {num}: {title}</b> <i>(Click để mở/thu gọn)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"segment_format": "<details open>\n<summary><b>Đoạn {num}</b> <i>(Click để mở/thu gọn)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"content_format": "<details open>\n<summary><b>Nội dung</b> <i>(Click để mở/thu gọn)</i></summary>\n<br>\n\n{content}\n\n</details>\n\n",
"rewritten_segments_summary": "Toàn bộ các đoạn",
"view_all": "🔍 Xem tất cả",
"chapter": "Chương",
"segment": "Đoạn",
"select_chapter_to_view": "📑 Chọn chương hiển thị",
"select_segment_to_view": "📑 Chọn đoạn hiển thị"
},
"api_client": {
"no_backends": "Lỗi: Không có backend khả dụng, vui lòng kiểm tra cài đặt",
"invalid_messages": "Lỗi: messages phải là danh sách không rỗng",
"no_api_client": "Lỗi: Không có API client khả dụng",
"invalid_content": "API trả về nội dung không hợp lệ (độ dài: {length} ký tự)",
"error_prefix": "Lỗi: {error}",
"retry_failed": "Lỗi: Vẫn thất bại sau {max} lần thử lại",
"rate_limit_error": "Lỗi: Đã vượt quá giới hạn tần suất gọi API (Rate Limit): {error}",
"auth_error": "Lỗi xác thực: Vui lòng kiểm tra API Key hoặc cấu hình backend: {error}",
"connection_error": "Lỗi kết nối: Không thể kết nối tới máy chủ API: {error}",
"api_error": "Lỗi API: Máy chủ API trả về lỗi: {error}",
"test_prompt": "Bạn là một trợ lý hữu ích",
"test_hello": "Xin chào",
"gen_image_success": "Tạo hình ảnh thành công",
"gen_image_failed": "Tạo hình ảnh thất bại: {error}",
"image_gen_unsupported": "Nhà cung cấp API hiện tại không hỗ trợ chức năng tạo hình ảnh. Vui lòng sử dụng API trả phí của OpenAI, Together AI hoặc SiliconFlow."
},
"file_parser": {
"no_file": "Không có tệp",
"file_too_large": "Lỗi: Tệp quá lớn ({size}MB > 50MB)",
"parse_complete": "Phân tích hoàn tất, tổng cộng {count} đoạn, khoảng {chars} ký tự",
"read_failed": "Đọc thất bại: {error}",
"missing_pymupdf": "Lỗi: Thiếu thư viện PyMuPDF, vui lòng chạy: pip install PyMuPDF",
"missing_ebooklib": "Lỗi: Thiếu thư viện ebooklib hoặc beautifulsoup4, vui lòng chạy: pip install ebooklib beautifulsoup4",
"missing_docx": "Lỗi: Thiếu thư viện python-docx, vui lòng chạy: pip install python-docx",
"unsupported_format": "Định dạng tệp không được hỗ trợ (hỗ trợ txt/pdf/epub/md/docx)",
"unsupported_chapter_format": "Định dạng tệp không được hỗ trợ",
"chapter_parse_complete": "Phân tích hoàn tất, tổng cộng {count} chương",
"chapter_parse_failed": "Phân tích thất bại: {error}",
"split_word_done": "Phân đoạn theo số chữ hoàn tất, tổng cộng {count} đoạn, mỗi đoạn khoảng {words} chữ",
"split_pattern_empty": "Đánh dấu phân đoạn không được để trống",
"word_count_positive": "Số chữ phải lớn hơn 0",
"invalid_regex": "Biểu thức chính quy không hợp lệ: {error}",
"file_not_exist": "Tệp không tồn tại: {path}",
"upload_read_failed": "Đọc tệp tải lên thất bại: {error}"
},
"config": {
"backend_name_empty": "Tên backend không được để trống",
"unsupported_type": "Loại không được hỗ trợ: {type}",
"base_url_invalid": "Base URL phải bắt đầu bằng http hoặc https",
"api_key_empty": "API Key không được để trống",
"model_empty": "Tên mô hình không được để trống",
"timeout_range": "Thời gian chờ phải từ 5-10000 giây",
"retry_range": "Số lần thử lại phải từ 1-10",
"temp_range": "Giá trị temperature phải từ 0.1-2.0",
"top_p_range": "top_p phải từ 0.1-1.0",
"max_tokens_range": "max_tokens phải từ 100-100000",
"chapter_words_range": "Số chữ mục tiêu mỗi chương phải từ 500-65536",
"backend_exists": "Backend '{name}' đã tồn tại",
"backend_add_success": "Thêm backend thành công",
"backend_update_success": "Cập nhật backend thành công",
"backend_not_found": "Backend '{name}' không tồn tại",
"backend_delete_success": "Backend '{name}' đã xóa",
"gen_params_update_success": "Cập nhật tham số tạo thành công",
"config_export_success": "Cấu hình đã xuất tới {filepath}",
"config_export_failed": "Xuất cấu hình thất bại: {error}",
"config_save_success": "Lưu cấu hình thành công",
"config_save_failed": "Lưu cấu hình thất bại: {error}",
"config_load_success": "Tải cấu hình thành công",
"config_file_not_found": "File cấu hình không tồn tại, sử dụng cấu hình mặc định",
"config_load_failed": "Tải cấu hình thất bại: {error}",
"config_file_missing": "File cấu hình không tồn tại: {path}",
"config_format_unsupported": "Định dạng cấu hình không được hỗ trợ: {ext}",
"default_backend_name": "Ollama Cục bộ"
}
}
+115
View File
@@ -0,0 +1,115 @@
# Công cụ sáng tác tiểu thuyết AI Pro v4.0 - Hướng dẫn sử dụng
## 📦 Bắt đầu nhanh
### Khởi động nhanh một chạm (Đề xuất)
1. Đảm bảo đã cài đặt Python 3.8+
2. Chạy lệnh sau trong thư mục gốc của dự án:
```bash
python start_venv.py
```
3. Script sẽ tự động:
- Kiểm tra phiên bản Python
- Tạo môi trường ảo (nếu cần)
- Cài đặt/cập nhật các thư viện phụ thuộc
- Khởi động ứng dụng
### Khởi động thủ công
1. Tạo môi trường ảo:
```bash
python -m venv venv
```
2. Kích hoạt môi trường ảo:
- Windows: `venv\Scripts\activate`
- Linux/Mac: `source venv/bin/activate`
3. Cài đặt các thư viện phụ thuộc:
```bash
pip install -r requirements.txt
```
4. Khởi động ứng dụng:
```bash
python app.py
```
## 📚 Thông tin bản quyền
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
## 🔗 Liên kết liên quan
- GitHub: <https://github.com/yangqi1309134997-coder/ai-novel-generator>
- Ghi chú đám mây Huyễn Thành: <https://hcnote.cn/>
## ❓ Câu hỏi thường gặp
### 1. Cổng (Port) bị chiếm dụng
**Vấn đề**: Khi khởi động báo cổng 7860 đã bị chiếm dụng.
**Cách giải quyết**: Script sẽ tự động tìm kiếm cổng khả dụng, không cần xử lý thủ công.
### 2. Cài đặt thư viện phụ thuộc thất bại
**Vấn đề**: Cài đặt pip thất bại.
**Cách giải quyết**:
- Nâng cấp pip: `python -m pip install --upgrade pip`
- Sử dụng mirror lân cận: `pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt`
### 3. Tạo môi trường ảo thất bại
**Vấn đề**: Module venv không khả dụng.
**Cách giải quyết**: Sử dụng virtualenv:
```bash
pip install virtualenv
virtualenv venv
```
### 4. Lỗi import module
**Vấn đề**: Khi khởi động báo không tìm thấy module.
**Cách giải quyết**: Đảm bảo đã cài đặt tất cả các thư viện phụ thuộc trong môi trường ảo.
### 5. Lỗi cấu hình API
**Vấn đề**: Không thể kết nối API.
**Cách giải quyết**:
1. Kiểm tra khóa API (API key) đã chính xác chưa.
2. Kiểm tra kết nối backend trong mục "Cài đặt hệ thống".
3. Xem file log: `logs/novel_tool_*.log`
## 📁 Cấu trúc dự án
```
ai-novel-generator-4.0/
├── app.py # Chương trình chính
├── start_venv.py # Script khởi động nhanh một chạm
├── requirements.txt # Danh sách thư viện phụ thuộc
├── config/ # Thư mục cấu hình
├── logs/ # Thư mục log
├── projects/ # Dữ liệu dự án
├── exports/ # File xuất ra
└── cache/ # Thư mục bộ nhớ đệm (cache)
```
## 📖 Tính năng nổi bật
- ✅ Sáng tác thông minh: Sáng tác tiểu thuyết dài kỳ từ con số không
- ✅ Viết lại thông minh: Hỗ trợ viết lại với 17 phong cách thiết lập sẵn
- ✅ Viết tiếp thông minh: Tự động viết tiếp tiểu thuyết hiện có
- ✅ Trau chuốt tiểu thuyết: 8 lựa chọn trau chuốt (đánh bóng) câu chữ
- ✅ Phân đoạn linh hoạt: Hỗ trợ phân đoạn tự động, theo số lượng chữ, hoặc theo văn bản cố định
- ✅ Quản lý dự án: Quản lý đa dự án, hỗ trợ tải lên tiếp tục (resume) tại điểm dừng
- ✅ Xuất ra nhiều định dạng: Word, TXT, Markdown, HTML
- ✅ Tự động thử lại khi lỗi: Tự động lặp lại các thao tác bị lỗi
+1
View File
@@ -0,0 +1 @@
from locales.i18n import t, set_language, get_language, load_locale
+104
View File
@@ -0,0 +1,104 @@
"""
i18n (Internationalization) module for AI Novel Generator.
Loads locale-specific JSON files and provides a simple t(key) function to retrieve translated strings.
Usage:
from locales.i18n import t, set_language
set_language("VI") # Switch to Vietnamese
print(t("app.title")) # Prints Vietnamese title
Language can also be set via environment variable APP_LANGUAGE (default: "VI").
"""
import json
import os
import logging
logger = logging.getLogger("i18n")
# Global state
_current_language = None
_translations = {}
_locales_dir = os.path.dirname(os.path.abspath(__file__))
def load_locale(lang: str) -> dict:
"""Load a locale JSON file and return the translations dict."""
locale_path = os.path.join(_locales_dir, lang, "messages.json")
if not os.path.exists(locale_path):
logger.error(f"Locale file not found: {locale_path}")
return {}
try:
with open(locale_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.error(f"Failed to load locale '{lang}': {e}")
return {}
def set_language(lang: str) -> None:
"""Set the active language and load its translations."""
global _current_language, _translations
_current_language = lang
_translations = load_locale(lang)
if _translations:
logger.info(f"Language set to: {lang}")
else:
logger.warning(f"No translations loaded for language: {lang}")
def get_language() -> str:
"""Return the current language code."""
return _current_language or "VI"
def t(key: str, **kwargs) -> str:
"""
Translate a dot-notation key to the localized string.
Args:
key: Dot-separated key path, e.g. "tabs.rewrite" or "messages.polish_success"
**kwargs: Optional format parameters to interpolate into the string.
Returns:
The translated string, or the key itself if not found (for debugging).
"""
global _translations
# Auto-initialize if not yet loaded
if not _translations:
init_lang = os.getenv("APP_LANGUAGE", "VI")
set_language(init_lang)
# Traverse the nested dict
parts = key.split(".")
value = _translations
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
logger.warning(f"Missing translation key: '{key}' (language: {_current_language})")
return key # Return the key itself as fallback
if isinstance(value, str):
if kwargs:
try:
return value.format(**kwargs)
except KeyError:
return value
return value
# If value is a list (e.g. dropdown choices), return it directly
if isinstance(value, list):
return value
# If value is still a dict, return the key
logger.warning(f"Translation key '{key}' resolved to a dict, not a string")
return key
# Auto-initialize on import
_init_lang = os.getenv("APP_LANGUAGE", "VI")
set_language(_init_lang)
+145
View File
@@ -0,0 +1,145 @@
"""
-đun theo dõi nhật - Hệ thống nhật cấp sản xuất
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 logging
import logging.handlers
import os
from datetime import datetime
from typing import Optional
LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)
# Cấu hình tệp nhật ký
LOG_FILE = os.path.join(LOG_DIR, f"novel_tool_{datetime.now().strftime('%Y%m%d')}.log")
ERROR_LOG_FILE = os.path.join(LOG_DIR, f"errors_{datetime.now().strftime('%Y%m%d')}.log")
def setup_logger(
name: str,
log_level: int = logging.INFO,
log_to_file: bool = True,
force_reconfigure: bool = False
) -> logging.Logger:
"""
Khởi tạo logger cấp sản xuất
Args:
name: Tên logger
log_level: Cấp độ nhật
log_to_file: xuất ra tệp hay không
force_reconfigure: bắt buộc cấu hình lại không (xóa các handler )
Returns:
Biến logger đã được cấu hình
"""
logger = logging.getLogger(name)
logger.setLevel(log_level)
# Nếu bắt buộc cấu hình lại, xóa bỏ handler hiện có
if force_reconfigure:
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Tránh thêm handler lặp lại
elif logger.handlers:
return logger
# Định dạng đầu ra bảng điều khiển
console_formatter = logging.Formatter(
'[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# Đầu ra tệp (nếu được bật)
if log_to_file:
file_formatter = logging.Formatter(
'[%(asctime)s] [%(name)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Nhật ký chung
file_handler = logging.handlers.RotatingFileHandler(
LOG_FILE,
maxBytes=10*1024*1024, # 10MB
backupCount=5,
encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Nhật ký lỗi - Luôn thêm vào, đảm bảo chắc chắn sẽ thu thập được nhật ký cấp độ ERROR
error_handler = logging.handlers.RotatingFileHandler(
ERROR_LOG_FILE,
maxBytes=10*1024*1024,
backupCount=5,
encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(file_formatter)
logger.addHandler(error_handler)
return logger
class PerformanceMonitor:
"""Công cụ theo dõi hiệu suất"""
def __init__(self):
self.logger = setup_logger("PerformanceMonitor")
self.metrics = {}
def record_metric(self, name: str, value: float, unit: str = "ms") -> None:
"""Ghi lại số liệu hiệu suất"""
if name not in self.metrics:
self.metrics[name] = []
self.metrics[name].append(value)
# Chỉ lưu 1000 bản ghi gần nhất
if len(self.metrics[name]) > 1000:
self.metrics[name] = self.metrics[name][-1000:]
def get_average(self, name: str) -> Optional[float]:
"""Lấy giá trị trung bình"""
if name not in self.metrics or not self.metrics[name]:
return None
return sum(self.metrics[name]) / len(self.metrics[name])
def report(self) -> str:
"""Tạo báo cáo hiệu suất"""
if not self.metrics:
return "No performance data yet"
report = "=== Performance Report ===\n"
for name, values in self.metrics.items():
if values:
avg = sum(values) / len(values)
max_val = max(values)
min_val = min(values)
report += f"{name}: avg={avg:.2f}ms, max={max_val:.2f}ms, min={min_val:.2f}ms, count={len(values)}\n"
return report
# Phiên bản toàn cục
_logger = setup_logger("NovelTool")
_performance_monitor = PerformanceMonitor()
def get_logger(name: str = "NovelTool") -> logging.Logger:
"""Lấy biến instance của logger"""
return logging.getLogger(name)
def get_performance_monitor() -> PerformanceMonitor:
"""Lấy trình theo dõi hiệu suất"""
return _performance_monitor
+1370
View File
File diff suppressed because it is too large Load Diff
+331
View File
@@ -0,0 +1,331 @@
"""
-đun Quản dự án - Hỗ trợ lưu, tải, xuất dự án
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 json
import os
import re
import tempfile
import logging
from typing import List, Dict, Optional, Tuple
from datetime import datetime
from pathlib import Path
from novel_generator import NovelProject, Chapter
from locales.i18n import t
from database import get_db
logger = logging.getLogger(__name__)
class ProjectManager:
"""quản lý dự án"""
@staticmethod
def _slugify(name: str) -> str:
s = str(name or "").lower()
s = re.sub(r'[^\w\s-]', '', s)
s = re.sub(r'[\s_]+', '-', s)
s = re.sub(r'-+', '-', s).strip('-')
return s or "untitled"
@staticmethod
def create_project(
title: str,
genre: str,
sub_genres: List[str],
character_setting: str,
world_setting: str,
plot_idea: str
) -> Tuple[Optional[NovelProject], str]:
"""
Tạo dự án mới
Returns:
(Đối tượng dự án, Thông tin trạng thái)
"""
try:
if not title or not title.strip():
return None, "Title cannot be empty"
project_id = ProjectManager._slugify(title)
now = datetime.now().isoformat()
project = NovelProject(
title=title.strip(),
genre=genre.strip() if genre else "",
sub_genres=sub_genres if sub_genres else [],
character_setting=character_setting.strip() if character_setting else "",
world_setting=world_setting.strip() if world_setting else "",
plot_idea=plot_idea.strip() if plot_idea else "",
id=project_id,
created_at=now,
updated_at=now
)
logger.info(f"Project created: {project_id}")
return project, t("project_manager.create_success", title=title)
except Exception as e:
logger.error(f"Project create failed: {e}")
return None, t("project_manager.create_failed", error=str(e))
@staticmethod
def save_project(project: NovelProject) -> Tuple[bool, str]:
"""
Lưu dự án vào SQLite
Returns:
(Cờ thành công (boolean), Thông tin trạng thái)
"""
try:
if not project or not project.title:
return False, "Project data incomplete"
# Sử dụng project.id hiện có hoặc tạo mới
if getattr(project, 'id', None):
project_id = project.id
else:
project_id = ProjectManager._slugify(project.title)
project.id = project_id
conn = get_db()
now = datetime.now().isoformat()
project.updated_at = now
# Lưu project
conn.execute("""
INSERT OR REPLACE INTO projects
(id, title, genre, sub_genres, character_setting, world_setting, plot_idea, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
project_id,
project.title,
project.genre,
json.dumps(project.sub_genres if isinstance(project.sub_genres, list) else [], ensure_ascii=False),
project.character_setting,
project.world_setting,
project.plot_idea,
project.created_at,
now
))
# Xóa chapters cũ và insert lại
conn.execute("DELETE FROM chapters WHERE project_id = ?", (project_id,))
for ch in project.chapters:
conn.execute("""
INSERT INTO chapters
(project_id, num, title, desc, content, word_count, generated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
project_id,
ch.num,
ch.title,
ch.desc,
ch.content,
ch.word_count,
ch.generated_at
))
conn.commit()
logger.info(f"Project saved to database: {project_id}")
return True, t("project_manager.save_success", title=project_id)
except Exception as e:
logger.error(f"Project save failed: {e}")
return False, t("project_manager.save_failed", error=str(e))
@staticmethod
def load_project(project_id: str) -> Tuple[Optional[NovelProject], str]:
"""
Tải dự án từ SQLite
Returns:
(Đối tượng dự án, Thông tin trạng thái)
"""
try:
conn = get_db()
row = conn.execute(
"SELECT * FROM projects WHERE id = ?", (project_id,)
).fetchone()
if not row:
return None, t("project_manager.load_not_found", id=project_id)
# Lấy list json string
try:
sg_str = row["sub_genres"]
except (IndexError, KeyError):
sg_str = "[]"
try:
sg_list = json.loads(sg_str) if sg_str else []
except:
sg_list = []
# Xây dựng lại dự án
project = NovelProject(
title=row["title"],
genre=row["genre"],
sub_genres=sg_list,
character_setting=row["character_setting"],
world_setting=row["world_setting"],
plot_idea=row["plot_idea"],
created_at=row["created_at"],
updated_at=row["updated_at"]
)
project.id = row["id"]
# Tải chapters
ch_rows = conn.execute(
"SELECT * FROM chapters WHERE project_id = ? ORDER BY num", (project_id,)
).fetchall()
for ch_row in ch_rows:
chapter = Chapter(
num=ch_row["num"],
title=ch_row["title"],
desc=ch_row["desc"],
content=ch_row["content"],
word_count=ch_row["word_count"],
generated_at=ch_row["generated_at"]
)
project.chapters.append(chapter)
logger.info(f"Project loaded from database: {project_id}")
return project, t("project_manager.load_success")
except Exception as e:
logger.error(f"Project load failed: {e}")
return None, t("project_manager.load_failed", error=str(e))
@staticmethod
def list_projects() -> List[Dict]:
"""
Liệt tất cả dự án từ SQLite
Returns:
Danh sách thông tin dự án
"""
try:
conn = get_db()
rows = conn.execute(
"SELECT id, title, genre, created_at, updated_at FROM projects ORDER BY updated_at DESC"
).fetchall()
projects = []
for row in rows:
# Đếm chapters
ch_count = conn.execute(
"SELECT COUNT(*) as total, SUM(CASE WHEN content != '' THEN 1 ELSE 0 END) as completed FROM chapters WHERE project_id = ?",
(row["id"],)
).fetchone()
projects.append({
"id": row["id"],
"title": row["title"],
"genre": row["genre"],
"created_at": row["created_at"],
"updated_at": row["updated_at"],
"chapter_count": ch_count["total"] if ch_count else 0,
"completed_chapters": ch_count["completed"] if ch_count else 0
})
logger.info(f"Found {len(projects)} projects in database")
return projects
except Exception as e:
logger.error(f"List projects failed: {e}")
return []
@staticmethod
def get_project_by_title(project_title: str) -> Optional[Dict]:
"""
Lấy thông tin dự án theo tiêu đề
Returns:
Từ điển dự án hoặc None
"""
projects = ProjectManager.list_projects()
for project in projects:
if project.get("title") == project_title:
return project
return None
@staticmethod
def delete_project(project_id: str) -> Tuple[bool, str]:
"""
Xóa dự án từ SQLite
Returns:
(Cờ thành công (boolean), Thông tin trạng thái)
"""
try:
conn = get_db()
cursor = conn.execute("DELETE FROM projects WHERE id = ?", (project_id,))
conn.commit()
if cursor.rowcount == 0:
return False, t("project_manager.delete_not_found", id=project_id)
logger.info(f"Project deleted from database: {project_id}")
return True, t("project_manager.delete_success")
except Exception as e:
logger.error(f"Project delete failed: {e}")
return False, t("project_manager.delete_failed", error=str(e))
@staticmethod
def export_project(project: NovelProject, export_format: str = "json") -> Tuple[Optional[str], str]:
"""
Xuất cấu hình dự án (để chia sẻ hoặc sao lưu)
Args:
project: Đối tượng dự án
export_format: Định dạng xuất (json/zip)
Returns:
(Đường dẫn tệp, Thông tin trạng thái)
"""
try:
if not project:
return None, ""
export_dir = os.path.join("exports", "project_backups")
os.makedirs(export_dir, exist_ok=True)
if export_format == "json":
data = {
"title": project.title,
"genre": project.genre,
"sub_genres": project.sub_genres,
"character_setting": project.character_setting,
"world_setting": project.world_setting,
"plot_idea": project.plot_idea,
"created_at": project.created_at,
"chapters": [ch.to_dict() for ch in project.chapters]
}
filename = f"{project.title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(export_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
logger.info(f"Project exported: {filename}")
return filepath, t("project_manager.export_success", filepath=filename)
else:
return None, f"Unsupported format: {export_format}"
except Exception as e:
logger.error(f"Project export failed: {e}")
return None, t("project_manager.export_failed", error=str(e))
def get_project_manager() -> ProjectManager:
"""Nhận phiên bản quản lý dự án"""
return ProjectManager()
+10
View File
@@ -0,0 +1,10 @@
# AI小说创作工具Pro - 核心依赖
gradio>=4.0.0
pandas>=2.0.0
openai>=1.0.0
python-docx>=1.0.0
# 可选依赖(用于文件解析)
PyMuPDF>=1.23.0 # PDF支持
ebooklib>=0.18 # EPUB支持
beautifulsoup4>=4.12.0 # EPUB支持
+139
View File
@@ -0,0 +1,139 @@
import os
import json
import logging
from typing import Dict, List, Optional
from locales.i18n import t
logger = logging.getLogger(__name__)
SUBGENRES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "sub_genres.json")
class SubGenreManager:
"""Quản lý các chủ đề con (hashtags) và mô tả hướng dẫn viết"""
_cached_sub_genres = None
_cached_mtime = 0
@classmethod
def get_default_sub_genres(cls) -> List[Dict[str, str]]:
"""Lấy danh sách chủ đề con mặc định nếu chưa có file"""
# Mặc định sử dụng danh sách cũ từ file ngôn ngữ
default_names = t("create.sub_genres")
if isinstance(default_names, str):
default_names = [
"Xuyên không", "Xuyên sách", "Trọng sinh", "Hệ thống", "Bàn tay vàng", "Không gian tùy thân", "Linh tuyền", "Đọc tâm thuật",
"Vô địch lưu", "Cẩu đạo", "Nhiệt huyết", "Hài hước", "Sảng văn", "Ngọt sủng", "Ngược luyến", "Gương vỡ lại lành",
"Cưới trước yêu sau", "Oan gia ngõ hẹp", "Thanh mai trúc mã", "Hào môn thế gia", "Tổng tài", "Minh tinh", "Giới giải trí",
"Vườn trường", "Học bá", "Võng du", "E-sports", "Livestream", "Mỹ thực", "Nông trại", "Điền văn", "Nuôi con", "Làm giàu",
"Cung đấu", "Gia đấu", "Quyền mưu", "Nữ cường", "Nam cường", "Song khiết", "Phế Sài", "Thiên tài", "Mỹ cường thảm", "Trà xanh",
"Bạch liên hoa", "Hắc hóa", "Cứu rỗi", "Chữa lành", "Não tàn", "Bức hôn", "Thế thân", "Mang thai chạy trốn", "Manh bảo",
"Khoa cử", "Khoa học kỹ thuật", "Linh khí khôi phục", "Dị năng", "Dị dã", "Thần minh", "Tu ma", "Phật tu", "Đạo sĩ", "Yêu tu",
"Quỷ tu", "Sư đồ luyến", "Huynh đệ", "Tỷ muội", "Ngụy huynh muội", "Đại thúc luyến", "Tỷ đệ luyến", "Niên hạ", "Song hướng thầm mến",
"Tình hữu độc chung", "Một kiến chung tình", "Pháp sư", "Kiếm khách", "Kỵ sĩ", "Tinh tế", "Cơ giáp", "Trùng tộc", "Dị thú",
"Mạt thế khổng lồ", "Mạt thế luân hồi", "Hào môn ân oán", "Phá án", "Huyền nghi", "Phiêu lưu", "Mạo hiểm", "Sống sót", "Man hoang",
"Bộ lạc", "Trí tuệ nhân tạo", "Biến dị", "Độc y", "Sát thủ", "Ma pháp sơ nguyên", "Khế ước", "Hậu cung", "1v1", "NP"
]
default_sub_genres = []
for name in default_names:
desc = ""
default_sub_genres.append({
"name": name,
"description": desc
})
return default_sub_genres
@classmethod
def ensure_data_dir(cls):
"""Đảm bảo thư mục data tồn tại"""
os.makedirs(os.path.dirname(SUBGENRES_FILE), exist_ok=True)
@classmethod
def load_sub_genres(cls) -> List[Dict[str, str]]:
"""Tải danh sách chủ đề con từ file (có cache theo mtime)"""
cls.ensure_data_dir()
if not os.path.exists(SUBGENRES_FILE):
default_sub_genres = cls.get_default_sub_genres()
cls.save_sub_genres(default_sub_genres)
return default_sub_genres
try:
current_mtime = os.path.getmtime(SUBGENRES_FILE)
if cls._cached_sub_genres is not None and cls._cached_mtime == current_mtime:
return cls._cached_sub_genres
with open(SUBGENRES_FILE, 'r', encoding='utf-8') as f:
sub_genres = json.load(f)
cls._cached_sub_genres = sub_genres
cls._cached_mtime = current_mtime
return sub_genres
except Exception as e:
logger.error(f"Error loading sub genres: {e}")
return cls.get_default_sub_genres()
@classmethod
def save_sub_genres(cls, sub_genres: List[Dict[str, str]]) -> bool:
"""Lưu danh sách chủ đề con xuống file"""
cls.ensure_data_dir()
try:
with open(SUBGENRES_FILE, 'w', encoding='utf-8') as f:
json.dump(sub_genres, f, ensure_ascii=False, indent=4)
# Invalidate cache
cls._cached_sub_genres = sub_genres
cls._cached_mtime = os.path.getmtime(SUBGENRES_FILE)
return True
except Exception as e:
logger.error(f"Error saving sub genres: {e}")
return False
@classmethod
def add_sub_genre(cls, name: str, description: str = "") -> bool:
"""Thêm một chủ đề con mới"""
sub_genres = cls.load_sub_genres()
# Kiểm tra trùng tên
if any(g["name"] == name for g in sub_genres):
return False
sub_genres.append({"name": name, "description": description})
return cls.save_sub_genres(sub_genres)
@classmethod
def update_sub_genre(cls, old_name: str, new_name: str, description: str) -> bool:
"""Cập nhật thông tin chủ đề con"""
sub_genres = cls.load_sub_genres()
for i, g in enumerate(sub_genres):
if g["name"] == old_name:
# Nếu đổi tên, kiểm tra trùng tên mới
if old_name != new_name and any(x["name"] == new_name for x in sub_genres):
return False
sub_genres[i] = {"name": new_name, "description": description}
return cls.save_sub_genres(sub_genres)
return False
@classmethod
def delete_sub_genre(cls, name: str) -> bool:
"""Xóa chủ đề con"""
sub_genres = cls.load_sub_genres()
initial_length = len(sub_genres)
sub_genres = [g for g in sub_genres if g["name"] != name]
if len(sub_genres) < initial_length:
return cls.save_sub_genres(sub_genres)
return False
@classmethod
def get_sub_genre_names(cls) -> List[str]:
"""Lấy danh sách tên các chủ đề con để hiển thị UI"""
sub_genres = cls.load_sub_genres()
return [g["name"] for g in sub_genres]
@classmethod
def get_sub_genre_description(cls, name: str) -> str:
"""Lấy mô tả hướng dẫn của một chủ đề con"""
sub_genres = cls.load_sub_genres()
for g in sub_genres:
if g["name"] == name:
return g["description"]
return ""
+23
View File
@@ -0,0 +1,23 @@
import re
import json
content = """Sure, here are some suggestions:
```json
{
"suggestions": [
{"title": "A", "description": "B"}
]
}
```
Good luck!
"""
match = re.search(r'(\{[\s\S]*"suggestions"[\s\S]*\})', content)
if match:
try:
data = json.loads(match.group(1))
print("SUCCESS:", data)
except Exception as e:
print("FAIL LOAD:", e)
else:
print("NO MATCH")