File size: 8,088 Bytes
b88427a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
from flask import Flask, request, jsonify, render_template, Response
import os
from dotenv import load_dotenv
import json
import requests
from PIL import Image
import base64
from io import BytesIO
import logging
import time
# 加载环境变量
load_dotenv()
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_headers():
"""创建API请求头"""
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"
}
def image_to_base64(image):
"""将PIL图像转换为base64"""
buffered = BytesIO()
image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode()
def process_image_with_vision(image_base64):
"""使用Vision API处理图像"""
url = f"{os.getenv('OPENAI_API_BASE')}/v1/chat/completions"
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": """请仔细分析这张图片中的题目,要求:
1. 识别内容类型:
- 判断是否为数学题、物理题、化学题等
- 识别是选择题、填空题还是解答题
- 判断是否包含图表或特殊符号
2. 内容提取:
- 提取所有文字内容,包括题干、选项(如果有)
- 识别所有数学公式、化学方程式或特殊符号
- 保留原有的排版格式(如换行、缩进等)
3. 公式处理:
- 将所有数学公式转换为LaTeX格式
- 使用[formula_n]作为占位符,其中n为公式编号
- 保持公式的完整性和准确性
请使用以下JSON格式返回:
{
"type": "题目类型(如:数学/物理/化学)",
"format": "题目格式(如:选择题/填空题/解答题)",
"text": "包含[formula_1], [formula_2]等占位符的完整文本",
"formulas": ["latex公式1", "latex公式2"],
"options": ["A. xxx", "B. xxx"] // 如果是选择题则包含此字段
"notes": ["可能存在的问题说明1", "问题说明2"]
}"""
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_base64}"
}
}
]
}
]
payload = {
"model": os.getenv('OPENAI_VISION_MODEL'),
"messages": messages,
"max_tokens": 1500,
"temperature": 0.2
}
try:
response = requests.post(url, headers=create_headers(), json=payload)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content']
try:
return json.loads(content)
except json.JSONDecodeError:
return {
"type": "unknown",
"format": "unknown",
"text": content,
"formulas": [],
"notes": ["无法解析为JSON格式"]
}
except Exception as e:
logger.error(f"Vision API调用错误: {str(e)}")
return {"error": str(e)}
def handle_sse_response(raw_data):
"""处理SSE响应数据"""
if raw_data:
try:
data = json.loads(raw_data)
if len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
content = delta.get('content', '')
print(f"Stream content: {content}", flush=True) # 直接打印流式内容
return content
except json.JSONDecodeError:
pass
return ''
def stream_solve(problem_text, formulas):
url = f"{os.getenv('OPENAI_API_BASE')}/v1/chat/completions"
full_problem = problem_text
for i, formula in enumerate(formulas, 1):
full_problem = full_problem.replace(f"[formula_{i}]", f"$${formula}$$")
print(f"\n问题内容: {full_problem}")
messages = [
{
"role": "system",
"content": """你是一位经验丰富的教师。请按照以下格式回答问题:
[开始解答]
这是答案:(写出明确的答案,如"答案为:2.5米","答案选 A"等)
[解答结束]
[开始解析]
这是解析:(写出详细的解析过程)
[解析结束]"""
},
{
"role": "user",
"content": full_problem
}
]
payload = {
"model": os.getenv('OPENAI_CHAT_MODEL'),
"messages": messages,
"stream": True,
"temperature": 0.3
}
try:
response = requests.post(
url,
headers=create_headers(),
json=payload,
stream=True
)
response.raise_for_status()
complete_response = ""
for line in response.iter_lines():
if not line or not line.startswith(b'data: '):
continue
try:
data = json.loads(line[6:].decode('utf-8'))
content = data['choices'][0].get('delta', {}).get('content', '')
complete_response += content
except json.JSONDecodeError:
continue
except Exception as e:
print(f"\n错误: {str(e)}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return
# 提取答案和解析
try:
if '[开始解答]' in complete_response and '[开始解析]' in complete_response:
parts = complete_response.split('[开始解答]')
answer_part = parts[1].split('[解答结束]')[0].strip()
parts = complete_response.split('[开始解析]')
analysis_part = parts[1].split('[解析结束]')[0].strip()
# 控制台打印
print("\n完整回答:")
print(f"答案: {answer_part}")
print(f"解析: {analysis_part}")
# 返回结果给前端
yield f"data: {json.dumps({'type': 'answer', 'content': answer_part})}\n\n"
yield f"data: {json.dumps({'type': 'analysis', 'content': analysis_part})}\n\n"
else:
print("\n响应格式不符合预期")
yield f"data: {json.dumps({'error': '响应格式不符合预期'})}\n\n"
except Exception as e:
print(f"\n解析错误: {str(e)}")
yield f"data: {json.dumps({'error': f'解析响应时出错: {str(e)}'})}\n\n"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/process', methods=['POST'])
def process():
if 'file' not in request.files:
return jsonify({'error': '没有文件上传'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
try:
image = Image.open(file)
image_base64 = image_to_base64(image)
result = process_image_with_vision(image_base64)
if 'error' in result:
return jsonify({'error': result['error']}), 500
return jsonify({
'original_image': image_base64,
'result': result
})
except Exception as e:
logger.error(f"处理图像错误: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/solve', methods=['POST'])
def solve():
data = request.json
if not data or 'text' not in data or 'formulas' not in data:
return jsonify({'error': '无效的请求数据'}), 400
return Response(
stream_solve(data['text'], data['formulas']),
content_type='text/event-stream'
)
if __name__ == '__main__':
app.run(debug=True) |