feat: AI 生成增强 - 风格/长度/评分/历史/复制
- 后端 prompt 支持风格选择(6种)和字数范围 - 后端 AI 输出改为 JSON 格式,含评分和推荐理由 - GenerateRequest/Response schema 新增 style/length/score/reason 字段 - 前端新增风格选择、字数范围控件 - 前端新增随机选题按钮 - 前端新增复制内容功能 - 前端新增生成历史记录(会话级 20 条) - 结果卡片展示 AI 评分和推荐理由
This commit is contained in:
+85
-15
@@ -1,7 +1,8 @@
|
||||
"""智能笑话生成器 API"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from openai import OpenAI
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -21,17 +22,43 @@ GENERATION_PROMPT = """你是一位幽默大师,专门创作轻松搞笑的短
|
||||
要求:
|
||||
1. 根据场景和关键词创作一条原创笑话
|
||||
2. 笑话要有反转或意外结局
|
||||
3. 语言简洁,30-150字
|
||||
4. 直接输出笑话内容,不需要解释
|
||||
3. 语言风格:{style}
|
||||
4. 字数要求:{length_requirement}
|
||||
5. 直接输出笑话内容,不需要解释
|
||||
|
||||
格式:
|
||||
标题:xxx
|
||||
内容:xxx
|
||||
"""
|
||||
请严格按照以下 JSON 格式输出,不要加任何额外说明:
|
||||
{{
|
||||
"title": "笑话标题",
|
||||
"content": "笑话正文",
|
||||
"score": 8,
|
||||
"reason": "这个笑话巧妙结合了场景和关键词,结尾有反转"
|
||||
}}
|
||||
|
||||
score 是 1-10 的整数评分,reason 是用一句话说明亮点。"""
|
||||
|
||||
STYLE_MAP = {
|
||||
"cold": "冷幽默 / 无厘头",
|
||||
"warm": "温馨幽默 / 暖心搞笑",
|
||||
"twist": "反转 / 神转折",
|
||||
"pun": "谐音梗 / 文字游戏",
|
||||
"sketch": "段子 / 吐槽调侃",
|
||||
"irony": "讽刺幽默 / 黑色幽默",
|
||||
}
|
||||
|
||||
LENGTH_MAP = {
|
||||
"short": "30-80字,非常简短",
|
||||
"medium": "80-150字,正常长度",
|
||||
"long": "150-300字,可以描述一个小场景",
|
||||
}
|
||||
|
||||
|
||||
def _build_prompt(scenarios: list[str], keywords: list[str]) -> str:
|
||||
"""构建 AI prompt"""
|
||||
def _build_prompt(
|
||||
scenarios: list[str],
|
||||
keywords: list[str],
|
||||
style: str = "twist",
|
||||
length: str = "medium",
|
||||
) -> str:
|
||||
"""构建 AI prompt,支持风格和长度控制"""
|
||||
parts = []
|
||||
if scenarios:
|
||||
parts.append(f"场景:{', '.join(scenarios)}")
|
||||
@@ -39,7 +66,11 @@ def _build_prompt(scenarios: list[str], keywords: list[str]) -> str:
|
||||
parts.append(f"关键词:{', '.join(keywords)}")
|
||||
if not parts:
|
||||
parts.append("场景:日常生活的各种趣事(不指定具体场景)")
|
||||
return GENERATION_PROMPT.format(context="\n".join(parts))
|
||||
return GENERATION_PROMPT.format(
|
||||
context="\n".join(parts),
|
||||
style=STYLE_MAP.get(style, "反转 / 神转折"),
|
||||
length_requirement=LENGTH_MAP.get(length, "80-150字,正常长度"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=GenerateResponse)
|
||||
@@ -47,11 +78,10 @@ def generate_joke(
|
||||
req: GenerateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""调用 AI 生成笑话"""
|
||||
"""调用 AI 生成笑话,支持风格和长度控制"""
|
||||
# 获取激活的 AI 配置
|
||||
setting = db.query(AiSetting).filter(AiSetting.is_active == True).first()
|
||||
if not setting:
|
||||
# 如果没有配置,尝试返回默认配置
|
||||
setting = db.query(AiSetting).first()
|
||||
|
||||
if not setting or not setting.api_key:
|
||||
@@ -60,22 +90,62 @@ def generate_joke(
|
||||
# 调用 AI
|
||||
try:
|
||||
client = OpenAI(base_url=setting.api_base, api_key=setting.api_key)
|
||||
prompt = _build_prompt(req.scenarios, req.keywords)
|
||||
prompt = _build_prompt(req.scenarios, req.keywords, req.style, req.length)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=setting.model_name,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=setting.temperature,
|
||||
temperature=req.temperature,
|
||||
max_tokens=setting.max_tokens,
|
||||
)
|
||||
|
||||
raw = response.choices[0].message.content
|
||||
return _parse_response(raw)
|
||||
return _parse_json_response(raw)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=500, detail="AI 返回格式异常,请重新生成")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
def _parse_json_response(raw: str | None) -> GenerateResponse:
|
||||
"""解析 AI 返回的 JSON 格式"""
|
||||
if not raw or not raw.strip():
|
||||
return GenerateResponse(title="生成的笑话", content="(内容生成失败,请重新生成)", score=0)
|
||||
|
||||
# 尝试从 markdown 代码块中提取 JSON
|
||||
raw = raw.strip()
|
||||
if raw.startswith("```"):
|
||||
lines = raw.split("\n")
|
||||
# 去掉第一行 ```json 和最后一行 ```
|
||||
if len(lines) >= 3:
|
||||
raw = "\n".join(lines[1:-1]).strip()
|
||||
|
||||
# 移除可能的尾部分号
|
||||
if raw.endswith(","):
|
||||
raw = raw[:-1]
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
# 如果 JSON 解析失败,尝试查找花括号内的内容
|
||||
try:
|
||||
start = raw.index("{")
|
||||
end = raw.rindex("}") + 1
|
||||
raw = raw[start:end]
|
||||
data = json.loads(raw)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
return _parse_response(raw)
|
||||
|
||||
return GenerateResponse(
|
||||
title=data.get("title", "生成的笑话"),
|
||||
content=data.get("content", ""),
|
||||
score=data.get("score", 0),
|
||||
reason=data.get("reason", ""),
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
def _parse_response(raw: str | None) -> GenerateResponse:
|
||||
"""解析 AI 返回内容,提取标题和内容"""
|
||||
# 防御:处理空或 None 输入
|
||||
|
||||
@@ -74,12 +74,18 @@ class GenerateRequest(BaseModel):
|
||||
"""笑话生成请求"""
|
||||
keywords: list[str] = []
|
||||
scenarios: list[str] = []
|
||||
style: str = "twist"
|
||||
length: str = "medium"
|
||||
temperature: float = 0.8
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"keywords": ["加班"],
|
||||
"scenarios": ["职场"]
|
||||
"scenarios": ["职场"],
|
||||
"style": "twist",
|
||||
"length": "medium",
|
||||
"temperature": 0.8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +94,8 @@ class GenerateResponse(BaseModel):
|
||||
"""笑话生成响应"""
|
||||
title: str
|
||||
content: str
|
||||
score: int = 0
|
||||
reason: str = ""
|
||||
created_at: datetime | None = None
|
||||
|
||||
class Config:
|
||||
|
||||
Reference in New Issue
Block a user