Files
joke/optimizer/optimizer.py
T
bwstudio db8cea02f8 feat: 提示词和 AI 参数移至数据库管理
- AiSetting 模型新增 6 个提示词字段 + 3 个阶段温度字段
- main.py 迁移逻辑改为检测 ai_settings 表的新字段并填充默认值
- generate.py 从数据库读取生成提示词,去掉硬编码
- optimizer 从 API 读取各阶段提示词和温度,删除 prompts.py
- crawler 从 API 读取提取/改写提示词和温度,删除 prompts.py
- settings/active 端点去掉 token 认证(供爬虫/优化器使用)
- 后台设置页新增提示词编辑区和温度调节控件
- 新增 _ensure_default_settings 自动创建默认配置
2026-06-13 17:27:09 +08:00

438 lines
16 KiB
Python

"""笑话优化核心逻辑:质量检测 → AI 润色 → 评价分类。"""
import hashlib
import json
import sys
import time
import httpx
from openai import OpenAI
class Optimizer:
def __init__(self, api_base: str, username: str, password: str):
self.api_base = api_base.rstrip("/")
self.username = username
self.password = password
self.token = None
self.ai_client = None
self.model_name = ""
self.ai_config = None # 完整 AI 配置(含提示词和温度)
self.types = []
self.crowds = []
# 统计
self.stats = {"checked": 0, "rejected": 0, "polished": 0, "evaluated": 0, "skipped": 0}
# === API 认证 ===
def _login(self) -> str:
resp = httpx.post(
f"{self.api_base}/api/auth/login",
json={"username": self.username, "password": self.password},
timeout=30,
)
resp.raise_for_status()
return resp.json()["access_token"]
def _get(self, path: str) -> dict | list:
resp = httpx.get(
f"{self.api_base}{path}",
headers={"Authorization": f"Bearer {self.token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()
def _put(self, path: str, data: dict) -> dict:
resp = httpx.put(
f"{self.api_base}{path}",
json=data,
headers={"Authorization": f"Bearer {self.token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()
# === 初始化 ===
def setup(self):
print("[*] 正在登录...")
self.token = self._login()
print("[*] 登录成功")
ai_config = self._get("/api/admin/settings/active")
self.ai_config = ai_config
self.ai_client = OpenAI(
base_url=ai_config["api_base"],
api_key=ai_config["api_key"],
)
self.model_name = ai_config["model_name"]
print(f"[*] AI 模型: {self.model_name}")
self.types = self._get("/api/categories/types")
self.crowds = self._get("/api/categories/crowds")
print(f"[*] 分类: {len(self.types)} 种类型, {len(self.crowds)} 种人群")
def _get_prompt(self, key: str, default: str) -> str:
"""从数据库配置中读取提示词,没有则返回默认"""
if self.ai_config:
val = self.ai_config.get(key)
if val and val.strip():
return val
return default
def _get_temp(self, key: str, default: float) -> float:
"""从数据库配置中读取温度"""
if self.ai_config:
val = self.ai_config.get(key)
if val is not None:
return float(val)
return default
# === 读取笑话 ===
def get_jokes(self, status: str | None = None, limit: int | None = None,
ids: list[int] | None = None) -> list[dict]:
"""从 API 分页读取笑话"""
if ids:
jokes = []
for jid in ids:
try:
j = self._get(f"/api/admin/jokes/{jid}")
jokes.append(j)
except Exception as e:
print(f" [!] 获取笑话 #{jid} 失败: {e}")
return jokes
page = 1
page_size = 100
all_jokes = []
while True:
try:
path = f"/api/admin/jokes?page={page}&page_size={page_size}"
if status:
path += f"&status={status}"
data = self._get(path)
items = data.get("items", [])
if not items:
break
all_jokes.extend(items)
print(f" [*] 已读取 {len(all_jokes)} 条...")
if limit and len(all_jokes) >= limit:
all_jokes = all_jokes[:limit]
break
page += 1
except Exception as e:
print(f" [!] 分页读取失败 (page={page}): {e}")
break
return all_jokes
# === Stage 1: 质量检测 ===
def quality_check(self, content: str) -> dict:
"""判断笑话是否有笑点,返回 {"has_punchline": bool, "reason": str}"""
system_prompt = self._get_prompt("optimizer_quality_prompt",
"""你是一个幽默内容审核专家。判断以下内容是否是一个合格的笑话/段子。
合格标准(满足任一即可):
1. 有明确的笑点或反转(punchline)
2. 有幽默的语言表达或双关
3. 有意外结局或情理之中意料之外
不合格标准(符合任一即判定不合格):
1. 纯粹的事实陈述,没有任何幽默元素
2. 只是对话片段,没有笑点
3. 普通故事或叙事,没有幽默设计
4. 说教或道理阐述
5. 内容不完整或难以理解
始终返回 JSON 格式:{"has_punchline": true/false, "reason": "简要说明判断理由"}""")
temperature = self._get_temp("optimizer_quality_temperature", 0.3)
def _call():
return self.ai_client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请判断以下内容是否为合格笑话:\n\n{content[:2000]}\n\n返回 JSON 格式。"},
],
temperature=temperature,
max_tokens=200,
)
resp = self._safe_api_call(_call)
raw = resp.choices[0].message.content.strip()
return self._parse_json(raw, {"has_punchline": True, "reason": ""})
# === Stage 2: AI 润色 ===
def polish(self, content: str) -> str:
"""润色笑话内容"""
system_prompt = self._get_prompt("optimizer_polish_prompt",
"""你是一个专业的幽默文案编辑。请润色以下笑话,要求:
1. 保持核心笑点不变
2. 优化语言表达,使其更通顺、更精炼
3. 增强节奏感和幽默效果,但不改变原意
4. 字数控制在原内容的 80%-120%
5. 不要添加额外解释或评论
6. 直接输出润色后的内容,不要加任何前缀""")
temperature = self._get_temp("optimizer_polish_temperature", 0.8)
def _call():
return self.ai_client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请润色以下笑话:\n\n{content}"},
],
temperature=temperature,
max_tokens=1024,
)
resp = self._safe_api_call(_call)
return resp.choices[0].message.content.strip()
# === Stage 3: 评价分类 ===
def evaluate(self, content: str) -> dict:
"""评价并分类,返回 {"types": [...], "crowds": [...], "score": int, "comment": str}"""
type_names = [t.get("name", "") for t in self.types]
crowd_names = [c.get("name", "") for c in self.crowds]
system_prompt = self._get_prompt("optimizer_evaluate_prompt",
"""你是一个笑话分类和评价专家。对给定的笑话进行分析,返回 JSON 格式的分类和评分结果。
要求:
1. types: 从提供的类型列表中选择所有匹配的类型名称(数组,可以选多个)
2. crowds: 从提供的人群列表中选择所有匹配的人群名称(数组,可以选多个)
3. score: 1-10 分,基于幽默程度、创意和表达效果
4. comment: 简短评语(10字以内)
始终返回 JSON 格式。""")
temperature = self._get_temp("optimizer_evaluate_temperature", 0.3)
user_content = f"""笑话内容:
{content[:2000]}
可选类型:{', '.join(type_names)}
可选人群:{', '.join(crowd_names)}
返回 JSON 格式:{{"types": ["类型1", "类型2"], "crowds": ["人群1", "人群2"], "score": 8, "comment": "简短评语"}}"""
def _call():
return self.ai_client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
temperature=temperature,
max_tokens=300,
)
resp = self._safe_api_call(_call)
raw = resp.choices[0].message.content.strip()
result = self._parse_json(raw, {"types": [], "crowds": [], "score": 5, "comment": ""})
# Backward compatibility
if isinstance(result.get("types"), str):
result["types"] = [result["types"]] if result["types"] else []
if isinstance(result.get("crowds"), str):
result["crowds"] = [result["crowds"]] if result["crowds"] else []
return result
# === 辅助方法 ===
def _safe_api_call(self, func, *args, max_retries: int = 3, **kwargs):
"""带重试的 API 调用,自动处理限流"""
import time as time_module
last_error = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
error_str = str(e)
# 检查是否是限流错误
if "429" in error_str or "rate" in error_str.lower():
wait_time = (attempt + 1) * 30 # 30, 60, 90 秒
print(f" [!] API 限流,等待 {wait_time} 秒后重试 ({attempt+1}/{max_retries})...")
time_module.sleep(wait_time)
else:
# 其他错误,直接重试一次
if attempt < max_retries - 1:
time_module.sleep(5)
raise last_error
def _parse_json(self, raw: str, default: dict) -> dict:
"""安全解析 LLM 返回的 JSON"""
try:
data = json.loads(raw)
if isinstance(data, dict):
return data
return default
except json.JSONDecodeError:
if "```json" in raw:
raw = raw.split("```json")[1].split("```")[0]
elif "```" in raw:
raw = raw.split("```")[1].split("```")[0]
try:
return json.loads(raw.strip())
except Exception:
return default
def _get_type_id(self, type_name: str) -> int | None:
for t in self.types:
if t.get("name") == type_name:
return t.get("id")
return None
def _get_crowd_id(self, crowd_name: str) -> int | None:
for c in self.crowds:
if c.get("name") == crowd_name:
return c.get("id")
return None
# === 单条笑话处理(3 阶段) ===
def process_joke(self, joke: dict) -> bool:
"""处理单条笑话:质量检测 → 润色 → 评价分类,返回是否成功"""
joke_id = joke.get("id")
title = joke.get("title", "")
content = joke.get("content", "")
if not content:
print(f" [!] #{joke_id} 内容为空,跳过")
self.stats["skipped"] += 1
return False
print(f"\n {'='*40}")
print(f" 处理 #{joke_id}: {title[:30]}")
print(f" {'='*40}")
# Stage 1: 质量检测
print(f" [1/3] 质量检测...")
try:
check = self.quality_check(content)
if not check.get("has_punchline", True):
reason = check.get("reason", "无笑点")
print(f" [!] 无笑点: {reason}")
# 标记为 rejected
self._put(f"/api/admin/jokes/{joke_id}", {"status": "rejected"})
self.stats["rejected"] += 1
self.stats["checked"] += 1
return True # 处理完成(已拒绝)
print(f" [OK] 有笑点: {check.get('reason', '')}")
except Exception as e:
print(f" [!] 质量检测失败: {e},跳过本条")
self.stats["skipped"] += 1
return False
self.stats["checked"] += 1
# Stage 2: AI 润色
print(f" [2/3] AI 润色...")
try:
polished = self.polish(content)
if polished and polished != content:
print(f" [OK] 润色完成 ({len(content)} -> {len(polished)} 字)")
else:
print(f" [*] 润色后无变化")
except Exception as e:
print(f" [!] 润色失败: {e}")
polished = content # 润色失败时使用原文
# Stage 3: 评价分类
print(f" [3/3] 评价分类...")
try:
eval_result = self.evaluate(polished)
type_names = eval_result.get("types", [])
crowd_names = eval_result.get("crowds", [])
score = eval_result.get("score", 5)
print(f" [OK] 类型={type_names}, 人群={crowd_names}, 评分={score}/10")
except Exception as e:
print(f" [!] 评价分类失败: {e}")
type_names = []
crowd_names = []
score = None
# 根据评分计算 AI 等级
ai_level = None
if score is not None:
if score >= 8:
ai_level = "excellent"
elif score >= 6:
ai_level = "good"
elif score >= 4:
ai_level = "ordinary"
else:
ai_level = "poor"
# 保存更新
try:
update = {
"polished_content": polished,
"status": "approved" if (score or 5) >= 4 else "pending",
}
if score is not None:
update["ai_score"] = float(score)
if ai_level:
update["ai_level"] = ai_level
if type_names:
update["type_ids"] = []
for n in type_names:
tid = self._get_type_id(n)
if tid:
update["type_ids"].append(tid)
if not update["type_ids"]:
del update["type_ids"]
if crowd_names:
update["crowd_ids"] = []
for n in crowd_names:
cid = self._get_crowd_id(n)
if cid:
update["crowd_ids"].append(cid)
if not update["crowd_ids"]:
del update["crowd_ids"]
self._put(f"/api/admin/jokes/{joke_id}", update)
self.stats["polished"] += 1
self.stats["evaluated"] += 1
print(f" [OK] 更新成功")
return True
except Exception as e:
print(f" [!] 更新失败: {e}")
return False
# === 主循环 ===
def run(self, status: str | None = None, limit: int | None = None,
ids: list[int] | None = None):
"""主入口:读取笑话并逐个处理"""
print(f"\n>> 笑话优化模式启动")
print(f" 筛选状态: {status or '全部'}")
if limit:
print(f" 处理数量: {limit}")
if ids:
print(f" 指定 ID: {ids}")
self.setup()
jokes = self.get_jokes(status, limit, ids)
print(f"\n[*] 共读取 {len(jokes)} 条笑话,开始处理")
for idx, joke in enumerate(jokes):
print(f"\n --- 进度 {idx+1}/{len(jokes)} ---")
try:
self.process_joke(joke)
except KeyboardInterrupt:
print("\n用户中断")
break
except Exception as e:
print(f" [!] 处理异常: {e}")
self.stats["skipped"] += 1
# 每条间稍等,避免 API 限流(每次请求间隔 2-3 秒)
if idx < len(jokes) - 1:
import time
time.sleep(2)
# 输出统计
print(f"\n{'='*40}")
print(f" 处理完成")
print(f" {'='*40}")
print(f" 检查: {self.stats['checked']} 条")
print(f" 拒绝(无笑点): {self.stats['rejected']} 条")
print(f" 润色: {self.stats['polished']} 条")
print(f" 评价分类: {self.stats['evaluated']} 条")
print(f" 跳过(失败): {self.stats['skipped']} 条")
print(f"{'='*40}")