hxm83's picture
Upload 3 files
23e74d8 verified
# # ==============================================================================
# # 完整 main.py - 【V6.5 灵魂注入最终版】
# # ==============================================================================
# # --- Python标准库 ---
# import os
# import io
# import base64
# import re
# import uuid
# import contextlib
# # --- 第三方库 ---
# from fastapi import FastAPI, File, UploadFile, HTTPException
# from fastapi.staticfiles import StaticFiles
# from fastapi.middleware.cors import CORSMiddleware
# from PIL import Image
# from pix2text import Pix2Text
# from dotenv import load_dotenv
# # --- AI库 (根据官方推荐的兼容模式) ---
# from openai import OpenAI # 用于调用Kimi
# import dashscope # 通义千问官方SDK
# # --- 1. 初始化 ---
# load_dotenv()
# app = FastAPI()
# print("正在初始化 Pix2Text...")
# p2t = Pix2Text()
# print("Pix2Text 初始化完成。")
# print("正在初始化Kimi客户端 (OpenAI兼容模式)...")
# try:
# kimi_client = OpenAI(
# api_key=os.getenv("MOONSHOT_API_KEY"),
# base_url="https://api.moonshot.cn/v1",
# )
# if not os.getenv("MOONSHOT_API_KEY"): raise ValueError("API Key not found in .env")
# print("Kimi客户端初始化成功。")
# except Exception as e:
# print(f"!!! 初始化Kimi客户端失败,请检查.env文件中的MOONSHOT_API_KEY: {e}")
# kimi_client = None
# print("正在配置通义千问API Key...")
# try:
# dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
# if not dashscope.api_key: raise ValueError("API Key not found in .env")
# print("通义千问API Key配置成功。")
# except Exception as e:
# print(f"!!! 配置通义千问API Key失败,请检查.env文件中的DASHSCOPE_API_KEY: {e}")
# # ==============================================================================
# # 完整 main.py - 第二部分: FastAPI配置
# # ==============================================================================
# # --- 2. 静态文件目录与CORS配置 ---
# GENERATED_IMAGES_DIR = "generated_images"
# os.makedirs(GENERATED_IMAGES_DIR, exist_ok=True)
# app.mount("/static", StaticFiles(directory=GENERATED_IMAGES_DIR), name="static")
# app.add_middleware(
# CORSMiddleware,
# allow_origins=["*"],
# allow_credentials=True,
# allow_methods=["*"],
# allow_headers=["*"],
# )
# @app.get("/")
# def read_root():
# return {"message": "AI解题后端服务正在运行 (V6.5 灵魂注入版)"}
# # ==============================================================================
# # 完整 main.py - 第三部分: 核心API接口
# # ==============================================================================
# @app.post("/solve")
# async def solve_from_image(file: UploadFile = File(...)):
# if not kimi_client or not dashscope.api_key:
# raise HTTPException(status_code=500, detail="核心AI服务客户端未成功初始化,请检查后端日志和API Keys。")
# temp_image_path = f"temp_{uuid.uuid4()}.png"
# try:
# image_bytes = await file.read()
# # --- A路: Pix2Text OCR ---
# print("\n--- [A路] 开始Pix2Text OCR识别 ---")
# question_text = p2t.recognize(Image.open(io.BytesIO(image_bytes)), return_text=True)
# print(f"--- [A路] 识别结果: {question_text[:100].strip()}...")
# # --- B路: 通义千问 qwen-vl-max 进行图形理解 ---
# print("\n--- [B路] 开始通义千问Vision图形理解 ---")
# vision_prompt = "请用简洁的语言描述这张图片中的几何图形信息(顶点、关系、已知条件等),忽略所有文字。"
# with open(temp_image_path, "wb") as f:
# f.write(image_bytes)
# messages = [{
# 'role': 'user',
# 'content': [
# {'text': vision_prompt},
# {'image': f'file://{os.path.abspath(temp_image_path)}'}
# ]
# }]
# vision_response = dashscope.MultiModalConversation.call(model='qwen-vl-max', messages=messages)
# if vision_response.status_code != 200:
# raise Exception(f"通义千问API调用失败: Code {vision_response.status_code}, Message: {vision_response.message}")
# raw_content_list = vision_response.output.choices[0].message.content
# geometry_description = ""
# if isinstance(raw_content_list, list):
# for part in raw_content_list:
# if part.get("text"):
# geometry_description = part["text"]
# break
# elif isinstance(raw_content_list, str):
# geometry_description = raw_content_list
# if not geometry_description:
# print("--- [B路] 通义千问未返回有效的文本描述。")
# else:
# print(f"--- [B路] 通义千问描述结果: {geometry_description[:100].strip()}...")
# # --- C路: 【灵魂注入版Prompt】 ---
# print("\n--- [C路] 开始信息融合并调用Kimi ---")
# final_prompt = f"""
# **背景情景**: 你是一位非常有经验和亲和力的数学老师,正在为一名有些困惑的学生进行一对一辅导。你的目标不仅是给出答案,更是要用循循诱导的方式,让学生彻底理解解题的思路和方法。
# **你的教学风格**:
# * **亲切自然**: 使用“好的,同学”、“我们一起来看”、“首先,我们要明确...”这样的口吻,就像在和学生面对面交流。
# * **聚焦思路**: 在给出具体计算前,先用一两句话点明这一步的“核心思路”或“关键公式”,让学生知道为什么这么做。
# * **自信从容**: 你是老师,已经对答案了然于胸。请直接展示正确、流畅的推导过程。**绝对不要**在回答中出现“我算错了”、“让我们重新检查”、“这里可能存在误解”等自我怀疑或暴露思考过程的语言。你只需要呈现最终的、完美的教学内容。
# * **详尽完整**: **绝对不能**以任何理由省略任何关键的证明或计算步骤。每一个问题都必须得到完整的解答。
# **输出格式**:
# * 使用标准的Markdown来组织段落和列表。
# * 所有数学变量、符号和公式,都必须严格使用标准的LaTeX语法包裹(行内公式用`$...$`,块级公式用`$$...$$`)。
# ---
# **【辅导材料】**
# [学生遇到的题目 - 文字与公式]:
# {question_text}
# [学生遇到的题目 - 图形信息]:
# {geometry_description}
# ---
# 好了,老师,这位同学正在期待你的讲解。请开始吧!
# """
# # 调用Kimi API
# solution_response = kimi_client.chat.completions.create(
# model="moonshot-v1-32k",
# messages=[
# {"role": "system", "content": "你是一位顶级的、富有同理心的数学家教,擅长将复杂问题讲得清晰易懂。"},
# {"role": "user", "content": final_prompt}
# ],
# temperature=0.3,
# max_tokens=8192
# )
# raw_solution_markdown = solution_response.choices[0].message.content
# # --- D路: 直接返回原始输出 ---
# print("\n--- [D路] AI返回原始答案,交由前端处理格式 ---")
# return {"solution": raw_solution_markdown}
# except Exception as e:
# print(f"!!! 发生严重错误 !!!")
# print(f"错误类型: {type(e).__name__}")
# print(f"错误详情: {e}")
# raise HTTPException(status_code=500, detail=f"处理时发生错误: {str(e)}")
# finally:
# # 无论成功或失败,都确保临时文件被删除
# if os.path.exists(temp_image_path):
# os.remove(temp_image_path)
# ==============================================================================
# 完整 main.py - 【V10.0 最终架构版,AI规划+计算机执行+AI总结】
# ==============================================================================
# --- Python标准库 ---
# --- 【核心修复】: 在所有import之前,设置环境变量禁用多进程 ---
import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
import io
import base64
import re
import uuid
import contextlib
# --- 第三方库 ---
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image
from pix2text import Pix2Text
from dotenv import load_dotenv
# --- AI库 ---
from openai import OpenAI # 用于调用Kimi
import dashscope # 通义千问官方SDK
# --- 数学计算库 ---
import sympy as sp
import numpy as np
# --- 1. 初始化 ---
load_dotenv()
app = FastAPI()
# print("正在初始化 Pix2Text...")
# # --- 【核心修复】: 强制指定使用CPU ---
# p2t = Pix2Text(device='cpu')
# print("Pix2Text 初始化完成,已强制使用CPU模式。")
# 【核心修复】: 只在全局定义一个空的p2t变量
p2t = None
# 【核心修复】: 创建一个函数来处理耗时的初始化
def initialize_pix2text():
global p2t
if p2t is None:
print("首次请求:正在初始化 Pix2Text (这可能需要一些时间)...")
# 强制指定缓存目录到/tmp,这是一个保证可写的临时目录
cache_dir = "/tmp/pix2text_cache"
os.makedirs(cache_dir, exist_ok=True)
p2t = Pix2Text(device='cpu', root=cache_dir)
print("Pix2Text 初始化完成。")
print("正在初始化Kimi客户端 (OpenAI兼容模式)...")
try:
kimi_client = OpenAI(
api_key=os.getenv("MOONSHOT_API_KEY"),
base_url="https://api.moonshot.cn/v1",
)
if not os.getenv("MOONSHOT_API_KEY"): raise ValueError("API Key not found in .env")
print("Kimi客户端初始化成功。")
except Exception as e:
print(f"!!! 初始化Kimi客户端失败,请检查.env文件中的MOONSHOT_API_KEY: {e}")
kimi_client = None
print("正在配置通义千问API Key...")
try:
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
if not dashscope.api_key: raise ValueError("API Key not found in .env")
print("通义千问API Key配置成功。")
except Exception as e:
print(f"!!! 配置通义千问API Key失败,请检查.env文件中的DASHSCOPE_API_KEY: {e}")
# --- 2. FastAPI应用配置 ---
GENERATED_IMAGES_DIR = "generated_images"
os.makedirs(GENERATED_IMAGES_DIR, exist_ok=True)
app.mount("/static", StaticFiles(directory=GENERATED_IMAGES_DIR), name="static")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def read_root():
return {"message": "AI解题后端服务正在运行 (V10.0 最终架构版)"}
# ==============================================================================
# 完整 main.py - 第二部分: 核心API接口
# ==============================================================================
@app.post("/solve")
async def solve_from_image(file: UploadFile = File(...)):
# 【核心修复】: 在处理请求的开始,调用初始化函数
try:
initialize_pix2text()
except Exception as e:
print(f"!!! Pix2Text 初始化失败: {e}")
raise HTTPException(status_code=500, detail=f"核心OCR服务初始化失败: {e}")
if not p2t or not kimi_client or not dashscope.api_key:
raise HTTPException(status_code=500, detail="核心AI服务未就绪")
temp_image_path = f"/tmp/temp_{uuid.uuid4()}.png" # 使用/tmp目录
try:
image_bytes = await file.read()
# --- A路 & B路: 提取题目信息 ---
print("\n--- [A&B路] 提取题目信息... ---")
question_text = p2t.recognize(Image.open(io.BytesIO(image_bytes)), return_text=True)
with open(temp_image_path, "wb") as f:
f.write(image_bytes)
vision_prompt = "请用简洁的语言描述这张图片中的几何图形信息(顶点、关系、已知条件等),忽略所有文字。"
messages = [{'role': 'user', 'content': [{'text': vision_prompt}, {'image': f'file://{os.path.abspath(temp_image_path)}'}]}]
vision_response = dashscope.MultiModalConversation.call(model='qwen-vl-max', messages=messages)
if vision_response.status_code != 200:
geometry_description = "视觉模型分析失败。"
print(f"!!! 通义千问API调用失败: {vision_response.message}")
else:
raw_content_list = vision_response.output.choices[0].message.content
geometry_description = ""
if isinstance(raw_content_list, list):
for part in raw_content_list:
if part.get("text"):
geometry_description = part["text"]
break
elif isinstance(raw_content_list, str):
geometry_description = raw_content_list
print(f"--- 识别到的文本: {question_text[:100].strip()}...")
print(f"--- 识别到的图形信息: {geometry_description[:100].strip()}...")
# --- 步骤 1: 【终极版】AI规划师Prompt ---
planner_prompt = f"""
**角色**: 你是一个顶级的Python程序员和数学专家,任务是为下面的数学题编写一个**单一、完整、自给自足**的Python脚本来解决它。
**脚本核心要求**:
1. **展示过程 (Show Your Work)**: 在进行任何求解或化简之前,**必须**先用 `print()` 和 `sp.pretty()` 打印出你将要操作的**原始符号方程式**。
2. **清晰输出**: **必须** 通过 `print()` 语句,分步输出所有关键的中间计算结果和最终答案。每个print输出前,请用一个简短的描述性文字说明。
3. **精确计算**: 使用 `sympy` 库进行所有代数运算。
4. **代码块**: 整个脚本必须被包裹在 ````python ... ``` ` 中。
**【一个完美的脚本示例】**:
```python
import sympy as sp
x = sp.Symbol('x')
# 首先,打印出要解的方程
print("需要求解的方程是:")
equation = sp.Eq(x**2 - 4, 0)
print(sp.pretty(equation))
# 然后,求解并打印结果
solutions = sp.solve(equation, x)
print("方程的解是:")
print(solutions)
```
**题目信息**:
[OCR文本]: {question_text}
[图形描述]: {geometry_description}
请开始编写一个能够展示完整思考过程的Python脚本。
"""
print("\n--- [步骤 1] 正在请求AI生成解题脚本... ---")
script_response = kimi_client.chat.completions.create(model="moonshot-v1-32k", messages=[{"role": "user", "content": planner_prompt}], temperature=0.0)
script_markdown = script_response.choices[0].message.content
# --- 步骤 2: 后端执行脚本并捕获结果 ---
print("\n--- [步骤 2] 正在执行AI脚本并捕获结果... ---")
pattern = re.compile(r"```python\n(.*?)\n```", re.DOTALL)
match = pattern.search(script_markdown)
if not match:
print("--- [步骤 2] AI未生成代码,将其回答作为最终讲解。")
return {"solution": script_markdown}
code_to_execute = match.group(1)
image_url = None
if 'plt.savefig' in code_to_execute:
image_filename = f"{uuid.uuid4()}.png"
image_path = os.path.join(GENERATED_IMAGES_DIR, image_filename)
code_to_execute = code_to_execute.replace("savefig('image.png')", f"savefig('{image_path}')")
image_url = f"/static/{image_filename}"
# 捕获脚本的所有print输出
stdout_capture = io.StringIO()
try:
with contextlib.redirect_stdout(stdout_capture):
# 为sympy的pretty print准备环境,并提供所有需要的库
execution_scope = {
"sp": sp,
"np": np,
"plt": plt,
"__builtins__": None,
}
exec("import sympy as sp; import numpy as np; import matplotlib.pyplot as plt", execution_scope)
# 执行AI生成的代码
exec(code_to_execute, execution_scope)
if image_url:
print(f"--- 图片生成成功: {image_url} ---")
except Exception as e:
print(f"!!! AI脚本执行失败: {e}")
stdout_capture.write(f"\n在执行解题代码时发生了错误: {e}")
calculation_results = stdout_capture.getvalue()
print(f"--- 捕获的计算结果 ---\n{calculation_results}")
# --- 步骤 3: 【终极版】AI讲解员Prompt ---
teacher_prompt = f"""
**背景情景**: 你是一位和蔼可亲、经验丰富的数学老师。
**核心任务**: 这里有一份由计算机程序生成的、**包含详细中间步骤**的计算日志。请你**只使用**这些材料,为学生写一篇流畅、自然、循序渐进的讲解。
**【不可违背的黄金法则】**:
1. **忠于过程**: 你必须将“计算结果日志”中提供的**所有中间方程式和推导步骤**,自然地融入到你的讲解中,向学生展示问题是如何一步步被解决的。
2. **严禁自己计算**: 你的所有结论都必须基于日志内容。
3. **严禁出现代码**: 你的回答中 **绝对不能** 出现任何Python代码。
4. **扮演好老师**: 解释每一步的“为什么”,而不仅仅是“是什么”。
**【可使用的材料】**
[原始题目]:
{question_text}
[计算机生成的、包含过程的计算结果日志]:
```
{calculation_results}
```
---
好了,老师,请用上面的详细材料,为学生带来一堂完美的解题课吧!
"""
print("\n--- [步骤 3] 正在请求AI生成最终讲解... ---")
final_solution_response = kimi_client.chat.completions.create(model="moonshot-v1-32k", messages=[
{"role": "system", "content": "你是一位顶级的数学家教,擅长将复杂的计算过程,转述为学生能听懂的、富有亲和力的讲解。"},
{"role": "user", "content": teacher_prompt}
], temperature=0.5, max_tokens=8192)
final_solution_markdown = final_solution_response.choices[0].message.content
return {"solution": final_solution_markdown}
except Exception as e:
print(f"!!! 发生严重错误 !!!")
print(f"错误类型: {type(e).__name__}")
print(f"错误详情: {e}")
raise HTTPException(status_code=500, detail=f"处理时发生错误: {str(e)}")
finally:
# 无论成功或失败,都确保临时文件被删除
if os.path.exists(temp_image_path):
os.remove(temp_image_path)