1 line
13 KiB
JSON
1 line
13 KiB
JSON
{"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"}}}} |