File size: 19,934 Bytes
23e74d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# # ==============================================================================
# # 完整 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)