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

This commit is contained in:
tinix-ai
2026-03-17 14:54:16 +07:00
parent 6699c516cd
commit a40d8ef4a6
28 changed files with 1970 additions and 953 deletions
View File
+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 core.config import get_config, Backend
from locales.i18n import t
from core.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()
+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 (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."
}
]
+402
View File
@@ -0,0 +1,402 @@
[
{
"name": "Hậu cung",
"description": ""
},
{
"name": "1v1",
"description": ""
},
{
"name": "NP",
"description": ""
},
{
"name": "Nữ phẫn nam trang",
"description": ""
},
{
"name": "Nam phẫn nữ trang",
"description": ""
},
{
"name": "Xuyên không",
"description": ""
},
{
"name": "Xuyên sách",
"description": ""
},
{
"name": "Trọng sinh",
"description": ""
},
{
"name": "Hệ thống",
"description": ""
},
{
"name": "Bàn tay vàng",
"description": ""
},
{
"name": "Không gian tùy thân",
"description": ""
},
{
"name": "Linh tuyền",
"description": ""
},
{
"name": "Đọc tâm thuật",
"description": ""
},
{
"name": "Vô địch lưu",
"description": ""
},
{
"name": "Cẩu đạo",
"description": ""
},
{
"name": "Nhiệt huyết",
"description": ""
},
{
"name": "Hài hước",
"description": ""
},
{
"name": "Sảng văn",
"description": ""
},
{
"name": "Ngọt sủng",
"description": ""
},
{
"name": "Ngược luyến",
"description": ""
},
{
"name": "Gương vỡ lại lành",
"description": ""
},
{
"name": "Cưới trước yêu sau",
"description": ""
},
{
"name": "Oan gia ngõ hẹp",
"description": ""
},
{
"name": "Thanh mai trúc mã",
"description": ""
},
{
"name": "Hào môn thế gia",
"description": ""
},
{
"name": "Tổng tài",
"description": ""
},
{
"name": "Minh tinh",
"description": ""
},
{
"name": "Giới giải trí",
"description": ""
},
{
"name": "Vườn trường",
"description": ""
},
{
"name": "Học bá",
"description": ""
},
{
"name": "Võng du",
"description": ""
},
{
"name": "E-sports",
"description": ""
},
{
"name": "Livestream",
"description": ""
},
{
"name": "Mỹ thực",
"description": ""
},
{
"name": "Nông trại",
"description": ""
},
{
"name": "Điền văn",
"description": ""
},
{
"name": "Nuôi con",
"description": ""
},
{
"name": "Làm giàu",
"description": ""
},
{
"name": "Cung đấu",
"description": ""
},
{
"name": "Gia đấu",
"description": ""
},
{
"name": "Quyền mưu",
"description": ""
},
{
"name": "Nữ cường",
"description": ""
},
{
"name": "Nam cường",
"description": ""
},
{
"name": "Song khiết",
"description": ""
},
{
"name": "Phế Sài",
"description": ""
},
{
"name": "Thiên tài",
"description": ""
},
{
"name": "Từ hôn",
"description": ""
},
{
"name": "Linh khí khôi phục",
"description": ""
},
{
"name": "Mạt thế",
"description": ""
},
{
"name": "Cơ giáp",
"description": ""
},
{
"name": "Tinh tế",
"description": ""
},
{
"name": "ABO",
"description": ""
},
{
"name": "Người sói",
"description": ""
},
{
"name": "Ma cà rồng",
"description": ""
},
{
"name": "Tây huyễn",
"description": ""
},
{
"name": "Ma pháp",
"description": ""
},
{
"name": "Kiếm ma",
"description": ""
},
{
"name": "Pháp sư",
"description": ""
},
{
"name": "Giả heo ăn hổ",
"description": ""
},
{
"name": "Bi kịch",
"description": ""
},
{
"name": "Chữa lành",
"description": ""
},
{
"name": "Hắc ám",
"description": ""
},
{
"name": "Gothic",
"description": ""
},
{
"name": "Tâm lý",
"description": ""
},
{
"name": "Tâm thần phân liệt",
"description": ""
},
{
"name": "Đa nhân cách",
"description": ""
},
{
"name": "Trinh thám",
"description": ""
},
{
"name": "Phá án",
"description": ""
},
{
"name": "Đạo mộ",
"description": ""
},
{
"name": "Cương thi",
"description": ""
},
{
"name": "Quy tắc quái đàm",
"description": ""
},
{
"name": "Vô hạn lưu",
"description": ""
},
{
"name": "Mau xuyên",
"description": ""
},
{
"name": "Thực dân",
"description": ""
},
{
"name": "Chế tạo",
"description": ""
},
{
"name": "Lĩnh chúa",
"description": ""
},
{
"name": "Trồng trọt",
"description": ""
},
{
"name": "Câu cá",
"description": ""
},
{
"name": "Nuôi thú",
"description": ""
},
{
"name": "Pokemon",
"description": ""
},
{
"name": "Anime",
"description": ""
},
{
"name": "Harry Potter",
"description": ""
},
{
"name": "Marvel",
"description": ""
},
{
"name": "Biển sao",
"description": ""
},
{
"name": "Truyền thuyết đô thị",
"description": ""
},
{
"name": "Cổ đại",
"description": ""
},
{
"name": "Dân quốc",
"description": ""
},
{
"name": "Thập niên 70",
"description": ""
},
{
"name": "Thập niên 80",
"description": ""
},
{
"name": "Thập niên 90",
"description": ""
},
{
"name": "Dị thế",
"description": ""
},
{
"name": "Đại lục",
"description": ""
},
{
"name": "Bộ lạc",
"description": ""
},
{
"name": "Nguyên thủy",
"description": ""
},
{
"name": "Tu tiên",
"description": ""
},
{
"name": "Trả thù",
"description": ""
},
{
"name": "Pháo hôi",
"description": ""
},
{
"name": "Vai ác",
"description": ""
},
{
"name": "Tiên tri",
"description": ""
},
{
"name": "Phép thuật",
"description": ""
}
]
+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 ""
File diff suppressed because it is too large Load Diff
+339
View File
@@ -0,0 +1,339 @@
"""
Mô-đun Quản lý 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 services.novel_generator import NovelProject, Chapter
from locales.i18n import t
from core.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 core.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 kê 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 core.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()
def list_project_titles():
"""Lấy danh sách tiêu đề dự án"""
try:
projects = ProjectManager.list_projects()
return [p["title"] for p in projects]
except Exception:
return []
+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 ""