调用 LLM 识别拼接图片中的目录范围,并保存原始响应
(client: AsyncOpenAI, model: str, b64_img: str, start_p: int, end_p: int, raw_save_path: str = None)
| 117 | 【目录的严格定义】:一页中必须存在多个“标题 - 页码”对。如果某一页没有这个特征(例如纯文本正文、封面、版权页、序言),则它绝对不是目录。只要不存在页码,那这页绝对不是目录。相对应地,如果一页有这个特征,那么它必然是目录。 |
| 118 | |
| 119 | 【注意】 |
| 120 | 1. 应以下方的 PDFNumber 作为页码。例如 PDFNumber 为 2-3 的图片是目录,那么起始页码就是 2,结束页码就是 3。 |
| 121 | 2. 注意空白页不可以算作目录的一部分,空白页一定不是目录! |
| 122 | 3. 例如 PDFNumber 为 16 的图片是空白,为 17-18 的图片是目录,那么起始页码就是 17(切记,不是 16!),结束页码就是 18。请切记不要把空白页面识别为目录的一部分,这会导致后续程序出现严重错误!!! |
| 123 | |
| 124 | 【输出要求】: |
| 125 | 1. 仅输出 JSON 格式,不要包含任何 markdown 标记(如 ```json )、解释或额外文本。 |
| 126 | 2. 如果这几页中存在目录,输出格式为:{{"toc_start": 起始页码,"toc_end": 结束页码}} |
| 127 | 3. 如果这几页中没有任何一页是目录,输出格式为:{{"toc_start": null, "toc_end": null}}""" |
| 128 | |
| 129 | try: |
| 130 | completion = await client.chat.completions.create( |
| 131 | model=model, |
| 132 | messages=[ |
| 133 | { |
| 134 | "role": "user", |
| 135 | "content": [ |
| 136 | {"type": "text", "text": prompt}, |
| 137 | {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_img}"}}, |
| 138 | {"type": "text", "text": prompt}, |
| 139 | ] |
| 140 | } |
| 141 | ], |
| 142 | extra_body={"enable_thinking": False}, |
| 143 | temperature=0, |
| 144 | ) |
| 145 | raw_content = completion.choices[0].message.content.strip() |
| 146 | |
| 147 | if raw_save_path: |
| 148 | response_data = { |
| 149 | "page_range": f"{start_p}-{end_p}", |
| 150 | "raw_response": raw_content, |
| 151 | "model": model |
| 152 | } |
| 153 | with open(raw_save_path, 'w', encoding='utf-8') as f: |
| 154 | json.dump(response_data, f, ensure_ascii=False, indent=2) |
| 155 | logger.debug(f"原始响应已保存至:{raw_save_path}") |
| 156 | write_log(f"原始响应已保存至:{raw_save_path}") |
| 157 | |
| 158 | return raw_content |
| 159 | except Exception as e: |
| 160 | error_msg = f"获取目录范围失败 ({start_p}-{end_p}): {e}" |
| 161 | logger.error(error_msg) |
| 162 | write_log(error_msg) |
| 163 | if raw_save_path: |
| 164 | with open(raw_save_path, 'w', encoding='utf-8') as f: |
| 165 | json.dump({"error": str(e), "page_range": f"{start_p}-{end_p}"}, f, ensure_ascii=False, indent=2) |
| 166 | return "" |
| 167 | |
| 168 | def parse_toc_json(text: str) -> tuple: |
| 169 | """安全解析 LLM 输出的 JSON,剥离 Markdown 标记""" |
| 170 | if not text: |
| 171 | return None, None |
| 172 | try: |
| 173 | clean_text = re.sub(r'^```(?:json)?\s*', '', text.strip(), flags=re.MULTILINE) |
| 174 | clean_text = re.sub(r'\s*```$', '', clean_text, flags=re.MULTILINE) |
| 175 | data = json.loads(clean_text) |
| 176 | return data.get("toc_start"), data.get("toc_end") |
no test coverage detected