Files

1 line
13 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ai-fange/autoexam", "files": {"answer.py": {"ba48fa97ba483b80ae8e35925133c77a96d723d8": {"t": 1785674057.8641076, "n": 9772, "h": "c73650faa59d520d", "c": "import os\nimport re\nimport json\nimport datetime\nfrom openai import OpenAI\n\n# ===== 路径配置 =====\n\nROOT_DIR = r\"E:\\沙粒云\\自媒体\\2025视频制作\\20250610ai高考\\流程\\anwser\\data\"\nINPUT_FULL_TEXT = os.path.join(ROOT_DIR, \"output_full_exam.txt\")\nANWSER_DIR = os.path.join(ROOT_DIR, \"anwser\")\n# 虽然不再输出 JSON 格式单元,这个常量可以保留或忽略\nOUTPUT_UNIT_JSON = os.path.join(ANWSER_DIR, \"unit_split.json\")\nOUTPUT_ANSWERS = os.path.join(ANWSER_DIR, \"answer_list.json\")\nUNITS_FOLDER = os.path.join(ANWSER_DIR, \"units\")\nUNIT_ANSWERS_FOLDER = os.path.join(ANWSER_DIR, \"unit_answers\")\nLOG_FILE = os.path.join(ANWSER_DIR, \"b_pipeline_log.txt\")\n\n# ===== Qwen-3 接口配置 =====\n\nAPI_KEY = \"sk-c48712427ba84b59b9c89fc2883e48b6\"\nBASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\nQWEN_3_MODEL = \"qwen3-235b-a22b\"\n\n# 初始化 Qwen 客户端\ntry:\n qwen_client = OpenAI(api_key=API_KEY, base_url=BASE_URL)\nexcept Exception as e:\n print(f\"[ERROR] OpenAI 客户端初始化失败: {e}\")\n raise\n\n\n# ===== DeepSeek 接口配置 (用于答题) =====\n\nDEEPSEEK_API_KEY = \"sk-55bcb4efc5c4402d86c30c7eb8a3bdf5\" # <--- 请务必替换为您自己的 DeepSeek Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-coder\" # 或者使用 \"deepseek-reasoner\"\n# 初始化 DeepSeek 客户端\ntry:\n deepseek_client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\nexcept Exception as e:\n print(f\"[ERROR] DeepSeek 客户端初始化失败: {e}\")\n raise\n\n\ndef log(msg: str):\n \"\"\"\n 在控制台打印并写入日志文件 b_pipeline_log.txt\n \"\"\"\n timestamp = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n line = f\"[{timestamp}] {msg}\"\n print(line)\n os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)\n with open(LOG_FILE, \"a\", encoding=\"utf-8\") as f:\n f.write(line + \"\\n\")\n\ndef load_full_text(path: str) -> str:\n \"\"\"\n 读取完整 OCR 文本(output_full_exam.txt)\n \"\"\"\n if not os.path.isfile(path):\n log(f\"❌ 文件不存在: {path}\")\n raise FileNotFoundError(f\"{path} 不存在\")\n with open(path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n log(f\"成功读取 OCR 文本,字符长度: {len(content)}\")\n return content\n\ndef split_into_units_text(full_text: str) -> str:\n \"\"\"\n 不要求模型输出 JSON,而是让模型按以下格式输出纯文本:\n ==== Unit1 ====\n <单元1标题>\n <阅读材料原文>\n 【第<index>题】\n <题干文本>\n (题与题之间留一个空行)\n ==== Unit2 ====\n <单元2标题>\n ……\n \"\"\"\n log(f\"进入 split_into_units_text,收到 full_text 长度: {len(full_text)} 字符\")\n\n prompt = (\n \"请将下面的 OCR 完整文本切分成若干“单元”,每个单元严格按照如下格式输出:\\n\"\n \"==== UnitN ====\\n\"\n \"<单元N的标题>\\n\"\n \"<该单元阅读材料原文(跨页内容一并写出)>\\n\"\n \"每道题以“【第<index>题】”开头,后跟题干原文,题与题之间留一个空行。\\n\"\n \"不要输出多余说明,保持和原文一样只是分开,当单元跨了多页,可以删去单元中间的(第x页/共x页【第x页】)除此之外不要比原文增加或者减少任何一个字。只输出上述格式的纯文本。\\n\\n\"\n f\"{full_text}\"\n )\n\n log(\"调用 Qwen-3 切割单元(split_into_units_text),开始发送请求\")\n try:\n response = qwen_client.chat.completions.create(\n model=QWEN_3_MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"你是一个文本切割助手,输出时严格按照约定格式。\"},\n {\"role\": \"user\", \"content\": prompt}\n ],\n stream=True,\n stream_options={\"include_usage\": False}\n )\n except Exception as e:\n log(f\"❌ split_into_units_text 请求异常: {e}\")\n raise\n\n accumulated = \"\"\n chunk_count = 0\n log(\"流式调用已发出,开始接收流式返回\")\n for chunk in response:\n chunk_count += 1\n try:\n chunk_data = json.loads(chunk.model_dump_json())\n delta = chunk_data[\"choices\"][0].get(\"delta\", {})\n piece = delta.get(\"content\", \"\")\n if piece:\n accumulated += piece\n if chunk_count == 1:\n preview = piece[:50].replace(\"\\n\", \" \")\n log(f\"[split_text] 第1个 chunk 内容预览: {preview} …\")\n print(piece, end=\"\", flush=True)\n except Exception as e:\n log(f\"[split_text] 解析第 {chunk_count} 个 chunk 时出错: {e}\")\n continue\n\n if chunk_count == 0:\n log(\"❗ 警告:split_into_units_text 未收到任何 chunk\")\n\n log(f\"流式接收结束,共收到 {chunk_count} 个 chunk,累计字符数: {len(accumulated.strip())}\")\n return accumulated.strip()\n\ndef save_units_from_text(raw: str):\n \"\"\"\n 将 split_into_units_text 返回的整段带分隔符的文本按 “==== UnitN ====”\n 拆分,并将每个单元的内容(标题+阅读材料+题干列表)分别写入独立的 .txt 文件。\n \"\"\"\n os.makedirs(UNITS_FOLDER, exist_ok=True)\n log(\"开始将模型返回的原始文本按单元拆分并保存\")\n\n # 用正则匹配所有 “==== UnitN ====”\n pattern = re.compile(r\"^====\\s*(Unit\\d+)\\s*====\\s*$\", re.MULTILINE)\n matches = list(pattern.finditer(raw))\n\n if not matches:\n log(\"❌ 未找到任何单元标志 “==== UnitN ====”,无法拆分\")\n return\n\n for idx, match in enumerate(matches):\n unit_name = match.group(1) # e.g., \"Unit1\"\n start_pos = match.end()\n if idx + 1 < len(matches):\n end_pos = matches[idx + 1].start()\n else:\n end_pos = len(raw)\n body = raw[start_pos:end_pos].strip()\n\n filename = os.path.join(UNITS_FOLDER, f\"{unit_name}.txt\")\n try:\n with open(filename, \"w\", encoding=\"utf-8\") as f:\n f.write(body)\n log(f\" 已保存 {unit_name}.txt → {filename}\")\n except Exception as e:\n log(f\" ❌ 保存 {unit_name}.txt 失败: {e}\")\n\ndef get_unit_filenames(folder: str) -> list:\n \"\"\"\n 返回 folder 目录下所有 .txt 文件的绝对路径列表(按名称排序)\n \"\"\"\n if not os.path.isdir(folder):\n log(f\"❗ 单元目录不存在: {folder}\")\n return []\n files = [f for f in os.listdir(folder) if f.lower().endswith(\".txt\")]\n files.sort()\n return [os.path.join(folder, f) for f in files]\n\ndef ask_model_for_answers(unit_text: str, unit_name: str) -> str:\n \"\"\"\n 将某个单元的完整文本发送给 Qwen-3,让它输出每道题的答案(每行一个“第N题: 答案”格式)。\n 对于子题标签,要保留完整编号,例如 13(1)、13(2)。\n 返回模型原始回复(文本)。\n \"\"\"\n prompt = (\n f\"你是一个严格遵守格式的答题机器人。请根据下方的题目文本,严格模仿示例格式,生成答案列表。\\n\\n\"\n \"### 输出格式示例 ###\\n\"\n \"第1题: A\\n\"\n \"第2题: B\\n\"\n \"第13(1)题: 这是第一小题的答案。\\n\"\n \"第13(2)题: 这是第二小题的答案。\\n\"\n \"第14题: 汉皇重色思倾国\\n\"\n \"### 输出格式示例结束 ###\\n\\n\"\n f\"### “{unit_name}”单元的题目文本 ###\\n\"\n f\"{unit_text}\\n\"\n \"### 题目文本结束 ###\\n\\n\"\n \"请开始作答,你的回答中除了答案列表外,不要包含任何其他内容。\"\n )\n log(f\"调用 DeepSeek 解答 {unit_name}\")\n try:\n # 使用 deepseek_client 和 DEEPSEEK_MODEL\n response = deepseek_client.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=[\n # System message 强调了遵守格式的重要性,并保留了作文要求\n {\"role\": \"system\", \"content\": \"你是一个高考解题专家,请严格按照用户要求的格式,分析以下试卷题目并给出答案。当题目中出现“写作”“写一篇文章”,请按照满分标准写一篇大于1200字的文章。\"},\n {\"role\": \"user\", \"content\": prompt}\n ],\n stream=True\n )\n except Exception as e:\n log(f\" ❌ {unit_name} 解题请求失败: {e}\")\n return f\"模型请求失败:{e}\"\n\n accumulated = \"\"\n chunk_count = 0\n log(f\" [answer-{unit_name}] DeepSeek 流式调用已发出,开始接收返回...\")\n for chunk in response:\n chunk_count += 1\n try:\n # DeepSeek 的流式数据结构更直接\n piece = chunk.choices[0].delta.content\n\n if piece:\n accumulated += piece\n if chunk_count == 1:\n preview = piece[:50].replace(\"\\n\", \" \")\n log(f\" [answer-{unit_name}] 第1个 chunk 内容预览: {preview} …\")\n print(piece, end=\"\", flush=True)\n\n except Exception as e:\n log(f\" [answer-{unit_name}] 解析第 {chunk_count} 个 chunk 时出错: {e}\")\n continue\n\n if chunk_count == 0:\n log(f\" ❗ 警告:{unit_name} 未收到任何解题 chunk\")\n\n # 换行以确保日志格式清晰\n print() \n\n ans = accumulated.strip()\n log(f\" {unit_name} 返回答案,共 {len(ans.splitlines())} 行\")\n return ans\n\ndef main():\n # 准备目录\n os.makedirs(ANWSER_DIR, exist_ok=True)\n os.makedirs(UNITS_FOLDER, exist_ok=True)\n os.makedirs(UNIT_ANSWERS_FOLDER, exist_ok=True)\n\n # 初始化日志文件\n with open(LOG_FILE, \"w\", encoding=\"utf-8\") as f:\n f.write(f\"[{datetime.datetime.now()}] 全流程脚本启动\\n\")\n\n # 1. 读取 OCR 文本\n log(\"读取完整试卷文本\")\n try:\n full_text = load_full_text(INPUT_FULL_TEXT)\n except Exception as e:\n log(f\"❌ 读取 OCR 文本失败:{e}\")\n return\n\n # 2. 调用模型生成带分隔符的单元文本\n log(\"调用模型生成带分隔符的单元文本\")\n try:\n raw_units_text = split_into_units_text(full_text)\n except Exception as e:\n log(f\"❌ split_into_units_text 执行失败: {e}\")\n return\n\n # 3. 按分隔符拆分并保存为 .txt 文件\n save_units_from_text(raw_units_text)\n\n # 4. 逐单元读取 .txt 并让模型解答,保存答案\n unit_files = get_unit_filenames(UNITS_FOLDER)\n if not unit_files:\n log(\"未找到任何单元 txt,脚本退出。\")\n return\n\n all_answers = []\n for unit_path in unit_files:\n unit_name = os.path.splitext(os.path.basename(unit_path))[0]\n log(f\"开始处理 {unit_name}\")\n try:\n with open(unit_path, \"r\", encoding=\"utf-8\") as f:\n unit_text = f.read().strip()\n except Exception as e:\n log(f\"❌ 读取单元文件 {unit_path} 失败: {e}\")\n continue\n\n ans_text = ask_model_for_answers(unit_text, unit_name)\n\n answer_file = os.path.join(UNIT_ANSWERS_FOLDER, f\"{unit_name}_answer.txt\")\n try:\n with open(answer_file, \"w\", encoding=\"utf-8\") as f:\n f.write(ans_text)\n log(f\" 已保存 {unit_name}_answer.txt → {answer_file}\")\n except Exception as e:\n log(f\" ❌ 保存 {unit_name}_answer.txt 失败: {e}\")\n\n ans_lines = [line.strip() for line in ans_text.splitlines() if line.strip()]\n all_answers.append({\"unit\": unit_name, \"answers\": ans_lines})\n\n # 5. 汇总所有单元答案到 answer_list.json\n try:\n with open(OUTPUT_ANSWERS, \"w\", encoding=\"utf-8\") as f:\n json.dump(all_answers, f, ensure_ascii=False, indent=2)\n log(f\"所有单元答案汇总已保存到:{OUTPUT_ANSWERS}\")\n except Exception as e:\n log(f\"❌ 保存 answer_list.json 失败: {e}\")\n return\n\n log(\"全流程脚本执行完毕\")\n\nif __name__ == \"__main__\":\n main()\n"}}}}