240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
|
|
"""
|
|||
|
|
PPTist AI 后端服务
|
|||
|
|
接收 PPTist 的 AIPPT 请求,调用 deepseek-v3 生成大纲和PPT内容
|
|||
|
|
端口: 18086
|
|||
|
|
"""
|
|||
|
|
from fastapi import FastAPI, Request
|
|||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|||
|
|
from fastapi.responses import StreamingResponse
|
|||
|
|
import httpx
|
|||
|
|
import json
|
|||
|
|
|
|||
|
|
app = FastAPI()
|
|||
|
|
|
|||
|
|
app.add_middleware(
|
|||
|
|
CORSMiddleware,
|
|||
|
|
allow_origins=["*"],
|
|||
|
|
allow_methods=["*"],
|
|||
|
|
allow_headers=["*"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
LLM_API = "http://10.102.24.75:3000/v1/chat/completions"
|
|||
|
|
LLM_KEY = "sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb"
|
|||
|
|
LLM_MODEL = "deepseek-v3"
|
|||
|
|
|
|||
|
|
OUTLINE_SYSTEM_PROMPT = """你是一个专业的PPT大纲生成助手。用户会给你一个主题,你需要生成一份内容丰富、结构清晰的PPT大纲。
|
|||
|
|
|
|||
|
|
要求:
|
|||
|
|
1. 使用Markdown格式,用 # 表示演示文稿标题,## 表示章节标题,### 表示该章节下的内容页标题
|
|||
|
|
2. 必须包含6个主要章节(##)
|
|||
|
|
3. 每个章节下必须有2-3个内容页(###)
|
|||
|
|
4. 每个内容页标题要具体、有信息量,不要太笼统
|
|||
|
|
5. 内容要专业、逻辑清晰、层层递进
|
|||
|
|
6. 直接输出Markdown内容,不要有多余的解释
|
|||
|
|
|
|||
|
|
示例格式:
|
|||
|
|
# 人工智能技术发展
|
|||
|
|
## 人工智能概述
|
|||
|
|
### 定义与发展历史
|
|||
|
|
### 核心技术与分支
|
|||
|
|
## 机器学习基础
|
|||
|
|
### 监督学习与无监督学习
|
|||
|
|
### 深度学习与神经网络
|
|||
|
|
### 常用算法对比
|
|||
|
|
..."""
|
|||
|
|
|
|||
|
|
AIPPT_SYSTEM_PROMPT = """你是一个专业的PPT内容生成助手。用户会给你一份PPT大纲,你需要根据大纲生成完整、丰富、详实的PPT内容。
|
|||
|
|
|
|||
|
|
你必须严格按照以下JSON数组格式输出,不要输出任何其他内容。
|
|||
|
|
|
|||
|
|
结构规则:
|
|||
|
|
1. 第一页:cover类型,包含演示标题和副标题描述
|
|||
|
|
2. 第二页:contents类型,列出所有章节名称
|
|||
|
|
3. 每个章节:先一个transition过渡页,然后每个###子内容对应一个content页
|
|||
|
|
4. 每个content页必须包含4个items,每个item有title和text
|
|||
|
|
5. text内容必须详实具体,每条20-40字,包含实质性的数据、观点或分析,不要空泛
|
|||
|
|
6. 最后一页:end类型
|
|||
|
|
|
|||
|
|
输出格式(JSON数组,每个元素是一页slide):
|
|||
|
|
[
|
|||
|
|
{"type": "cover", "data": {"title": "演示标题", "text": "一句话描述演示主题和价值"}},
|
|||
|
|
{"type": "contents", "data": {"items": ["章节1名称", "章节2名称", "章节3名称", "章节4名称", "章节5名称", "章节6名称"]}},
|
|||
|
|
{"type": "transition", "data": {"title": "章节标题", "text": "本章将介绍什么内容,一句话概述"}},
|
|||
|
|
{"type": "content", "data": {"title": "内容页标题", "items": [
|
|||
|
|
{"title": "要点1标题", "text": "要点1的详细说明,包含具体信息、数据或分析"},
|
|||
|
|
{"title": "要点2标题", "text": "要点2的详细说明"},
|
|||
|
|
{"title": "要点3标题", "text": "要点3的详细说明"},
|
|||
|
|
{"title": "要点4标题", "text": "要点4的详细说明"}
|
|||
|
|
]}},
|
|||
|
|
{"type": "end"}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
重要:
|
|||
|
|
- 生成的总页数应在20-25页之间
|
|||
|
|
- 只输出JSON数组,不要markdown代码块,不要任何解释
|
|||
|
|
- 确保JSON格式完全正确,可被直接解析"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def stream_llm(messages, temperature=0.7, max_tokens=4096):
|
|||
|
|
"""流式调用LLM并逐块返回文本"""
|
|||
|
|
async with httpx.AsyncClient(timeout=120) as client:
|
|||
|
|
async with client.stream(
|
|||
|
|
"POST",
|
|||
|
|
LLM_API,
|
|||
|
|
json={
|
|||
|
|
"model": LLM_MODEL,
|
|||
|
|
"messages": messages,
|
|||
|
|
"temperature": temperature,
|
|||
|
|
"max_tokens": max_tokens,
|
|||
|
|
"stream": True,
|
|||
|
|
},
|
|||
|
|
headers={"Authorization": f"Bearer {LLM_KEY}"},
|
|||
|
|
) as resp:
|
|||
|
|
async for line in resp.aiter_lines():
|
|||
|
|
if line.startswith("data: "):
|
|||
|
|
data = line[6:]
|
|||
|
|
if data.strip() == "[DONE]":
|
|||
|
|
break
|
|||
|
|
try:
|
|||
|
|
chunk = json.loads(data)
|
|||
|
|
delta = chunk["choices"][0].get("delta", {})
|
|||
|
|
content = delta.get("content", "")
|
|||
|
|
if content:
|
|||
|
|
yield content
|
|||
|
|
except (json.JSONDecodeError, KeyError, IndexError):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.post("/tools/aippt_outline")
|
|||
|
|
async def aippt_outline(request: Request):
|
|||
|
|
data = await request.json()
|
|||
|
|
content = data.get("content", "")
|
|||
|
|
language = data.get("language", "中文")
|
|||
|
|
print(f"[AIPPT] Outline request: {content}, language: {language}")
|
|||
|
|
|
|||
|
|
user_prompt = f"请用{language}为以下主题生成PPT大纲:\n\n{content}"
|
|||
|
|
|
|||
|
|
async def generate():
|
|||
|
|
async for text in stream_llm(
|
|||
|
|
[
|
|||
|
|
{"role": "system", "content": OUTLINE_SYSTEM_PROMPT},
|
|||
|
|
{"role": "user", "content": user_prompt},
|
|||
|
|
],
|
|||
|
|
temperature=0.7,
|
|||
|
|
max_tokens=2048,
|
|||
|
|
):
|
|||
|
|
yield text
|
|||
|
|
|
|||
|
|
return StreamingResponse(generate(), media_type="text/event-stream")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.post("/tools/aippt")
|
|||
|
|
async def aippt(request: Request):
|
|||
|
|
data = await request.json()
|
|||
|
|
content = data.get("content", "")
|
|||
|
|
language = data.get("language", "中文")
|
|||
|
|
style = data.get("style", "通用")
|
|||
|
|
print(f"[AIPPT] Generate request, style: {style}, language: {language}")
|
|||
|
|
|
|||
|
|
user_prompt = f"请根据以下大纲生成PPT内容,风格为「{style}」,语言为{language}:\n\n{content}"
|
|||
|
|
|
|||
|
|
async def generate():
|
|||
|
|
"""流式收集LLM输出,按JSON对象级别缓冲,每检测到一个完整对象就立即发送"""
|
|||
|
|
buf = ""
|
|||
|
|
brace_depth = 0
|
|||
|
|
in_string = False
|
|||
|
|
escape_next = False
|
|||
|
|
found_array = False # 是否已经跳过了数组开头的 [
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
async for chunk in stream_llm(
|
|||
|
|
[
|
|||
|
|
{"role": "system", "content": AIPPT_SYSTEM_PROMPT},
|
|||
|
|
{"role": "user", "content": user_prompt},
|
|||
|
|
],
|
|||
|
|
temperature=0.7,
|
|||
|
|
max_tokens=8192,
|
|||
|
|
):
|
|||
|
|
for ch in chunk:
|
|||
|
|
# 跳过数组开头的 [ 和结尾的 ] 以及逗号分隔符
|
|||
|
|
if not found_array:
|
|||
|
|
if ch == '[':
|
|||
|
|
found_array = True
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if brace_depth == 0 and ch in (' ', '\n', '\r', '\t', ',', ']'):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
buf += ch
|
|||
|
|
|
|||
|
|
if escape_next:
|
|||
|
|
escape_next = False
|
|||
|
|
continue
|
|||
|
|
if ch == '\\' and in_string:
|
|||
|
|
escape_next = True
|
|||
|
|
continue
|
|||
|
|
if ch == '"' and not escape_next:
|
|||
|
|
in_string = not in_string
|
|||
|
|
continue
|
|||
|
|
if in_string:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if ch == '{':
|
|||
|
|
brace_depth += 1
|
|||
|
|
elif ch == '}':
|
|||
|
|
brace_depth -= 1
|
|||
|
|
if brace_depth == 0:
|
|||
|
|
# 得到一个完整的JSON对象
|
|||
|
|
try:
|
|||
|
|
obj = json.loads(buf)
|
|||
|
|
yield json.dumps(obj, ensure_ascii=False) + "\n"
|
|||
|
|
print(f"[AIPPT] Sent slide: {obj.get('type', '?')}")
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"[AIPPT] JSON parse error: {e}, buf: {buf[:100]}")
|
|||
|
|
buf = ""
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[AIPPT] Error: {e}")
|
|||
|
|
yield json.dumps({"type": "cover", "data": {"title": "生成失败", "text": str(e)[:100]}}) + "\n"
|
|||
|
|
yield json.dumps({"type": "end"}) + "\n"
|
|||
|
|
|
|||
|
|
return StreamingResponse(generate(), media_type="text/event-stream")
|
|||
|
|
|
|||
|
|
|
|||
|
|
WRITING_COMMANDS = {
|
|||
|
|
"rewrite": "请改写以下文本,保持原意但使用不同的表达方式:",
|
|||
|
|
"expand": "请扩写以下文本,补充更多细节和内容:",
|
|||
|
|
"abbreviate": "请缩写以下文本,保留核心要点,使其更简洁:",
|
|||
|
|
"polish": "请润色以下文本,使其更流畅、专业:",
|
|||
|
|
"translate": "请将以下文本翻译为英文:",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.post("/tools/ai_writing")
|
|||
|
|
async def ai_writing(request: Request):
|
|||
|
|
data = await request.json()
|
|||
|
|
content = data.get("content", "")
|
|||
|
|
command = data.get("command", "rewrite")
|
|||
|
|
print(f"[AIPPT] Writing request, command: {command}")
|
|||
|
|
|
|||
|
|
instruction = WRITING_COMMANDS.get(command, WRITING_COMMANDS["rewrite"])
|
|||
|
|
user_prompt = f"{instruction}\n\n{content}"
|
|||
|
|
|
|||
|
|
async def generate():
|
|||
|
|
async for text in stream_llm(
|
|||
|
|
[
|
|||
|
|
{"role": "system", "content": "你是一个专业的文字助手,帮助用户改写、扩写、缩写、润色或翻译文本。直接输出结果,不要有额外解释。"},
|
|||
|
|
{"role": "user", "content": user_prompt},
|
|||
|
|
],
|
|||
|
|
temperature=0.7,
|
|||
|
|
max_tokens=2048,
|
|||
|
|
):
|
|||
|
|
yield text
|
|||
|
|
|
|||
|
|
return StreamingResponse(generate(), media_type="text/event-stream")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
import uvicorn
|
|||
|
|
uvicorn.run(app, host="0.0.0.0", port=18086)
|