Compare commits
132 Commits
boyin_rag
...
2706263a4b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2706263a4b | ||
|
|
ee34f13307 | ||
|
|
65a4cf59c2 | ||
|
|
a7a56b5058 | ||
|
|
8c21432291 | ||
|
|
87b3f79ae9 | ||
|
|
f42aad5093 | ||
|
|
725f60fba3 | ||
|
|
be83907394 | ||
|
|
eba48a0f1a | ||
|
|
ee1a9e7cce | ||
|
|
fc06be6f7a | ||
|
|
883b513b91 | ||
|
|
24cebaf4ec | ||
|
|
858b5f69b0 | ||
|
|
63c61e6204 | ||
|
|
82aac97980 | ||
|
|
045cdb15d8 | ||
|
|
e78e8b0909 | ||
|
|
07974a26d0 | ||
|
|
3e56c074cc | ||
|
|
72dbe856d2 | ||
|
|
0055ea2df7 | ||
|
|
4a79aa6a93 | ||
|
|
5dffe8627f | ||
|
|
2aefef26db | ||
|
|
957da731db | ||
|
|
155e7e0deb | ||
|
|
add29eba08 | ||
|
|
163e59c0f3 | ||
|
|
07ece29c7c | ||
|
|
991a903fa9 | ||
|
|
cf7c81170c | ||
|
|
6dda2061dd | ||
|
|
e9de41b7e8 | ||
|
|
b34c79a94b | ||
|
|
8a0d96afd3 | ||
|
|
37f9b94dee | ||
|
|
95284d859b | ||
|
|
a552592b5a | ||
|
|
e305f1b4a8 | ||
| a88497c3ab | |||
|
|
0f1d2e0e48 | ||
|
|
936e2f5206 | ||
|
|
7f4b87a633 | ||
|
|
2ddd1bb634 | ||
|
|
c68285aeac | ||
|
|
caaebe4296 | ||
|
|
39d50c1c95 | ||
|
|
25dc7bf912 | ||
|
|
0458590a77 | ||
|
|
44fe78fff5 | ||
|
|
5ddd657ebc | ||
|
|
9b0b2cf260 | ||
|
|
9f39a6571a | ||
|
|
d07e736214 | ||
|
|
a1f7ae5b55 | ||
|
|
1213ef19e5 | ||
|
|
aaafe2a797 | ||
|
|
2716606f0c | ||
|
|
286f7303be | ||
|
|
7eeab9e376 | ||
|
|
4ca331fb28 | ||
|
|
9487829930 | ||
|
|
a73074b89e | ||
|
|
fd93622840 | ||
|
|
09a82a572d | ||
|
|
c53ddf65aa | ||
|
|
ac64a77c2d | ||
|
|
dae8a0affc | ||
|
|
97a81e9388 | ||
|
|
1dd1d0ed6c | ||
|
|
060af0d2e6 | ||
|
|
a848f714b6 | ||
|
|
924f8e30c7 | ||
|
|
f40347665b | ||
|
|
734c40bbde | ||
|
|
4ec87fbb54 | ||
|
|
17b5c22e61 | ||
|
|
c6cd04a407 | ||
|
|
f60a12f8b4 | ||
|
|
8413fb15ba | ||
|
|
72b2ce9b62 | ||
|
|
f43ef909e2 | ||
|
|
9651ad488f | ||
|
|
81da7bb1a5 | ||
|
|
4127162ee7 | ||
|
|
98e5cb7b77 | ||
|
|
c88d8047dd | ||
|
|
e4bebea28d | ||
|
|
294df6c2d5 | ||
|
|
239894544e | ||
|
|
ed5fc84d4e | ||
|
|
e3f84069ee | ||
|
|
7bf094b6b6 | ||
|
|
57d7dc33d3 | ||
|
|
94ccd77480 | ||
|
|
48e53cba05 | ||
|
|
e9a7f9439f | ||
|
|
a88b119bf0 | ||
|
|
eee8115434 | ||
|
|
4f6a272113 | ||
|
|
cf3dd5ddb6 | ||
|
|
f6f10b7230 | ||
|
|
bd7b219e8f | ||
|
|
e62decac21 | ||
|
|
588b22e039 | ||
|
|
ef18aeda81 | ||
|
|
3520131ca2 | ||
|
|
ff5901d8c0 | ||
|
|
2305576410 | ||
|
|
52f23c505c | ||
|
|
34cc484635 | ||
|
|
d152f62894 | ||
|
|
ca35f56f9b | ||
|
|
d616fd121a | ||
|
|
f3fda0d3fc | ||
|
|
197287fc30 | ||
|
|
c37fcc9299 | ||
|
|
8f91a048a8 | ||
|
|
58eac38b4d | ||
|
|
d7f0cbe68e | ||
|
|
04c9077265 | ||
|
|
6afd7db1e3 | ||
|
|
42d10a9481 | ||
|
|
50a1ea83ef | ||
|
|
a9c86a7fb8 | ||
|
|
2b299cf579 | ||
|
|
0121cacc84 | ||
|
|
f2dcd6ad55 | ||
|
|
42d9712f20 | ||
|
|
7f0ffa58f0 |
56
.github/workflows/conda-pack-windows.yml
vendored
Normal file
56
.github/workflows/conda-pack-windows.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Create Conda Environment Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
auto-activate-base: true
|
||||
activate-environment: ""
|
||||
|
||||
- name: Create new Conda environment
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
conda create -n gpt python=3.11 -y
|
||||
conda activate gpt
|
||||
|
||||
- name: Install requirements
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
conda activate gpt
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install conda-pack
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
conda activate gpt
|
||||
conda install conda-pack -y
|
||||
|
||||
- name: Pack conda environment
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
conda activate gpt
|
||||
conda pack -n gpt -o gpt.tar.gz
|
||||
|
||||
- name: Create workspace zip
|
||||
shell: pwsh
|
||||
run: |
|
||||
mkdir workspace
|
||||
Get-ChildItem -Exclude "workspace" | Copy-Item -Destination workspace -Recurse
|
||||
Remove-Item -Path workspace/.git* -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item gpt.tar.gz workspace/ -Force
|
||||
|
||||
- name: Upload packed files
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gpt-academic-package
|
||||
path: workspace
|
||||
7
.github/workflows/stale.yml
vendored
7
.github/workflows/stale.yml
vendored
@@ -7,7 +7,7 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/5 * * * *'
|
||||
- cron: '*/30 * * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
@@ -19,7 +19,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 100 days with no activity. Remove stale label or comment or this will be closed in 1 days.'
|
||||
stale-issue-message: 'This issue is stale because it has been open 100 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
days-before-stale: 100
|
||||
days-before-close: 1
|
||||
debug-only: true
|
||||
days-before-close: 7
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -161,4 +161,5 @@ temp.*
|
||||
objdump*
|
||||
*.min.*.js
|
||||
TODO
|
||||
*.cursorrules
|
||||
experimental_mods
|
||||
search_results
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -3,37 +3,36 @@
|
||||
# - 如何构建: 先修改 `config.py`, 然后 `docker build -t gpt-academic . `
|
||||
# - 如何运行(Linux下): `docker run --rm -it --net=host gpt-academic `
|
||||
# - 如何运行(其他操作系统,选择任意一个固定端口50923): `docker run --rm -it -e WEB_PORT=50923 -p 50923:50923 gpt-academic `
|
||||
FROM python:3.11
|
||||
|
||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm
|
||||
|
||||
# 非必要步骤,更换pip源 (以下三行,可以删除)
|
||||
RUN echo '[global]' > /etc/pip.conf && \
|
||||
echo 'index-url = https://mirrors.aliyun.com/pypi/simple/' >> /etc/pip.conf && \
|
||||
echo 'trusted-host = mirrors.aliyun.com' >> /etc/pip.conf
|
||||
|
||||
|
||||
# 语音输出功能(以下两行,第一行更换阿里源,第二行安装ffmpeg,都可以删除)
|
||||
RUN UBUNTU_VERSION=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release); echo "deb https://mirrors.aliyun.com/debian/ $UBUNTU_VERSION main non-free contrib" > /etc/apt/sources.list; apt-get update
|
||||
# 语音输出功能(以下1,2行更换阿里源,第3,4行安装ffmpeg,都可以删除)
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
|
||||
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
|
||||
apt-get update
|
||||
RUN apt-get install ffmpeg -y
|
||||
|
||||
RUN apt-get clean
|
||||
|
||||
# 进入工作路径(必要)
|
||||
WORKDIR /gpt
|
||||
|
||||
|
||||
# 安装大部分依赖,利用Docker缓存加速以后的构建 (以下两行,可以删除)
|
||||
COPY requirements.txt ./
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
RUN uv venv --python=3.12 && uv pip install --verbose -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PATH="/gpt/.venv/bin:$PATH"
|
||||
RUN python -c 'import loguru'
|
||||
|
||||
# 装载项目文件,安装剩余依赖(必要)
|
||||
COPY . .
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
|
||||
# 非必要步骤,用于预热模块(可以删除)
|
||||
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
RUN uv venv --python=3.12 && uv pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
||||
|
||||
# # 非必要步骤,用于预热模块(可以删除)
|
||||
RUN python -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
|
||||
# 启动(必要)
|
||||
CMD ["python3", "-u", "main.py"]
|
||||
CMD ["bash", "-c", "python main.py"]
|
||||
|
||||
53
README.md
53
README.md
@@ -1,9 +1,15 @@
|
||||
> [!IMPORTANT]
|
||||
> 2024.10.10: 突发停电,紧急恢复了提供[whl包](https://drive.google.com/file/d/19U_hsLoMrjOlQSzYS3pzWX9fTzyusArP/view?usp=sharing)的文件服务器
|
||||
> 2024.10.8: 版本3.90加入对llama-index的初步支持,版本3.80加入插件二级菜单功能(详见wiki)
|
||||
> `master主分支`最新动态(2025.7.31): 新GUI前端,Coming Soon
|
||||
> `master主分支`最新动态(2025.3.2): 修复大量代码typo / 联网组件支持Jina的api / 增加deepseek-r1支持
|
||||
> `frontier开发分支`最新动态(2024.12.9): 更新对话时间线功能,优化xelatex论文翻译
|
||||
> `wiki文档`最新动态(2024.12.5): 更新ollama接入指南
|
||||
>
|
||||
> 2025.2.2: 三分钟快速接入最强qwen2.5-max[视频](https://www.bilibili.com/video/BV1LeFuerEG4)
|
||||
> 2025.2.1: 支持自定义字体
|
||||
> 2024.10.10: 突发停电,紧急恢复了提供[whl包](https://drive.google.com/drive/folders/14kR-3V-lIbvGxri4AHc8TpiA1fqsw7SK?usp=sharing)的文件服务器
|
||||
> 2024.5.1: 加入Doc2x翻译PDF论文的功能,[查看详情](https://github.com/binary-husky/gpt_academic/wiki/Doc2x)
|
||||
> 2024.3.11: 全力支持Qwen、GLM、DeepseekCoder等中文大语言模型! SoVits语音克隆模块,[查看详情](https://www.bilibili.com/video/BV1Rp421S7tF/)
|
||||
> 2024.1.17: 安装依赖时,请选择`requirements.txt`中**指定的版本**。 安装命令:`pip install -r requirements.txt`。本项目完全开源免费,您可通过订阅[在线服务](https://github.com/binary-husky/gpt_academic/wiki/online)的方式鼓励本项目的发展。
|
||||
> 2024.1.17: 安装依赖时,请选择`requirements.txt`中**指定的版本**。 安装命令:`pip install -r requirements.txt`。
|
||||
|
||||
<br>
|
||||
|
||||
@@ -124,20 +130,20 @@ Latex论文一键校对 | [插件] 仿Grammarly对Latex文章进行语法、拼
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A{"安装方法"} --> W1("I. 🔑直接运行 (Windows, Linux or MacOS)")
|
||||
W1 --> W11["1. Python pip包管理依赖"]
|
||||
W1 --> W12["2. Anaconda包管理依赖(推荐⭐)"]
|
||||
A{"安装方法"} --> W1("I 🔑直接运行 (Windows, Linux or MacOS)")
|
||||
W1 --> W11["1 Python pip包管理依赖"]
|
||||
W1 --> W12["2 Anaconda包管理依赖(推荐⭐)"]
|
||||
|
||||
A --> W2["II. 🐳使用Docker (Windows, Linux or MacOS)"]
|
||||
A --> W2["II 🐳使用Docker (Windows, Linux or MacOS)"]
|
||||
|
||||
W2 --> k1["1. 部署项目全部能力的大镜像(推荐⭐)"]
|
||||
W2 --> k2["2. 仅在线模型(GPT, GLM4等)镜像"]
|
||||
W2 --> k3["3. 在线模型 + Latex的大镜像"]
|
||||
W2 --> k1["1 部署项目全部能力的大镜像(推荐⭐)"]
|
||||
W2 --> k2["2 仅在线模型(GPT, GLM4等)镜像"]
|
||||
W2 --> k3["3 在线模型 + Latex的大镜像"]
|
||||
|
||||
A --> W4["IV. 🚀其他部署方法"]
|
||||
W4 --> C1["1. Windows/MacOS 一键安装运行脚本(推荐⭐)"]
|
||||
W4 --> C2["2. Huggingface, Sealos远程部署"]
|
||||
W4 --> C4["3. ... 其他 ..."]
|
||||
A --> W4["IV 🚀其他部署方法"]
|
||||
W4 --> C1["1 Windows/MacOS 一键安装运行脚本(推荐⭐)"]
|
||||
W4 --> C2["2 Huggingface, Sealos远程部署"]
|
||||
W4 --> C4["3 其他 ..."]
|
||||
```
|
||||
|
||||
### 安装方法I:直接运行 (Windows, Linux or MacOS)
|
||||
@@ -170,26 +176,32 @@ flowchart TD
|
||||
```
|
||||
|
||||
|
||||
<details><summary>如果需要支持清华ChatGLM2/复旦MOSS/RWKV作为后端,请点击展开此处</summary>
|
||||
<details><summary>如果需要支持清华ChatGLM系列/复旦MOSS/RWKV作为后端,请点击展开此处</summary>
|
||||
<p>
|
||||
|
||||
【可选步骤】如果需要支持清华ChatGLM3/复旦MOSS作为后端,需要额外安装更多依赖(前提条件:熟悉Python + 用过Pytorch + 电脑配置够强):
|
||||
【可选步骤】如果需要支持清华ChatGLM系列/复旦MOSS作为后端,需要额外安装更多依赖(前提条件:熟悉Python + 用过Pytorch + 电脑配置够强):
|
||||
|
||||
```sh
|
||||
# 【可选步骤I】支持清华ChatGLM3。清华ChatGLM备注:如果遇到"Call ChatGLM fail 不能正常加载ChatGLM的参数" 错误,参考如下: 1:以上默认安装的为torch+cpu版,使用cuda需要卸载torch重新安装torch+cuda; 2:如因本机配置不够无法加载模型,可以修改request_llm/bridge_chatglm.py中的模型精度, 将 AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True) 都修改为 AutoTokenizer.from_pretrained("THUDM/chatglm-6b-int4", trust_remote_code=True)
|
||||
python -m pip install -r request_llms/requirements_chatglm.txt
|
||||
|
||||
# 【可选步骤II】支持复旦MOSS
|
||||
# 【可选步骤II】支持清华ChatGLM4 注意:此模型至少需要24G显存
|
||||
python -m pip install -r request_llms/requirements_chatglm4.txt
|
||||
# 可使用modelscope下载ChatGLM4模型
|
||||
# pip install modelscope
|
||||
# modelscope download --model ZhipuAI/glm-4-9b-chat --local_dir ./THUDM/glm-4-9b-chat
|
||||
|
||||
# 【可选步骤III】支持复旦MOSS
|
||||
python -m pip install -r request_llms/requirements_moss.txt
|
||||
git clone --depth=1 https://github.com/OpenLMLab/MOSS.git request_llms/moss # 注意执行此行代码时,必须处于项目根路径
|
||||
|
||||
# 【可选步骤III】支持RWKV Runner
|
||||
# 【可选步骤IV】支持RWKV Runner
|
||||
参考wiki:https://github.com/binary-husky/gpt_academic/wiki/%E9%80%82%E9%85%8DRWKV-Runner
|
||||
|
||||
# 【可选步骤IV】确保config.py配置文件的AVAIL_LLM_MODELS包含了期望的模型,目前支持的全部模型如下(jittorllms系列目前仅支持docker方案):
|
||||
# 【可选步骤V】确保config.py配置文件的AVAIL_LLM_MODELS包含了期望的模型,目前支持的全部模型如下(jittorllms系列目前仅支持docker方案):
|
||||
AVAIL_LLM_MODELS = ["gpt-3.5-turbo", "api2d-gpt-3.5-turbo", "gpt-4", "api2d-gpt-4", "chatglm", "moss"] # + ["jittorllms_rwkv", "jittorllms_pangualpha", "jittorllms_llama"]
|
||||
|
||||
# 【可选步骤V】支持本地模型INT8,INT4量化(这里所指的模型本身不是量化版本,目前deepseek-coder支持,后面测试后会加入更多模型量化选择)
|
||||
# 【可选步骤VI】支持本地模型INT8,INT4量化(这里所指的模型本身不是量化版本,目前deepseek-coder支持,后面测试后会加入更多模型量化选择)
|
||||
pip install bitsandbyte
|
||||
# windows用户安装bitsandbytes需要使用下面bitsandbytes-windows-webui
|
||||
python -m pip install bitsandbytes --prefer-binary --extra-index-url=https://jllllll.github.io/bitsandbytes-windows-webui
|
||||
@@ -417,7 +429,6 @@ timeline LR
|
||||
1. `master` 分支: 主分支,稳定版
|
||||
2. `frontier` 分支: 开发分支,测试版
|
||||
3. 如何[接入其他大模型](request_llms/README.md)
|
||||
4. 访问GPT-Academic的[在线服务并支持我们](https://github.com/binary-husky/gpt_academic/wiki/online)
|
||||
|
||||
### V:参考与学习
|
||||
|
||||
|
||||
89
config.py
89
config.py
@@ -7,11 +7,16 @@
|
||||
Configuration reading priority: environment variable > config_private.py > config.py
|
||||
"""
|
||||
|
||||
# [step 1]>> API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
||||
API_KEY = "此处填API密钥" # 可同时填写多个API-KEY,用英文逗号分割,例如API_KEY = "sk-openaikey1,sk-openaikey2,fkxxxx-api2dkey3,azure-apikey4"
|
||||
# [step 1-1]>> ( 接入OpenAI模型家族 ) API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
||||
API_KEY = "在此处填写APIKEY" # 可同时填写多个API-KEY,用英文逗号分割,例如API_KEY = "sk-openaikey1,sk-openaikey2,fkxxxx-api2dkey3,azure-apikey4"
|
||||
|
||||
# [step 1-2]>> ( 强烈推荐!接入通义家族 & 大模型服务平台百炼 ) 接入通义千问在线大模型,api-key获取地址 https://dashscope.console.aliyun.com/
|
||||
DASHSCOPE_API_KEY = "" # 阿里灵积云API_KEY(用于接入qwen-max,dashscope-qwen3-14b,dashscope-deepseek-r1等)
|
||||
|
||||
# [step 2]>> 改为True应用代理,如果直接在海外服务器部署,此处不修改;如果使用本地或无地域限制的大模型时,此处也不需要修改
|
||||
# [step 1-3]>> ( 接入 deepseek-reasoner, 即 deepseek-r1 ) 深度求索(DeepSeek) API KEY,默认请求地址为"https://api.deepseek.com/v1/chat/completions"
|
||||
DEEPSEEK_API_KEY = ""
|
||||
|
||||
# [step 2]>> 改为True应用代理。如果使用本地或无地域限制的大模型时,此处不修改;如果直接在海外服务器部署,此处不修改
|
||||
USE_PROXY = False
|
||||
if USE_PROXY:
|
||||
"""
|
||||
@@ -32,11 +37,16 @@ else:
|
||||
|
||||
# [step 3]>> 模型选择是 (注意: LLM_MODEL是默认选中的模型, 它*必须*被包含在AVAIL_LLM_MODELS列表中 )
|
||||
LLM_MODEL = "gpt-3.5-turbo-16k" # 可选 ↓↓↓
|
||||
AVAIL_LLM_MODELS = ["gpt-4-1106-preview", "gpt-4-turbo-preview", "gpt-4-vision-preview",
|
||||
AVAIL_LLM_MODELS = ["qwen-max", "o1-mini", "o1-mini-2024-09-12", "o1", "o1-2024-12-17", "o1-preview", "o1-preview-2024-09-12",
|
||||
"gpt-4-1106-preview", "gpt-4-turbo-preview", "gpt-4-vision-preview",
|
||||
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||
"gpt-3.5-turbo-1106", "gpt-3.5-turbo-16k", "gpt-3.5-turbo", "azure-gpt-3.5",
|
||||
"gpt-4", "gpt-4-32k", "azure-gpt-4", "glm-4", "glm-4v", "glm-3-turbo",
|
||||
"gemini-1.5-pro", "chatglm3"
|
||||
"gemini-1.5-pro", "chatglm3", "chatglm4",
|
||||
"deepseek-chat", "deepseek-coder", "deepseek-reasoner",
|
||||
"volcengine-deepseek-r1-250120", "volcengine-deepseek-v3-241226",
|
||||
"dashscope-deepseek-r1", "dashscope-deepseek-v3",
|
||||
"dashscope-qwen3-14b", "dashscope-qwen3-235b-a22b", "dashscope-qwen3-32b",
|
||||
]
|
||||
|
||||
EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
@@ -47,7 +57,7 @@ EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
# "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-flash",
|
||||
# "qianfan", "deepseekcoder",
|
||||
# "spark", "sparkv2", "sparkv3", "sparkv3.5", "sparkv4",
|
||||
# "qwen-turbo", "qwen-plus", "qwen-max", "qwen-local",
|
||||
# "qwen-turbo", "qwen-plus", "qwen-local",
|
||||
# "moonshot-v1-128k", "moonshot-v1-32k", "moonshot-v1-8k",
|
||||
# "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-0125", "gpt-4o-2024-05-13"
|
||||
# "claude-3-haiku-20240307","claude-3-sonnet-20240229","claude-3-opus-20240229", "claude-2.1", "claude-instant-1.2",
|
||||
@@ -55,6 +65,7 @@ EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
# "deepseek-chat" ,"deepseek-coder",
|
||||
# "gemini-1.5-flash",
|
||||
# "yi-34b-chat-0205","yi-34b-chat-200k","yi-large","yi-medium","yi-spark","yi-large-turbo","yi-large-preview",
|
||||
# "grok-beta",
|
||||
# ]
|
||||
# --- --- --- ---
|
||||
# 此外,您还可以在接入one-api/vllm/ollama/Openroute时,
|
||||
@@ -73,7 +84,7 @@ API_URL_REDIRECT = {}
|
||||
|
||||
# 多线程函数插件中,默认允许多少路线程同时访问OpenAI。Free trial users的限制是每分钟3次,Pay-as-you-go users的限制是每分钟3500次
|
||||
# 一言以蔽之:免费(5刀)用户填3,OpenAI绑了信用卡的用户可以填 16 或者更高。提高限制请查询:https://platform.openai.com/docs/guides/rate-limits/overview
|
||||
DEFAULT_WORKER_NUM = 3
|
||||
DEFAULT_WORKER_NUM = 8
|
||||
|
||||
|
||||
# 色彩主题, 可选 ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast"]
|
||||
@@ -81,6 +92,31 @@ DEFAULT_WORKER_NUM = 3
|
||||
THEME = "Default"
|
||||
AVAIL_THEMES = ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast", "Gstaff/Xkcd", "NoCrypt/Miku"]
|
||||
|
||||
FONT = "Theme-Default-Font"
|
||||
AVAIL_FONTS = [
|
||||
"默认值(Theme-Default-Font)",
|
||||
"宋体(SimSun)",
|
||||
"黑体(SimHei)",
|
||||
"楷体(KaiTi)",
|
||||
"仿宋(FangSong)",
|
||||
"华文细黑(STHeiti Light)",
|
||||
"华文楷体(STKaiti)",
|
||||
"华文仿宋(STFangsong)",
|
||||
"华文宋体(STSong)",
|
||||
"华文中宋(STZhongsong)",
|
||||
"华文新魏(STXinwei)",
|
||||
"华文隶书(STLiti)",
|
||||
# 备注:以下字体需要网络支持,您可以自定义任意您喜欢的字体,如下所示,需要满足的格式为 "字体昵称(字体英文真名@字体css下载链接)"
|
||||
"思源宋体(Source Han Serif CN VF@https://chinese-fonts-cdn.deno.dev/packages/syst/dist/SourceHanSerifCN/result.css)",
|
||||
"月星楷(Moon Stars Kai HW@https://chinese-fonts-cdn.deno.dev/packages/moon-stars-kai/dist/MoonStarsKaiHW-Regular/result.css)",
|
||||
"珠圆体(MaokenZhuyuanTi@https://chinese-fonts-cdn.deno.dev/packages/mkzyt/dist/猫啃珠圆体/result.css)",
|
||||
"平方萌萌哒(PING FANG MENG MNEG DA@https://chinese-fonts-cdn.deno.dev/packages/pfmmd/dist/平方萌萌哒/result.css)",
|
||||
"Helvetica",
|
||||
"ui-sans-serif",
|
||||
"sans-serif",
|
||||
"system-ui"
|
||||
]
|
||||
|
||||
|
||||
# 默认的系统提示词(system prompt)
|
||||
INIT_SYS_PROMPT = "Serve me as a writing and programming assistant."
|
||||
@@ -132,16 +168,15 @@ MULTI_QUERY_LLM_MODELS = "gpt-3.5-turbo&chatglm3"
|
||||
QWEN_LOCAL_MODEL_SELECTION = "Qwen/Qwen-1_8B-Chat-Int8"
|
||||
|
||||
|
||||
# 接入通义千问在线大模型 https://dashscope.console.aliyun.com/
|
||||
DASHSCOPE_API_KEY = "" # 阿里灵积云API_KEY
|
||||
|
||||
|
||||
# 百度千帆(LLM_MODEL="qianfan")
|
||||
BAIDU_CLOUD_API_KEY = ''
|
||||
BAIDU_CLOUD_SECRET_KEY = ''
|
||||
BAIDU_CLOUD_QIANFAN_MODEL = 'ERNIE-Bot' # 可选 "ERNIE-Bot-4"(文心大模型4.0), "ERNIE-Bot"(文心一言), "ERNIE-Bot-turbo", "BLOOMZ-7B", "Llama-2-70B-Chat", "Llama-2-13B-Chat", "Llama-2-7B-Chat", "ERNIE-Speed-128K", "ERNIE-Speed-8K", "ERNIE-Lite-8K"
|
||||
|
||||
|
||||
# 如果使用ChatGLM3或ChatGLM4本地模型,请把 LLM_MODEL="chatglm3" 或LLM_MODEL="chatglm4",并在此处指定模型路径
|
||||
CHATGLM_LOCAL_MODEL_PATH = "THUDM/glm-4-9b-chat" # 例如"/home/hmp/ChatGLM3-6B/"
|
||||
|
||||
# 如果使用ChatGLM2微调模型,请把 LLM_MODEL="chatglmft",并在此处指定模型路径
|
||||
CHATGLM_PTUNING_CHECKPOINT = "" # 例如"/home/hmp/ChatGLM2-6B/ptuning/output/6b-pt-128-1e-2/checkpoint-100"
|
||||
|
||||
@@ -235,13 +270,15 @@ MOONSHOT_API_KEY = ""
|
||||
YIMODEL_API_KEY = ""
|
||||
|
||||
|
||||
# 深度求索(DeepSeek) API KEY,默认请求地址为"https://api.deepseek.com/v1/chat/completions"
|
||||
DEEPSEEK_API_KEY = ""
|
||||
# 接入火山引擎的在线大模型),api-key获取地址 https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint
|
||||
ARK_API_KEY = "00000000-0000-0000-0000-000000000000" # 火山引擎 API KEY
|
||||
|
||||
|
||||
# 紫东太初大模型 https://ai-maas.wair.ac.cn
|
||||
TAICHU_API_KEY = ""
|
||||
|
||||
# Grok API KEY
|
||||
GROK_API_KEY = ""
|
||||
|
||||
# Mathpix 拥有执行PDF的OCR功能,但是需要注册账号
|
||||
MATHPIX_APPID = ""
|
||||
@@ -273,8 +310,8 @@ GROBID_URLS = [
|
||||
]
|
||||
|
||||
|
||||
# Searxng互联网检索服务
|
||||
SEARXNG_URL = "https://cloud-1.agent-matrix.com/"
|
||||
# Searxng互联网检索服务(这是一个huggingface空间,请前往huggingface复制该空间,然后把自己新的空间地址填在这里)
|
||||
SEARXNG_URLS = [ f"https://kaletianlre-beardvs{i}dd.hf.space/" for i in range(1,5) ]
|
||||
|
||||
|
||||
# 是否允许通过自然语言描述修改本页的配置,该功能具有一定的危险性,默认关闭
|
||||
@@ -298,7 +335,7 @@ ARXIV_CACHE_DIR = "gpt_log/arxiv_cache"
|
||||
|
||||
|
||||
# 除了连接OpenAI之外,还有哪些场合允许使用代理,请尽量不要修改
|
||||
WHEN_TO_USE_PROXY = ["Download_LLM", "Download_Gradio_Theme", "Connect_Grobid",
|
||||
WHEN_TO_USE_PROXY = ["Connect_OpenAI", "Download_LLM", "Download_Gradio_Theme", "Connect_Grobid",
|
||||
"Warmup_Modules", "Nougat_Download", "AutoGen", "Connect_OpenAI_Embedding"]
|
||||
|
||||
|
||||
@@ -310,6 +347,23 @@ PLUGIN_HOT_RELOAD = False
|
||||
NUM_CUSTOM_BASIC_BTN = 4
|
||||
|
||||
|
||||
# 媒体智能体的服务地址(这是一个huggingface空间,请前往huggingface复制该空间,然后把自己新的空间地址填在这里)
|
||||
DAAS_SERVER_URLS = [ f"https://niuziniu-biligpt{i}.hf.space/stream" for i in range(1,5) ]
|
||||
|
||||
|
||||
# 在互联网搜索组件中,负责将搜索结果整理成干净的Markdown
|
||||
JINA_API_KEY = ""
|
||||
|
||||
|
||||
# 是否自动裁剪上下文长度(是否启动,默认不启动)
|
||||
AUTO_CONTEXT_CLIP_ENABLE = False
|
||||
# 目标裁剪上下文的token长度(如果超过这个长度,则会自动裁剪)
|
||||
AUTO_CONTEXT_CLIP_TRIGGER_TOKEN_LEN = 30*1000
|
||||
# 无条件丢弃x以上的轮数
|
||||
AUTO_CONTEXT_MAX_ROUND = 64
|
||||
# 在裁剪上下文时,倒数第x次对话能“最多”保留的上下文token的比例占 AUTO_CONTEXT_CLIP_TRIGGER_TOKEN_LEN 的多少
|
||||
AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.07, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01]
|
||||
|
||||
|
||||
"""
|
||||
--------------- 配置关联关系说明 ---------------
|
||||
@@ -369,6 +423,7 @@ NUM_CUSTOM_BASIC_BTN = 4
|
||||
|
||||
本地大模型示意图
|
||||
│
|
||||
├── "chatglm4"
|
||||
├── "chatglm3"
|
||||
├── "chatglm"
|
||||
├── "chatglm_onnx"
|
||||
@@ -399,7 +454,7 @@ NUM_CUSTOM_BASIC_BTN = 4
|
||||
插件在线服务配置依赖关系示意图
|
||||
│
|
||||
├── 互联网检索
|
||||
│ └── SEARXNG_URL
|
||||
│ └── SEARXNG_URLS
|
||||
│
|
||||
├── 语音功能
|
||||
│ ├── ENABLE_AUDIO
|
||||
|
||||
444
config_private.py
Normal file
444
config_private.py
Normal file
@@ -0,0 +1,444 @@
|
||||
"""
|
||||
以下所有配置也都支持利用环境变量覆写,环境变量配置格式见docker-compose.yml。
|
||||
读取优先级:环境变量 > config_private.py > config.py
|
||||
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
All the following configurations also support using environment variables to override,
|
||||
and the environment variable configuration format can be seen in docker-compose.yml.
|
||||
Configuration reading priority: environment variable > config_private.py > config.py
|
||||
"""
|
||||
|
||||
# [step 1-1]>> ( 接入GPT等模型 ) API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
||||
API_KEY = "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" # 可同时填写多个API-KEY,用英文逗号分割,例如API_KEY = "sk-openaikey1,sk-openaikey2,fkxxxx-api2dkey1,fkxxxx-api2dkey2"
|
||||
|
||||
# [step 1-2]>> ( 接入通义 qwen-max ) 接入通义千问在线大模型,api-key获取地址 https://dashscope.console.aliyun.com/
|
||||
DASHSCOPE_API_KEY = "" # 阿里灵积云API_KEY
|
||||
|
||||
# [step 1-3]>> ( 接入 deepseek-reasoner, 即 deepseek-r1 ) 深度求索(DeepSeek) API KEY,默认请求地址为"https://api.deepseek.com/v1/chat/completions"
|
||||
DEEPSEEK_API_KEY = "sk-d99b8cc6b7414cc88a5d950a3ff7585e"
|
||||
|
||||
# [step 2]>> 改为True应用代理。如果使用本地或无地域限制的大模型时,此处不修改;如果直接在海外服务器部署,此处不修改
|
||||
USE_PROXY = True
|
||||
if USE_PROXY:
|
||||
proxies = {
|
||||
"http":"socks5h://192.168.8.9:1070", # 再例如 "http": "http://127.0.0.1:7890",
|
||||
"https":"socks5h://192.168.8.9:1070", # 再例如 "https": "http://127.0.0.1:7890",
|
||||
}
|
||||
else:
|
||||
proxies = None
|
||||
DEFAULT_WORKER_NUM = 256
|
||||
|
||||
# [step 3]>> 模型选择是 (注意: LLM_MODEL是默认选中的模型, 它*必须*被包含在AVAIL_LLM_MODELS列表中 )
|
||||
LLM_MODEL = "gpt-4-32k" # 可选 ↓↓↓
|
||||
AVAIL_LLM_MODELS = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner",
|
||||
"gpt-4-1106-preview", "gpt-4-turbo-preview", "gpt-4-vision-preview",
|
||||
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||
"gpt-3.5-turbo-1106", "gpt-3.5-turbo-16k", "gpt-3.5-turbo", "azure-gpt-3.5",
|
||||
"gpt-4", "gpt-4-32k", "azure-gpt-4", "glm-4", "glm-4v", "glm-3-turbo",
|
||||
"gemini-1.5-pro", "chatglm3", "chatglm4",
|
||||
]
|
||||
|
||||
EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
|
||||
# --- --- --- ---
|
||||
# P.S. 其他可用的模型还包括
|
||||
# AVAIL_LLM_MODELS = [
|
||||
# "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-flash",
|
||||
# "qianfan", "deepseekcoder",
|
||||
# "spark", "sparkv2", "sparkv3", "sparkv3.5", "sparkv4",
|
||||
# "qwen-turbo", "qwen-plus", "qwen-local",
|
||||
# "moonshot-v1-128k", "moonshot-v1-32k", "moonshot-v1-8k",
|
||||
# "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-0125", "gpt-4o-2024-05-13"
|
||||
# "claude-3-haiku-20240307","claude-3-sonnet-20240229","claude-3-opus-20240229", "claude-2.1", "claude-instant-1.2",
|
||||
# "moss", "llama2", "chatglm_onnx", "internlm", "jittorllms_pangualpha", "jittorllms_llama",
|
||||
# "deepseek-chat" ,"deepseek-coder",
|
||||
# "gemini-1.5-flash",
|
||||
# "yi-34b-chat-0205","yi-34b-chat-200k","yi-large","yi-medium","yi-spark","yi-large-turbo","yi-large-preview",
|
||||
# "grok-beta",
|
||||
# ]
|
||||
# --- --- --- ---
|
||||
# 此外,您还可以在接入one-api/vllm/ollama/Openroute时,
|
||||
# 使用"one-api-*","vllm-*","ollama-*","openrouter-*"前缀直接使用非标准方式接入的模型,例如
|
||||
# AVAIL_LLM_MODELS = ["one-api-claude-3-sonnet-20240229(max_token=100000)", "ollama-phi3(max_token=4096)","openrouter-openai/gpt-4o-mini","openrouter-openai/chatgpt-4o-latest"]
|
||||
# --- --- --- ---
|
||||
|
||||
|
||||
# --------------- 以下配置可以优化体验 ---------------
|
||||
|
||||
# 重新URL重新定向,实现更换API_URL的作用(高危设置! 常规情况下不要修改! 通过修改此设置,您将把您的API-KEY和对话隐私完全暴露给您设定的中间人!)
|
||||
# 格式: API_URL_REDIRECT = {"https://api.openai.com/v1/chat/completions": "在这里填写重定向的api.openai.com的URL"}
|
||||
# 举例: API_URL_REDIRECT = {"https://api.openai.com/v1/chat/completions": "https://reverse-proxy-url/v1/chat/completions", "http://localhost:11434/api/chat": "在这里填写您ollama的URL"}
|
||||
API_URL_REDIRECT = {}
|
||||
|
||||
|
||||
# 多线程函数插件中,默认允许多少路线程同时访问OpenAI。Free trial users的限制是每分钟3次,Pay-as-you-go users的限制是每分钟3500次
|
||||
# 一言以蔽之:免费(5刀)用户填3,OpenAI绑了信用卡的用户可以填 16 或者更高。提高限制请查询:https://platform.openai.com/docs/guides/rate-limits/overview
|
||||
DEFAULT_WORKER_NUM = 64
|
||||
|
||||
|
||||
# 色彩主题, 可选 ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast"]
|
||||
# 更多主题, 请查阅Gradio主题商店: https://huggingface.co/spaces/gradio/theme-gallery 可选 ["Gstaff/Xkcd", "NoCrypt/Miku", ...]
|
||||
THEME = "Default"
|
||||
AVAIL_THEMES = ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast", "Gstaff/Xkcd", "NoCrypt/Miku"]
|
||||
|
||||
FONT = "Theme-Default-Font"
|
||||
AVAIL_FONTS = [
|
||||
"默认值(Theme-Default-Font)",
|
||||
"宋体(SimSun)",
|
||||
"黑体(SimHei)",
|
||||
"楷体(KaiTi)",
|
||||
"仿宋(FangSong)",
|
||||
"华文细黑(STHeiti Light)",
|
||||
"华文楷体(STKaiti)",
|
||||
"华文仿宋(STFangsong)",
|
||||
"华文宋体(STSong)",
|
||||
"华文中宋(STZhongsong)",
|
||||
"华文新魏(STXinwei)",
|
||||
"华文隶书(STLiti)",
|
||||
"思源宋体(Source Han Serif CN VF@https://chinese-fonts-cdn.deno.dev/packages/syst/dist/SourceHanSerifCN/result.css)",
|
||||
"月星楷(Moon Stars Kai HW@https://chinese-fonts-cdn.deno.dev/packages/moon-stars-kai/dist/MoonStarsKaiHW-Regular/result.css)",
|
||||
"珠圆体(MaokenZhuyuanTi@https://chinese-fonts-cdn.deno.dev/packages/mkzyt/dist/猫啃珠圆体/result.css)",
|
||||
"平方萌萌哒(PING FANG MENG MNEG DA@https://chinese-fonts-cdn.deno.dev/packages/pfmmd/dist/平方萌萌哒/result.css)",
|
||||
"Helvetica",
|
||||
"ui-sans-serif",
|
||||
"sans-serif",
|
||||
"system-ui"
|
||||
]
|
||||
|
||||
|
||||
# 默认的系统提示词(system prompt)
|
||||
INIT_SYS_PROMPT = " "
|
||||
|
||||
|
||||
# 对话窗的高度 (仅在LAYOUT="TOP-DOWN"时生效)
|
||||
CHATBOT_HEIGHT = 1115
|
||||
|
||||
|
||||
# 代码高亮
|
||||
CODE_HIGHLIGHT = True
|
||||
|
||||
|
||||
# 窗口布局
|
||||
LAYOUT = "LEFT-RIGHT" # "LEFT-RIGHT"(左右布局) # "TOP-DOWN"(上下布局)
|
||||
|
||||
|
||||
# 暗色模式 / 亮色模式
|
||||
DARK_MODE = True
|
||||
|
||||
|
||||
# 发送请求到OpenAI后,等待多久判定为超时
|
||||
TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
# 网页的端口, -1代表随机端口
|
||||
WEB_PORT = 19998
|
||||
|
||||
# 是否自动打开浏览器页面
|
||||
AUTO_OPEN_BROWSER = True
|
||||
|
||||
|
||||
# 如果OpenAI不响应(网络卡顿、代理失败、KEY失效),重试的次数限制
|
||||
MAX_RETRY = 5
|
||||
|
||||
|
||||
# 插件分类默认选项
|
||||
DEFAULT_FN_GROUPS = ['对话', '编程', '学术', '智能体']
|
||||
|
||||
|
||||
# 定义界面上“询问多个GPT模型”插件应该使用哪些模型,请从AVAIL_LLM_MODELS中选择,并在不同模型之间用`&`间隔,例如"gpt-3.5-turbo&chatglm3&azure-gpt-4"
|
||||
MULTI_QUERY_LLM_MODELS = "gpt-3.5-turbo&chatglm3"
|
||||
|
||||
|
||||
# 选择本地模型变体(只有当AVAIL_LLM_MODELS包含了对应本地模型时,才会起作用)
|
||||
# 如果你选择Qwen系列的模型,那么请在下面的QWEN_MODEL_SELECTION中指定具体的模型
|
||||
# 也可以是具体的模型路径
|
||||
QWEN_LOCAL_MODEL_SELECTION = "Qwen/Qwen-1_8B-Chat-Int8"
|
||||
|
||||
|
||||
# 百度千帆(LLM_MODEL="qianfan")
|
||||
BAIDU_CLOUD_API_KEY = ''
|
||||
BAIDU_CLOUD_SECRET_KEY = ''
|
||||
BAIDU_CLOUD_QIANFAN_MODEL = 'ERNIE-Bot' # 可选 "ERNIE-Bot-4"(文心大模型4.0), "ERNIE-Bot"(文心一言), "ERNIE-Bot-turbo", "BLOOMZ-7B", "Llama-2-70B-Chat", "Llama-2-13B-Chat", "Llama-2-7B-Chat", "ERNIE-Speed-128K", "ERNIE-Speed-8K", "ERNIE-Lite-8K"
|
||||
|
||||
|
||||
# 如果使用ChatGLM3或ChatGLM4本地模型,请把 LLM_MODEL="chatglm3" 或LLM_MODEL="chatglm4",并在此处指定模型路径
|
||||
CHATGLM_LOCAL_MODEL_PATH = "THUDM/glm-4-9b-chat" # 例如"/home/hmp/ChatGLM3-6B/"
|
||||
|
||||
# 如果使用ChatGLM2微调模型,请把 LLM_MODEL="chatglmft",并在此处指定模型路径
|
||||
CHATGLM_PTUNING_CHECKPOINT = "" # 例如"/home/hmp/ChatGLM2-6B/ptuning/output/6b-pt-128-1e-2/checkpoint-100"
|
||||
|
||||
|
||||
# 本地LLM模型如ChatGLM的执行方式 CPU/GPU
|
||||
LOCAL_MODEL_DEVICE = "cpu" # 可选 "cuda"
|
||||
LOCAL_MODEL_QUANT = "FP16" # 默认 "FP16" "INT4" 启用量化INT4版本 "INT8" 启用量化INT8版本
|
||||
|
||||
|
||||
# 设置gradio的并行线程数(不需要修改)
|
||||
CONCURRENT_COUNT = 100
|
||||
|
||||
|
||||
# 是否在提交时自动清空输入框
|
||||
AUTO_CLEAR_TXT = False
|
||||
|
||||
|
||||
# 加一个live2d装饰
|
||||
ADD_WAIFU = False
|
||||
|
||||
|
||||
# 设置用户名和密码(不需要修改)(相关功能不稳定,与gradio版本和网络都相关,如果本地使用不建议加这个)
|
||||
# [("username", "password"), ("username2", "password2"), ...]
|
||||
AUTHENTICATION = [("van", "L807878712"),("林", "L807878712"),("源", "L807878712"),("欣", "L807878712"),("z", "czh123456789")]
|
||||
|
||||
|
||||
# 如果需要在二级路径下运行(常规情况下,不要修改!!)
|
||||
# (举例 CUSTOM_PATH = "/gpt_academic",可以让软件运行在 http://ip:port/gpt_academic/ 下。)
|
||||
CUSTOM_PATH = "/"
|
||||
|
||||
|
||||
# HTTPS 秘钥和证书(不需要修改)
|
||||
SSL_KEYFILE = ""
|
||||
SSL_CERTFILE = ""
|
||||
|
||||
|
||||
# 极少数情况下,openai的官方KEY需要伴随组织编码(格式如org-xxxxxxxxxxxxxxxxxxxxxxxx)使用
|
||||
API_ORG = ""
|
||||
|
||||
|
||||
# 如果需要使用Slack Claude,使用教程详情见 request_llms/README.md
|
||||
SLACK_CLAUDE_BOT_ID = ''
|
||||
SLACK_CLAUDE_USER_TOKEN = ''
|
||||
|
||||
|
||||
# 如果需要使用AZURE(方法一:单个azure模型部署)详情请见额外文档 docs\use_azure.md
|
||||
AZURE_ENDPOINT = "https://你亲手写的api名称.openai.azure.com/"
|
||||
AZURE_API_KEY = "填入azure openai api的密钥" # 建议直接在API_KEY处填写,该选项即将被弃用
|
||||
AZURE_ENGINE = "填入你亲手写的部署名" # 读 docs\use_azure.md
|
||||
|
||||
|
||||
# 如果需要使用AZURE(方法二:多个azure模型部署+动态切换)详情请见额外文档 docs\use_azure.md
|
||||
AZURE_CFG_ARRAY = {}
|
||||
|
||||
|
||||
# 阿里云实时语音识别 配置难度较高
|
||||
# 参考 https://github.com/binary-husky/gpt_academic/blob/master/docs/use_audio.md
|
||||
ENABLE_AUDIO = False
|
||||
ALIYUN_TOKEN="" # 例如 f37f30e0f9934c34a992f6f64f7eba4f
|
||||
ALIYUN_APPKEY="" # 例如 RoPlZrM88DnAFkZK
|
||||
ALIYUN_ACCESSKEY="" # (无需填写)
|
||||
ALIYUN_SECRET="" # (无需填写)
|
||||
|
||||
|
||||
# GPT-SOVITS 文本转语音服务的运行地址(将语言模型的生成文本朗读出来)
|
||||
TTS_TYPE = "DISABLE" # EDGE_TTS / LOCAL_SOVITS_API / DISABLE
|
||||
GPT_SOVITS_URL = ""
|
||||
EDGE_TTS_VOICE = "zh-CN-XiaoxiaoNeural"
|
||||
|
||||
|
||||
# 接入讯飞星火大模型 https://console.xfyun.cn/services/iat
|
||||
XFYUN_APPID = "00000000"
|
||||
XFYUN_API_SECRET = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
XFYUN_API_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
|
||||
# 接入智谱大模型
|
||||
ZHIPUAI_API_KEY = ""
|
||||
ZHIPUAI_MODEL = "" # 此选项已废弃,不再需要填写
|
||||
|
||||
|
||||
# Claude API KEY
|
||||
ANTHROPIC_API_KEY = ""
|
||||
|
||||
|
||||
# 月之暗面 API KEY
|
||||
MOONSHOT_API_KEY = ""
|
||||
|
||||
|
||||
# 零一万物(Yi Model) API KEY
|
||||
YIMODEL_API_KEY = ""
|
||||
|
||||
|
||||
# 紫东太初大模型 https://ai-maas.wair.ac.cn
|
||||
TAICHU_API_KEY = ""
|
||||
|
||||
# Grok API KEY
|
||||
GROK_API_KEY = ""
|
||||
|
||||
# Mathpix 拥有执行PDF的OCR功能,但是需要注册账号
|
||||
MATHPIX_APPID = ""
|
||||
MATHPIX_APPKEY = ""
|
||||
|
||||
|
||||
# DOC2X的PDF解析服务,注册账号并获取API KEY: https://doc2x.noedgeai.com/login
|
||||
DOC2X_API_KEY = ""
|
||||
|
||||
|
||||
# 自定义API KEY格式
|
||||
CUSTOM_API_KEY_PATTERN = ""
|
||||
|
||||
|
||||
# Google Gemini API-Key
|
||||
GEMINI_API_KEY = ''
|
||||
|
||||
|
||||
# HUGGINGFACE的TOKEN,下载LLAMA时起作用 https://huggingface.co/docs/hub/security-tokens
|
||||
HUGGINGFACE_ACCESS_TOKEN = "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"
|
||||
|
||||
|
||||
# GROBID服务器地址(填写多个可以均衡负载),用于高质量地读取PDF文档
|
||||
# 获取方法:复制以下空间https://huggingface.co/spaces/qingxu98/grobid,设为public,然后GROBID_URL = "https://(你的hf用户名如qingxu98)-(你的填写的空间名如grobid).hf.space"
|
||||
GROBID_URLS = [
|
||||
"https://qingxu98-grobid.hf.space","https://qingxu98-grobid2.hf.space","https://qingxu98-grobid3.hf.space",
|
||||
"https://qingxu98-grobid4.hf.space","https://qingxu98-grobid5.hf.space", "https://qingxu98-grobid6.hf.space",
|
||||
"https://qingxu98-grobid7.hf.space", "https://qingxu98-grobid8.hf.space",
|
||||
]
|
||||
|
||||
|
||||
# Searxng互联网检索服务(这是一个huggingface空间,请前往huggingface复制该空间,然后把自己新的空间地址填在这里)
|
||||
SEARXNG_URLS = [ f"https://kaletianlre-beardvs{i}dd.hf.space/" for i in range(1,5) ]
|
||||
|
||||
|
||||
# 是否允许通过自然语言描述修改本页的配置,该功能具有一定的危险性,默认关闭
|
||||
ALLOW_RESET_CONFIG = False
|
||||
|
||||
|
||||
# 在使用AutoGen插件时,是否使用Docker容器运行代码
|
||||
AUTOGEN_USE_DOCKER = False
|
||||
|
||||
|
||||
# 临时的上传文件夹位置,请尽量不要修改
|
||||
PATH_PRIVATE_UPLOAD = "private_upload"
|
||||
|
||||
|
||||
# 日志文件夹的位置,请尽量不要修改
|
||||
PATH_LOGGING = "gpt_log"
|
||||
|
||||
|
||||
# 存储翻译好的arxiv论文的路径,请尽量不要修改
|
||||
ARXIV_CACHE_DIR = "gpt_log/arxiv_cache"
|
||||
|
||||
|
||||
# 除了连接OpenAI之外,还有哪些场合允许使用代理,请尽量不要修改
|
||||
WHEN_TO_USE_PROXY = ["Connect_OpenAI", "Download_LLM", "Download_Gradio_Theme", "Connect_Grobid",
|
||||
"Warmup_Modules", "Nougat_Download", "AutoGen", "Connect_OpenAI_Embedding"]
|
||||
|
||||
|
||||
# 启用插件热加载
|
||||
PLUGIN_HOT_RELOAD = False
|
||||
|
||||
|
||||
# 自定义按钮的最大数量限制
|
||||
NUM_CUSTOM_BASIC_BTN = 4
|
||||
|
||||
|
||||
# 媒体智能体的服务地址(这是一个huggingface空间,请前往huggingface复制该空间,然后把自己新的空间地址填在这里)
|
||||
DAAS_SERVER_URLS = [ f"https://niuziniu-biligpt{i}.hf.space/stream" for i in range(1,5) ]
|
||||
|
||||
|
||||
|
||||
"""
|
||||
--------------- 配置关联关系说明 ---------------
|
||||
|
||||
在线大模型配置关联关系示意图
|
||||
│
|
||||
├── "gpt-3.5-turbo" 等openai模型
|
||||
│ ├── API_KEY
|
||||
│ ├── CUSTOM_API_KEY_PATTERN(不常用)
|
||||
│ ├── API_ORG(不常用)
|
||||
│ └── API_URL_REDIRECT(不常用)
|
||||
│
|
||||
├── "azure-gpt-3.5" 等azure模型(单个azure模型,不需要动态切换)
|
||||
│ ├── API_KEY
|
||||
│ ├── AZURE_ENDPOINT
|
||||
│ ├── AZURE_API_KEY
|
||||
│ ├── AZURE_ENGINE
|
||||
│ └── API_URL_REDIRECT
|
||||
│
|
||||
├── "azure-gpt-3.5" 等azure模型(多个azure模型,需要动态切换,高优先级)
|
||||
│ └── AZURE_CFG_ARRAY
|
||||
│
|
||||
├── "spark" 星火认知大模型 spark & sparkv2
|
||||
│ ├── XFYUN_APPID
|
||||
│ ├── XFYUN_API_SECRET
|
||||
│ └── XFYUN_API_KEY
|
||||
│
|
||||
├── "claude-3-opus-20240229" 等claude模型
|
||||
│ └── ANTHROPIC_API_KEY
|
||||
│
|
||||
├── "stack-claude"
|
||||
│ ├── SLACK_CLAUDE_BOT_ID
|
||||
│ └── SLACK_CLAUDE_USER_TOKEN
|
||||
│
|
||||
├── "qianfan" 百度千帆大模型库
|
||||
│ ├── BAIDU_CLOUD_QIANFAN_MODEL
|
||||
│ ├── BAIDU_CLOUD_API_KEY
|
||||
│ └── BAIDU_CLOUD_SECRET_KEY
|
||||
│
|
||||
├── "glm-4", "glm-3-turbo", "zhipuai" 智谱AI大模型
|
||||
│ └── ZHIPUAI_API_KEY
|
||||
│
|
||||
├── "yi-34b-chat-0205", "yi-34b-chat-200k" 等零一万物(Yi Model)大模型
|
||||
│ └── YIMODEL_API_KEY
|
||||
│
|
||||
├── "qwen-turbo" 等通义千问大模型
|
||||
│ └── DASHSCOPE_API_KEY
|
||||
│
|
||||
├── "Gemini"
|
||||
│ └── GEMINI_API_KEY
|
||||
│
|
||||
└── "one-api-...(max_token=...)" 用一种更方便的方式接入one-api多模型管理界面
|
||||
├── AVAIL_LLM_MODELS
|
||||
├── API_KEY
|
||||
└── API_URL_REDIRECT
|
||||
|
||||
|
||||
本地大模型示意图
|
||||
│
|
||||
├── "chatglm4"
|
||||
├── "chatglm3"
|
||||
├── "chatglm"
|
||||
├── "chatglm_onnx"
|
||||
├── "chatglmft"
|
||||
├── "internlm"
|
||||
├── "moss"
|
||||
├── "jittorllms_pangualpha"
|
||||
├── "jittorllms_llama"
|
||||
├── "deepseekcoder"
|
||||
├── "qwen-local"
|
||||
├── RWKV的支持见Wiki
|
||||
└── "llama2"
|
||||
|
||||
|
||||
用户图形界面布局依赖关系示意图
|
||||
│
|
||||
├── CHATBOT_HEIGHT 对话窗的高度
|
||||
├── CODE_HIGHLIGHT 代码高亮
|
||||
├── LAYOUT 窗口布局
|
||||
├── DARK_MODE 暗色模式 / 亮色模式
|
||||
├── DEFAULT_FN_GROUPS 插件分类默认选项
|
||||
├── THEME 色彩主题
|
||||
├── AUTO_CLEAR_TXT 是否在提交时自动清空输入框
|
||||
├── ADD_WAIFU 加一个live2d装饰
|
||||
└── ALLOW_RESET_CONFIG 是否允许通过自然语言描述修改本页的配置,该功能具有一定的危险性
|
||||
|
||||
|
||||
插件在线服务配置依赖关系示意图
|
||||
│
|
||||
├── 互联网检索
|
||||
│ └── SEARXNG_URLS
|
||||
│
|
||||
├── 语音功能
|
||||
│ ├── ENABLE_AUDIO
|
||||
│ ├── ALIYUN_TOKEN
|
||||
│ ├── ALIYUN_APPKEY
|
||||
│ ├── ALIYUN_ACCESSKEY
|
||||
│ └── ALIYUN_SECRET
|
||||
│
|
||||
└── PDF文档精准解析
|
||||
├── GROBID_URLS
|
||||
├── MATHPIX_APPID
|
||||
└── MATHPIX_APPKEY
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from toolbox import HotReload # HotReload 的意思是热更新,修改函数
|
||||
from toolbox import trimmed_format_exc
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def get_crazy_functions():
|
||||
from crazy_functions.读文章写摘要 import 读文章写摘要
|
||||
from crazy_functions.生成函数注释 import 批量生成函数注释
|
||||
@@ -15,13 +14,13 @@ def get_crazy_functions():
|
||||
from crazy_functions.SourceCode_Analyse import 解析一个Rust项目
|
||||
from crazy_functions.SourceCode_Analyse import 解析一个Java项目
|
||||
from crazy_functions.SourceCode_Analyse import 解析一个前端项目
|
||||
from crazy_functions.Arxiv_论文对话 import Arxiv论文对话
|
||||
from crazy_functions.高级功能函数模板 import 高阶功能模板函数
|
||||
from crazy_functions.高级功能函数模板 import Demo_Wrap
|
||||
from crazy_functions.Latex全文润色 import Latex英文润色
|
||||
from crazy_functions.Latex_Project_Polish import Latex英文润色
|
||||
from crazy_functions.询问多个大语言模型 import 同时问询
|
||||
from crazy_functions.SourceCode_Analyse import 解析一个Lua项目
|
||||
from crazy_functions.SourceCode_Analyse import 解析一个CSharp项目
|
||||
from crazy_functions.总结word文档 import 总结word文档
|
||||
from crazy_functions.解析JupyterNotebook import 解析ipynb文件
|
||||
from crazy_functions.Conversation_To_File import 载入对话历史存档
|
||||
from crazy_functions.Conversation_To_File import 对话历史存档
|
||||
@@ -31,12 +30,10 @@ def get_crazy_functions():
|
||||
from crazy_functions.Markdown_Translate import Markdown英译中
|
||||
from crazy_functions.批量总结PDF文档 import 批量总结PDF文档
|
||||
from crazy_functions.PDF_Translate import 批量翻译PDF文档
|
||||
from crazy_functions.批量文件询问 import 批量文件询问
|
||||
|
||||
from crazy_functions.谷歌检索小助手 import 谷歌检索小助手
|
||||
from crazy_functions.理解PDF文档内容 import 理解PDF文档内容标准文件输入
|
||||
from crazy_functions.Latex全文润色 import Latex中文润色
|
||||
from crazy_functions.Latex全文润色 import Latex英文纠错
|
||||
from crazy_functions.Latex_Project_Polish import Latex中文润色
|
||||
from crazy_functions.Latex_Project_Polish import Latex英文纠错
|
||||
from crazy_functions.Markdown_Translate import Markdown中译英
|
||||
from crazy_functions.虚空终端 import 虚空终端
|
||||
from crazy_functions.生成多种Mermaid图表 import Mermaid_Gen
|
||||
@@ -52,8 +49,16 @@ def get_crazy_functions():
|
||||
from crazy_functions.Image_Generate_Wrap import ImageGen_Wrap
|
||||
from crazy_functions.SourceCode_Comment import 注释Python项目
|
||||
from crazy_functions.SourceCode_Comment_Wrap import SourceCodeComment_Wrap
|
||||
from crazy_functions.VideoResource_GPT import 多媒体任务
|
||||
|
||||
function_plugins = {
|
||||
"多媒体智能体": {
|
||||
"Group": "智能体",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info": "【仅测试】多媒体任务",
|
||||
"Function": HotReload(多媒体任务),
|
||||
},
|
||||
"虚空终端": {
|
||||
"Group": "对话|编程|学术|智能体",
|
||||
"Color": "stop",
|
||||
@@ -61,6 +66,34 @@ def get_crazy_functions():
|
||||
"Info": "使用自然语言实现您的想法",
|
||||
"Function": HotReload(虚空终端),
|
||||
},
|
||||
"解析整个Python项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": True,
|
||||
"Info": "解析一个Python项目的所有源文件(.py) | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Python项目),
|
||||
},
|
||||
"注释Python项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info": "上传一系列python源文件(或者压缩包), 为这些代码添加docstring | 输入参数为路径",
|
||||
"Function": HotReload(注释Python项目),
|
||||
"Class": SourceCodeComment_Wrap,
|
||||
},
|
||||
"载入对话历史存档(先上传存档或输入路径)": {
|
||||
"Group": "对话",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info": "载入对话历史存档 | 输入参数为路径",
|
||||
"Function": HotReload(载入对话历史存档),
|
||||
},
|
||||
"删除所有本地对话历史记录(谨慎操作)": {
|
||||
"Group": "对话",
|
||||
"AsButton": False,
|
||||
"Info": "删除所有本地对话历史记录,谨慎操作 | 不需要输入参数",
|
||||
"Function": HotReload(删除所有本地对话历史记录),
|
||||
},
|
||||
"清除所有缓存文件(谨慎操作)": {
|
||||
"Group": "对话",
|
||||
"Color": "stop",
|
||||
@@ -68,33 +101,107 @@ def get_crazy_functions():
|
||||
"Info": "清除所有缓存文件,谨慎操作 | 不需要输入参数",
|
||||
"Function": HotReload(清除缓存),
|
||||
},
|
||||
"生成多种Mermaid图表(从当前对话或路径(.pdf/.md/.docx)中生产图表)": {
|
||||
"Group": "对话",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info" : "基于当前对话或文件生成多种Mermaid图表,图表类型由模型判断",
|
||||
"Function": None,
|
||||
"Class": Mermaid_Gen
|
||||
},
|
||||
"Arxiv论文翻译": {
|
||||
"Group": "学术",
|
||||
"Color": "stop",
|
||||
"AsButton": True,
|
||||
"Info": "Arixv论文精细翻译 | 输入参数arxiv论文的ID,比如1812.10695",
|
||||
"Info": "ArXiv论文精细翻译 | 输入参数arxiv论文的ID,比如1812.10695",
|
||||
"Function": HotReload(Latex翻译中文并重新编译PDF), # 当注册Class后,Function旧接口仅会在“虚空终端”中起作用
|
||||
"Class": Arxiv_Localize, # 新一代插件需要注册Class
|
||||
},
|
||||
"批量文件询问": {
|
||||
"批量总结Word文档": {
|
||||
"Group": "学术",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"AdvancedArgs": True,
|
||||
"Info": "通过在高级参数区写入prompt,可自定义询问逻辑,默认情况下为总结逻辑 | 输入参数为路径",
|
||||
"ArgsReminder": r"1、请不要更改上方输入框中以“private_upload/...”开头的路径。 "
|
||||
r"2、请在下方高级参数区中输入你的prompt,文档中的内容将被添加你的prompt后。3、示例:“请总结下面的内容:”,此时,文档内容将添加在“:”后 ",
|
||||
"Function": HotReload(批量文件询问),
|
||||
"Info": "批量总结word文档 | 输入参数为路径",
|
||||
"Function": HotReload(总结word文档),
|
||||
},
|
||||
"Arxiv论文对话": {
|
||||
"解析整个Matlab项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info": "解析一个Matlab项目的所有源文件(.m) | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Matlab项目),
|
||||
},
|
||||
"解析整个C++项目头文件": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个C++项目的所有头文件(.h/.hpp) | 输入参数为路径",
|
||||
"Function": HotReload(解析一个C项目的头文件),
|
||||
},
|
||||
"解析整个C++项目(.cpp/.hpp/.c/.h)": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个C++项目的所有源文件(.cpp/.hpp/.c/.h)| 输入参数为路径",
|
||||
"Function": HotReload(解析一个C项目),
|
||||
},
|
||||
"解析整个Go项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个Go项目的所有源文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Golang项目),
|
||||
},
|
||||
"解析整个Rust项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个Rust项目的所有源文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Rust项目),
|
||||
},
|
||||
"解析整个Java项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个Java项目的所有源文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Java项目),
|
||||
},
|
||||
"解析整个前端项目(js,ts,css等)": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个前端项目的所有源文件(js,ts,css等) | 输入参数为路径",
|
||||
"Function": HotReload(解析一个前端项目),
|
||||
},
|
||||
"解析整个Lua项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个Lua项目的所有源文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析一个Lua项目),
|
||||
},
|
||||
"解析整个CSharp项目": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False, # 加入下拉菜单中
|
||||
"Info": "解析一个CSharp项目的所有源文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析一个CSharp项目),
|
||||
},
|
||||
"解析Jupyter Notebook文件": {
|
||||
"Group": "编程",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"Info": "解析Jupyter Notebook文件 | 输入参数为路径",
|
||||
"Function": HotReload(解析ipynb文件),
|
||||
"AdvancedArgs": True, # 调用时,唤起高级参数输入区(默认False)
|
||||
"ArgsReminder": "若输入0,则不解析notebook中的Markdown块", # 高级参数输入区的显示提示
|
||||
},
|
||||
"读Tex论文写摘要": {
|
||||
"Group": "学术",
|
||||
"Color": "stop",
|
||||
"AsButton": False,
|
||||
"AdvancedArgs": True,
|
||||
"Info": "在输入区中输入论文ID,在高级参数区中输入问题",
|
||||
"ArgsReminder": r"1、请在输入区中输入arxiv ID。 "
|
||||
r"2、请在下方高级参数区中输入你的问题,示例:“这篇文章的方法是什么,请用中文回答我” ",
|
||||
"Function": HotReload(Arxiv论文对话),
|
||||
"Info": "读取Tex论文并写摘要 | 输入参数为路径",
|
||||
"Function": HotReload(读文章写摘要),
|
||||
},
|
||||
"翻译README或MD": {
|
||||
"Group": "编程",
|
||||
@@ -245,7 +352,7 @@ def get_crazy_functions():
|
||||
"ArgsReminder": r"如果有必要, 请在此处给出自定义翻译命令, 解决部分词汇翻译不准确的问题。 "
|
||||
r"例如当单词'agent'翻译不准确时, 请尝试把以下指令复制到高级参数区: "
|
||||
r'If the term "agent" is used in this section, it should be translated to "智能体". ',
|
||||
"Info": "Arixv论文精细翻译 | 输入参数arxiv论文的ID,比如1812.10695",
|
||||
"Info": "ArXiv论文精细翻译 | 输入参数arxiv论文的ID,比如1812.10695",
|
||||
"Function": HotReload(Latex翻译中文并重新编译PDF), # 当注册Class后,Function旧接口仅会在“虚空终端”中起作用
|
||||
"Class": Arxiv_Localize, # 新一代插件需要注册Class
|
||||
},
|
||||
@@ -327,36 +434,6 @@ def get_crazy_functions():
|
||||
logger.error(trimmed_format_exc())
|
||||
logger.error("Load function plugin failed")
|
||||
|
||||
# try:
|
||||
# from crazy_functions.联网的ChatGPT import 连接网络回答问题
|
||||
|
||||
# function_plugins.update(
|
||||
# {
|
||||
# "连接网络回答问题(输入问题后点击该插件,需要访问谷歌)": {
|
||||
# "Group": "对话",
|
||||
# "Color": "stop",
|
||||
# "AsButton": False, # 加入下拉菜单中
|
||||
# # "Info": "连接网络回答问题(需要访问谷歌)| 输入参数是一个问题",
|
||||
# "Function": HotReload(连接网络回答问题),
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
# from crazy_functions.联网的ChatGPT_bing版 import 连接bing搜索回答问题
|
||||
|
||||
# function_plugins.update(
|
||||
# {
|
||||
# "连接网络回答问题(中文Bing版,输入问题后点击该插件)": {
|
||||
# "Group": "对话",
|
||||
# "Color": "stop",
|
||||
# "AsButton": False, # 加入下拉菜单中
|
||||
# "Info": "连接网络回答问题(需要访问中文Bing)| 输入参数是一个问题",
|
||||
# "Function": HotReload(连接bing搜索回答问题),
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
# except:
|
||||
# logger.error(trimmed_format_exc())
|
||||
# logger.error("Load function plugin failed")
|
||||
|
||||
try:
|
||||
from crazy_functions.SourceCode_Analyse import 解析任意code项目
|
||||
@@ -620,12 +697,6 @@ def get_crazy_functions():
|
||||
logger.error("Load function plugin failed")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# try:
|
||||
# from crazy_functions.高级功能函数模板 import 测试图表渲染
|
||||
# function_plugins.update({
|
||||
@@ -640,19 +711,6 @@ def get_crazy_functions():
|
||||
# logger.error(trimmed_format_exc())
|
||||
# print('Load function plugin failed')
|
||||
|
||||
# try:
|
||||
# from crazy_functions.chatglm微调工具 import 微调数据集生成
|
||||
# function_plugins.update({
|
||||
# "黑盒模型学习: 微调数据集生成 (先上传数据集)": {
|
||||
# "Color": "stop",
|
||||
# "AsButton": False,
|
||||
# "AdvancedArgs": True,
|
||||
# "ArgsReminder": "针对数据集输入(如 绿帽子*深蓝色衬衫*黑色运动裤)给出指令,例如您可以将以下命令复制到下方: --llm_to_learn=azure-gpt-3.5 --prompt_prefix='根据下面的服装类型提示,想象一个穿着者,对这个人外貌、身处的环境、内心世界、过去经历进行描写。要求:100字以内,用第二人称。' --system_prompt=''",
|
||||
# "Function": HotReload(微调数据集生成)
|
||||
# }
|
||||
# })
|
||||
# except:
|
||||
# print('Load function plugin failed')
|
||||
|
||||
"""
|
||||
设置默认值:
|
||||
@@ -672,3 +730,26 @@ def get_crazy_functions():
|
||||
function_plugins[name]["Color"] = "secondary"
|
||||
|
||||
return function_plugins
|
||||
|
||||
|
||||
|
||||
|
||||
def get_multiplex_button_functions():
|
||||
"""多路复用主提交按钮的功能映射
|
||||
"""
|
||||
return {
|
||||
"常规对话":
|
||||
"",
|
||||
|
||||
"查互联网后回答":
|
||||
"查互联网后回答",
|
||||
|
||||
"多模型对话":
|
||||
"询问多个GPT模型", # 映射到上面的 `询问多个GPT模型` 插件
|
||||
|
||||
"智能召回 RAG":
|
||||
"Rag智能召回", # 映射到上面的 `Rag智能召回` 插件
|
||||
|
||||
"多媒体查询":
|
||||
"多媒体智能体", # 映射到上面的 `多媒体智能体` 插件
|
||||
}
|
||||
|
||||
@@ -1,573 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from threading import Lock as ThreadLock
|
||||
from typing import Generator
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from crazy_functions.crazy_utils import input_clipping
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from crazy_functions.rag_fns.arxiv_fns.arxiv_splitter import ArxivSplitter, save_fragments_to_file, process_arxiv_sync
|
||||
from crazy_functions.rag_fns.arxiv_fns.section_fragment import SectionFragment as Fragment
|
||||
from crazy_functions.rag_fns.llama_index_worker import LlamaIndexRagWorker
|
||||
from toolbox import CatchException, update_ui, get_log_folder, update_ui_lastest_msg
|
||||
|
||||
# 全局常量配置
|
||||
MAX_HISTORY_ROUND = 5 # 最大历史对话轮数
|
||||
MAX_CONTEXT_TOKEN_LIMIT = 4096 # 上下文最大token数
|
||||
REMEMBER_PREVIEW = 1000 # 记忆预览长度
|
||||
VECTOR_STORE_TYPE = "Simple" # 向量存储类型:Simple或Milvus
|
||||
MAX_CONCURRENT_PAPERS = 20 # 最大并行处理论文数
|
||||
MAX_WORKERS = 3 # 最大工作线程数
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessingTask:
|
||||
"""论文处理任务数据类"""
|
||||
arxiv_id: str
|
||||
status: str = "pending" # pending, processing, completed, failed
|
||||
error: Optional[str] = None
|
||||
fragments: List[Fragment] = None
|
||||
start_time: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
class ArxivRagWorker:
|
||||
def __init__(self, user_name: str, llm_kwargs: Dict, arxiv_id: str = None):
|
||||
"""初始化ArxivRagWorker"""
|
||||
self.user_name = user_name
|
||||
self.llm_kwargs = llm_kwargs
|
||||
self.arxiv_id = self._normalize_arxiv_id(arxiv_id) if arxiv_id else None
|
||||
self.fragments = None
|
||||
|
||||
|
||||
# 初始化基础目录
|
||||
self.base_dir = Path(get_log_folder( plugin_name='arxiv_rag_cache'))
|
||||
self._setup_directories()
|
||||
|
||||
# 初始化处理状态
|
||||
|
||||
# 线程安全的计数器和集合
|
||||
self._processing_lock = ThreadLock()
|
||||
self._processed_fragments = set()
|
||||
self._processed_count = 0
|
||||
# 优化的线程池配置
|
||||
cpu_count = os.cpu_count() or 1
|
||||
self.thread_pool = ThreadPoolExecutor(
|
||||
max_workers=min(32, cpu_count * 4),
|
||||
thread_name_prefix="arxiv_worker"
|
||||
)
|
||||
|
||||
# 批处理配置
|
||||
self._batch_size = min(20, cpu_count * 2) # 动态设置批大小
|
||||
self.max_concurrent_papers = MAX_CONCURRENT_PAPERS
|
||||
self._semaphore = None
|
||||
self._loop = None
|
||||
|
||||
# 初始化处理队列
|
||||
self.processing_queue = {}
|
||||
|
||||
# 初始化工作组件
|
||||
self._init_workers()
|
||||
|
||||
def _setup_directories(self):
|
||||
"""设置工作目录"""
|
||||
|
||||
if self.arxiv_id:
|
||||
self.checkpoint_dir = self.base_dir / self.arxiv_id
|
||||
self.vector_store_dir = self.checkpoint_dir / "vector_store"
|
||||
self.fragment_store_dir = self.checkpoint_dir / "fragments"
|
||||
else:
|
||||
self.checkpoint_dir = self.base_dir
|
||||
self.vector_store_dir = self.base_dir / "vector_store"
|
||||
self.fragment_store_dir = self.base_dir / "fragments"
|
||||
|
||||
self.paper_path = self.checkpoint_dir / f"{self.arxiv_id}.processed"
|
||||
self.loading = self.paper_path.exists()
|
||||
# 创建必要的目录
|
||||
for directory in [self.checkpoint_dir, self.vector_store_dir, self.fragment_store_dir]:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Created directory: {directory}")
|
||||
|
||||
def _init_workers(self):
|
||||
"""初始化工作组件"""
|
||||
try:
|
||||
self.rag_worker = LlamaIndexRagWorker(
|
||||
user_name=self.user_name,
|
||||
llm_kwargs=self.llm_kwargs,
|
||||
checkpoint_dir=str(self.vector_store_dir),
|
||||
auto_load_checkpoint=True
|
||||
)
|
||||
|
||||
self.arxiv_splitter = ArxivSplitter(
|
||||
root_dir=str(self.checkpoint_dir / "arxiv_cache")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing workers: {str(e)}")
|
||||
raise
|
||||
|
||||
def _ensure_loop(self):
|
||||
"""确保存在事件循环"""
|
||||
if threading.current_thread() is threading.main_thread():
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
else:
|
||||
try:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
return self._loop
|
||||
|
||||
@property
|
||||
def semaphore(self):
|
||||
"""延迟创建semaphore"""
|
||||
if self._semaphore is None:
|
||||
self._semaphore = asyncio.Semaphore(self.max_concurrent_papers)
|
||||
return self._semaphore
|
||||
|
||||
async def _process_fragments(self, fragments: List[Fragment]) -> None:
|
||||
"""优化的并行处理论文片段"""
|
||||
if not fragments:
|
||||
logger.warning("No fragments to process")
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
total_fragments = len(fragments)
|
||||
|
||||
try:
|
||||
# 1. 处理论文概述
|
||||
overview = self._create_overview(fragments[0])
|
||||
overview_success = self._safe_add_to_vector_store_sync(overview['text'])
|
||||
if not overview_success:
|
||||
raise RuntimeError("Failed to add overview to vector store")
|
||||
|
||||
# 2. 并行处理片段
|
||||
successful_fragments = await self._parallel_process_fragments(fragments)
|
||||
|
||||
# 3. 保存处理结果
|
||||
if successful_fragments > 0:
|
||||
await self._save_results(fragments, overview['arxiv_id'], successful_fragments)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fragment processing: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
self._log_processing_stats(start_time, total_fragments)
|
||||
|
||||
def _create_overview(self, first_fragment: Fragment) -> Dict:
|
||||
"""创建论文概述"""
|
||||
return {
|
||||
'arxiv_id': first_fragment.arxiv_id,
|
||||
'text': (
|
||||
f"Paper Title: {first_fragment.title}\n"
|
||||
f"ArXiv ID: {first_fragment.arxiv_id}\n"
|
||||
f"Abstract: {first_fragment.abstract}\n"
|
||||
f"Table of contents:{first_fragment.catalogs}\n"
|
||||
f"Type: OVERVIEW"
|
||||
)
|
||||
}
|
||||
|
||||
async def _parallel_process_fragments(self, fragments: List[Fragment]) -> int:
|
||||
"""并行处理所有片段"""
|
||||
successful_count = 0
|
||||
loop = self._ensure_loop()
|
||||
|
||||
for i in range(0, len(fragments), self._batch_size):
|
||||
batch = fragments[i:i + self._batch_size]
|
||||
batch_futures = []
|
||||
|
||||
for j, fragment in enumerate(batch):
|
||||
if not self._is_fragment_processed(fragment, i + j):
|
||||
future = loop.run_in_executor(
|
||||
self.thread_pool,
|
||||
self._process_single_fragment_sync,
|
||||
fragment,
|
||||
i + j
|
||||
)
|
||||
batch_futures.append(future)
|
||||
|
||||
if batch_futures:
|
||||
results = await asyncio.gather(*batch_futures, return_exceptions=True)
|
||||
successful_count += sum(1 for r in results if isinstance(r, bool) and r)
|
||||
|
||||
return successful_count
|
||||
|
||||
def _is_fragment_processed(self, fragment: Fragment, index: int) -> bool:
|
||||
"""检查片段是否已处理"""
|
||||
fragment_id = f"{fragment.arxiv_id}_{index}"
|
||||
with self._processing_lock:
|
||||
return fragment_id in self._processed_fragments
|
||||
|
||||
def _safe_add_to_vector_store_sync(self, text: str) -> bool:
|
||||
"""线程安全的向量存储添加"""
|
||||
with self._processing_lock:
|
||||
try:
|
||||
self.rag_worker.add_text_to_vector_store(text)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding to vector store: {str(e)}")
|
||||
return False
|
||||
|
||||
def _process_single_fragment_sync(self, fragment: Fragment, index: int) -> bool:
|
||||
"""处理单个片段"""
|
||||
fragment_id = f"{fragment.arxiv_id}_{index}"
|
||||
try:
|
||||
text = self._build_fragment_text(fragment)
|
||||
if self._safe_add_to_vector_store_sync(text):
|
||||
with self._processing_lock:
|
||||
self._processed_fragments.add(fragment_id)
|
||||
self._processed_count += 1
|
||||
logger.info(f"Successfully processed fragment {index}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing fragment {index}: {str(e)}")
|
||||
return False
|
||||
|
||||
def _build_fragment_text(self, fragment: Fragment) -> str:
|
||||
"""构建片段文本"""
|
||||
return "".join([
|
||||
f"Paper Title: {fragment.title}\n",
|
||||
f"Section: {fragment.current_section}\n",
|
||||
f"Content: {fragment.content}\n",
|
||||
f"Bibliography: {fragment.bibliography}\n",
|
||||
"Type: FRAGMENT"
|
||||
])
|
||||
|
||||
async def _save_results(self, fragments: List[Fragment], arxiv_id: str, successful_count: int) -> None:
|
||||
"""保存处理结果"""
|
||||
if successful_count > 0:
|
||||
loop = self._ensure_loop()
|
||||
await loop.run_in_executor(
|
||||
self.thread_pool,
|
||||
save_fragments_to_file,
|
||||
fragments,
|
||||
str(self.fragment_store_dir / f"{arxiv_id}_fragments.json")
|
||||
)
|
||||
|
||||
def _log_processing_stats(self, start_time: float, total_fragments: int) -> None:
|
||||
"""记录处理统计信息"""
|
||||
elapsed_time = time.time() - start_time
|
||||
processing_rate = total_fragments / elapsed_time if elapsed_time > 0 else 0
|
||||
logger.info(
|
||||
f"Processed {self._processed_count}/{total_fragments} fragments "
|
||||
f"in {elapsed_time:.2f}s (rate: {processing_rate:.2f} fragments/s)"
|
||||
)
|
||||
|
||||
async def process_paper(self, fragments: List[Fragment]) -> bool:
|
||||
"""处理论文主函数"""
|
||||
try:
|
||||
|
||||
if self.paper_path.exists():
|
||||
logger.info(f"Paper {self.arxiv_id} already processed")
|
||||
return True
|
||||
|
||||
task = self._create_processing_task(self.arxiv_id)
|
||||
try:
|
||||
async with self.semaphore:
|
||||
await self._process_fragments(fragments)
|
||||
self._complete_task(task, fragments, self.paper_path)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._fail_task(task, str(e))
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing paper {self.arxiv_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def _create_processing_task(self, arxiv_id: str) -> ProcessingTask:
|
||||
"""创建处理任务"""
|
||||
task = ProcessingTask(arxiv_id=arxiv_id)
|
||||
with self._processing_lock:
|
||||
self.processing_queue[arxiv_id] = task
|
||||
task.status = "processing"
|
||||
return task
|
||||
|
||||
def _complete_task(self, task: ProcessingTask, fragments: List[Fragment], paper_path: Path) -> None:
|
||||
"""完成任务处理"""
|
||||
with self._processing_lock:
|
||||
task.status = "completed"
|
||||
task.fragments = fragments
|
||||
paper_path.touch()
|
||||
logger.info(f"Paper {task.arxiv_id} processed successfully with {self._processed_count} fragments")
|
||||
|
||||
def _fail_task(self, task: ProcessingTask, error: str) -> None:
|
||||
"""任务失败处理"""
|
||||
with self._processing_lock:
|
||||
task.status = "failed"
|
||||
task.error = error
|
||||
|
||||
def _normalize_arxiv_id(self, input_str: str) -> str:
|
||||
"""规范化ArXiv ID"""
|
||||
if not input_str:
|
||||
return ""
|
||||
|
||||
input_str = input_str.strip().lower()
|
||||
if 'arxiv.org/' in input_str:
|
||||
if '/pdf/' in input_str:
|
||||
arxiv_id = input_str.split('/pdf/')[-1]
|
||||
else:
|
||||
arxiv_id = input_str.split('/abs/')[-1]
|
||||
return arxiv_id.split('v')[0].strip()
|
||||
return input_str.split('v')[0].strip()
|
||||
|
||||
async def wait_for_paper(self, arxiv_id: str, timeout: float = 300.0) -> bool:
|
||||
"""等待论文处理完成"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
while True:
|
||||
with self._processing_lock:
|
||||
task = self.processing_queue.get(arxiv_id)
|
||||
if not task:
|
||||
return False
|
||||
|
||||
if task.status == "completed":
|
||||
return True
|
||||
if task.status == "failed":
|
||||
return False
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
logger.error(f"Processing paper {arxiv_id} timed out")
|
||||
return False
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error waiting for paper {arxiv_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def retrieve_and_generate(self, query: str) -> str:
|
||||
"""检索相关内容并生成提示词"""
|
||||
try:
|
||||
nodes = self.rag_worker.retrieve_from_store_with_query(query)
|
||||
return self.rag_worker.build_prompt(query=query, nodes=nodes)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in retrieve and generate: {str(e)}")
|
||||
return ""
|
||||
|
||||
def remember_qa(self, question: str, answer: str) -> None:
|
||||
"""记忆问答对"""
|
||||
try:
|
||||
self.rag_worker.remember_qa(question, answer)
|
||||
except Exception as e:
|
||||
logger.error(f"Error remembering QA: {str(e)}")
|
||||
|
||||
async def auto_analyze_paper(self, chatbot: List, history: List, system_prompt: str) -> None:
|
||||
"""自动分析论文的关键问题"""
|
||||
key_questions = [
|
||||
"What is the main research question or problem addressed in this paper?",
|
||||
"What methods or approaches did the authors use to investigate the problem?",
|
||||
"What are the key findings or results presented in the paper?",
|
||||
"How do the findings of this paper contribute to the broader field or topic of study?",
|
||||
"What are the limitations of this study, and what future research directions do the authors suggest?"
|
||||
]
|
||||
|
||||
results = []
|
||||
for question in key_questions:
|
||||
try:
|
||||
prompt = self.retrieve_and_generate(question)
|
||||
if prompt:
|
||||
response = await request_gpt_model_in_new_thread_with_ui_alive(
|
||||
inputs=prompt,
|
||||
inputs_show_user=question,
|
||||
llm_kwargs=self.llm_kwargs,
|
||||
chatbot=chatbot,
|
||||
history=history,
|
||||
sys_prompt=system_prompt
|
||||
)
|
||||
results.append(f"Q: {question}\nA: {response}\n")
|
||||
self.remember_qa(question, response)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto analysis: {str(e)}")
|
||||
|
||||
# 合并所有结果
|
||||
summary = "\n\n".join(results)
|
||||
chatbot[-1] = (chatbot[-1][0], f"论文已成功加载并完成初步分析:\n\n{summary}\n\n您现在可以继续提问更多细节。")
|
||||
|
||||
@CatchException
|
||||
def Arxiv论文对话(txt: str, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List,
|
||||
history: List, system_prompt: str, web_port: str) -> Generator:
|
||||
"""
|
||||
Arxiv论文对话主函数
|
||||
Args:
|
||||
txt: arxiv ID/URL
|
||||
llm_kwargs: LLM配置参数
|
||||
plugin_kwargs: 插件配置参数,包含 advanced_arg 字段作为用户询问指令
|
||||
chatbot: 对话历史
|
||||
history: 聊天历史
|
||||
system_prompt: 系统提示词
|
||||
web_port: Web端口
|
||||
"""
|
||||
# 初始化时,提示用户需要 arxiv ID/URL
|
||||
from toolbox import promote_file_to_downloadzone
|
||||
if len(history) == 0 and not txt.lower().strip().startswith(('https://arxiv.org', 'arxiv.org', '0', '1', '2')):
|
||||
chatbot.append((txt, "请先提供Arxiv论文链接或ID。"))
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
user_name = chatbot.get_user()
|
||||
arxiv_worker = ArxivRagWorker(user_name, llm_kwargs, arxiv_id=txt)
|
||||
arxiv_id = arxiv_worker.arxiv_id
|
||||
|
||||
# 处理新论文的情况
|
||||
if txt.lower().strip().startswith(('https://arxiv.org', 'arxiv.org', '0', '1', '2')) and not arxiv_worker.loading:
|
||||
chatbot.append((txt, "正在处理论文,请稍等..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
fragments, formatted_content, fragment_output_files = process_arxiv_sync(arxiv_worker.arxiv_splitter, arxiv_id)
|
||||
for file in fragment_output_files:
|
||||
promote_file_to_downloadzone(file, chatbot=chatbot)
|
||||
chatbot.append(["论文文字内容已保存至下载区,接下来将进行论文编码,请耐心等待三分钟,论文的文字内容为:", formatted_content])
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
try:
|
||||
# 创建新的事件循环
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
# 设置超时时间为5分钟
|
||||
success = loop.run_until_complete(
|
||||
asyncio.wait_for(arxiv_worker.process_paper(fragments), timeout=300)
|
||||
)
|
||||
if success:
|
||||
success = loop.run_until_complete(
|
||||
asyncio.wait_for(arxiv_worker.wait_for_paper(arxiv_id), timeout=60)
|
||||
)
|
||||
if success:
|
||||
chatbot[-1] = (txt, "论文处理完成,您现在可以开始提问。")
|
||||
else:
|
||||
chatbot[-1] = (txt, "论文处理超时,请重试。")
|
||||
else:
|
||||
chatbot[-1] = (txt, "论文处理失败,请检查论文ID是否正确或稍后重试。")
|
||||
except asyncio.TimeoutError:
|
||||
chatbot[-1] = (txt, "论文处理超时,请重试。")
|
||||
success = False
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
if not success:
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in main process: {str(e)}")
|
||||
chatbot[-1] = (txt, f"处理过程中发生错误: {str(e)}")
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
# 处理用户询问的情况
|
||||
# 获取用户询问指令
|
||||
user_query = plugin_kwargs.get("advanced_arg",
|
||||
"What is the main research question or problem addressed in this paper?")
|
||||
if len(history)<2:
|
||||
fragments, formatted_content, fragment_output_files = process_arxiv_sync(arxiv_worker.arxiv_splitter, arxiv_id)
|
||||
for file in fragment_output_files:
|
||||
promote_file_to_downloadzone(file, chatbot=chatbot)
|
||||
chatbot.append(["论文文字内容已保存至下载区,论文的文字内容为:", formatted_content])
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
if not user_query:
|
||||
user_query = "What is the main research question or problem addressed in this paper?"
|
||||
# chatbot.append((txt, "请提供您的问题。"))
|
||||
# yield from update_ui(chatbot=chatbot, history=history)
|
||||
# return
|
||||
|
||||
# 处理历史对话长度
|
||||
if len(history) > MAX_HISTORY_ROUND * 2:
|
||||
history = history[-(MAX_HISTORY_ROUND * 2):]
|
||||
|
||||
# 处理询问指令
|
||||
query_clip, history, flags = input_clipping(
|
||||
user_query,
|
||||
history,
|
||||
max_token_limit=MAX_CONTEXT_TOKEN_LIMIT,
|
||||
return_clip_flags=True
|
||||
)
|
||||
|
||||
if flags["original_input_len"] != flags["clipped_input_len"]:
|
||||
yield from update_ui_lastest_msg('检测到长输入,正在处理...', chatbot, history, delay=0)
|
||||
if len(user_query) > REMEMBER_PREVIEW:
|
||||
HALF = REMEMBER_PREVIEW // 2
|
||||
query_to_remember = user_query[
|
||||
:HALF] + f" ...\n...(省略{len(user_query) - REMEMBER_PREVIEW}字)...\n... " + user_query[
|
||||
-HALF:]
|
||||
else:
|
||||
query_to_remember = query_clip
|
||||
else:
|
||||
query_to_remember = query_clip
|
||||
|
||||
chatbot.append((user_query, "正在思考中..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
|
||||
# 生成提示词
|
||||
prompt = arxiv_worker.retrieve_and_generate(query_clip)
|
||||
if not prompt:
|
||||
chatbot[-1] = (user_query, "抱歉,处理您的问题时出现错误,请重试。")
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
# 获取回答
|
||||
response = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||
inputs=prompt,
|
||||
inputs_show_user=query_clip,
|
||||
llm_kwargs=llm_kwargs,
|
||||
chatbot=chatbot,
|
||||
history=history,
|
||||
sys_prompt=system_prompt
|
||||
)
|
||||
|
||||
# 记忆问答对
|
||||
# worker.remember_qa(query_to_remember, response)
|
||||
history.extend([user_query, response])
|
||||
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
llm_kwargs = {
|
||||
'api_key': os.getenv("one_api_key"),
|
||||
'client_ip': '127.0.0.1',
|
||||
'embed_model': 'text-embedding-3-small',
|
||||
'llm_model': 'one-api-Qwen2.5-72B-Instruct',
|
||||
'max_length': 4096,
|
||||
'most_recent_uploaded': None,
|
||||
'temperature': 1,
|
||||
'top_p': 1
|
||||
}
|
||||
plugin_kwargs = {}
|
||||
chatbot = []
|
||||
history = []
|
||||
system_prompt = "You are a helpful assistant."
|
||||
web_port = "8080"
|
||||
|
||||
# 测试论文导入
|
||||
arxiv_url = "https://arxiv.org/abs/2312.12345"
|
||||
for response in Arxiv论文对话(
|
||||
arxiv_url, llm_kwargs, plugin_kwargs,
|
||||
chatbot, history, system_prompt, web_port
|
||||
):
|
||||
print(response)
|
||||
|
||||
# 测试问答
|
||||
question = "这篇论文的主要贡献是什么?"
|
||||
for response in Arxiv论文对话(
|
||||
question, llm_kwargs, plugin_kwargs,
|
||||
chatbot, history, system_prompt, web_port
|
||||
):
|
||||
print(response)
|
||||
@@ -1,10 +1,11 @@
|
||||
from toolbox import CatchException, update_ui, promote_file_to_downloadzone, get_log_folder, get_user
|
||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
||||
import re
|
||||
from toolbox import CatchException, update_ui, promote_file_to_downloadzone, get_log_folder, get_user, update_ui_latest_msg
|
||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
||||
from loguru import logger
|
||||
|
||||
f_prefix = 'GPT-Academic对话存档'
|
||||
|
||||
def write_chat_to_file(chatbot, history=None, file_name=None):
|
||||
def write_chat_to_file_legacy(chatbot, history=None, file_name=None):
|
||||
"""
|
||||
将对话记录history以Markdown格式写入文件中。如果没有指定文件名,则使用当前时间生成文件名。
|
||||
"""
|
||||
@@ -12,6 +13,9 @@ def write_chat_to_file(chatbot, history=None, file_name=None):
|
||||
import time
|
||||
from themes.theme import advanced_css
|
||||
|
||||
if (file_name is not None) and (file_name != "") and (not file_name.endswith('.html')): file_name += '.html'
|
||||
else: file_name = None
|
||||
|
||||
if file_name is None:
|
||||
file_name = f_prefix + time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + '.html'
|
||||
fp = os.path.join(get_log_folder(get_user(chatbot), plugin_name='chat_history'), file_name)
|
||||
@@ -68,6 +72,147 @@ def write_chat_to_file(chatbot, history=None, file_name=None):
|
||||
promote_file_to_downloadzone(fp, rename_file=file_name, chatbot=chatbot)
|
||||
return '对话历史写入:' + fp
|
||||
|
||||
def write_chat_to_file(chatbot, history=None, file_name=None):
|
||||
"""
|
||||
将对话记录history以多种格式(HTML、Word、Markdown)写入文件中。如果没有指定文件名,则使用当前时间生成文件名。
|
||||
|
||||
Args:
|
||||
chatbot: 聊天机器人对象,包含对话内容
|
||||
history: 对话历史记录
|
||||
file_name: 指定的文件名,如果为None则使用时间戳
|
||||
|
||||
Returns:
|
||||
str: 提示信息,包含文件保存路径
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
import aiofiles
|
||||
from toolbox import promote_file_to_downloadzone
|
||||
from crazy_functions.doc_fns.conversation_doc.excel_doc import save_chat_tables
|
||||
from crazy_functions.doc_fns.conversation_doc.html_doc import HtmlFormatter
|
||||
from crazy_functions.doc_fns.conversation_doc.markdown_doc import MarkdownFormatter
|
||||
from crazy_functions.doc_fns.conversation_doc.word_doc import WordFormatter
|
||||
from crazy_functions.doc_fns.conversation_doc.txt_doc import TxtFormatter
|
||||
from crazy_functions.doc_fns.conversation_doc.word2pdf import WordToPdfConverter
|
||||
|
||||
async def save_html():
|
||||
try:
|
||||
html_formatter = HtmlFormatter(chatbot, history)
|
||||
html_content = html_formatter.create_document()
|
||||
html_file = os.path.join(save_dir, base_name + '.html')
|
||||
async with aiofiles.open(html_file, 'w', encoding='utf8') as f:
|
||||
await f.write(html_content)
|
||||
return html_file
|
||||
except Exception as e:
|
||||
print(f"保存HTML格式失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def save_word():
|
||||
try:
|
||||
word_formatter = WordFormatter()
|
||||
doc = word_formatter.create_document(history)
|
||||
docx_file = os.path.join(save_dir, base_name + '.docx')
|
||||
# 由于python-docx不支持异步,使用线程池执行
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, doc.save, docx_file)
|
||||
return docx_file
|
||||
except Exception as e:
|
||||
print(f"保存Word格式失败: {str(e)}")
|
||||
return None
|
||||
async def save_pdf(docx_file):
|
||||
try:
|
||||
if docx_file:
|
||||
# 获取文件名和保存路径
|
||||
pdf_file = os.path.join(save_dir, base_name + '.pdf')
|
||||
|
||||
# 在线程池中执行转换
|
||||
loop = asyncio.get_event_loop()
|
||||
pdf_file = await loop.run_in_executor(
|
||||
None,
|
||||
WordToPdfConverter.convert_to_pdf,
|
||||
docx_file
|
||||
# save_dir
|
||||
)
|
||||
|
||||
return pdf_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存PDF格式失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def save_markdown():
|
||||
try:
|
||||
md_formatter = MarkdownFormatter()
|
||||
md_content = md_formatter.create_document(history)
|
||||
md_file = os.path.join(save_dir, base_name + '.md')
|
||||
async with aiofiles.open(md_file, 'w', encoding='utf8') as f:
|
||||
await f.write(md_content)
|
||||
return md_file
|
||||
except Exception as e:
|
||||
print(f"保存Markdown格式失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def save_txt():
|
||||
try:
|
||||
txt_formatter = TxtFormatter()
|
||||
txt_content = txt_formatter.create_document(history)
|
||||
txt_file = os.path.join(save_dir, base_name + '.txt')
|
||||
async with aiofiles.open(txt_file, 'w', encoding='utf8') as f:
|
||||
await f.write(txt_content)
|
||||
return txt_file
|
||||
except Exception as e:
|
||||
print(f"保存TXT格式失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def main():
|
||||
# 并发执行所有保存任务
|
||||
html_task = asyncio.create_task(save_html())
|
||||
word_task = asyncio.create_task(save_word())
|
||||
md_task = asyncio.create_task(save_markdown())
|
||||
txt_task = asyncio.create_task(save_txt())
|
||||
|
||||
# 等待所有任务完成
|
||||
html_file = await html_task
|
||||
docx_file = await word_task
|
||||
md_file = await md_task
|
||||
txt_file = await txt_task
|
||||
|
||||
# PDF转换需要等待word文件生成完成
|
||||
pdf_file = await save_pdf(docx_file)
|
||||
# 收集所有成功生成的文件
|
||||
result_files = [f for f in [html_file, docx_file, md_file, txt_file, pdf_file] if f]
|
||||
|
||||
# 保存Excel表格
|
||||
excel_files = save_chat_tables(history, save_dir, base_name)
|
||||
result_files.extend(excel_files)
|
||||
|
||||
return result_files
|
||||
|
||||
# 生成时间戳
|
||||
timestamp = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
|
||||
|
||||
# 获取保存目录
|
||||
save_dir = get_log_folder(get_user(chatbot), plugin_name='chat_history')
|
||||
|
||||
# 处理文件名
|
||||
base_name = file_name if file_name else f"聊天记录_{timestamp}"
|
||||
|
||||
# 运行异步任务
|
||||
result_files = asyncio.run(main())
|
||||
|
||||
# 将生成的文件添加到下载区
|
||||
for file in result_files:
|
||||
promote_file_to_downloadzone(file, rename_file=os.path.basename(file), chatbot=chatbot)
|
||||
|
||||
# 如果没有成功保存任何文件,返回错误信息
|
||||
if not result_files:
|
||||
return "保存对话记录失败,请检查错误日志"
|
||||
|
||||
ext_list = [os.path.splitext(f)[1] for f in result_files]
|
||||
# 返回成功信息和文件路径
|
||||
return f"对话历史已保存至以下格式文件:" + "、".join(ext_list)
|
||||
|
||||
def gen_file_preview(file_name):
|
||||
try:
|
||||
with open(file_name, 'r', encoding='utf8') as f:
|
||||
@@ -119,12 +264,21 @@ def 对话历史存档(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_
|
||||
user_request 当前用户的请求信息(IP地址等)
|
||||
"""
|
||||
file_name = plugin_kwargs.get("file_name", None)
|
||||
if (file_name is not None) and (file_name != "") and (not file_name.endswith('.html')): file_name += '.html'
|
||||
else: file_name = None
|
||||
|
||||
chatbot.append((None, f"[Local Message] {write_chat_to_file(chatbot, history, file_name)},您可以调用下拉菜单中的“载入对话历史存档”还原当下的对话。"))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 由于请求gpt需要一段时间,我们先及时地做一次界面更新
|
||||
|
||||
chatbot.append((None, f"[Local Message] {write_chat_to_file_legacy(chatbot, history, file_name)},您可以调用下拉菜单中的“载入对话历史存档”还原当下的对话。"))
|
||||
try:
|
||||
chatbot.append((None, f"[Local Message] 正在尝试生成pdf以及word格式的对话存档,请稍等..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 由于请求需要一段时间,我们先及时地做一次界面更新
|
||||
lastmsg = f"[Local Message] {write_chat_to_file(chatbot, history, file_name)}。" \
|
||||
f"您可以调用下拉菜单中的“载入对话历史会话”还原当下的对话,请注意,目前只支持html格式载入历史。" \
|
||||
f"当模型回答中存在表格,将提取表格内容存储为Excel的xlsx格式,如果你提供一些数据,然后输入指令要求模型帮你整理为表格" \
|
||||
f"(如“请帮我将下面的数据整理为表格:”),再利用此插件就可以获取到Excel表格。"
|
||||
yield from update_ui_latest_msg(lastmsg, chatbot, history) # 刷新界面 # 由于请求需要一段时间,我们先及时地做一次界面更新
|
||||
except Exception as e:
|
||||
logger.exception(f"已完成对话存档(pdf和word格式的对话存档生成未成功)。{str(e)}")
|
||||
lastmsg = "已完成对话存档(pdf和word格式的对话存档生成未成功)。"
|
||||
yield from update_ui_latest_msg(lastmsg, chatbot, history) # 刷新界面 # 由于请求需要一段时间,我们先及时地做一次界面更新
|
||||
return
|
||||
|
||||
class Conversation_To_File_Wrap(GptAcademicPluginTemplate):
|
||||
def __init__(self):
|
||||
@@ -152,6 +306,8 @@ class Conversation_To_File_Wrap(GptAcademicPluginTemplate):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def hide_cwd(str):
|
||||
import os
|
||||
current_path = os.getcwd()
|
||||
|
||||
@@ -7,7 +7,7 @@ from bs4 import BeautifulSoup
|
||||
from functools import lru_cache
|
||||
from itertools import zip_longest
|
||||
from check_proxy import check_proxy
|
||||
from toolbox import CatchException, update_ui, get_conf
|
||||
from toolbox import CatchException, update_ui, get_conf, update_ui_latest_msg
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive, input_clipping
|
||||
from request_llms.bridge_all import model_info
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
@@ -49,7 +49,7 @@ def search_optimizer(
|
||||
mutable = ["", time.time(), ""]
|
||||
llm_kwargs["temperature"] = 0.8
|
||||
try:
|
||||
querys_json = predict_no_ui_long_connection(
|
||||
query_json = predict_no_ui_long_connection(
|
||||
inputs=query,
|
||||
llm_kwargs=llm_kwargs,
|
||||
history=[],
|
||||
@@ -57,31 +57,31 @@ def search_optimizer(
|
||||
observe_window=mutable,
|
||||
)
|
||||
except Exception:
|
||||
querys_json = "1234"
|
||||
query_json = "null"
|
||||
#* 尝试解码优化后的搜索结果
|
||||
querys_json = re.sub(r"```json|```", "", querys_json)
|
||||
query_json = re.sub(r"```json|```", "", query_json)
|
||||
try:
|
||||
querys = json.loads(querys_json)
|
||||
queries = json.loads(query_json)
|
||||
except Exception:
|
||||
#* 如果解码失败,降低温度再试一次
|
||||
try:
|
||||
llm_kwargs["temperature"] = 0.4
|
||||
querys_json = predict_no_ui_long_connection(
|
||||
query_json = predict_no_ui_long_connection(
|
||||
inputs=query,
|
||||
llm_kwargs=llm_kwargs,
|
||||
history=[],
|
||||
sys_prompt=sys_prompt,
|
||||
observe_window=mutable,
|
||||
)
|
||||
querys_json = re.sub(r"```json|```", "", querys_json)
|
||||
querys = json.loads(querys_json)
|
||||
query_json = re.sub(r"```json|```", "", query_json)
|
||||
queries = json.loads(query_json)
|
||||
except Exception:
|
||||
#* 如果再次失败,直接返回原始问题
|
||||
querys = [query]
|
||||
queries = [query]
|
||||
links = []
|
||||
success = 0
|
||||
Exceptions = ""
|
||||
for q in querys:
|
||||
for q in queries:
|
||||
try:
|
||||
link = searxng_request(q, proxies, categories, searxng_url, engines=engines)
|
||||
if len(link) > 0:
|
||||
@@ -115,7 +115,8 @@ def get_auth_ip():
|
||||
|
||||
def searxng_request(query, proxies, categories='general', searxng_url=None, engines=None):
|
||||
if searxng_url is None:
|
||||
url = get_conf("SEARXNG_URL")
|
||||
urls = get_conf("SEARXNG_URLS")
|
||||
url = random.choice(urls)
|
||||
else:
|
||||
url = searxng_url
|
||||
|
||||
@@ -174,10 +175,17 @@ def scrape_text(url, proxies) -> str:
|
||||
Returns:
|
||||
str: The scraped text
|
||||
"""
|
||||
from loguru import logger
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36',
|
||||
'Content-Type': 'text/plain',
|
||||
}
|
||||
|
||||
# 首先采用Jina进行文本提取
|
||||
if get_conf("JINA_API_KEY"):
|
||||
try: return jina_scrape_text(url)
|
||||
except: logger.debug("Jina API 请求失败,回到旧方法")
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, proxies=proxies, timeout=8)
|
||||
if response.encoding == "ISO-8859-1": response.encoding = response.apparent_encoding
|
||||
@@ -193,6 +201,56 @@ def scrape_text(url, proxies) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def jina_scrape_text(url) -> str:
|
||||
"jina_39727421c8fa4e4fa9bd698e5211feaaDyGeVFESNrRaepWiLT0wmHYJSh-d"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36',
|
||||
'Content-Type': 'text/plain',
|
||||
"X-Retain-Images": "none",
|
||||
"Authorization": f'Bearer {get_conf("JINA_API_KEY")}'
|
||||
}
|
||||
response = requests.get("https://r.jina.ai/" + url, headers=headers, proxies=None, timeout=8)
|
||||
if response.status_code != 200:
|
||||
raise ValueError("Jina API 请求失败,开始尝试旧方法!" + response.text)
|
||||
if response.encoding == "ISO-8859-1": response.encoding = response.apparent_encoding
|
||||
result = response.text
|
||||
result = result.replace("\\[", "[").replace("\\]", "]").replace("\\(", "(").replace("\\)", ")")
|
||||
return response.text
|
||||
|
||||
|
||||
def internet_search_with_analysis_prompt(prompt, analysis_prompt, llm_kwargs, chatbot):
|
||||
from toolbox import get_conf
|
||||
proxies = get_conf('proxies')
|
||||
categories = 'general'
|
||||
searxng_url = None # 使用默认的searxng_url
|
||||
engines = None # 使用默认的搜索引擎
|
||||
yield from update_ui_latest_msg(lastmsg=f"检索中: {prompt} ...", chatbot=chatbot, history=[], delay=1)
|
||||
urls = searxng_request(prompt, proxies, categories, searxng_url, engines=engines)
|
||||
yield from update_ui_latest_msg(lastmsg=f"依次访问搜索到的网站 ...", chatbot=chatbot, history=[], delay=1)
|
||||
if len(urls) == 0:
|
||||
return None
|
||||
max_search_result = 5 # 最多收纳多少个网页的结果
|
||||
history = []
|
||||
for index, url in enumerate(urls[:max_search_result]):
|
||||
yield from update_ui_latest_msg(lastmsg=f"依次访问搜索到的网站: {url['link']} ...", chatbot=chatbot, history=[], delay=1)
|
||||
res = scrape_text(url['link'], proxies)
|
||||
prefix = f"第{index}份搜索结果 [源自{url['source'][0]}搜索] ({url['title'][:25]}):"
|
||||
history.extend([prefix, res])
|
||||
i_say = f"从以上搜索结果中抽取信息,然后回答问题:{prompt} {analysis_prompt}"
|
||||
i_say, history = input_clipping( # 裁剪输入,从最长的条目开始裁剪,防止爆token
|
||||
inputs=i_say,
|
||||
history=history,
|
||||
max_token_limit=8192
|
||||
)
|
||||
gpt_say = predict_no_ui_long_connection(
|
||||
inputs=i_say,
|
||||
llm_kwargs=llm_kwargs,
|
||||
history=history,
|
||||
sys_prompt="请从搜索结果中抽取信息,对最相关的两个搜索结果进行总结,然后回答问题。",
|
||||
console_silence=False,
|
||||
)
|
||||
return gpt_say
|
||||
|
||||
@CatchException
|
||||
def 连接网络回答问题(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
optimizer_history = history[:-8]
|
||||
@@ -213,23 +271,52 @@ def 连接网络回答问题(txt, llm_kwargs, plugin_kwargs, chatbot, history, s
|
||||
urls = search_optimizer(txt, proxies, optimizer_history, llm_kwargs, optimizer, categories, searxng_url, engines)
|
||||
history = []
|
||||
if len(urls) == 0:
|
||||
chatbot.append((f"结论:{txt}",
|
||||
"[Local Message] 受到限制,无法从searxng获取信息!请尝试更换搜索引擎。"))
|
||||
chatbot.append((f"结论:{txt}", "[Local Message] 受到限制,无法从searxng获取信息!请尝试更换搜索引擎。"))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
return
|
||||
|
||||
# ------------- < 第2步:依次访问网页 > -------------
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from textwrap import dedent
|
||||
max_search_result = 5 # 最多收纳多少个网页的结果
|
||||
if optimizer == "开启(增强)":
|
||||
max_search_result = 8
|
||||
chatbot.append(["联网检索中 ...", None])
|
||||
for index, url in enumerate(urls[:max_search_result]):
|
||||
res = scrape_text(url['link'], proxies)
|
||||
prefix = f"第{index}份搜索结果 [源自{url['source'][0]}搜索] ({url['title'][:25]}):"
|
||||
history.extend([prefix, res])
|
||||
res_squeeze = res.replace('\n', '...')
|
||||
chatbot[-1] = [prefix + "\n\n" + res_squeeze[:500] + "......", None]
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
template = dedent("""
|
||||
<details>
|
||||
<summary>{TITLE}</summary>
|
||||
<div class="search_result">{URL}</div>
|
||||
<div class="search_result">{CONTENT}</div>
|
||||
</details>
|
||||
""")
|
||||
|
||||
buffer = ""
|
||||
|
||||
# 创建线程池
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
# 提交任务到线程池
|
||||
futures = []
|
||||
for index, url in enumerate(urls[:max_search_result]):
|
||||
future = executor.submit(scrape_text, url['link'], proxies)
|
||||
futures.append((index, future, url))
|
||||
|
||||
# 处理完成的任务
|
||||
for index, future, url in futures:
|
||||
# 开始
|
||||
prefix = f"正在加载 第{index+1}份搜索结果 [源自{url['source'][0]}搜索] ({url['title'][:25]}):"
|
||||
string_structure = template.format(TITLE=prefix, URL=url['link'], CONTENT="正在加载,请稍后 ......")
|
||||
yield from update_ui_latest_msg(lastmsg=(buffer + string_structure), chatbot=chatbot, history=history, delay=0.1) # 刷新界面
|
||||
|
||||
# 获取结果
|
||||
res = future.result()
|
||||
|
||||
# 显示结果
|
||||
prefix = f"第{index+1}份搜索结果 [源自{url['source'][0]}搜索] ({url['title'][:25]}):"
|
||||
string_structure = template.format(TITLE=prefix, URL=url['link'], CONTENT=res[:1000] + "......")
|
||||
buffer += string_structure
|
||||
|
||||
# 更新历史
|
||||
history.extend([prefix, res])
|
||||
yield from update_ui_latest_msg(lastmsg=buffer, chatbot=chatbot, history=history, delay=0.1) # 刷新界面
|
||||
|
||||
# ------------- < 第3步:ChatGPT综合 > -------------
|
||||
if (optimizer != "开启(增强)"):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import random
|
||||
from toolbox import get_conf
|
||||
from crazy_functions.Internet_GPT import 连接网络回答问题
|
||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
||||
@@ -20,6 +20,9 @@ class NetworkGPT_Wrap(GptAcademicPluginTemplate):
|
||||
第三个参数,名称`allow_cache`,参数`type`声明这是一个下拉菜单,下拉菜单上方显示`title`+`description`,下拉菜单的选项为`options`,`default_value`为下拉菜单默认值;
|
||||
|
||||
"""
|
||||
urls = get_conf("SEARXNG_URLS")
|
||||
url = random.choice(urls)
|
||||
|
||||
gui_definition = {
|
||||
"main_input":
|
||||
ArgProperty(title="输入问题", description="待通过互联网检索的问题,会自动读取输入框内容", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
||||
@@ -30,16 +33,17 @@ class NetworkGPT_Wrap(GptAcademicPluginTemplate):
|
||||
"optimizer":
|
||||
ArgProperty(title="搜索优化", options=["关闭", "开启", "开启(增强)"], default_value="关闭", description="是否使用搜索增强。注意这可能会消耗较多token", type="dropdown").model_dump_json(),
|
||||
"searxng_url":
|
||||
ArgProperty(title="Searxng服务地址", description="输入Searxng的地址", default_value=get_conf("SEARXNG_URL"), type="string").model_dump_json(), # 主输入,自动从输入框同步
|
||||
ArgProperty(title="Searxng服务地址", description="输入Searxng的地址", default_value=url, type="string").model_dump_json(), # 主输入,自动从输入框同步
|
||||
|
||||
}
|
||||
return gui_definition
|
||||
|
||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
def execute(txt, llm_kwargs, plugin_kwargs:dict, chatbot, history, system_prompt, user_request):
|
||||
"""
|
||||
执行插件
|
||||
"""
|
||||
if plugin_kwargs["categories"] == "网页": plugin_kwargs["categories"] = "general"
|
||||
if plugin_kwargs["categories"] == "学术论文": plugin_kwargs["categories"] = "science"
|
||||
if plugin_kwargs.get("categories", None) == "网页": plugin_kwargs["categories"] = "general"
|
||||
elif plugin_kwargs.get("categories", None) == "学术论文": plugin_kwargs["categories"] = "science"
|
||||
else: plugin_kwargs["categories"] = "general"
|
||||
yield from 连接网络回答问题(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from toolbox import update_ui, trimmed_format_exc, get_conf, get_log_folder, promote_file_to_downloadzone, check_repeat_upload, map_file_to_sha256
|
||||
from toolbox import CatchException, report_exception, update_ui_lastest_msg, zip_result, gen_time_str
|
||||
from toolbox import CatchException, report_exception, update_ui_latest_msg, zip_result, gen_time_str
|
||||
from functools import partial
|
||||
from loguru import logger
|
||||
|
||||
@@ -41,7 +41,7 @@ def switch_prompt(pfg, mode, more_requirement):
|
||||
return inputs_array, sys_prompt_array
|
||||
|
||||
|
||||
def desend_to_extracted_folder_if_exist(project_folder):
|
||||
def descend_to_extracted_folder_if_exist(project_folder):
|
||||
"""
|
||||
Descend into the extracted folder if it exists, otherwise return the original folder.
|
||||
|
||||
@@ -130,7 +130,7 @@ def arxiv_download(chatbot, history, txt, allow_cache=True):
|
||||
|
||||
if not txt.startswith('https://arxiv.org/abs/'):
|
||||
msg = f"解析arxiv网址失败, 期望格式例如: https://arxiv.org/abs/1707.06690。实际得到格式: {url_}。"
|
||||
yield from update_ui_lastest_msg(msg, chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui_latest_msg(msg, chatbot=chatbot, history=history) # 刷新界面
|
||||
return msg, None
|
||||
# <-------------- set format ------------->
|
||||
arxiv_id = url_.split('/abs/')[-1]
|
||||
@@ -156,16 +156,16 @@ def arxiv_download(chatbot, history, txt, allow_cache=True):
|
||||
return False
|
||||
|
||||
if os.path.exists(dst) and allow_cache:
|
||||
yield from update_ui_lastest_msg(f"调用缓存 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui_latest_msg(f"调用缓存 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
success = True
|
||||
else:
|
||||
yield from update_ui_lastest_msg(f"开始下载 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui_latest_msg(f"开始下载 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
success = fix_url_and_download()
|
||||
yield from update_ui_lastest_msg(f"下载完成 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui_latest_msg(f"下载完成 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
|
||||
if not success:
|
||||
yield from update_ui_lastest_msg(f"下载失败 {arxiv_id}", chatbot=chatbot, history=history)
|
||||
yield from update_ui_latest_msg(f"下载失败 {arxiv_id}", chatbot=chatbot, history=history)
|
||||
raise tarfile.ReadError(f"论文下载失败 {arxiv_id}")
|
||||
|
||||
# <-------------- extract file ------------->
|
||||
@@ -288,7 +288,7 @@ def Latex英文纠错加PDF对比(txt, llm_kwargs, plugin_kwargs, chatbot, histo
|
||||
return
|
||||
|
||||
# <-------------- if is a zip/tar file ------------->
|
||||
project_folder = desend_to_extracted_folder_if_exist(project_folder)
|
||||
project_folder = descend_to_extracted_folder_if_exist(project_folder)
|
||||
|
||||
# <-------------- move latex project away from temp folder ------------->
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
@@ -365,7 +365,7 @@ def Latex翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot,
|
||||
try:
|
||||
txt, arxiv_id = yield from arxiv_download(chatbot, history, txt, allow_cache)
|
||||
except tarfile.ReadError as e:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
"无法自动下载该论文的Latex源码,请前往arxiv打开此论文下载页面,点other Formats,然后download source手动下载latex源码包。接下来调用本地Latex翻译插件即可。",
|
||||
chatbot=chatbot, history=history)
|
||||
return
|
||||
@@ -404,7 +404,7 @@ def Latex翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot,
|
||||
return
|
||||
|
||||
# <-------------- if is a zip/tar file ------------->
|
||||
project_folder = desend_to_extracted_folder_if_exist(project_folder)
|
||||
project_folder = descend_to_extracted_folder_if_exist(project_folder)
|
||||
|
||||
# <-------------- move latex project away from temp folder ------------->
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
@@ -518,7 +518,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
# repeat, project_folder = check_repeat_upload(file_manifest[0], hash_tag)
|
||||
|
||||
# if repeat:
|
||||
# yield from update_ui_lastest_msg(f"发现重复上传,请查收结果(压缩包)...", chatbot=chatbot, history=history)
|
||||
# yield from update_ui_latest_msg(f"发现重复上传,请查收结果(压缩包)...", chatbot=chatbot, history=history)
|
||||
# try:
|
||||
# translate_pdf = [f for f in glob.glob(f'{project_folder}/**/merge_translate_zh.pdf', recursive=True)][0]
|
||||
# promote_file_to_downloadzone(translate_pdf, rename_file=None, chatbot=chatbot)
|
||||
@@ -531,7 +531,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
# report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"发现重复上传,但是无法找到相关文件")
|
||||
# yield from update_ui(chatbot=chatbot, history=history)
|
||||
# else:
|
||||
# yield from update_ui_lastest_msg(f"未发现重复上传", chatbot=chatbot, history=history)
|
||||
# yield from update_ui_latest_msg(f"未发现重复上传", chatbot=chatbot, history=history)
|
||||
|
||||
# <-------------- convert pdf into tex ------------->
|
||||
chatbot.append([f"解析项目: {txt}", "正在将PDF转换为tex项目,请耐心等待..."])
|
||||
@@ -543,7 +543,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
return False
|
||||
|
||||
# <-------------- translate latex file into Chinese ------------->
|
||||
yield from update_ui_lastest_msg("正在tex项目将翻译为中文...", chatbot=chatbot, history=history)
|
||||
yield from update_ui_latest_msg("正在tex项目将翻译为中文...", chatbot=chatbot, history=history)
|
||||
file_manifest = [f for f in glob.glob(f'{project_folder}/**/*.tex', recursive=True)]
|
||||
if len(file_manifest) == 0:
|
||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"找不到任何.tex文件: {txt}")
|
||||
@@ -551,7 +551,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
return
|
||||
|
||||
# <-------------- if is a zip/tar file ------------->
|
||||
project_folder = desend_to_extracted_folder_if_exist(project_folder)
|
||||
project_folder = descend_to_extracted_folder_if_exist(project_folder)
|
||||
|
||||
# <-------------- move latex project away from temp folder ------------->
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
@@ -559,7 +559,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
project_folder = move_project(project_folder)
|
||||
|
||||
# <-------------- set a hash tag for repeat-checking ------------->
|
||||
with open(pj(project_folder, hash_tag + '.tag'), 'w') as f:
|
||||
with open(pj(project_folder, hash_tag + '.tag'), 'w', encoding='utf8') as f:
|
||||
f.write(hash_tag)
|
||||
f.close()
|
||||
|
||||
@@ -571,7 +571,7 @@ def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, h
|
||||
switch_prompt=_switch_prompt_)
|
||||
|
||||
# <-------------- compile PDF ------------->
|
||||
yield from update_ui_lastest_msg("正在将翻译好的项目tex项目编译为PDF...", chatbot=chatbot, history=history)
|
||||
yield from update_ui_latest_msg("正在将翻译好的项目tex项目编译为PDF...", chatbot=chatbot, history=history)
|
||||
success = yield from 编译Latex(chatbot, history, main_file_original='merge',
|
||||
main_file_modified='merge_translate_zh', mode='translate_zh',
|
||||
work_folder_original=project_folder, work_folder_modified=project_folder,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from toolbox import CatchException, check_packages, get_conf
|
||||
from toolbox import update_ui, update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui, update_ui_latest_msg, disable_auto_promotion
|
||||
from toolbox import trimmed_format_exc_markdown
|
||||
from crazy_functions.crazy_utils import get_files_from_everything
|
||||
from crazy_functions.pdf_fns.parse_pdf import get_avail_grobid_url
|
||||
@@ -47,7 +47,7 @@ def 批量翻译PDF文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
yield from 解析PDF_基于DOC2X(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, DOC2X_API_KEY, user_request)
|
||||
return
|
||||
except:
|
||||
chatbot.append([None, f"DOC2X服务不可用,现在将执行效果稍差的旧版代码。{trimmed_format_exc_markdown()}"])
|
||||
chatbot.append([None, f"DOC2X服务不可用,请检查报错详细。{trimmed_format_exc_markdown()}"])
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
|
||||
if method == "GROBID":
|
||||
@@ -57,9 +57,9 @@ def 批量翻译PDF文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
yield from 解析PDF_基于GROBID(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, grobid_url)
|
||||
return
|
||||
|
||||
if method == "ClASSIC":
|
||||
if method == "Classic":
|
||||
# ------- 第三种方法,早期代码,效果不理想 -------
|
||||
yield from update_ui_lastest_msg("GROBID服务不可用,请检查config中的GROBID_URL。作为替代,现在将执行效果稍差的旧版代码。", chatbot, history, delay=3)
|
||||
yield from update_ui_latest_msg("GROBID服务不可用,请检查config中的GROBID_URL。作为替代,现在将执行效果稍差的旧版代码。", chatbot, history, delay=3)
|
||||
yield from 解析PDF_简单拆解(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
||||
return
|
||||
|
||||
@@ -77,7 +77,7 @@ def 批量翻译PDF文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
if grobid_url is not None:
|
||||
yield from 解析PDF_基于GROBID(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, grobid_url)
|
||||
return
|
||||
yield from update_ui_lastest_msg("GROBID服务不可用,请检查config中的GROBID_URL。作为替代,现在将执行效果稍差的旧版代码。", chatbot, history, delay=3)
|
||||
yield from update_ui_latest_msg("GROBID服务不可用,请检查config中的GROBID_URL。作为替代,现在将执行效果稍差的旧版代码。", chatbot, history, delay=3)
|
||||
yield from 解析PDF_简单拆解(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
||||
return
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class PDF_Tran(GptAcademicPluginTemplate):
|
||||
"additional_prompt":
|
||||
ArgProperty(title="额外提示词", description="例如:对专有名词、翻译语气等方面的要求", default_value="", type="string").model_dump_json(), # 高级参数输入区,自动同步
|
||||
"pdf_parse_method":
|
||||
ArgProperty(title="PDF解析方法", options=["DOC2X", "GROBID", "ClASSIC"], description="无", default_value="GROBID", type="dropdown").model_dump_json(),
|
||||
ArgProperty(title="PDF解析方法", options=["DOC2X", "GROBID", "Classic"], description="无", default_value="GROBID", type="dropdown").model_dump_json(),
|
||||
}
|
||||
return gui_definition
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ from typing import List
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
|
||||
from toolbox import report_exception
|
||||
from toolbox import CatchException, update_ui, get_conf, get_log_folder, update_ui_lastest_msg
|
||||
from toolbox import CatchException, update_ui, get_conf, get_log_folder, update_ui_latest_msg
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
from crazy_functions.crazy_utils import input_clipping
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
|
||||
@@ -60,6 +61,7 @@ def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, u
|
||||
# 1. we retrieve rag worker from global context
|
||||
user_name = chatbot.get_user()
|
||||
checkpoint_dir = get_log_folder(user_name, plugin_name='experimental_rag')
|
||||
|
||||
if user_name in RAG_WORKER_REGISTER:
|
||||
rag_worker = RAG_WORKER_REGISTER[user_name]
|
||||
else:
|
||||
@@ -90,12 +92,9 @@ def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, u
|
||||
chatbot.append([txt, f'正在清空 ({current_context}) ...'])
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
rag_worker.purge_vector_store()
|
||||
yield from update_ui_lastest_msg('已清空', chatbot, history, delay=0) # 刷新界面
|
||||
yield from update_ui_latest_msg('已清空', chatbot, history, delay=0) # 刷新界面
|
||||
return
|
||||
|
||||
else:
|
||||
report_exception(chatbot, history, a=f"上传文件路径错误: {txt}", b="请检查并提供正确路径。")
|
||||
|
||||
# 3. Normal Q&A processing
|
||||
chatbot.append([txt, f'正在召回知识 ({current_context}) ...'])
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
@@ -110,10 +109,10 @@ def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, u
|
||||
|
||||
# 5. If input is clipped, add input to vector store before retrieve
|
||||
if input_is_clipped_flag:
|
||||
yield from update_ui_lastest_msg('检测到长输入, 正在向量化 ...', chatbot, history, delay=0) # 刷新界面
|
||||
yield from update_ui_latest_msg('检测到长输入, 正在向量化 ...', chatbot, history, delay=0) # 刷新界面
|
||||
# Save input to vector store
|
||||
rag_worker.add_text_to_vector_store(txt_origin)
|
||||
yield from update_ui_lastest_msg('向量化完成 ...', chatbot, history, delay=0) # 刷新界面
|
||||
yield from update_ui_latest_msg('向量化完成 ...', chatbot, history, delay=0) # 刷新界面
|
||||
|
||||
if len(txt_origin) > REMEMBER_PREVIEW:
|
||||
HALF = REMEMBER_PREVIEW // 2
|
||||
@@ -143,7 +142,7 @@ def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, u
|
||||
)
|
||||
|
||||
# 8. Remember Q&A
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
model_say + '</br></br>' + f'对话记忆中, 请稍等 ({current_context}) ...',
|
||||
chatbot, history, delay=0.5
|
||||
)
|
||||
@@ -151,4 +150,4 @@ def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, u
|
||||
history.extend([i_say, model_say])
|
||||
|
||||
# 9. Final UI Update
|
||||
yield from update_ui_lastest_msg(model_say, chatbot, history, delay=0, msg=tip)
|
||||
yield from update_ui_latest_msg(model_say, chatbot, history, delay=0, msg=tip)
|
||||
@@ -1,5 +1,5 @@
|
||||
import pickle, os, random
|
||||
from toolbox import CatchException, update_ui, get_conf, get_log_folder, update_ui_lastest_msg
|
||||
from toolbox import CatchException, update_ui, get_conf, get_log_folder, update_ui_latest_msg
|
||||
from crazy_functions.crazy_utils import input_clipping
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
@@ -9,7 +9,7 @@ from loguru import logger
|
||||
from typing import List
|
||||
|
||||
|
||||
SOCIAL_NETWOK_WORKER_REGISTER = {}
|
||||
SOCIAL_NETWORK_WORKER_REGISTER = {}
|
||||
|
||||
class SocialNetwork():
|
||||
def __init__(self):
|
||||
@@ -78,7 +78,7 @@ class SocialNetworkWorker(SaveAndLoad):
|
||||
for f in friend.friends_list:
|
||||
self.add_friend(f)
|
||||
msg = f"成功添加{len(friend.friends_list)}个联系人: {str(friend.friends_list)}"
|
||||
yield from update_ui_lastest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=0)
|
||||
yield from update_ui_latest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=0)
|
||||
|
||||
|
||||
def run(self, txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
@@ -104,12 +104,12 @@ class SocialNetworkWorker(SaveAndLoad):
|
||||
}
|
||||
|
||||
try:
|
||||
Explaination = '\n'.join([f'{k}: {v["explain_to_llm"]}' for k, v in self.tools_to_select.items()])
|
||||
Explanation = '\n'.join([f'{k}: {v["explain_to_llm"]}' for k, v in self.tools_to_select.items()])
|
||||
class UserSociaIntention(BaseModel):
|
||||
intention_type: str = Field(
|
||||
description=
|
||||
f"The type of user intention. You must choose from {self.tools_to_select.keys()}.\n\n"
|
||||
f"Explaination:\n{Explaination}",
|
||||
f"Explanation:\n{Explanation}",
|
||||
default="SocialAdvice"
|
||||
)
|
||||
pydantic_cls_instance, err_msg = select_tool(
|
||||
@@ -118,7 +118,7 @@ class SocialNetworkWorker(SaveAndLoad):
|
||||
pydantic_cls=UserSociaIntention
|
||||
)
|
||||
except Exception as e:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"无法理解用户意图 {err_msg}",
|
||||
chatbot=chatbot,
|
||||
history=history,
|
||||
@@ -150,10 +150,10 @@ def I人助手(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt,
|
||||
# 1. we retrieve worker from global context
|
||||
user_name = chatbot.get_user()
|
||||
checkpoint_dir=get_log_folder(user_name, plugin_name='experimental_rag')
|
||||
if user_name in SOCIAL_NETWOK_WORKER_REGISTER:
|
||||
social_network_worker = SOCIAL_NETWOK_WORKER_REGISTER[user_name]
|
||||
if user_name in SOCIAL_NETWORK_WORKER_REGISTER:
|
||||
social_network_worker = SOCIAL_NETWORK_WORKER_REGISTER[user_name]
|
||||
else:
|
||||
social_network_worker = SOCIAL_NETWOK_WORKER_REGISTER[user_name] = SocialNetworkWorker(
|
||||
social_network_worker = SOCIAL_NETWORK_WORKER_REGISTER[user_name] = SocialNetworkWorker(
|
||||
user_name,
|
||||
llm_kwargs,
|
||||
checkpoint_dir=checkpoint_dir,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os, copy, time
|
||||
from toolbox import CatchException, report_exception, update_ui, zip_result, promote_file_to_downloadzone, update_ui_lastest_msg, get_conf, generate_file_link
|
||||
from toolbox import CatchException, report_exception, update_ui, zip_result, promote_file_to_downloadzone, update_ui_latest_msg, get_conf, generate_file_link
|
||||
from shared_utils.fastapi_server import validate_path_safety
|
||||
from crazy_functions.crazy_utils import input_clipping
|
||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||
@@ -117,7 +117,7 @@ def 注释源代码(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
||||
logger.error(f"文件: {fp} 的注释结果未能成功")
|
||||
file_links = generate_file_link(preview_html_list)
|
||||
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
f"当前任务: <br/>{'<br/>'.join(tasks)}.<br/>" +
|
||||
f"剩余源文件数量: {remain}.<br/>" +
|
||||
f"已完成的文件: {sum(worker_done)}.<br/>" +
|
||||
|
||||
204
crazy_functions/VideoResource_GPT.py
Normal file
204
crazy_functions/VideoResource_GPT.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import requests
|
||||
import random
|
||||
import time
|
||||
import re
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
from functools import lru_cache
|
||||
from itertools import zip_longest
|
||||
from check_proxy import check_proxy
|
||||
from toolbox import CatchException, update_ui, get_conf, promote_file_to_downloadzone, update_ui_latest_msg, generate_file_link
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive, input_clipping
|
||||
from request_llms.bridge_all import model_info
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
from crazy_functions.prompts.internet import SearchOptimizerPrompt, SearchAcademicOptimizerPrompt
|
||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
||||
from textwrap import dedent
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class Query(BaseModel):
|
||||
search_keyword: str = Field(description="search query for video resource")
|
||||
|
||||
|
||||
class VideoResource(BaseModel):
|
||||
thought: str = Field(description="analysis of the search results based on the user's query")
|
||||
title: str = Field(description="title of the video")
|
||||
author: str = Field(description="author/uploader of the video")
|
||||
bvid: str = Field(description="unique ID of the video")
|
||||
another_failsafe_bvid: str = Field(description="provide another bvid, the other one is not working")
|
||||
|
||||
|
||||
def get_video_resource(search_keyword):
|
||||
from crazy_functions.media_fns.get_media import search_videos
|
||||
|
||||
# Search for videos and return the first result
|
||||
videos = search_videos(
|
||||
search_keyword
|
||||
)
|
||||
|
||||
# Return the first video if results exist, otherwise return None
|
||||
return videos
|
||||
|
||||
def download_video(bvid, user_name, chatbot, history):
|
||||
# from experimental_mods.get_bilibili_resource import download_bilibili
|
||||
from crazy_functions.media_fns.get_media import download_video
|
||||
# pause a while
|
||||
tic_time = 8
|
||||
for i in range(tic_time):
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"即将下载音频。等待{tic_time-i}秒后自动继续, 点击“停止”键取消此操作。",
|
||||
chatbot=chatbot, history=[], delay=1)
|
||||
|
||||
# download audio
|
||||
chatbot.append((None, "下载音频, 请稍等...")); yield from update_ui(chatbot=chatbot, history=history)
|
||||
downloaded_files = yield from download_video(bvid, only_audio=True, user_name=user_name, chatbot=chatbot, history=history)
|
||||
|
||||
if len(downloaded_files) == 0:
|
||||
# failed to download audio
|
||||
return []
|
||||
|
||||
# preview
|
||||
preview_list = [promote_file_to_downloadzone(fp) for fp in downloaded_files]
|
||||
file_links = generate_file_link(preview_list)
|
||||
yield from update_ui_latest_msg(f"已完成的文件: <br/>" + file_links, chatbot=chatbot, history=history, delay=0)
|
||||
chatbot.append((None, f"即将下载视频。"))
|
||||
|
||||
# pause a while
|
||||
tic_time = 16
|
||||
for i in range(tic_time):
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"即将下载视频。等待{tic_time-i}秒后自动继续, 点击“停止”键取消此操作。",
|
||||
chatbot=chatbot, history=[], delay=1)
|
||||
|
||||
# download video
|
||||
chatbot.append((None, "下载视频, 请稍等...")); yield from update_ui(chatbot=chatbot, history=history)
|
||||
downloaded_files_part2 = yield from download_video(bvid, only_audio=False, user_name=user_name, chatbot=chatbot, history=history)
|
||||
|
||||
# preview
|
||||
preview_list = [promote_file_to_downloadzone(fp) for fp in downloaded_files_part2]
|
||||
file_links = generate_file_link(preview_list)
|
||||
yield from update_ui_latest_msg(f"已完成的文件: <br/>" + file_links, chatbot=chatbot, history=history, delay=0)
|
||||
|
||||
# return
|
||||
return downloaded_files + downloaded_files_part2
|
||||
|
||||
|
||||
class Strategy(BaseModel):
|
||||
thought: str = Field(description="analysis of the user's wish, for example, can you recall the name of the resource?")
|
||||
which_methods: str = Field(description="Which method to use to find the necessary information? choose from 'method_1' and 'method_2'.")
|
||||
method_1_search_keywords: str = Field(description="Generate keywords to search the internet if you choose method 1, otherwise empty.")
|
||||
method_2_generate_keywords: str = Field(description="Generate keywords for video download engine if you choose method 2, otherwise empty.")
|
||||
|
||||
|
||||
@CatchException
|
||||
def 多媒体任务(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
user_wish: str = txt
|
||||
# query demos:
|
||||
# - "我想找一首歌,里面有句歌词是“turn your face towards the sun”"
|
||||
# - "一首歌,第一句是红豆生南国"
|
||||
# - "一首音乐,中国航天任务专用的那首"
|
||||
# - "戴森球计划在熔岩星球的音乐"
|
||||
# - "hanser的百变什么精"
|
||||
# - "打大圣残躯时的bgm"
|
||||
# - "渊下宫战斗音乐"
|
||||
|
||||
# 搜索
|
||||
chatbot.append((txt, "检索中, 请稍等..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
if "跳过联网搜索" not in user_wish:
|
||||
# 结构化生成
|
||||
internet_search_keyword = user_wish
|
||||
|
||||
yield from update_ui_latest_msg(lastmsg=f"发起互联网检索: {internet_search_keyword} ...", chatbot=chatbot, history=[], delay=1)
|
||||
from crazy_functions.Internet_GPT import internet_search_with_analysis_prompt
|
||||
result = yield from internet_search_with_analysis_prompt(
|
||||
prompt=internet_search_keyword,
|
||||
analysis_prompt="请根据搜索结果分析,获取用户需要找的资源的名称、作者、出处等信息。",
|
||||
llm_kwargs=llm_kwargs,
|
||||
chatbot=chatbot
|
||||
)
|
||||
|
||||
yield from update_ui_latest_msg(lastmsg=f"互联网检索结论: {result} \n\n 正在生成进一步检索方案 ...", chatbot=chatbot, history=[], delay=1)
|
||||
rf_req = dedent(f"""
|
||||
The user wish to get the following resource:
|
||||
{user_wish}
|
||||
Meanwhile, you can access another expert's opinion on the user's wish:
|
||||
{result}
|
||||
Generate search keywords (less than 5 keywords) for video download engine accordingly.
|
||||
""")
|
||||
else:
|
||||
user_wish = user_wish.replace("跳过联网搜索", "").strip()
|
||||
rf_req = dedent(f"""
|
||||
The user wish to get the following resource:
|
||||
{user_wish}
|
||||
Generate research keywords (less than 5 keywords) accordingly.
|
||||
""")
|
||||
gpt_json_io = GptJsonIO(Query)
|
||||
inputs = rf_req + gpt_json_io.format_instructions
|
||||
run_gpt_fn = lambda inputs, sys_prompt: predict_no_ui_long_connection(inputs=inputs, llm_kwargs=llm_kwargs, history=[], sys_prompt=sys_prompt, observe_window=[])
|
||||
analyze_res = run_gpt_fn(inputs, "")
|
||||
logger.info(analyze_res)
|
||||
query: Query = gpt_json_io.generate_output_auto_repair(analyze_res, run_gpt_fn)
|
||||
video_engine_keywords = query.search_keyword
|
||||
# 关键词展示
|
||||
chatbot.append((None, f"检索关键词已确认: {video_engine_keywords}。筛选中, 请稍等..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
# 获取候选资源
|
||||
candidate_dictionary: dict = get_video_resource(video_engine_keywords)
|
||||
candidate_dictionary_as_str = json.dumps(candidate_dictionary, ensure_ascii=False, indent=4)
|
||||
|
||||
# 展示候选资源
|
||||
candidate_display = "\n".join([f"{i+1}. {it['title']}" for i, it in enumerate(candidate_dictionary)])
|
||||
chatbot.append((None, f"候选:\n\n{candidate_display}"))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
# 结构化生成
|
||||
rf_req_2 = dedent(f"""
|
||||
The user wish to get the following resource:
|
||||
{user_wish}
|
||||
|
||||
Select the most relevant and suitable video resource from the following search results:
|
||||
{candidate_dictionary_as_str}
|
||||
|
||||
Note:
|
||||
1. The first several search video results are more likely to satisfy the user's wish.
|
||||
2. The time duration of the video should be less than 10 minutes.
|
||||
3. You should analyze the search results first, before giving your answer.
|
||||
4. Use Chinese if possible.
|
||||
5. Beside the primary video selection, give a backup video resource `bvid`.
|
||||
""")
|
||||
gpt_json_io = GptJsonIO(VideoResource)
|
||||
inputs = rf_req_2 + gpt_json_io.format_instructions
|
||||
run_gpt_fn = lambda inputs, sys_prompt: predict_no_ui_long_connection(inputs=inputs, llm_kwargs=llm_kwargs, history=[], sys_prompt=sys_prompt, observe_window=[])
|
||||
analyze_res = run_gpt_fn(inputs, "")
|
||||
logger.info(analyze_res)
|
||||
video_resource: VideoResource = gpt_json_io.generate_output_auto_repair(analyze_res, run_gpt_fn)
|
||||
|
||||
# Display
|
||||
chatbot.append(
|
||||
(None,
|
||||
f"分析:{video_resource.thought}" "<br/>"
|
||||
f"选择: `{video_resource.title}`。" "<br/>"
|
||||
f"作者:{video_resource.author}"
|
||||
)
|
||||
)
|
||||
chatbot.append((None, f"下载中, 请稍等..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
if video_resource and video_resource.bvid:
|
||||
logger.info(video_resource)
|
||||
downloaded = yield from download_video(video_resource.bvid, chatbot.get_user(), chatbot, history)
|
||||
if not downloaded:
|
||||
chatbot.append((None, f"下载失败, 尝试备选 ..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
downloaded = yield from download_video(video_resource.another_failsafe_bvid, chatbot.get_user(), chatbot, history)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@CatchException
|
||||
def debug(bvid, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
yield from download_video(bvid, chatbot.get_user(), chatbot, history)
|
||||
@@ -1,5 +1,5 @@
|
||||
from toolbox import CatchException, update_ui, gen_time_str, trimmed_format_exc, ProxyNetworkActivate
|
||||
from toolbox import report_exception, get_log_folder, update_ui_lastest_msg, Singleton
|
||||
from toolbox import report_exception, get_log_folder, update_ui_latest_msg, Singleton
|
||||
from crazy_functions.agent_fns.pipe import PluginMultiprocessManager, PipeCom
|
||||
from crazy_functions.agent_fns.general import AutoGenGeneral
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class EchoDemo(PluginMultiprocessManager):
|
||||
while True:
|
||||
msg = self.child_conn.recv() # PipeCom
|
||||
if msg.cmd == "user_input":
|
||||
# wait futher user input
|
||||
# wait father user input
|
||||
self.child_conn.send(PipeCom("show", msg.content))
|
||||
wait_success = self.subprocess_worker_wait_user_feedback(wait_msg="我准备好处理下一个问题了.")
|
||||
if not wait_success:
|
||||
|
||||
@@ -27,7 +27,7 @@ def gpt_academic_generate_oai_reply(
|
||||
llm_kwargs=llm_config,
|
||||
history=history,
|
||||
sys_prompt=self._oai_system_message[0]['content'],
|
||||
console_slience=True
|
||||
console_silence=True
|
||||
)
|
||||
assumed_done = reply.endswith('\nTERMINATE')
|
||||
return True, reply
|
||||
|
||||
@@ -10,7 +10,7 @@ from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_
|
||||
# TODO: 解决缩进问题
|
||||
|
||||
find_function_end_prompt = '''
|
||||
Below is a page of code that you need to read. This page may not yet complete, you job is to split this page to sperate functions, class functions etc.
|
||||
Below is a page of code that you need to read. This page may not yet complete, you job is to split this page to separate functions, class functions etc.
|
||||
- Provide the line number where the first visible function ends.
|
||||
- Provide the line number where the next visible function begins.
|
||||
- If there are no other functions in this page, you should simply return the line number of the last line.
|
||||
@@ -59,7 +59,7 @@ OUTPUT:
|
||||
|
||||
|
||||
|
||||
revise_funtion_prompt = '''
|
||||
revise_function_prompt = '''
|
||||
You need to read the following code, and revise the source code ({FILE_BASENAME}) according to following instructions:
|
||||
1. You should analyze the purpose of the functions (if there are any).
|
||||
2. You need to add docstring for the provided functions (if there are any).
|
||||
@@ -117,7 +117,7 @@ def zip_result(folder):
|
||||
'''
|
||||
|
||||
|
||||
revise_funtion_prompt_chinese = '''
|
||||
revise_function_prompt_chinese = '''
|
||||
您需要阅读以下代码,并根据以下说明修订源代码({FILE_BASENAME}):
|
||||
1. 如果源代码中包含函数的话, 你应该分析给定函数实现了什么功能
|
||||
2. 如果源代码中包含函数的话, 你需要为函数添加docstring, docstring必须使用中文
|
||||
@@ -188,9 +188,9 @@ class PythonCodeComment():
|
||||
self.language = language
|
||||
self.observe_window_update = observe_window_update
|
||||
if self.language == "chinese":
|
||||
self.core_prompt = revise_funtion_prompt_chinese
|
||||
self.core_prompt = revise_function_prompt_chinese
|
||||
else:
|
||||
self.core_prompt = revise_funtion_prompt
|
||||
self.core_prompt = revise_function_prompt
|
||||
self.path = None
|
||||
self.file_basename = None
|
||||
self.file_brief = ""
|
||||
@@ -222,7 +222,7 @@ class PythonCodeComment():
|
||||
history=[],
|
||||
sys_prompt="",
|
||||
observe_window=[],
|
||||
console_slience=True
|
||||
console_silence=True
|
||||
)
|
||||
|
||||
def extract_number(text):
|
||||
@@ -316,7 +316,7 @@ class PythonCodeComment():
|
||||
def tag_code(self, fn, hint):
|
||||
code = fn
|
||||
_, n_indent = self.dedent(code)
|
||||
indent_reminder = "" if n_indent == 0 else "(Reminder: as you can see, this piece of code has indent made up with {n_indent} whitespace, please preseve them in the OUTPUT.)"
|
||||
indent_reminder = "" if n_indent == 0 else "(Reminder: as you can see, this piece of code has indent made up with {n_indent} whitespace, please preserve them in the OUTPUT.)"
|
||||
brief_reminder = "" if self.file_brief == "" else f"({self.file_basename} abstract: {self.file_brief})"
|
||||
hint_reminder = "" if hint is None else f"(Reminder: do not ignore or modify code such as `{hint}`, provide complete code in the OUTPUT.)"
|
||||
self.llm_kwargs['temperature'] = 0
|
||||
@@ -333,7 +333,7 @@ class PythonCodeComment():
|
||||
history=[],
|
||||
sys_prompt="",
|
||||
observe_window=[],
|
||||
console_slience=True
|
||||
console_silence=True
|
||||
)
|
||||
|
||||
def get_code_block(reply):
|
||||
@@ -400,7 +400,7 @@ class PythonCodeComment():
|
||||
return revised
|
||||
|
||||
def begin_comment_source_code(self, chatbot=None, history=None):
|
||||
# from toolbox import update_ui_lastest_msg
|
||||
# from toolbox import update_ui_latest_msg
|
||||
assert self.path is not None
|
||||
assert '.py' in self.path # must be python source code
|
||||
# write_target = self.path + '.revised.py'
|
||||
@@ -409,10 +409,10 @@ class PythonCodeComment():
|
||||
# with open(self.path + '.revised.py', 'w+', encoding='utf8') as f:
|
||||
while True:
|
||||
try:
|
||||
# yield from update_ui_lastest_msg(f"({self.file_basename}) 正在读取下一段代码片段:\n", chatbot=chatbot, history=history, delay=0)
|
||||
# yield from update_ui_latest_msg(f"({self.file_basename}) 正在读取下一段代码片段:\n", chatbot=chatbot, history=history, delay=0)
|
||||
next_batch, line_no_start, line_no_end = self.get_next_batch()
|
||||
self.observe_window_update(f"正在处理{self.file_basename} - {line_no_start}/{len(self.full_context)}\n")
|
||||
# yield from update_ui_lastest_msg(f"({self.file_basename}) 处理代码片段:\n\n{next_batch}", chatbot=chatbot, history=history, delay=0)
|
||||
# yield from update_ui_latest_msg(f"({self.file_basename}) 处理代码片段:\n\n{next_batch}", chatbot=chatbot, history=history, delay=0)
|
||||
|
||||
hint = None
|
||||
MAX_ATTEMPT = 2
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
from toolbox import CatchException, update_ui, promote_file_to_downloadzone
|
||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||
import datetime, json
|
||||
|
||||
def fetch_items(list_of_items, batch_size):
|
||||
for i in range(0, len(list_of_items), batch_size):
|
||||
yield list_of_items[i:i + batch_size]
|
||||
|
||||
def string_to_options(arguments):
|
||||
import argparse
|
||||
import shlex
|
||||
|
||||
# Create an argparse.ArgumentParser instance
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Add command-line arguments
|
||||
parser.add_argument("--llm_to_learn", type=str, help="LLM model to learn", default="gpt-3.5-turbo")
|
||||
parser.add_argument("--prompt_prefix", type=str, help="Prompt prefix", default='')
|
||||
parser.add_argument("--system_prompt", type=str, help="System prompt", default='')
|
||||
parser.add_argument("--batch", type=int, help="System prompt", default=50)
|
||||
parser.add_argument("--pre_seq_len", type=int, help="pre_seq_len", default=50)
|
||||
parser.add_argument("--learning_rate", type=float, help="learning_rate", default=2e-2)
|
||||
parser.add_argument("--num_gpus", type=int, help="num_gpus", default=1)
|
||||
parser.add_argument("--json_dataset", type=str, help="json_dataset", default="")
|
||||
parser.add_argument("--ptuning_directory", type=str, help="ptuning_directory", default="")
|
||||
|
||||
|
||||
|
||||
# Parse the arguments
|
||||
args = parser.parse_args(shlex.split(arguments))
|
||||
|
||||
return args
|
||||
|
||||
@CatchException
|
||||
def 微调数据集生成(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
"""
|
||||
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
||||
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
||||
plugin_kwargs 插件模型的参数
|
||||
chatbot 聊天显示框的句柄,用于显示给用户
|
||||
history 聊天历史,前情提要
|
||||
system_prompt 给gpt的静默提醒
|
||||
user_request 当前用户的请求信息(IP地址等)
|
||||
"""
|
||||
history = [] # 清空历史,以免输入溢出
|
||||
chatbot.append(("这是什么功能?", "[Local Message] 微调数据集生成"))
|
||||
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
||||
args = plugin_kwargs.get("advanced_arg", None)
|
||||
if args is None:
|
||||
chatbot.append(("没给定指令", "退出"))
|
||||
yield from update_ui(chatbot=chatbot, history=history); return
|
||||
else:
|
||||
arguments = string_to_options(arguments=args)
|
||||
|
||||
dat = []
|
||||
with open(txt, 'r', encoding='utf8') as f:
|
||||
for line in f.readlines():
|
||||
json_dat = json.loads(line)
|
||||
dat.append(json_dat["content"])
|
||||
|
||||
llm_kwargs['llm_model'] = arguments.llm_to_learn
|
||||
for batch in fetch_items(dat, arguments.batch):
|
||||
res = yield from request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
inputs_array=[f"{arguments.prompt_prefix}\n\n{b}" for b in (batch)],
|
||||
inputs_show_user_array=[f"Show Nothing" for _ in (batch)],
|
||||
llm_kwargs=llm_kwargs,
|
||||
chatbot=chatbot,
|
||||
history_array=[[] for _ in (batch)],
|
||||
sys_prompt_array=[arguments.system_prompt for _ in (batch)],
|
||||
max_workers=10 # OpenAI所允许的最大并行过载
|
||||
)
|
||||
|
||||
with open(txt+'.generated.json', 'a+', encoding='utf8') as f:
|
||||
for b, r in zip(batch, res[1::2]):
|
||||
f.write(json.dumps({"content":b, "summary":r}, ensure_ascii=False)+'\n')
|
||||
|
||||
promote_file_to_downloadzone(txt+'.generated.json', rename_file='generated.json', chatbot=chatbot)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@CatchException
|
||||
def 启动微调(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
"""
|
||||
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
||||
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
||||
plugin_kwargs 插件模型的参数
|
||||
chatbot 聊天显示框的句柄,用于显示给用户
|
||||
history 聊天历史,前情提要
|
||||
system_prompt 给gpt的静默提醒
|
||||
user_request 当前用户的请求信息(IP地址等)
|
||||
"""
|
||||
import subprocess
|
||||
history = [] # 清空历史,以免输入溢出
|
||||
chatbot.append(("这是什么功能?", "[Local Message] 微调数据集生成"))
|
||||
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
||||
args = plugin_kwargs.get("advanced_arg", None)
|
||||
if args is None:
|
||||
chatbot.append(("没给定指令", "退出"))
|
||||
yield from update_ui(chatbot=chatbot, history=history); return
|
||||
else:
|
||||
arguments = string_to_options(arguments=args)
|
||||
|
||||
|
||||
|
||||
pre_seq_len = arguments.pre_seq_len # 128
|
||||
learning_rate = arguments.learning_rate # 2e-2
|
||||
num_gpus = arguments.num_gpus # 1
|
||||
json_dataset = arguments.json_dataset # 't_code.json'
|
||||
ptuning_directory = arguments.ptuning_directory # '/home/hmp/ChatGLM2-6B/ptuning'
|
||||
|
||||
command = f"torchrun --standalone --nnodes=1 --nproc-per-node={num_gpus} main.py \
|
||||
--do_train \
|
||||
--train_file AdvertiseGen/{json_dataset} \
|
||||
--validation_file AdvertiseGen/{json_dataset} \
|
||||
--preprocessing_num_workers 20 \
|
||||
--prompt_column content \
|
||||
--response_column summary \
|
||||
--overwrite_cache \
|
||||
--model_name_or_path THUDM/chatglm2-6b \
|
||||
--output_dir output/clothgen-chatglm2-6b-pt-{pre_seq_len}-{learning_rate} \
|
||||
--overwrite_output_dir \
|
||||
--max_source_length 256 \
|
||||
--max_target_length 256 \
|
||||
--per_device_train_batch_size 1 \
|
||||
--per_device_eval_batch_size 1 \
|
||||
--gradient_accumulation_steps 16 \
|
||||
--predict_with_generate \
|
||||
--max_steps 100 \
|
||||
--logging_steps 10 \
|
||||
--save_steps 20 \
|
||||
--learning_rate {learning_rate} \
|
||||
--pre_seq_len {pre_seq_len} \
|
||||
--quantization_bit 4"
|
||||
|
||||
process = subprocess.Popen(command, shell=True, cwd=ptuning_directory)
|
||||
try:
|
||||
process.communicate(timeout=3600*24)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
return
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import threading
|
||||
from loguru import logger
|
||||
from shared_utils.char_visual_effect import scolling_visual_effect
|
||||
from shared_utils.char_visual_effect import scrolling_visual_effect
|
||||
from toolbox import update_ui, get_conf, trimmed_format_exc, get_max_token, Singleton
|
||||
|
||||
def input_clipping(inputs, history, max_token_limit, return_clip_flags=False):
|
||||
@@ -169,6 +169,7 @@ def can_multi_process(llm) -> bool:
|
||||
def default_condition(llm) -> bool:
|
||||
# legacy condition
|
||||
if llm.startswith('gpt-'): return True
|
||||
if llm.startswith('chatgpt-'): return True
|
||||
if llm.startswith('api2d-'): return True
|
||||
if llm.startswith('azure-'): return True
|
||||
if llm.startswith('spark'): return True
|
||||
@@ -255,7 +256,7 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
# 【第一种情况】:顺利完成
|
||||
gpt_say = predict_no_ui_long_connection(
|
||||
inputs=inputs, llm_kwargs=llm_kwargs, history=history,
|
||||
sys_prompt=sys_prompt, observe_window=mutable[index], console_slience=True
|
||||
sys_prompt=sys_prompt, observe_window=mutable[index], console_silence=True
|
||||
)
|
||||
mutable[index][2] = "已成功"
|
||||
return gpt_say
|
||||
@@ -325,7 +326,7 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
mutable[thread_index][1] = time.time()
|
||||
# 在前端打印些好玩的东西
|
||||
for thread_index, _ in enumerate(worker_done):
|
||||
print_something_really_funny = f"[ ...`{scolling_visual_effect(mutable[thread_index][0], scroller_max_len)}`... ]"
|
||||
print_something_really_funny = f"[ ...`{scrolling_visual_effect(mutable[thread_index][0], scroller_max_len)}`... ]"
|
||||
observe_win.append(print_something_really_funny)
|
||||
# 在前端打印些好玩的东西
|
||||
stat_str = ''.join([f'`{mutable[thread_index][2]}`: {obs}\n\n'
|
||||
@@ -388,11 +389,11 @@ def read_and_clean_pdf_text(fp):
|
||||
"""
|
||||
提取文本块主字体
|
||||
"""
|
||||
fsize_statiscs = {}
|
||||
fsize_statistics = {}
|
||||
for wtf in l['spans']:
|
||||
if wtf['size'] not in fsize_statiscs: fsize_statiscs[wtf['size']] = 0
|
||||
fsize_statiscs[wtf['size']] += len(wtf['text'])
|
||||
return max(fsize_statiscs, key=fsize_statiscs.get)
|
||||
if wtf['size'] not in fsize_statistics: fsize_statistics[wtf['size']] = 0
|
||||
fsize_statistics[wtf['size']] += len(wtf['text'])
|
||||
return max(fsize_statistics, key=fsize_statistics.get)
|
||||
|
||||
def ffsize_same(a,b):
|
||||
"""
|
||||
@@ -432,11 +433,11 @@ def read_and_clean_pdf_text(fp):
|
||||
|
||||
############################## <第 2 步,获取正文主字体> ##################################
|
||||
try:
|
||||
fsize_statiscs = {}
|
||||
fsize_statistics = {}
|
||||
for span in meta_span:
|
||||
if span[1] not in fsize_statiscs: fsize_statiscs[span[1]] = 0
|
||||
fsize_statiscs[span[1]] += span[2]
|
||||
main_fsize = max(fsize_statiscs, key=fsize_statiscs.get)
|
||||
if span[1] not in fsize_statistics: fsize_statistics[span[1]] = 0
|
||||
fsize_statistics[span[1]] += span[2]
|
||||
main_fsize = max(fsize_statistics, key=fsize_statistics.get)
|
||||
if REMOVE_FOOT_NOTE:
|
||||
give_up_fize_threshold = main_fsize * REMOVE_FOOT_FFSIZE_PERCENT
|
||||
except:
|
||||
@@ -609,9 +610,9 @@ class nougat_interface():
|
||||
|
||||
|
||||
def NOUGAT_parse_pdf(self, fp, chatbot, history):
|
||||
from toolbox import update_ui_lastest_msg
|
||||
from toolbox import update_ui_latest_msg
|
||||
|
||||
yield from update_ui_lastest_msg("正在解析论文, 请稍候。进度:正在排队, 等待线程锁...",
|
||||
yield from update_ui_latest_msg("正在解析论文, 请稍候。进度:正在排队, 等待线程锁...",
|
||||
chatbot=chatbot, history=history, delay=0)
|
||||
self.threadLock.acquire()
|
||||
import glob, threading, os
|
||||
@@ -619,7 +620,7 @@ class nougat_interface():
|
||||
dst = os.path.join(get_log_folder(plugin_name='nougat'), gen_time_str())
|
||||
os.makedirs(dst)
|
||||
|
||||
yield from update_ui_lastest_msg("正在解析论文, 请稍候。进度:正在加载NOUGAT... (提示:首次运行需要花费较长时间下载NOUGAT参数)",
|
||||
yield from update_ui_latest_msg("正在解析论文, 请稍候。进度:正在加载NOUGAT... (提示:首次运行需要花费较长时间下载NOUGAT参数)",
|
||||
chatbot=chatbot, history=history, delay=0)
|
||||
command = ['nougat', '--out', os.path.abspath(dst), os.path.abspath(fp)]
|
||||
self.nougat_with_timeout(command, cwd=os.getcwd(), timeout=3600)
|
||||
|
||||
812
crazy_functions/doc_fns/AI_review_doc.py
Normal file
812
crazy_functions/doc_fns/AI_review_doc.py
Normal file
@@ -0,0 +1,812 @@
|
||||
import os
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from docx import Document
|
||||
from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Cm
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from typing import Dict, List, Tuple
|
||||
import markdown
|
||||
from crazy_functions.doc_fns.conversation_doc.word_doc import convert_markdown_to_word
|
||||
|
||||
|
||||
|
||||
class DocumentFormatter(ABC):
|
||||
"""文档格式化基类,定义文档格式化的基本接口"""
|
||||
|
||||
def __init__(self, final_summary: str, file_summaries_map: Dict, failed_files: List[Tuple]):
|
||||
self.final_summary = final_summary
|
||||
self.file_summaries_map = file_summaries_map
|
||||
self.failed_files = failed_files
|
||||
|
||||
@abstractmethod
|
||||
def format_failed_files(self) -> str:
|
||||
"""格式化失败文件列表"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def format_file_summaries(self) -> str:
|
||||
"""格式化文件总结内容"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_document(self) -> str:
|
||||
"""创建完整文档"""
|
||||
pass
|
||||
|
||||
|
||||
class WordFormatter(DocumentFormatter):
|
||||
"""Word格式文档生成器 - 符合中国政府公文格式规范(GB/T 9704-2012),并进行了优化"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.doc = Document()
|
||||
self._setup_document()
|
||||
self._create_styles()
|
||||
# 初始化三级标题编号系统
|
||||
self.numbers = {
|
||||
1: 0, # 一级标题编号
|
||||
2: 0, # 二级标题编号
|
||||
3: 0 # 三级标题编号
|
||||
}
|
||||
|
||||
def _setup_document(self):
|
||||
"""设置文档基本格式,包括页面设置和页眉"""
|
||||
sections = self.doc.sections
|
||||
for section in sections:
|
||||
# 设置页面大小为A4
|
||||
section.page_width = Cm(21)
|
||||
section.page_height = Cm(29.7)
|
||||
# 设置页边距
|
||||
section.top_margin = Cm(3.7) # 上边距37mm
|
||||
section.bottom_margin = Cm(3.5) # 下边距35mm
|
||||
section.left_margin = Cm(2.8) # 左边距28mm
|
||||
section.right_margin = Cm(2.6) # 右边距26mm
|
||||
# 设置页眉页脚距离
|
||||
section.header_distance = Cm(2.0)
|
||||
section.footer_distance = Cm(2.0)
|
||||
|
||||
# 添加页眉
|
||||
header = section.header
|
||||
header_para = header.paragraphs[0]
|
||||
header_para.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
|
||||
header_run = header_para.add_run("该文档由GPT-academic生成")
|
||||
header_run.font.name = '仿宋'
|
||||
header_run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
header_run.font.size = Pt(9)
|
||||
|
||||
def _create_styles(self):
|
||||
"""创建文档样式"""
|
||||
# 创建正文样式
|
||||
style = self.doc.styles.add_style('Normal_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
||||
style.font.name = '仿宋'
|
||||
style._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
style.font.size = Pt(14)
|
||||
style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
style.paragraph_format.space_after = Pt(0)
|
||||
style.paragraph_format.first_line_indent = Pt(28)
|
||||
|
||||
# 创建各级标题样式
|
||||
self._create_heading_style('Title_Custom', '方正小标宋简体', 32, WD_PARAGRAPH_ALIGNMENT.CENTER)
|
||||
self._create_heading_style('Heading1_Custom', '黑体', 22, WD_PARAGRAPH_ALIGNMENT.LEFT)
|
||||
self._create_heading_style('Heading2_Custom', '黑体', 18, WD_PARAGRAPH_ALIGNMENT.LEFT)
|
||||
self._create_heading_style('Heading3_Custom', '黑体', 16, WD_PARAGRAPH_ALIGNMENT.LEFT)
|
||||
|
||||
def _create_heading_style(self, style_name: str, font_name: str, font_size: int, alignment):
|
||||
"""创建标题样式"""
|
||||
style = self.doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
|
||||
style.font.name = font_name
|
||||
style._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
|
||||
style.font.size = Pt(font_size)
|
||||
style.font.bold = True
|
||||
style.paragraph_format.alignment = alignment
|
||||
style.paragraph_format.space_before = Pt(12)
|
||||
style.paragraph_format.space_after = Pt(12)
|
||||
style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
return style
|
||||
|
||||
def _get_heading_number(self, level: int) -> str:
|
||||
"""
|
||||
生成标题编号
|
||||
|
||||
Args:
|
||||
level: 标题级别 (0-3)
|
||||
|
||||
Returns:
|
||||
str: 格式化的标题编号
|
||||
"""
|
||||
if level == 0: # 主标题不需要编号
|
||||
return ""
|
||||
|
||||
self.numbers[level] += 1 # 增加当前级别的编号
|
||||
|
||||
# 重置下级标题编号
|
||||
for i in range(level + 1, 4):
|
||||
self.numbers[i] = 0
|
||||
|
||||
# 根据级别返回不同格式的编号
|
||||
if level == 1:
|
||||
return f"{self.numbers[1]}. "
|
||||
elif level == 2:
|
||||
return f"{self.numbers[1]}.{self.numbers[2]} "
|
||||
elif level == 3:
|
||||
return f"{self.numbers[1]}.{self.numbers[2]}.{self.numbers[3]} "
|
||||
return ""
|
||||
|
||||
def _add_heading(self, text: str, level: int):
|
||||
"""
|
||||
添加带编号的标题
|
||||
|
||||
Args:
|
||||
text: 标题文本
|
||||
level: 标题级别 (0-3)
|
||||
"""
|
||||
style_map = {
|
||||
0: 'Title_Custom',
|
||||
1: 'Heading1_Custom',
|
||||
2: 'Heading2_Custom',
|
||||
3: 'Heading3_Custom'
|
||||
}
|
||||
|
||||
number = self._get_heading_number(level)
|
||||
paragraph = self.doc.add_paragraph(style=style_map[level])
|
||||
|
||||
if number:
|
||||
number_run = paragraph.add_run(number)
|
||||
font_size = 22 if level == 1 else (18 if level == 2 else 16)
|
||||
self._get_run_style(number_run, '黑体', font_size, True)
|
||||
|
||||
text_run = paragraph.add_run(text)
|
||||
font_size = 32 if level == 0 else (22 if level == 1 else (18 if level == 2 else 16))
|
||||
self._get_run_style(text_run, '黑体', font_size, True)
|
||||
|
||||
# 主标题添加日期
|
||||
if level == 0:
|
||||
date_paragraph = self.doc.add_paragraph()
|
||||
date_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
date_run = date_paragraph.add_run(datetime.now().strftime('%Y年%m月%d日'))
|
||||
self._get_run_style(date_run, '仿宋', 16, False)
|
||||
|
||||
return paragraph
|
||||
|
||||
def _get_run_style(self, run, font_name: str, font_size: int, bold: bool = False):
|
||||
"""设置文本运行对象的样式"""
|
||||
run.font.name = font_name
|
||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
|
||||
run.font.size = Pt(font_size)
|
||||
run.font.bold = bold
|
||||
|
||||
def format_failed_files(self) -> str:
|
||||
"""格式化失败文件列表"""
|
||||
result = []
|
||||
if not self.failed_files:
|
||||
return "\n".join(result)
|
||||
|
||||
result.append("处理失败文件:")
|
||||
for fp, reason in self.failed_files:
|
||||
result.append(f"• {os.path.basename(fp)}: {reason}")
|
||||
|
||||
self._add_heading("处理失败文件", 1)
|
||||
for fp, reason in self.failed_files:
|
||||
self._add_content(f"• {os.path.basename(fp)}: {reason}", indent=False)
|
||||
self.doc.add_paragraph()
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
def _add_content(self, text: str, indent: bool = True):
|
||||
"""添加正文内容,使用convert_markdown_to_word处理文本"""
|
||||
# 使用convert_markdown_to_word处理markdown文本
|
||||
processed_text = convert_markdown_to_word(text)
|
||||
paragraph = self.doc.add_paragraph(processed_text, style='Normal_Custom')
|
||||
if not indent:
|
||||
paragraph.paragraph_format.first_line_indent = Pt(0)
|
||||
return paragraph
|
||||
|
||||
def format_file_summaries(self) -> str:
|
||||
"""
|
||||
格式化文件总结内容,确保正确的标题层级并处理markdown文本
|
||||
"""
|
||||
result = []
|
||||
# 首先对文件路径进行分组整理
|
||||
file_groups = {}
|
||||
for path in sorted(self.file_summaries_map.keys()):
|
||||
dir_path = os.path.dirname(path)
|
||||
if dir_path not in file_groups:
|
||||
file_groups[dir_path] = []
|
||||
file_groups[dir_path].append(path)
|
||||
|
||||
# 处理没有目录的文件
|
||||
root_files = file_groups.get("", [])
|
||||
if root_files:
|
||||
for path in sorted(root_files):
|
||||
file_name = os.path.basename(path)
|
||||
result.append(f"\n📄 {file_name}")
|
||||
result.append(self.file_summaries_map[path])
|
||||
# 无目录的文件作为二级标题
|
||||
self._add_heading(f"📄 {file_name}", 2)
|
||||
# 使用convert_markdown_to_word处理文件内容
|
||||
self._add_content(convert_markdown_to_word(self.file_summaries_map[path]))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 处理有目录的文件
|
||||
for dir_path in sorted(file_groups.keys()):
|
||||
if dir_path == "": # 跳过已处理的根目录文件
|
||||
continue
|
||||
|
||||
# 添加目录作为二级标题
|
||||
result.append(f"\n📁 {dir_path}")
|
||||
self._add_heading(f"📁 {dir_path}", 2)
|
||||
|
||||
# 该目录下的所有文件作为三级标题
|
||||
for path in sorted(file_groups[dir_path]):
|
||||
file_name = os.path.basename(path)
|
||||
result.append(f"\n📄 {file_name}")
|
||||
result.append(self.file_summaries_map[path])
|
||||
|
||||
# 添加文件名作为三级标题
|
||||
self._add_heading(f"📄 {file_name}", 3)
|
||||
# 使用convert_markdown_to_word处理文件内容
|
||||
self._add_content(convert_markdown_to_word(self.file_summaries_map[path]))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def create_document(self):
|
||||
"""创建完整Word文档并返回文档对象"""
|
||||
# 重置所有编号
|
||||
for level in self.numbers:
|
||||
self.numbers[level] = 0
|
||||
|
||||
# 添加主标题
|
||||
self._add_heading("文档总结报告", 0)
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 添加总体摘要,使用convert_markdown_to_word处理
|
||||
self._add_heading("总体摘要", 1)
|
||||
self._add_content(convert_markdown_to_word(self.final_summary))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 添加失败文件列表(如果有)
|
||||
if self.failed_files:
|
||||
self.format_failed_files()
|
||||
|
||||
# 添加文件详细总结
|
||||
self._add_heading("各文件详细总结", 1)
|
||||
self.format_file_summaries()
|
||||
|
||||
return self.doc
|
||||
|
||||
def save_as_pdf(self, word_path, pdf_path=None):
|
||||
"""将生成的Word文档转换为PDF
|
||||
|
||||
参数:
|
||||
word_path: Word文档的路径
|
||||
pdf_path: 可选,PDF文件的输出路径。如果未指定,将使用与Word文档相同的名称和位置
|
||||
|
||||
返回:
|
||||
生成的PDF文件路径,如果转换失败则返回None
|
||||
"""
|
||||
from crazy_functions.doc_fns.conversation_doc.word2pdf import WordToPdfConverter
|
||||
try:
|
||||
pdf_path = WordToPdfConverter.convert_to_pdf(word_path, pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"PDF转换失败: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
class MarkdownFormatter(DocumentFormatter):
|
||||
"""Markdown格式文档生成器"""
|
||||
|
||||
def format_failed_files(self) -> str:
|
||||
if not self.failed_files:
|
||||
return ""
|
||||
|
||||
formatted_text = ["\n## ⚠️ 处理失败的文件"]
|
||||
for fp, reason in self.failed_files:
|
||||
formatted_text.append(f"- {os.path.basename(fp)}: {reason}")
|
||||
formatted_text.append("\n---")
|
||||
return "\n".join(formatted_text)
|
||||
|
||||
def format_file_summaries(self) -> str:
|
||||
formatted_text = []
|
||||
sorted_paths = sorted(self.file_summaries_map.keys())
|
||||
current_dir = ""
|
||||
|
||||
for path in sorted_paths:
|
||||
dir_path = os.path.dirname(path)
|
||||
if dir_path != current_dir:
|
||||
if dir_path:
|
||||
formatted_text.append(f"\n## 📁 {dir_path}")
|
||||
current_dir = dir_path
|
||||
|
||||
file_name = os.path.basename(path)
|
||||
formatted_text.append(f"\n### 📄 {file_name}")
|
||||
formatted_text.append(self.file_summaries_map[path])
|
||||
formatted_text.append("\n---")
|
||||
|
||||
return "\n".join(formatted_text)
|
||||
|
||||
def create_document(self) -> str:
|
||||
document = [
|
||||
"# 📑 文档总结报告",
|
||||
"\n## 总体摘要",
|
||||
self.final_summary
|
||||
]
|
||||
|
||||
if self.failed_files:
|
||||
document.append(self.format_failed_files())
|
||||
|
||||
document.extend([
|
||||
"\n# 📚 各文件详细总结",
|
||||
self.format_file_summaries()
|
||||
])
|
||||
|
||||
return "\n".join(document)
|
||||
|
||||
|
||||
|
||||
class HtmlFormatter(DocumentFormatter):
|
||||
"""HTML格式文档生成器 - 优化版"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.md = markdown.Markdown(extensions=['extra','codehilite', 'tables','nl2br'])
|
||||
self.css_styles = """
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(-20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Enhanced color palette */
|
||||
--primary-color: #2563eb;
|
||||
--primary-light: #eff6ff;
|
||||
--secondary-color: #1e293b;
|
||||
--background-color: #f8fafc;
|
||||
--text-color: #334155;
|
||||
--text-light: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--error-color: #ef4444;
|
||||
--error-light: #fef2f2;
|
||||
--success-color: #22c55e;
|
||||
--warning-color: #f59e0b;
|
||||
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--hover-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Typography */
|
||||
--heading-font: "Plus Jakarta Sans", system-ui, sans-serif;
|
||||
--body-font: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--body-font);
|
||||
line-height: 1.8;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 3rem;
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
box-shadow: var(--hover-shadow);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--heading-font);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
font-size: 2.8em;
|
||||
text-align: center;
|
||||
margin: 2rem 0 3rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
letter-spacing: -0.03em;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
h1:hover::after {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.9em;
|
||||
margin: 2.5rem 0 1.5rem;
|
||||
padding-left: 1.2rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
letter-spacing: -0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h2:hover {
|
||||
color: var(--primary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-color);
|
||||
font-size: 1.5em;
|
||||
margin: 2rem 0 1rem;
|
||||
padding-bottom: 0.8rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
h3:hover {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: var(--primary-light);
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
margin: 2.5rem 0;
|
||||
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.summary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 12px -2px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.summary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--primary-color), rgba(37, 99, 235, 0.6));
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin: 1.2rem 0;
|
||||
line-height: 1.9;
|
||||
color: var(--text-color);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.summary:hover p {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 3.5rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 2px dashed var(--border-color);
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.failed-files {
|
||||
background: var(--error-light);
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
margin: 3rem 0;
|
||||
border-left: 4px solid var(--error-color);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.failed-files:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 8px 15px -3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.failed-files h2 {
|
||||
color: var(--error-color);
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.failed-files ul {
|
||||
margin: 1.8rem 0;
|
||||
padding-left: 1.2rem;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.failed-files li {
|
||||
margin: 1.2rem 0;
|
||||
padding: 1.2rem 1.8rem;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.failed-files li:hover {
|
||||
transform: translateX(8px);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.directory-section {
|
||||
margin: 3.5rem 0;
|
||||
padding: 2rem;
|
||||
background: var(--background-color);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.directory-section:hover {
|
||||
background: white;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
margin: 1.8rem 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
border-left: 4px solid var(--border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-summary:hover {
|
||||
border-left-color: var(--primary-color);
|
||||
transform: translateX(8px) translateY(-2px);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
margin: 1.8rem 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
border-left: 4px solid var(--border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-summary:hover {
|
||||
border-left-color: var(--primary-color);
|
||||
transform: translateX(8px) translateY(-2px);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2em;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-summary:hover .icon,
|
||||
.directory-section:hover .icon {
|
||||
transform: scale(1.1);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Selection style */
|
||||
::selection {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
.container {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
.file-summary, .failed-files {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
margin: 1.5rem 0 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.summary, .failed-files, .directory-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-light: rgba(37, 99, 235, 0.15);
|
||||
--background-color: #0f172a;
|
||||
--text-color: #e2e8f0;
|
||||
--text-light: #94a3b8;
|
||||
--border-color: #1e293b;
|
||||
--error-light: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.container, .file-summary {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.directory-section {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.directory-section:hover {
|
||||
background: #1e293b;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def format_failed_files(self) -> str:
|
||||
if not self.failed_files:
|
||||
return ""
|
||||
|
||||
failed_files_html = ['<div class="failed-files">']
|
||||
failed_files_html.append('<h2><span class="icon">⚠️</span> 处理失败的文件</h2>')
|
||||
failed_files_html.append("<ul>")
|
||||
for fp, reason in self.failed_files:
|
||||
failed_files_html.append(
|
||||
f'<li><strong>📄 {os.path.basename(fp)}</strong><br><span style="color: var(--text-light)">{reason}</span></li>'
|
||||
)
|
||||
failed_files_html.append("</ul></div>")
|
||||
return "\n".join(failed_files_html)
|
||||
|
||||
def format_file_summaries(self) -> str:
|
||||
formatted_html = []
|
||||
sorted_paths = sorted(self.file_summaries_map.keys())
|
||||
current_dir = ""
|
||||
|
||||
for path in sorted_paths:
|
||||
dir_path = os.path.dirname(path)
|
||||
if dir_path != current_dir:
|
||||
if dir_path:
|
||||
formatted_html.append('<div class="directory-section">')
|
||||
formatted_html.append(f'<h2><span class="icon">📁</span> {dir_path}</h2>')
|
||||
formatted_html.append('</div>')
|
||||
current_dir = dir_path
|
||||
|
||||
file_name = os.path.basename(path)
|
||||
formatted_html.append('<div class="file-summary">')
|
||||
formatted_html.append(f'<h3><span class="icon">📄</span> {file_name}</h3>')
|
||||
formatted_html.append(self.md.convert(self.file_summaries_map[path]))
|
||||
formatted_html.append('</div>')
|
||||
|
||||
return "\n".join(formatted_html)
|
||||
|
||||
def create_document(self) -> str:
|
||||
"""生成HTML文档
|
||||
Returns:
|
||||
str: 完整的HTML文档字符串
|
||||
"""
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>文档总结报告</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/inter/3.19.3/inter.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>{self.css_styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><span class="icon">📑</span> 文档总结报告</h1>
|
||||
<div class="summary">
|
||||
<h2><span class="icon">📋</span> 总体摘要</h2>
|
||||
<p>{self.md.convert(self.final_summary)}</p>
|
||||
</div>
|
||||
{self.format_failed_files()}
|
||||
<div class="details">
|
||||
<h2><span class="icon">📚</span> 各文件详细总结</h2>
|
||||
{self.format_file_summaries()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
@@ -9,6 +9,9 @@ from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Cm
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from typing import Dict, List, Tuple
|
||||
import markdown
|
||||
from crazy_functions.doc_fns.conversation_doc.word_doc import convert_markdown_to_word
|
||||
|
||||
|
||||
|
||||
class DocumentFormatter(ABC):
|
||||
@@ -194,26 +197,17 @@ class WordFormatter(DocumentFormatter):
|
||||
return "\n".join(result)
|
||||
|
||||
def _add_content(self, text: str, indent: bool = True):
|
||||
"""添加正文内容"""
|
||||
paragraph = self.doc.add_paragraph(text, style='Normal_Custom')
|
||||
"""添加正文内容,使用convert_markdown_to_word处理文本"""
|
||||
# 使用convert_markdown_to_word处理markdown文本
|
||||
processed_text = convert_markdown_to_word(text)
|
||||
paragraph = self.doc.add_paragraph(processed_text, style='Normal_Custom')
|
||||
if not indent:
|
||||
paragraph.paragraph_format.first_line_indent = Pt(0)
|
||||
return paragraph
|
||||
|
||||
def format_file_summaries(self) -> str:
|
||||
"""
|
||||
格式化文件总结内容,确保正确的标题层级
|
||||
|
||||
返回:
|
||||
str: 格式化后的文件总结字符串
|
||||
|
||||
标题层级规则:
|
||||
1. 一级标题为"各文件详细总结"
|
||||
2. 如果文件有目录路径:
|
||||
- 目录路径作为二级标题 (2.1, 2.2 等)
|
||||
- 该目录下所有文件作为三级标题 (2.1.1, 2.1.2 等)
|
||||
3. 如果文件没有目录路径:
|
||||
- 文件直接作为二级标题 (2.1, 2.2 等)
|
||||
格式化文件总结内容,确保正确的标题层级并处理markdown文本
|
||||
"""
|
||||
result = []
|
||||
# 首先对文件路径进行分组整理
|
||||
@@ -233,7 +227,8 @@ class WordFormatter(DocumentFormatter):
|
||||
result.append(self.file_summaries_map[path])
|
||||
# 无目录的文件作为二级标题
|
||||
self._add_heading(f"📄 {file_name}", 2)
|
||||
self._add_content(self.file_summaries_map[path])
|
||||
# 使用convert_markdown_to_word处理文件内容
|
||||
self._add_content(convert_markdown_to_word(self.file_summaries_map[path]))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 处理有目录的文件
|
||||
@@ -253,7 +248,8 @@ class WordFormatter(DocumentFormatter):
|
||||
|
||||
# 添加文件名作为三级标题
|
||||
self._add_heading(f"📄 {file_name}", 3)
|
||||
self._add_content(self.file_summaries_map[path])
|
||||
# 使用convert_markdown_to_word处理文件内容
|
||||
self._add_content(convert_markdown_to_word(self.file_summaries_map[path]))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
return "\n".join(result)
|
||||
@@ -269,9 +265,9 @@ class WordFormatter(DocumentFormatter):
|
||||
self._add_heading("文档总结报告", 0)
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 添加总体摘要
|
||||
# 添加总体摘要,使用convert_markdown_to_word处理
|
||||
self._add_heading("总体摘要", 1)
|
||||
self._add_content(self.final_summary)
|
||||
self._add_content(convert_markdown_to_word(self.final_summary))
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# 添加失败文件列表(如果有)
|
||||
@@ -284,6 +280,24 @@ class WordFormatter(DocumentFormatter):
|
||||
|
||||
return self.doc
|
||||
|
||||
def save_as_pdf(self, word_path, pdf_path=None):
|
||||
"""将生成的Word文档转换为PDF
|
||||
|
||||
参数:
|
||||
word_path: Word文档的路径
|
||||
pdf_path: 可选,PDF文件的输出路径。如果未指定,将使用与Word文档相同的名称和位置
|
||||
|
||||
返回:
|
||||
生成的PDF文件路径,如果转换失败则返回None
|
||||
"""
|
||||
from crazy_functions.doc_fns.conversation_doc.word2pdf import WordToPdfConverter
|
||||
try:
|
||||
pdf_path = WordToPdfConverter.convert_to_pdf(word_path, pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"PDF转换失败: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
class MarkdownFormatter(DocumentFormatter):
|
||||
"""Markdown格式文档生成器"""
|
||||
@@ -335,61 +349,395 @@ class MarkdownFormatter(DocumentFormatter):
|
||||
return "\n".join(document)
|
||||
|
||||
|
||||
|
||||
class HtmlFormatter(DocumentFormatter):
|
||||
"""HTML格式文档生成器"""
|
||||
"""HTML格式文档生成器 - 优化版"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.md = markdown.Markdown(extensions=['extra','codehilite', 'tables','nl2br'])
|
||||
self.css_styles = """
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(-20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Enhanced color palette */
|
||||
--primary-color: #2563eb;
|
||||
--primary-light: #eff6ff;
|
||||
--secondary-color: #1e293b;
|
||||
--background-color: #f8fafc;
|
||||
--text-color: #334155;
|
||||
--text-light: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--error-color: #ef4444;
|
||||
--error-light: #fef2f2;
|
||||
--success-color: #22c55e;
|
||||
--warning-color: #f59e0b;
|
||||
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--hover-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Typography */
|
||||
--heading-font: "Plus Jakarta Sans", system-ui, sans-serif;
|
||||
--body-font: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Microsoft YaHei", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1000px;
|
||||
font-family: var(--body-font);
|
||||
line-height: 1.8;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 3rem;
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
box-shadow: var(--hover-shadow);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--heading-font);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
font-size: 2.8em;
|
||||
text-align: center;
|
||||
margin: 2rem 0 3rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
letter-spacing: -0.03em;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
h1:hover::after {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-top: 30px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid #3498db;
|
||||
padding-left: 10px;
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.9em;
|
||||
margin: 2.5rem 0 1.5rem;
|
||||
padding-left: 1.2rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
letter-spacing: -0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h2:hover {
|
||||
color: var(--primary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
font-size: 18px;
|
||||
margin-top: 20px;
|
||||
color: var(--text-color);
|
||||
font-size: 1.5em;
|
||||
margin: 2rem 0 1rem;
|
||||
padding-bottom: 0.8rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
h3:hover {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.summary {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
background: var(--primary-light);
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
margin: 2.5rem 0;
|
||||
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.summary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 12px -2px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.summary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--primary-color), rgba(37, 99, 235, 0.6));
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin: 1.2rem 0;
|
||||
line-height: 1.9;
|
||||
color: var(--text-color);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.summary:hover p {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 40px;
|
||||
margin-top: 3.5rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 2px dashed var(--border-color);
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.failed-files {
|
||||
background-color: #fff3f3;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #e74c3c;
|
||||
margin: 20px 0;
|
||||
background: var(--error-light);
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
margin: 3rem 0;
|
||||
border-left: 4px solid var(--error-color);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.failed-files:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 8px 15px -3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.failed-files h2 {
|
||||
color: var(--error-color);
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.failed-files ul {
|
||||
margin: 1.8rem 0;
|
||||
padding-left: 1.2rem;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.failed-files li {
|
||||
margin: 1.2rem 0;
|
||||
padding: 1.2rem 1.8rem;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.failed-files li:hover {
|
||||
transform: translateX(8px);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.directory-section {
|
||||
margin: 3.5rem 0;
|
||||
padding: 2rem;
|
||||
background: var(--background-color);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.directory-section:hover {
|
||||
background: white;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
margin: 1.8rem 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
border-left: 4px solid var(--border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-summary:hover {
|
||||
border-left-color: var(--primary-color);
|
||||
transform: translateX(8px) translateY(-2px);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
margin: 1.8rem 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
border-left: 4px solid var(--border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-summary:hover {
|
||||
border-left-color: var(--primary-color);
|
||||
transform: translateX(8px) translateY(-2px);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2em;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-summary:hover .icon,
|
||||
.directory-section:hover .icon {
|
||||
transform: scale(1.1);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Selection style */
|
||||
::selection {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
.container {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
.file-summary, .failed-files {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
margin: 1.5rem 0 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.summary, .failed-files, .directory-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.file-summary {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-light: rgba(37, 99, 235, 0.15);
|
||||
--background-color: #0f172a;
|
||||
--text-color: #e2e8f0;
|
||||
--text-light: #94a3b8;
|
||||
--border-color: #1e293b;
|
||||
--error-light: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.container, .file-summary {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.directory-section {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.directory-section:hover {
|
||||
background: #1e293b;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -398,10 +746,12 @@ class HtmlFormatter(DocumentFormatter):
|
||||
return ""
|
||||
|
||||
failed_files_html = ['<div class="failed-files">']
|
||||
failed_files_html.append("<h2>⚠️ 处理失败的文件</h2>")
|
||||
failed_files_html.append('<h2><span class="icon">⚠️</span> 处理失败的文件</h2>')
|
||||
failed_files_html.append("<ul>")
|
||||
for fp, reason in self.failed_files:
|
||||
failed_files_html.append(f"<li><strong>{os.path.basename(fp)}:</strong> {reason}</li>")
|
||||
failed_files_html.append(
|
||||
f'<li><strong>📄 {os.path.basename(fp)}</strong><br><span style="color: var(--text-light)">{reason}</span></li>'
|
||||
)
|
||||
failed_files_html.append("</ul></div>")
|
||||
return "\n".join(failed_files_html)
|
||||
|
||||
@@ -414,37 +764,49 @@ class HtmlFormatter(DocumentFormatter):
|
||||
dir_path = os.path.dirname(path)
|
||||
if dir_path != current_dir:
|
||||
if dir_path:
|
||||
formatted_html.append(f'<h2>📁 {dir_path}</h2>')
|
||||
formatted_html.append('<div class="directory-section">')
|
||||
formatted_html.append(f'<h2><span class="icon">📁</span> {dir_path}</h2>')
|
||||
formatted_html.append('</div>')
|
||||
current_dir = dir_path
|
||||
|
||||
file_name = os.path.basename(path)
|
||||
formatted_html.append('<div class="file-summary">')
|
||||
formatted_html.append(f'<h3>📄 {file_name}</h3>')
|
||||
formatted_html.append(f'<p>{self.file_summaries_map[path]}</p>')
|
||||
formatted_html.append(f'<h3><span class="icon">📄</span> {file_name}</h3>')
|
||||
formatted_html.append(self.md.convert(self.file_summaries_map[path]))
|
||||
formatted_html.append('</div>')
|
||||
|
||||
return "\n".join(formatted_html)
|
||||
|
||||
def create_document(self) -> str:
|
||||
"""生成HTML文档
|
||||
Returns:
|
||||
str: 完整的HTML文档字符串
|
||||
"""
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>文档总结报告</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/inter/3.19.3/inter.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>{self.css_styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📑 文档总结报告</h1>
|
||||
<h2>总体摘要</h2>
|
||||
<div class="summary">{self.final_summary}</div>
|
||||
{self.format_failed_files()}
|
||||
<div class="details">
|
||||
<h2>📚 各文件详细总结</h2>
|
||||
{self.format_file_summaries()}
|
||||
<div class="container">
|
||||
<h1><span class="icon">📑</span> 文档总结报告</h1>
|
||||
<div class="summary">
|
||||
<h2><span class="icon">📋</span> 总体摘要</h2>
|
||||
<p>{self.md.convert(self.final_summary)}</p>
|
||||
</div>
|
||||
{self.format_failed_files()}
|
||||
<div class="details">
|
||||
<h2><span class="icon">📚</span> 各文件详细总结</h2>
|
||||
{self.format_file_summaries()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
@@ -1,10 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Type, TypeVar, Generic, Union
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from crazy_functions.rag_fns.arxiv_fns.section_fragment import SectionFragment
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -235,153 +235,3 @@ class ContentFoldingManager:
|
||||
|
||||
return formatter.format(content, metadata, options)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaperMetadata(BaseMetadata):
|
||||
"""论文元数据"""
|
||||
title: str
|
||||
authors: str
|
||||
abstract: str
|
||||
catalogs: str
|
||||
arxiv_id: str = ""
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""验证论文元数据的有效性"""
|
||||
try:
|
||||
if not self._validate_non_empty_str(self.title):
|
||||
return False
|
||||
if not self._validate_non_empty_str(self.authors):
|
||||
return False
|
||||
if not self._validate_non_empty_str(self.abstract):
|
||||
return False
|
||||
if not self._validate_non_empty_str(self.catalogs):
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Paper metadata validation error: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class PaperContentFormatter(ContentFormatter[PaperMetadata]):
|
||||
"""论文内容格式化器"""
|
||||
|
||||
def format(self,
|
||||
fragments: list[SectionFragment],
|
||||
metadata: PaperMetadata,
|
||||
options: Optional[FoldingOptions] = None) -> str:
|
||||
"""格式化论文内容
|
||||
|
||||
Args:
|
||||
fragments: 论文片段列表
|
||||
metadata: 论文元数据
|
||||
options: 折叠选项
|
||||
|
||||
Returns:
|
||||
str: 格式化后的论文内容
|
||||
"""
|
||||
if not metadata.validate():
|
||||
raise MetadataError("Invalid paper metadata")
|
||||
|
||||
try:
|
||||
options = options or FoldingOptions()
|
||||
|
||||
# 1. 生成标题部分(不折叠)
|
||||
result = [f"# {metadata.title}\n"]
|
||||
|
||||
# 2. 生成作者信息(折叠)
|
||||
result.append(self._create_folded_section(
|
||||
"Authors",
|
||||
metadata.authors,
|
||||
options
|
||||
))
|
||||
|
||||
# 3. 生成摘要(折叠)
|
||||
result.append(self._create_folded_section(
|
||||
"Abstract",
|
||||
metadata.abstract,
|
||||
options
|
||||
))
|
||||
|
||||
# 4. 生成目录树(折叠)
|
||||
result.append(self._create_folded_section(
|
||||
"Table of Contents",
|
||||
f"```\n{metadata.catalogs}\n```",
|
||||
options
|
||||
))
|
||||
|
||||
# 5. 按章节组织并生成内容
|
||||
sections = self._organize_sections(fragments)
|
||||
for section, section_fragments in sections.items():
|
||||
# 拼接该章节的所有内容
|
||||
section_content = "\n\n".join(
|
||||
fragment.content for fragment in section_fragments
|
||||
)
|
||||
|
||||
result.append(self._create_folded_section(
|
||||
section,
|
||||
section_content,
|
||||
options
|
||||
))
|
||||
|
||||
# 6. 生成参考文献(折叠)
|
||||
# 收集所有非空的参考文献
|
||||
all_refs = "\n".join(filter(None,
|
||||
(fragment.bibliography for fragment in fragments)
|
||||
))
|
||||
if all_refs:
|
||||
result.append(self._create_folded_section(
|
||||
"Bibliography",
|
||||
f"```bibtex\n{all_refs}\n```",
|
||||
options
|
||||
))
|
||||
|
||||
return "\n\n".join(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting paper content: {str(e)}")
|
||||
raise FormattingError(f"Failed to format paper content: {str(e)}")
|
||||
|
||||
def _create_folded_section(self,
|
||||
title: str,
|
||||
content: str,
|
||||
options: FoldingOptions) -> str:
|
||||
"""创建折叠区块
|
||||
|
||||
Args:
|
||||
title: 区块标题
|
||||
content: 区块内容
|
||||
options: 折叠选项
|
||||
|
||||
Returns:
|
||||
str: 格式化后的折叠区块
|
||||
"""
|
||||
css_class = f' class="{options.custom_css}"' if options.custom_css else ''
|
||||
|
||||
result = (
|
||||
f'<details{css_class}><summary>{title}</summary>\n\n'
|
||||
f'{content}\n\n'
|
||||
f'</details>'
|
||||
)
|
||||
|
||||
return self._add_indent(result, options.indent_level)
|
||||
|
||||
def _organize_sections(self,
|
||||
fragments: list[SectionFragment]
|
||||
) -> Dict[str, list[SectionFragment]]:
|
||||
"""将片段按章节分组
|
||||
|
||||
Args:
|
||||
fragments: 论文片段列表
|
||||
|
||||
Returns:
|
||||
Dict[str, list[SectionFragment]]: 按章节分组的片段字典
|
||||
"""
|
||||
sections: Dict[str, list[SectionFragment]] = {}
|
||||
|
||||
for fragment in fragments:
|
||||
section = fragment.current_section or "Uncategorized"
|
||||
if section not in sections:
|
||||
sections[section] = []
|
||||
sections[section].append(fragment)
|
||||
|
||||
return sections
|
||||
211
crazy_functions/doc_fns/conversation_doc/excel_doc.py
Normal file
211
crazy_functions/doc_fns/conversation_doc/excel_doc.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import re
|
||||
import os
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from openpyxl import Workbook
|
||||
|
||||
|
||||
class ExcelTableFormatter:
|
||||
"""聊天记录中Markdown表格转Excel生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Excel文档对象"""
|
||||
self.workbook = Workbook()
|
||||
self._table_count = 0
|
||||
self._current_sheet = None
|
||||
|
||||
def _normalize_table_row(self, row):
|
||||
"""标准化表格行,处理不同的分隔符情况"""
|
||||
row = row.strip()
|
||||
if row.startswith('|'):
|
||||
row = row[1:]
|
||||
if row.endswith('|'):
|
||||
row = row[:-1]
|
||||
return [cell.strip() for cell in row.split('|')]
|
||||
|
||||
def _is_separator_row(self, row):
|
||||
"""检查是否是分隔行(由 - 或 : 组成)"""
|
||||
clean_row = re.sub(r'[\s|]', '', row)
|
||||
return bool(re.match(r'^[-:]+$', clean_row))
|
||||
|
||||
def _extract_tables_from_text(self, text):
|
||||
"""从文本中提取所有表格内容"""
|
||||
if not isinstance(text, str):
|
||||
return []
|
||||
|
||||
tables = []
|
||||
current_table = []
|
||||
is_in_table = False
|
||||
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if is_in_table and current_table:
|
||||
if len(current_table) >= 2:
|
||||
tables.append(current_table)
|
||||
current_table = []
|
||||
is_in_table = False
|
||||
continue
|
||||
|
||||
if '|' in line:
|
||||
if not is_in_table:
|
||||
is_in_table = True
|
||||
current_table.append(line)
|
||||
else:
|
||||
if is_in_table and current_table:
|
||||
if len(current_table) >= 2:
|
||||
tables.append(current_table)
|
||||
current_table = []
|
||||
is_in_table = False
|
||||
|
||||
if is_in_table and current_table and len(current_table) >= 2:
|
||||
tables.append(current_table)
|
||||
|
||||
return tables
|
||||
|
||||
def _parse_table(self, table_lines):
|
||||
"""解析表格内容为结构化数据"""
|
||||
try:
|
||||
headers = self._normalize_table_row(table_lines[0])
|
||||
|
||||
separator_index = next(
|
||||
(i for i, line in enumerate(table_lines) if self._is_separator_row(line)),
|
||||
1
|
||||
)
|
||||
|
||||
data_rows = []
|
||||
for line in table_lines[separator_index + 1:]:
|
||||
cells = self._normalize_table_row(line)
|
||||
# 确保单元格数量与表头一致
|
||||
while len(cells) < len(headers):
|
||||
cells.append('')
|
||||
cells = cells[:len(headers)]
|
||||
data_rows.append(cells)
|
||||
|
||||
if headers and data_rows:
|
||||
return {
|
||||
'headers': headers,
|
||||
'data': data_rows
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"解析表格时发生错误: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _create_sheet(self, question_num, table_num):
|
||||
"""创建新的工作表"""
|
||||
sheet_name = f'Q{question_num}_T{table_num}'
|
||||
if len(sheet_name) > 31:
|
||||
sheet_name = f'Table{self._table_count}'
|
||||
|
||||
if sheet_name in self.workbook.sheetnames:
|
||||
sheet_name = f'{sheet_name}_{datetime.now().strftime("%H%M%S")}'
|
||||
|
||||
return self.workbook.create_sheet(title=sheet_name)
|
||||
|
||||
def create_document(self, history):
|
||||
"""
|
||||
处理聊天历史中的所有表格并创建Excel文档
|
||||
|
||||
Args:
|
||||
history: 聊天历史列表
|
||||
|
||||
Returns:
|
||||
Workbook: 处理完成的Excel工作簿对象,如果没有表格则返回None
|
||||
"""
|
||||
has_tables = False
|
||||
|
||||
# 删除默认创建的工作表
|
||||
default_sheet = self.workbook['Sheet']
|
||||
self.workbook.remove(default_sheet)
|
||||
|
||||
# 遍历所有回答
|
||||
for i in range(1, len(history), 2):
|
||||
answer = history[i]
|
||||
tables = self._extract_tables_from_text(answer)
|
||||
|
||||
for table_lines in tables:
|
||||
parsed_table = self._parse_table(table_lines)
|
||||
if parsed_table:
|
||||
self._table_count += 1
|
||||
sheet = self._create_sheet(i // 2 + 1, self._table_count)
|
||||
|
||||
# 写入表头
|
||||
for col, header in enumerate(parsed_table['headers'], 1):
|
||||
sheet.cell(row=1, column=col, value=header)
|
||||
|
||||
# 写入数据
|
||||
for row_idx, row_data in enumerate(parsed_table['data'], 2):
|
||||
for col_idx, value in enumerate(row_data, 1):
|
||||
sheet.cell(row=row_idx, column=col_idx, value=value)
|
||||
|
||||
has_tables = True
|
||||
|
||||
return self.workbook if has_tables else None
|
||||
|
||||
|
||||
def save_chat_tables(history, save_dir, base_name):
|
||||
"""
|
||||
保存聊天历史中的表格到Excel文件
|
||||
|
||||
Args:
|
||||
history: 聊天历史列表
|
||||
save_dir: 保存目录
|
||||
base_name: 基础文件名
|
||||
|
||||
Returns:
|
||||
list: 保存的文件路径列表
|
||||
"""
|
||||
result_files = []
|
||||
|
||||
try:
|
||||
# 创建Excel格式
|
||||
excel_formatter = ExcelTableFormatter()
|
||||
workbook = excel_formatter.create_document(history)
|
||||
|
||||
if workbook is not None:
|
||||
# 确保保存目录存在
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
# 生成Excel文件路径
|
||||
excel_file = os.path.join(save_dir, base_name + '.xlsx')
|
||||
|
||||
# 保存Excel文件
|
||||
workbook.save(excel_file)
|
||||
result_files.append(excel_file)
|
||||
print(f"已保存表格到Excel文件: {excel_file}")
|
||||
except Exception as e:
|
||||
print(f"保存Excel格式失败: {str(e)}")
|
||||
|
||||
return result_files
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 示例聊天历史
|
||||
history = [
|
||||
"问题1",
|
||||
"""这是第一个表格:
|
||||
| A | B | C |
|
||||
|---|---|---|
|
||||
| 1 | 2 | 3 |""",
|
||||
|
||||
"问题2",
|
||||
"这是没有表格的回答",
|
||||
|
||||
"问题3",
|
||||
"""回答包含多个表格:
|
||||
| Name | Age |
|
||||
|------|-----|
|
||||
| Tom | 20 |
|
||||
|
||||
第二个表格:
|
||||
| X | Y |
|
||||
|---|---|
|
||||
| 1 | 2 |"""
|
||||
]
|
||||
|
||||
# 保存表格
|
||||
save_dir = "output"
|
||||
base_name = "chat_tables"
|
||||
saved_files = save_chat_tables(history, save_dir, base_name)
|
||||
190
crazy_functions/doc_fns/conversation_doc/html_doc.py
Normal file
190
crazy_functions/doc_fns/conversation_doc/html_doc.py
Normal file
@@ -0,0 +1,190 @@
|
||||
|
||||
|
||||
class HtmlFormatter:
|
||||
"""聊天记录HTML格式生成器"""
|
||||
|
||||
def __init__(self, chatbot, history):
|
||||
self.chatbot = chatbot
|
||||
self.history = history
|
||||
self.css_styles = """
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
--primary-light: #eff6ff;
|
||||
--secondary-color: #1e293b;
|
||||
--background-color: #f8fafc;
|
||||
--text-color: #334155;
|
||||
--border-color: #e2e8f0;
|
||||
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
::selection {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(-20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.container {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.QaBox {
|
||||
animation: slideIn 0.5s ease-out;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.QaBox:hover {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
.Question, .Answer, .historyBox {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.chat-title {
|
||||
color: var(--primary-color);
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
margin: 1rem 0 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.QaBox {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.Question {
|
||||
color: var(--secondary-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.Answer {
|
||||
color: var(--text-color);
|
||||
background: var(--primary-light);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.history-title {
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.historyBox {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: #0f172a;
|
||||
--text-color: #e2e8f0;
|
||||
--border-color: #1e293b;
|
||||
}
|
||||
|
||||
.container, .QaBox {
|
||||
background: #1e293b;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def format_chat_content(self) -> str:
|
||||
"""格式化聊天内容"""
|
||||
chat_content = []
|
||||
for q, a in self.chatbot:
|
||||
question = str(q) if q is not None else ""
|
||||
answer = str(a) if a is not None else ""
|
||||
chat_content.append(f'''
|
||||
<div class="QaBox">
|
||||
<div class="Question">{question}</div>
|
||||
<div class="Answer">{answer}</div>
|
||||
</div>
|
||||
''')
|
||||
return "\n".join(chat_content)
|
||||
|
||||
def format_history_content(self) -> str:
|
||||
"""格式化历史记录内容"""
|
||||
if not self.history:
|
||||
return ""
|
||||
|
||||
history_content = []
|
||||
for entry in self.history:
|
||||
history_content.append(f'''
|
||||
<div class="historyBox">
|
||||
<div class="entry">{entry}</div>
|
||||
</div>
|
||||
''')
|
||||
return "\n".join(history_content)
|
||||
|
||||
def create_document(self) -> str:
|
||||
"""生成完整的HTML文档
|
||||
|
||||
Returns:
|
||||
str: 完整的HTML文档字符串
|
||||
"""
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>对话存档</title>
|
||||
<style>{self.css_styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="chat-title">对话存档</h1>
|
||||
<div class="chat-body">
|
||||
{self.format_chat_content()}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
39
crazy_functions/doc_fns/conversation_doc/markdown_doc.py
Normal file
39
crazy_functions/doc_fns/conversation_doc/markdown_doc.py
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
class MarkdownFormatter:
|
||||
"""Markdown格式文档生成器 - 用于生成对话记录的markdown文档"""
|
||||
|
||||
def __init__(self):
|
||||
self.content = []
|
||||
|
||||
def _add_content(self, text: str):
|
||||
"""添加正文内容"""
|
||||
if text:
|
||||
self.content.append(f"\n{text}\n")
|
||||
|
||||
def create_document(self, history: list) -> str:
|
||||
"""
|
||||
创建完整的Markdown文档
|
||||
Args:
|
||||
history: 历史记录列表,偶数位置为问题,奇数位置为答案
|
||||
Returns:
|
||||
str: 生成的Markdown文本
|
||||
"""
|
||||
self.content = []
|
||||
|
||||
# 处理问答对
|
||||
for i in range(0, len(history), 2):
|
||||
question = history[i]
|
||||
answer = history[i + 1]
|
||||
|
||||
# 添加问题
|
||||
self.content.append(f"\n### 问题 {i//2 + 1}")
|
||||
self._add_content(question)
|
||||
|
||||
# 添加回答
|
||||
self.content.append(f"\n### 回答 {i//2 + 1}")
|
||||
self._add_content(answer)
|
||||
|
||||
# 添加分隔线
|
||||
self.content.append("\n---\n")
|
||||
|
||||
return "\n".join(self.content)
|
||||
172
crazy_functions/doc_fns/conversation_doc/pdf_doc.py
Normal file
172
crazy_functions/doc_fns/conversation_doc/pdf_doc.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import re
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
def convert_markdown_to_pdf(markdown_text):
|
||||
"""将Markdown文本转换为PDF格式的纯文本"""
|
||||
if not markdown_text:
|
||||
return ""
|
||||
|
||||
# 标准化换行符
|
||||
markdown_text = markdown_text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
# 处理标题、粗体、斜体
|
||||
markdown_text = re.sub(r'^#\s+(.+)$', r'\1', markdown_text, flags=re.MULTILINE)
|
||||
markdown_text = re.sub(r'\*\*(.+?)\*\*', r'\1', markdown_text)
|
||||
markdown_text = re.sub(r'\*(.+?)\*', r'\1', markdown_text)
|
||||
|
||||
# 处理列表
|
||||
markdown_text = re.sub(r'^\s*[-*+]\s+(.+?)(?=\n|$)', r'• \1', markdown_text, flags=re.MULTILINE)
|
||||
markdown_text = re.sub(r'^\s*\d+\.\s+(.+?)(?=\n|$)', r'\1', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 处理链接
|
||||
markdown_text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', markdown_text)
|
||||
|
||||
# 处理段落
|
||||
markdown_text = re.sub(r'\n{2,}', '\n', markdown_text)
|
||||
markdown_text = re.sub(r'(?<!\n)(?<!^)(?<!•\s)(?<!\d\.\s)\n(?![\s•\d])', '\n\n', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 清理空白
|
||||
markdown_text = re.sub(r' +', ' ', markdown_text)
|
||||
markdown_text = re.sub(r'(?m)^\s+|\s+$', '', markdown_text)
|
||||
|
||||
return markdown_text.strip()
|
||||
|
||||
class PDFFormatter:
|
||||
"""聊天记录PDF文档生成器 - 使用 Noto Sans CJK 字体"""
|
||||
|
||||
def __init__(self):
|
||||
self._init_reportlab()
|
||||
self._register_fonts()
|
||||
self.styles = self._get_reportlab_lib()['getSampleStyleSheet']()
|
||||
self._create_styles()
|
||||
|
||||
def _init_reportlab(self):
|
||||
"""初始化 ReportLab 相关组件"""
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||
|
||||
self._lib = {
|
||||
'A4': A4,
|
||||
'getSampleStyleSheet': getSampleStyleSheet,
|
||||
'ParagraphStyle': ParagraphStyle,
|
||||
'cm': cm
|
||||
}
|
||||
|
||||
self._platypus = {
|
||||
'SimpleDocTemplate': SimpleDocTemplate,
|
||||
'Paragraph': Paragraph,
|
||||
'Spacer': Spacer
|
||||
}
|
||||
|
||||
def _get_reportlab_lib(self):
|
||||
return self._lib
|
||||
|
||||
def _get_reportlab_platypus(self):
|
||||
return self._platypus
|
||||
|
||||
def _register_fonts(self):
|
||||
"""注册 Noto Sans CJK 字体"""
|
||||
possible_font_paths = [
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
||||
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
|
||||
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc'
|
||||
]
|
||||
|
||||
font_registered = False
|
||||
for path in possible_font_paths:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont('NotoSansCJK', path))
|
||||
font_registered = True
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not font_registered:
|
||||
print("Warning: Could not find Noto Sans CJK font. Using fallback font.")
|
||||
self.font_name = 'Helvetica'
|
||||
else:
|
||||
self.font_name = 'NotoSansCJK'
|
||||
|
||||
def _create_styles(self):
|
||||
"""创建文档样式"""
|
||||
ParagraphStyle = self._lib['ParagraphStyle']
|
||||
|
||||
# 标题样式
|
||||
self.styles.add(ParagraphStyle(
|
||||
name='Title_Custom',
|
||||
fontName=self.font_name,
|
||||
fontSize=24,
|
||||
leading=38,
|
||||
alignment=1,
|
||||
spaceAfter=32
|
||||
))
|
||||
|
||||
# 日期样式
|
||||
self.styles.add(ParagraphStyle(
|
||||
name='Date_Style',
|
||||
fontName=self.font_name,
|
||||
fontSize=16,
|
||||
leading=20,
|
||||
alignment=1,
|
||||
spaceAfter=20
|
||||
))
|
||||
|
||||
# 问题样式
|
||||
self.styles.add(ParagraphStyle(
|
||||
name='Question_Style',
|
||||
fontName=self.font_name,
|
||||
fontSize=12,
|
||||
leading=18,
|
||||
leftIndent=28,
|
||||
spaceAfter=6
|
||||
))
|
||||
|
||||
# 回答样式
|
||||
self.styles.add(ParagraphStyle(
|
||||
name='Answer_Style',
|
||||
fontName=self.font_name,
|
||||
fontSize=12,
|
||||
leading=18,
|
||||
leftIndent=28,
|
||||
spaceAfter=12
|
||||
))
|
||||
|
||||
def create_document(self, history, output_path):
|
||||
"""生成PDF文档"""
|
||||
# 创建PDF文档
|
||||
doc = self._platypus['SimpleDocTemplate'](
|
||||
output_path,
|
||||
pagesize=self._lib['A4'],
|
||||
rightMargin=2.6 * self._lib['cm'],
|
||||
leftMargin=2.8 * self._lib['cm'],
|
||||
topMargin=3.7 * self._lib['cm'],
|
||||
bottomMargin=3.5 * self._lib['cm']
|
||||
)
|
||||
|
||||
# 构建内容
|
||||
story = []
|
||||
Paragraph = self._platypus['Paragraph']
|
||||
|
||||
# 添加对话内容
|
||||
for i in range(0, len(history), 2):
|
||||
question = history[i]
|
||||
answer = convert_markdown_to_pdf(history[i + 1]) if i + 1 < len(history) else ""
|
||||
|
||||
if question:
|
||||
q_text = f'问题 {i // 2 + 1}:{str(question)}'
|
||||
story.append(Paragraph(q_text, self.styles['Question_Style']))
|
||||
|
||||
if answer:
|
||||
a_text = f'回答 {i // 2 + 1}:{str(answer)}'
|
||||
story.append(Paragraph(a_text, self.styles['Answer_Style']))
|
||||
|
||||
# 构建PDF
|
||||
doc.build(story)
|
||||
|
||||
return doc
|
||||
79
crazy_functions/doc_fns/conversation_doc/txt_doc.py
Normal file
79
crazy_functions/doc_fns/conversation_doc/txt_doc.py
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def convert_markdown_to_txt(markdown_text):
|
||||
"""Convert markdown text to plain text while preserving formatting"""
|
||||
# Standardize line endings
|
||||
markdown_text = markdown_text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
# 1. Handle headers but keep their formatting instead of removing them
|
||||
markdown_text = re.sub(r'^#\s+(.+)$', r'# \1', markdown_text, flags=re.MULTILINE)
|
||||
markdown_text = re.sub(r'^##\s+(.+)$', r'## \1', markdown_text, flags=re.MULTILINE)
|
||||
markdown_text = re.sub(r'^###\s+(.+)$', r'### \1', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 2. Handle bold and italic - simply remove markers
|
||||
markdown_text = re.sub(r'\*\*(.+?)\*\*', r'\1', markdown_text)
|
||||
markdown_text = re.sub(r'\*(.+?)\*', r'\1', markdown_text)
|
||||
|
||||
# 3. Handle lists but preserve formatting
|
||||
markdown_text = re.sub(r'^\s*[-*+]\s+(.+?)(?=\n|$)', r'• \1', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 4. Handle links - keep only the text
|
||||
markdown_text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1 (\2)', markdown_text)
|
||||
|
||||
# 5. Handle HTML links - convert to user-friendly format
|
||||
markdown_text = re.sub(r'<a href=[\'"]([^\'"]+)[\'"](?:\s+target=[\'"][^\'"]+[\'"])?>([^<]+)</a>', r'\2 (\1)',
|
||||
markdown_text)
|
||||
|
||||
# 6. Preserve paragraph breaks
|
||||
markdown_text = re.sub(r'\n{3,}', '\n\n', markdown_text) # normalize multiple newlines to double newlines
|
||||
|
||||
# 7. Clean up extra spaces but maintain indentation
|
||||
markdown_text = re.sub(r' +', ' ', markdown_text)
|
||||
|
||||
return markdown_text.strip()
|
||||
|
||||
|
||||
class TxtFormatter:
|
||||
"""Chat history TXT document generator"""
|
||||
|
||||
def __init__(self):
|
||||
self.content = []
|
||||
self._setup_document()
|
||||
|
||||
def _setup_document(self):
|
||||
"""Initialize document with header"""
|
||||
self.content.append("=" * 50)
|
||||
self.content.append("GPT-Academic对话记录".center(48))
|
||||
self.content.append("=" * 50)
|
||||
|
||||
def _format_header(self):
|
||||
"""Create document header with current date"""
|
||||
from datetime import datetime
|
||||
date_str = datetime.now().strftime('%Y年%m月%d日')
|
||||
return [
|
||||
date_str.center(48),
|
||||
"\n" # Add blank line after date
|
||||
]
|
||||
|
||||
def create_document(self, history):
|
||||
"""Generate document from chat history"""
|
||||
# Add header with date
|
||||
self.content.extend(self._format_header())
|
||||
|
||||
# Add conversation content
|
||||
for i in range(0, len(history), 2):
|
||||
question = history[i]
|
||||
answer = convert_markdown_to_txt(history[i + 1]) if i + 1 < len(history) else ""
|
||||
|
||||
if question:
|
||||
self.content.append(f"问题 {i // 2 + 1}:{str(question)}")
|
||||
self.content.append("") # Add blank line
|
||||
|
||||
if answer:
|
||||
self.content.append(f"回答 {i // 2 + 1}:{str(answer)}")
|
||||
self.content.append("") # Add blank line
|
||||
|
||||
# Join all content with newlines
|
||||
return "\n".join(self.content)
|
||||
155
crazy_functions/doc_fns/conversation_doc/word2pdf.py
Normal file
155
crazy_functions/doc_fns/conversation_doc/word2pdf.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from docx2pdf import convert
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from typing import Union
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
class WordToPdfConverter:
|
||||
"""Word文档转PDF转换器"""
|
||||
|
||||
@staticmethod
|
||||
def convert_to_pdf(word_path: Union[str, Path], pdf_path: Union[str, Path] = None) -> str:
|
||||
"""
|
||||
将Word文档转换为PDF
|
||||
|
||||
参数:
|
||||
word_path: Word文档的路径
|
||||
pdf_path: 可选,PDF文件的输出路径。如果未指定,将使用与Word文档相同的名称和位置
|
||||
|
||||
返回:
|
||||
生成的PDF文件路径
|
||||
|
||||
异常:
|
||||
如果转换失败,将抛出相应异常
|
||||
"""
|
||||
try:
|
||||
# 确保输入路径是Path对象
|
||||
word_path = Path(word_path)
|
||||
|
||||
# 如果未指定pdf_path,则使用与word文档相同的名称
|
||||
if pdf_path is None:
|
||||
pdf_path = word_path.with_suffix('.pdf')
|
||||
else:
|
||||
pdf_path = Path(pdf_path)
|
||||
|
||||
# 检查操作系统
|
||||
if platform.system() == 'Linux':
|
||||
# Linux系统需要安装libreoffice
|
||||
which_result = subprocess.run(['which', 'libreoffice'], capture_output=True, text=True)
|
||||
if which_result.returncode != 0:
|
||||
raise RuntimeError("请先安装LibreOffice: sudo apt-get install libreoffice")
|
||||
|
||||
print(f"开始转换Word文档: {word_path} 到 PDF")
|
||||
|
||||
# 使用subprocess代替os.system
|
||||
result = subprocess.run(
|
||||
['libreoffice', '--headless', '--convert-to', 'pdf:writer_pdf_Export',
|
||||
str(word_path), '--outdir', str(pdf_path.parent)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr or "未知错误"
|
||||
print(f"LibreOffice转换失败,错误信息: {error_msg}")
|
||||
raise RuntimeError(f"LibreOffice转换失败: {error_msg}")
|
||||
|
||||
print(f"LibreOffice转换输出: {result.stdout}")
|
||||
|
||||
# 如果输出路径与默认生成的不同,则重命名
|
||||
default_pdf = word_path.with_suffix('.pdf')
|
||||
if default_pdf != pdf_path and default_pdf.exists():
|
||||
os.rename(default_pdf, pdf_path)
|
||||
print(f"已将PDF从 {default_pdf} 重命名为 {pdf_path}")
|
||||
|
||||
# 验证PDF是否成功生成
|
||||
if not pdf_path.exists() or pdf_path.stat().st_size == 0:
|
||||
raise RuntimeError("PDF生成失败或文件为空")
|
||||
|
||||
print(f"PDF转换成功,文件大小: {pdf_path.stat().st_size} 字节")
|
||||
else:
|
||||
# Windows和MacOS使用docx2pdf
|
||||
print(f"使用docx2pdf转换 {word_path} 到 {pdf_path}")
|
||||
convert(word_path, pdf_path)
|
||||
|
||||
# 验证PDF是否成功生成
|
||||
if not pdf_path.exists() or pdf_path.stat().st_size == 0:
|
||||
raise RuntimeError("PDF生成失败或文件为空")
|
||||
|
||||
print(f"PDF转换成功,文件大小: {pdf_path.stat().st_size} 字节")
|
||||
|
||||
return str(pdf_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"PDF转换异常: {str(e)}")
|
||||
raise Exception(f"转换PDF失败: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def batch_convert(word_dir: Union[str, Path], pdf_dir: Union[str, Path] = None) -> list:
|
||||
"""
|
||||
批量转换目录下的所有Word文档
|
||||
|
||||
参数:
|
||||
word_dir: 包含Word文档的目录路径
|
||||
pdf_dir: 可选,PDF文件的输出目录。如果未指定,将使用与Word文档相同的目录
|
||||
|
||||
返回:
|
||||
生成的PDF文件路径列表
|
||||
"""
|
||||
word_dir = Path(word_dir)
|
||||
if pdf_dir:
|
||||
pdf_dir = Path(pdf_dir)
|
||||
pdf_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
converted_files = []
|
||||
|
||||
for word_file in word_dir.glob("*.docx"):
|
||||
try:
|
||||
if pdf_dir:
|
||||
pdf_path = pdf_dir / word_file.with_suffix('.pdf').name
|
||||
else:
|
||||
pdf_path = word_file.with_suffix('.pdf')
|
||||
|
||||
pdf_file = WordToPdfConverter.convert_to_pdf(word_file, pdf_path)
|
||||
converted_files.append(pdf_file)
|
||||
|
||||
except Exception as e:
|
||||
print(f"转换 {word_file} 失败: {str(e)}")
|
||||
|
||||
return converted_files
|
||||
|
||||
@staticmethod
|
||||
def convert_doc_to_pdf(doc, output_dir: Union[str, Path] = None) -> str:
|
||||
"""
|
||||
将docx对象直接转换为PDF
|
||||
|
||||
参数:
|
||||
doc: python-docx的Document对象
|
||||
output_dir: 可选,输出目录。如果未指定,将使用当前目录
|
||||
|
||||
返回:
|
||||
生成的PDF文件路径
|
||||
"""
|
||||
try:
|
||||
# 设置临时文件路径和输出路径
|
||||
output_dir = Path(output_dir) if output_dir else Path.cwd()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成临时word文件
|
||||
temp_docx = output_dir / f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
||||
doc.save(temp_docx)
|
||||
|
||||
# 转换为PDF
|
||||
pdf_path = temp_docx.with_suffix('.pdf')
|
||||
WordToPdfConverter.convert_to_pdf(temp_docx, pdf_path)
|
||||
|
||||
# 删除临时word文件
|
||||
temp_docx.unlink()
|
||||
|
||||
return str(pdf_path)
|
||||
|
||||
except Exception as e:
|
||||
if temp_docx.exists():
|
||||
temp_docx.unlink()
|
||||
raise Exception(f"转换PDF失败: {str(e)}")
|
||||
177
crazy_functions/doc_fns/conversation_doc/word_doc.py
Normal file
177
crazy_functions/doc_fns/conversation_doc/word_doc.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import re
|
||||
from docx import Document
|
||||
from docx.shared import Cm, Pt
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING
|
||||
from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.oxml.ns import qn
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def convert_markdown_to_word(markdown_text):
|
||||
# 0. 首先标准化所有换行符为\n
|
||||
markdown_text = markdown_text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
# 1. 处理标题 - 支持更多级别的标题,使用更精确的正则
|
||||
# 保留标题标记,以便后续处理时还能识别出标题级别
|
||||
markdown_text = re.sub(r'^(#{1,6})\s+(.+?)(?:\s+#+)?$', r'\1 \2', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 2. 处理粗体、斜体和加粗斜体
|
||||
markdown_text = re.sub(r'\*\*\*(.+?)\*\*\*', r'\1', markdown_text) # 加粗斜体
|
||||
markdown_text = re.sub(r'\*\*(.+?)\*\*', r'\1', markdown_text) # 加粗
|
||||
markdown_text = re.sub(r'\*(.+?)\*', r'\1', markdown_text) # 斜体
|
||||
markdown_text = re.sub(r'_(.+?)_', r'\1', markdown_text) # 下划线斜体
|
||||
markdown_text = re.sub(r'__(.+?)__', r'\1', markdown_text) # 下划线加粗
|
||||
|
||||
# 3. 处理代码块 - 不移除,而是简化格式
|
||||
# 多行代码块
|
||||
markdown_text = re.sub(r'```(?:\w+)?\n([\s\S]*?)```', r'[代码块]\n\1[/代码块]', markdown_text)
|
||||
# 单行代码
|
||||
markdown_text = re.sub(r'`([^`]+)`', r'[代码]\1[/代码]', markdown_text)
|
||||
|
||||
# 4. 处理列表 - 保留列表结构
|
||||
# 匹配无序列表
|
||||
markdown_text = re.sub(r'^(\s*)[-*+]\s+(.+?)$', r'\1• \2', markdown_text, flags=re.MULTILINE)
|
||||
|
||||
# 5. 处理Markdown链接
|
||||
markdown_text = re.sub(r'\[([^\]]+)\]\(([^)]+?)\s*(?:"[^"]*")?\)', r'\1 (\2)', markdown_text)
|
||||
|
||||
# 6. 处理HTML链接
|
||||
markdown_text = re.sub(r'<a href=[\'"]([^\'"]+)[\'"](?:\s+target=[\'"][^\'"]+[\'"])?>([^<]+)</a>', r'\2 (\1)',
|
||||
markdown_text)
|
||||
|
||||
# 7. 处理图片
|
||||
markdown_text = re.sub(r'!\[([^\]]*)\]\([^)]+\)', r'[图片:\1]', markdown_text)
|
||||
|
||||
return markdown_text
|
||||
|
||||
|
||||
class WordFormatter:
|
||||
"""聊天记录Word文档生成器 - 符合中国政府公文格式规范(GB/T 9704-2012)"""
|
||||
|
||||
def __init__(self):
|
||||
self.doc = Document()
|
||||
self._setup_document()
|
||||
self._create_styles()
|
||||
|
||||
def _setup_document(self):
|
||||
"""设置文档基本格式,包括页面设置和页眉"""
|
||||
sections = self.doc.sections
|
||||
for section in sections:
|
||||
# 设置页面大小为A4
|
||||
section.page_width = Cm(21)
|
||||
section.page_height = Cm(29.7)
|
||||
# 设置页边距
|
||||
section.top_margin = Cm(3.7) # 上边距37mm
|
||||
section.bottom_margin = Cm(3.5) # 下边距35mm
|
||||
section.left_margin = Cm(2.8) # 左边距28mm
|
||||
section.right_margin = Cm(2.6) # 右边距26mm
|
||||
# 设置页眉页脚距离
|
||||
section.header_distance = Cm(2.0)
|
||||
section.footer_distance = Cm(2.0)
|
||||
|
||||
# 添加页眉
|
||||
header = section.header
|
||||
header_para = header.paragraphs[0]
|
||||
header_para.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
|
||||
header_run = header_para.add_run("GPT-Academic对话记录")
|
||||
header_run.font.name = '仿宋'
|
||||
header_run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
header_run.font.size = Pt(9)
|
||||
|
||||
def _create_styles(self):
|
||||
"""创建文档样式"""
|
||||
# 创建正文样式
|
||||
style = self.doc.styles.add_style('Normal_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
||||
style.font.name = '仿宋'
|
||||
style._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
style.font.size = Pt(12) # 调整为12磅
|
||||
style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
style.paragraph_format.space_after = Pt(0)
|
||||
|
||||
# 创建问题样式
|
||||
question_style = self.doc.styles.add_style('Question_Style', WD_STYLE_TYPE.PARAGRAPH)
|
||||
question_style.font.name = '黑体'
|
||||
question_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
||||
question_style.font.size = Pt(14) # 调整为14磅
|
||||
question_style.font.bold = True
|
||||
question_style.paragraph_format.space_before = Pt(12) # 减小段前距
|
||||
question_style.paragraph_format.space_after = Pt(6)
|
||||
question_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
question_style.paragraph_format.left_indent = Pt(0) # 移除左缩进
|
||||
|
||||
# 创建回答样式
|
||||
answer_style = self.doc.styles.add_style('Answer_Style', WD_STYLE_TYPE.PARAGRAPH)
|
||||
answer_style.font.name = '仿宋'
|
||||
answer_style._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
answer_style.font.size = Pt(12) # 调整为12磅
|
||||
answer_style.paragraph_format.space_before = Pt(6)
|
||||
answer_style.paragraph_format.space_after = Pt(12)
|
||||
answer_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
answer_style.paragraph_format.left_indent = Pt(0) # 移除左缩进
|
||||
|
||||
# 创建标题样式
|
||||
title_style = self.doc.styles.add_style('Title_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
||||
title_style.font.name = '黑体' # 改用黑体
|
||||
title_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
||||
title_style.font.size = Pt(22) # 调整为22磅
|
||||
title_style.font.bold = True
|
||||
title_style.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
title_style.paragraph_format.space_before = Pt(0)
|
||||
title_style.paragraph_format.space_after = Pt(24)
|
||||
title_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
# 添加参考文献样式
|
||||
ref_style = self.doc.styles.add_style('Reference_Style', WD_STYLE_TYPE.PARAGRAPH)
|
||||
ref_style.font.name = '宋体'
|
||||
ref_style._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
|
||||
ref_style.font.size = Pt(10.5) # 参考文献使用小号字体
|
||||
ref_style.paragraph_format.space_before = Pt(3)
|
||||
ref_style.paragraph_format.space_after = Pt(3)
|
||||
ref_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
ref_style.paragraph_format.left_indent = Pt(21)
|
||||
ref_style.paragraph_format.first_line_indent = Pt(-21)
|
||||
|
||||
# 添加参考文献标题样式
|
||||
ref_title_style = self.doc.styles.add_style('Reference_Title_Style', WD_STYLE_TYPE.PARAGRAPH)
|
||||
ref_title_style.font.name = '黑体'
|
||||
ref_title_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
||||
ref_title_style.font.size = Pt(16)
|
||||
ref_title_style.font.bold = True
|
||||
ref_title_style.paragraph_format.space_before = Pt(24)
|
||||
ref_title_style.paragraph_format.space_after = Pt(12)
|
||||
ref_title_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
def create_document(self, history):
|
||||
"""写入聊天历史"""
|
||||
# 添加标题
|
||||
title_para = self.doc.add_paragraph(style='Title_Custom')
|
||||
title_run = title_para.add_run('GPT-Academic 对话记录')
|
||||
|
||||
# 添加日期
|
||||
date_para = self.doc.add_paragraph()
|
||||
date_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
date_run = date_para.add_run(datetime.now().strftime('%Y年%m月%d日'))
|
||||
date_run.font.name = '仿宋'
|
||||
date_run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
||||
date_run.font.size = Pt(16)
|
||||
|
||||
self.doc.add_paragraph() # 添加空行
|
||||
|
||||
# 添加对话内容
|
||||
for i in range(0, len(history), 2):
|
||||
question = history[i]
|
||||
answer = convert_markdown_to_word(history[i + 1])
|
||||
|
||||
if question:
|
||||
q_para = self.doc.add_paragraph(style='Question_Style')
|
||||
q_para.add_run(f'问题 {i//2 + 1}:').bold = True
|
||||
q_para.add_run(str(question))
|
||||
|
||||
if answer:
|
||||
a_para = self.doc.add_paragraph(style='Answer_Style')
|
||||
a_para.add_run(f'回答 {i//2 + 1}:').bold = True
|
||||
a_para.add_run(str(answer))
|
||||
|
||||
|
||||
return self.doc
|
||||
|
||||
0
crazy_functions/doc_fns/read_fns/__init__.py
Normal file
0
crazy_functions/doc_fns/read_fns/__init__.py
Normal file
6
crazy_functions/doc_fns/read_fns/docx_reader.py
Normal file
6
crazy_functions/doc_fns/read_fns/docx_reader.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import nltk
|
||||
nltk.data.path.append('~/nltk_data')
|
||||
nltk.download('averaged_perceptron_tagger', download_dir='~/nltk_data',
|
||||
)
|
||||
nltk.download('punkt', download_dir='~/nltk_data',
|
||||
)
|
||||
286
crazy_functions/doc_fns/read_fns/excel_reader.py
Normal file
286
crazy_functions/doc_fns/read_fns/excel_reader.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Set, Dict, Union, Iterator, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import chardet
|
||||
from functools import lru_cache
|
||||
import os
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractorConfig:
|
||||
"""提取器配置类"""
|
||||
encoding: str = 'auto'
|
||||
na_filter: bool = True
|
||||
skip_blank_lines: bool = True
|
||||
chunk_size: int = 10000
|
||||
max_workers: int = 4
|
||||
preserve_format: bool = True
|
||||
read_all_sheets: bool = True # 新增:是否读取所有工作表
|
||||
text_cleanup: Dict[str, bool] = field(default_factory=lambda: {
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': False,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
})
|
||||
|
||||
|
||||
class ExcelTextExtractor:
|
||||
"""增强的Excel格式文件文本内容提取器"""
|
||||
|
||||
SUPPORTED_EXTENSIONS: Set[str] = {
|
||||
'.xlsx', '.xls', '.csv', '.tsv', '.xlsm', '.xltx', '.xltm', '.ods'
|
||||
}
|
||||
|
||||
def __init__(self, config: Optional[ExtractorConfig] = None):
|
||||
self.config = config or ExtractorConfig()
|
||||
self._setup_logging()
|
||||
self._detect_encoding = lru_cache(maxsize=128)(self._detect_encoding)
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录器"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
fh = logging.FileHandler('excel_extractor.log')
|
||||
fh.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
def _detect_encoding(self, file_path: Path) -> str:
|
||||
if self.config.encoding != 'auto':
|
||||
return self.config.encoding
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
raw_data = f.read(10000)
|
||||
result = chardet.detect(raw_data)
|
||||
return result['encoding'] or 'utf-8'
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Encoding detection failed: {e}. Using utf-8")
|
||||
return 'utf-8'
|
||||
|
||||
def _validate_file(self, file_path: Union[str, Path]) -> Path:
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"File not found: {path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Not a file: {path}")
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
raise PermissionError(f"No read permission: {path}")
|
||||
|
||||
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(
|
||||
f"Unsupported format: {path.suffix}. "
|
||||
f"Supported: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}"
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def _format_value(self, value: Any) -> str:
|
||||
if pd.isna(value) or value is None:
|
||||
return ''
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
return str(value).strip()
|
||||
|
||||
def _process_chunk(self, chunk: pd.DataFrame, columns: Optional[List[str]] = None, sheet_name: str = '') -> str:
|
||||
"""处理数据块,新增sheet_name参数"""
|
||||
try:
|
||||
if columns:
|
||||
chunk = chunk[columns]
|
||||
|
||||
if self.config.preserve_format:
|
||||
formatted_chunk = chunk.applymap(self._format_value)
|
||||
rows = []
|
||||
|
||||
# 添加工作表名称作为标题
|
||||
if sheet_name:
|
||||
rows.append(f"[Sheet: {sheet_name}]")
|
||||
|
||||
# 添加表头
|
||||
headers = [str(col) for col in formatted_chunk.columns]
|
||||
rows.append('\t'.join(headers))
|
||||
|
||||
# 添加数据行
|
||||
for _, row in formatted_chunk.iterrows():
|
||||
rows.append('\t'.join(row.values))
|
||||
|
||||
return '\n'.join(rows)
|
||||
else:
|
||||
flat_values = (
|
||||
chunk.astype(str)
|
||||
.replace({'nan': '', 'None': '', 'NaN': ''})
|
||||
.values.flatten()
|
||||
)
|
||||
return ' '.join(v for v in flat_values if v)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing chunk: {e}")
|
||||
raise
|
||||
|
||||
def _read_file(self, file_path: Path) -> Union[pd.DataFrame, Iterator[pd.DataFrame], Dict[str, pd.DataFrame]]:
|
||||
"""读取文件,支持多工作表"""
|
||||
try:
|
||||
encoding = self._detect_encoding(file_path)
|
||||
|
||||
if file_path.suffix.lower() in {'.csv', '.tsv'}:
|
||||
sep = '\t' if file_path.suffix.lower() == '.tsv' else ','
|
||||
|
||||
# 对大文件使用分块读取
|
||||
if file_path.stat().st_size > self.config.chunk_size * 1024:
|
||||
return pd.read_csv(
|
||||
file_path,
|
||||
encoding=encoding,
|
||||
na_filter=self.config.na_filter,
|
||||
skip_blank_lines=self.config.skip_blank_lines,
|
||||
sep=sep,
|
||||
chunksize=self.config.chunk_size,
|
||||
on_bad_lines='warn'
|
||||
)
|
||||
else:
|
||||
return pd.read_csv(
|
||||
file_path,
|
||||
encoding=encoding,
|
||||
na_filter=self.config.na_filter,
|
||||
skip_blank_lines=self.config.skip_blank_lines,
|
||||
sep=sep
|
||||
)
|
||||
else:
|
||||
# Excel文件处理,支持多工作表
|
||||
if self.config.read_all_sheets:
|
||||
# 读取所有工作表
|
||||
return pd.read_excel(
|
||||
file_path,
|
||||
na_filter=self.config.na_filter,
|
||||
keep_default_na=self.config.na_filter,
|
||||
engine='openpyxl',
|
||||
sheet_name=None # None表示读取所有工作表
|
||||
)
|
||||
else:
|
||||
# 只读取第一个工作表
|
||||
return pd.read_excel(
|
||||
file_path,
|
||||
na_filter=self.config.na_filter,
|
||||
keep_default_na=self.config.na_filter,
|
||||
engine='openpyxl',
|
||||
sheet_name=0 # 读取第一个工作表
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reading file {file_path}: {e}")
|
||||
raise
|
||||
|
||||
def extract_text(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
columns: Optional[List[str]] = None,
|
||||
separator: str = '\n'
|
||||
) -> str:
|
||||
"""提取文本,支持多工作表"""
|
||||
try:
|
||||
path = self._validate_file(file_path)
|
||||
self.logger.info(f"Processing: {path}")
|
||||
|
||||
reader = self._read_file(path)
|
||||
texts = []
|
||||
|
||||
# 处理Excel多工作表
|
||||
if isinstance(reader, dict):
|
||||
for sheet_name, df in reader.items():
|
||||
sheet_text = self._process_chunk(df, columns, sheet_name)
|
||||
if sheet_text:
|
||||
texts.append(sheet_text)
|
||||
return separator.join(texts)
|
||||
|
||||
# 处理单个DataFrame
|
||||
elif isinstance(reader, pd.DataFrame):
|
||||
return self._process_chunk(reader, columns)
|
||||
|
||||
# 处理DataFrame迭代器
|
||||
else:
|
||||
with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor:
|
||||
futures = {
|
||||
executor.submit(self._process_chunk, chunk, columns): i
|
||||
for i, chunk in enumerate(reader)
|
||||
}
|
||||
|
||||
chunk_texts = []
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
text = future.result()
|
||||
if text:
|
||||
chunk_texts.append((futures[future], text))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in chunk {futures[future]}: {e}")
|
||||
|
||||
# 按块的顺序排序
|
||||
chunk_texts.sort(key=lambda x: x[0])
|
||||
texts = [text for _, text in chunk_texts]
|
||||
|
||||
# 合并文本,保留格式
|
||||
if texts and self.config.preserve_format:
|
||||
result = texts[0] # 第一块包含表头
|
||||
if len(texts) > 1:
|
||||
# 跳过后续块的表头行
|
||||
for text in texts[1:]:
|
||||
result += '\n' + '\n'.join(text.split('\n')[1:])
|
||||
return result
|
||||
else:
|
||||
return separator.join(texts)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Extraction failed: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_supported_formats() -> List[str]:
|
||||
"""获取支持的文件格式列表"""
|
||||
return sorted(ExcelTextExtractor.SUPPORTED_EXTENSIONS)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:演示用法"""
|
||||
config = ExtractorConfig(
|
||||
encoding='auto',
|
||||
preserve_format=True,
|
||||
read_all_sheets=True, # 启用多工作表读取
|
||||
text_cleanup={
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': False,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
}
|
||||
)
|
||||
|
||||
extractor = ExcelTextExtractor(config)
|
||||
|
||||
try:
|
||||
sample_file = 'example.xlsx'
|
||||
if Path(sample_file).exists():
|
||||
text = extractor.extract_text(
|
||||
sample_file,
|
||||
columns=['title', 'content']
|
||||
)
|
||||
print("提取的文本:")
|
||||
print(text)
|
||||
else:
|
||||
print(f"示例文件 {sample_file} 不存在")
|
||||
|
||||
print("\n支持的格式:", extractor.get_supported_formats())
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
359
crazy_functions/doc_fns/read_fns/markitdown/markdown_reader.py
Normal file
359
crazy_functions/doc_fns/read_fns/markitdown/markdown_reader.py
Normal file
@@ -0,0 +1,359 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set, Dict, Union, List
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
@dataclass
|
||||
class MarkdownConverterConfig:
|
||||
"""PDF 到 Markdown 转换器配置类
|
||||
|
||||
Attributes:
|
||||
extract_images: 是否提取图片
|
||||
extract_tables: 是否尝试保留表格结构
|
||||
extract_code_blocks: 是否识别代码块
|
||||
extract_math: 是否转换数学公式
|
||||
output_dir: 输出目录路径
|
||||
image_dir: 图片保存目录路径
|
||||
paragraph_separator: 段落之间的分隔符
|
||||
text_cleanup: 文本清理选项字典
|
||||
docintel_endpoint: Document Intelligence端点URL (可选)
|
||||
enable_plugins: 是否启用插件
|
||||
llm_client: LLM客户端对象 (例如OpenAI client)
|
||||
llm_model: 要使用的LLM模型名称
|
||||
"""
|
||||
extract_images: bool = True
|
||||
extract_tables: bool = True
|
||||
extract_code_blocks: bool = True
|
||||
extract_math: bool = True
|
||||
output_dir: str = ""
|
||||
image_dir: str = "images"
|
||||
paragraph_separator: str = '\n\n'
|
||||
text_cleanup: Dict[str, bool] = field(default_factory=lambda: {
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
})
|
||||
docintel_endpoint: str = ""
|
||||
enable_plugins: bool = False
|
||||
llm_client: Optional[object] = None
|
||||
llm_model: str = ""
|
||||
|
||||
|
||||
class MarkdownConverter:
|
||||
"""PDF 到 Markdown 转换器
|
||||
|
||||
使用 markitdown 库实现 PDF 到 Markdown 的转换,支持多种配置选项。
|
||||
"""
|
||||
|
||||
SUPPORTED_EXTENSIONS: Set[str] = {
|
||||
'.pdf',
|
||||
}
|
||||
|
||||
def __init__(self, config: Optional[MarkdownConverterConfig] = None):
|
||||
"""初始化转换器
|
||||
|
||||
Args:
|
||||
config: 转换器配置对象,如果为None则使用默认配置
|
||||
"""
|
||||
self.config = config or MarkdownConverterConfig()
|
||||
self._setup_logging()
|
||||
|
||||
# 检查是否安装了 markitdown
|
||||
self._check_markitdown_installation()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录器"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 添加文件处理器
|
||||
fh = logging.FileHandler('markdown_converter.log')
|
||||
fh.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
def _check_markitdown_installation(self) -> None:
|
||||
"""检查是否安装了 markitdown"""
|
||||
try:
|
||||
# 尝试导入 markitdown 库
|
||||
from markitdown import MarkItDown
|
||||
self.logger.info("markitdown 库已安装")
|
||||
except ImportError:
|
||||
self.logger.warning("markitdown 库未安装,尝试安装...")
|
||||
try:
|
||||
subprocess.check_call(["pip", "install", "markitdown"])
|
||||
self.logger.info("markitdown 库安装成功")
|
||||
from markitdown import MarkItDown
|
||||
except (subprocess.SubprocessError, ImportError):
|
||||
self.logger.error("无法安装 markitdown 库,请手动安装")
|
||||
self.markitdown_available = False
|
||||
return
|
||||
|
||||
self.markitdown_available = True
|
||||
|
||||
def _validate_file(self, file_path: Union[str, Path], max_size_mb: int = 100) -> Path:
|
||||
"""验证文件
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
max_size_mb: 允许的最大文件大小(MB)
|
||||
|
||||
Returns:
|
||||
Path: 验证后的Path对象
|
||||
|
||||
Raises:
|
||||
ValueError: 文件不存在、格式不支持或大小超限
|
||||
PermissionError: 没有读取权限
|
||||
"""
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"文件不存在: {path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"不是一个文件: {path}")
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
raise PermissionError(f"没有读取权限: {path}")
|
||||
|
||||
file_size_mb = path.stat().st_size / (1024 * 1024)
|
||||
if file_size_mb > max_size_mb:
|
||||
raise ValueError(
|
||||
f"文件大小 ({file_size_mb:.1f}MB) 超过限制 {max_size_mb}MB"
|
||||
)
|
||||
|
||||
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(
|
||||
f"不支持的格式: {path.suffix}. "
|
||||
f"支持的格式: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}"
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def _cleanup_text(self, text: str) -> str:
|
||||
"""清理文本
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
|
||||
Returns:
|
||||
str: 清理后的文本
|
||||
"""
|
||||
if self.config.text_cleanup['remove_extra_spaces']:
|
||||
text = ' '.join(text.split())
|
||||
|
||||
if self.config.text_cleanup['normalize_whitespace']:
|
||||
text = text.replace('\t', ' ').replace('\r', '\n')
|
||||
|
||||
if self.config.text_cleanup['lowercase']:
|
||||
text = text.lower()
|
||||
|
||||
return text.strip()
|
||||
|
||||
@staticmethod
|
||||
def get_supported_formats() -> List[str]:
|
||||
"""获取支持的文件格式列表"""
|
||||
return sorted(MarkdownConverter.SUPPORTED_EXTENSIONS)
|
||||
|
||||
def convert_to_markdown(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
output_path: Optional[Union[str, Path]] = None
|
||||
) -> str:
|
||||
"""将 PDF 转换为 Markdown
|
||||
|
||||
Args:
|
||||
file_path: PDF 文件路径
|
||||
output_path: 输出 Markdown 文件路径,如果为 None 则返回内容而不保存
|
||||
|
||||
Returns:
|
||||
str: 转换后的 Markdown 内容
|
||||
|
||||
Raises:
|
||||
Exception: 转换过程中的错误
|
||||
"""
|
||||
try:
|
||||
path = self._validate_file(file_path)
|
||||
self.logger.info(f"处理: {path}")
|
||||
|
||||
if not self.markitdown_available:
|
||||
raise ImportError("markitdown 库未安装,无法进行转换")
|
||||
|
||||
# 导入 markitdown 库
|
||||
from markitdown import MarkItDown
|
||||
|
||||
# 准备输出目录
|
||||
if output_path:
|
||||
output_path = Path(output_path)
|
||||
output_dir = output_path.parent
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
# 创建临时目录作为输出目录
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
output_dir = Path(temp_dir)
|
||||
output_path = output_dir / f"{path.stem}.md"
|
||||
|
||||
# 图片目录
|
||||
image_dir = output_dir / self.config.image_dir
|
||||
image_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建 MarkItDown 实例并进行转换
|
||||
if self.config.docintel_endpoint:
|
||||
md = MarkItDown(docintel_endpoint=self.config.docintel_endpoint)
|
||||
elif self.config.llm_client and self.config.llm_model:
|
||||
md = MarkItDown(
|
||||
enable_plugins=self.config.enable_plugins,
|
||||
llm_client=self.config.llm_client,
|
||||
llm_model=self.config.llm_model
|
||||
)
|
||||
else:
|
||||
md = MarkItDown(enable_plugins=self.config.enable_plugins)
|
||||
|
||||
# 执行转换
|
||||
result = md.convert(str(path))
|
||||
markdown_content = result.text_content
|
||||
|
||||
# 清理文本
|
||||
markdown_content = self._cleanup_text(markdown_content)
|
||||
|
||||
# 如果需要保存到文件
|
||||
if output_path:
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
self.logger.info(f"转换成功,输出到: {output_path}")
|
||||
|
||||
return markdown_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"转换失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
# 如果使用了临时目录且没有指定输出路径,则清理临时目录
|
||||
if 'temp_dir' in locals() and not output_path:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
def convert_to_markdown_and_save(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
output_path: Union[str, Path]
|
||||
) -> Path:
|
||||
"""将 PDF 转换为 Markdown 并保存到指定路径
|
||||
|
||||
Args:
|
||||
file_path: PDF 文件路径
|
||||
output_path: 输出 Markdown 文件路径
|
||||
|
||||
Returns:
|
||||
Path: 输出文件的 Path 对象
|
||||
|
||||
Raises:
|
||||
Exception: 转换过程中的错误
|
||||
"""
|
||||
self.convert_to_markdown(file_path, output_path)
|
||||
return Path(output_path)
|
||||
|
||||
def batch_convert(
|
||||
self,
|
||||
file_paths: List[Union[str, Path]],
|
||||
output_dir: Union[str, Path]
|
||||
) -> List[Path]:
|
||||
"""批量转换多个 PDF 文件为 Markdown
|
||||
|
||||
Args:
|
||||
file_paths: PDF 文件路径列表
|
||||
output_dir: 输出目录路径
|
||||
|
||||
Returns:
|
||||
List[Path]: 输出文件路径列表
|
||||
|
||||
Raises:
|
||||
Exception: 转换过程中的错误
|
||||
"""
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_paths = []
|
||||
for file_path in file_paths:
|
||||
path = Path(file_path)
|
||||
output_path = output_dir / f"{path.stem}.md"
|
||||
|
||||
try:
|
||||
self.convert_to_markdown(file_path, output_path)
|
||||
output_paths.append(output_path)
|
||||
self.logger.info(f"成功转换: {path} -> {output_path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"转换失败 {path}: {e}")
|
||||
|
||||
return output_paths
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:演示用法"""
|
||||
# 配置
|
||||
config = MarkdownConverterConfig(
|
||||
extract_images=True,
|
||||
extract_tables=True,
|
||||
extract_code_blocks=True,
|
||||
extract_math=True,
|
||||
enable_plugins=False,
|
||||
text_cleanup={
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
}
|
||||
)
|
||||
|
||||
# 创建转换器
|
||||
converter = MarkdownConverter(config)
|
||||
|
||||
# 使用示例
|
||||
try:
|
||||
# 替换为实际的文件路径
|
||||
sample_file = './crazy_functions/doc_fns/read_fns/paper/2501.12599v1.pdf'
|
||||
if Path(sample_file).exists():
|
||||
# 转换为 Markdown 并打印内容
|
||||
markdown_content = converter.convert_to_markdown(sample_file)
|
||||
print("转换后的 Markdown 内容:")
|
||||
print(markdown_content[:500] + "...") # 只打印前500个字符
|
||||
|
||||
# 转换并保存到文件
|
||||
output_file = f"./output_{Path(sample_file).stem}.md"
|
||||
output_path = converter.convert_to_markdown_and_save(sample_file, output_file)
|
||||
print(f"\n已保存到: {output_path}")
|
||||
|
||||
# 使用LLM增强的示例 (需要添加相应的导入和配置)
|
||||
# try:
|
||||
# from openai import OpenAI
|
||||
# client = OpenAI()
|
||||
# llm_config = MarkdownConverterConfig(
|
||||
# llm_client=client,
|
||||
# llm_model="gpt-4o"
|
||||
# )
|
||||
# llm_converter = MarkdownConverter(llm_config)
|
||||
# llm_result = llm_converter.convert_to_markdown("example.jpg")
|
||||
# print("LLM增强的结果:")
|
||||
# print(llm_result[:500] + "...")
|
||||
# except ImportError:
|
||||
# print("未安装OpenAI库,跳过LLM示例")
|
||||
else:
|
||||
print(f"示例文件 {sample_file} 不存在")
|
||||
|
||||
print("\n支持的格式:", converter.get_supported_formats())
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,493 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set, Dict, Union, List
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from unstructured.partition.auto import partition
|
||||
from unstructured.documents.elements import (
|
||||
Text, Title, NarrativeText, ListItem, Table,
|
||||
Footer, Header, PageBreak, Image, Address
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaperMetadata:
|
||||
"""论文元数据类"""
|
||||
title: str = ""
|
||||
authors: List[str] = field(default_factory=list)
|
||||
affiliations: List[str] = field(default_factory=list)
|
||||
journal: str = ""
|
||||
volume: str = ""
|
||||
issue: str = ""
|
||||
year: str = ""
|
||||
doi: str = ""
|
||||
date: str = ""
|
||||
publisher: str = ""
|
||||
conference: str = ""
|
||||
abstract: str = ""
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractorConfig:
|
||||
"""元数据提取器配置类"""
|
||||
paragraph_separator: str = '\n\n'
|
||||
text_cleanup: Dict[str, bool] = field(default_factory=lambda: {
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
})
|
||||
|
||||
|
||||
class PaperMetadataExtractor:
|
||||
"""论文元数据提取器
|
||||
|
||||
使用unstructured库从多种文档格式中提取论文的标题、作者、摘要等元数据信息。
|
||||
"""
|
||||
|
||||
SUPPORTED_EXTENSIONS: Set[str] = {
|
||||
'.pdf', '.docx', '.doc', '.txt', '.ppt', '.pptx',
|
||||
'.xlsx', '.xls', '.md', '.org', '.odt', '.rst',
|
||||
'.rtf', '.epub', '.html', '.xml', '.json'
|
||||
}
|
||||
|
||||
# 定义论文各部分的关键词模式
|
||||
SECTION_PATTERNS = {
|
||||
'abstract': r'\b(摘要|abstract|summary|概要|résumé|zusammenfassung|аннотация)\b',
|
||||
'keywords': r'\b(关键词|keywords|key\s+words|关键字|mots[- ]clés|schlüsselwörter|ключевые слова)\b',
|
||||
}
|
||||
|
||||
def __init__(self, config: Optional[ExtractorConfig] = None):
|
||||
"""初始化提取器
|
||||
|
||||
Args:
|
||||
config: 提取器配置对象,如果为None则使用默认配置
|
||||
"""
|
||||
self.config = config or ExtractorConfig()
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录器"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 添加文件处理器
|
||||
fh = logging.FileHandler('paper_metadata_extractor.log')
|
||||
fh.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
def _validate_file(self, file_path: Union[str, Path], max_size_mb: int = 100) -> Path:
|
||||
"""验证文件
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
max_size_mb: 允许的最大文件大小(MB)
|
||||
|
||||
Returns:
|
||||
Path: 验证后的Path对象
|
||||
|
||||
Raises:
|
||||
ValueError: 文件不存在、格式不支持或大小超限
|
||||
PermissionError: 没有读取权限
|
||||
"""
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"文件不存在: {path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"不是文件: {path}")
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
raise PermissionError(f"没有读取权限: {path}")
|
||||
|
||||
file_size_mb = path.stat().st_size / (1024 * 1024)
|
||||
if file_size_mb > max_size_mb:
|
||||
raise ValueError(
|
||||
f"文件大小 ({file_size_mb:.1f}MB) 超过限制 {max_size_mb}MB"
|
||||
)
|
||||
|
||||
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(
|
||||
f"不支持的文件格式: {path.suffix}. "
|
||||
f"支持的格式: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}"
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def _cleanup_text(self, text: str) -> str:
|
||||
"""清理文本
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
|
||||
Returns:
|
||||
str: 清理后的文本
|
||||
"""
|
||||
if self.config.text_cleanup['remove_extra_spaces']:
|
||||
text = ' '.join(text.split())
|
||||
|
||||
if self.config.text_cleanup['normalize_whitespace']:
|
||||
text = text.replace('\t', ' ').replace('\r', '\n')
|
||||
|
||||
if self.config.text_cleanup['lowercase']:
|
||||
text = text.lower()
|
||||
|
||||
return text.strip()
|
||||
|
||||
@staticmethod
|
||||
def get_supported_formats() -> List[str]:
|
||||
"""获取支持的文件格式列表"""
|
||||
return sorted(PaperMetadataExtractor.SUPPORTED_EXTENSIONS)
|
||||
|
||||
def extract_metadata(self, file_path: Union[str, Path], strategy: str = "fast") -> PaperMetadata:
|
||||
"""提取论文元数据
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
strategy: 提取策略 ("fast" 或 "accurate")
|
||||
|
||||
Returns:
|
||||
PaperMetadata: 提取的论文元数据
|
||||
|
||||
Raises:
|
||||
Exception: 提取过程中的错误
|
||||
"""
|
||||
try:
|
||||
path = self._validate_file(file_path)
|
||||
self.logger.info(f"正在处理: {path}")
|
||||
|
||||
# 使用unstructured库分解文档
|
||||
elements = partition(
|
||||
str(path),
|
||||
strategy=strategy,
|
||||
include_metadata=True,
|
||||
nlp=False,
|
||||
)
|
||||
|
||||
# 提取元数据
|
||||
metadata = PaperMetadata()
|
||||
|
||||
# 提取标题和作者
|
||||
self._extract_title_and_authors(elements, metadata)
|
||||
|
||||
# 提取摘要和关键词
|
||||
self._extract_abstract_and_keywords(elements, metadata)
|
||||
|
||||
# 提取其他元数据
|
||||
self._extract_additional_metadata(elements, metadata)
|
||||
|
||||
return metadata
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"元数据提取失败: {e}")
|
||||
raise
|
||||
|
||||
def _extract_title_and_authors(self, elements, metadata: PaperMetadata) -> None:
|
||||
"""从文档中提取标题和作者信息 - 改进版"""
|
||||
# 收集所有潜在的标题候选
|
||||
title_candidates = []
|
||||
all_text = []
|
||||
raw_text = []
|
||||
|
||||
# 首先收集文档前30个元素的文本,用于辅助判断
|
||||
for i, element in enumerate(elements[:30]):
|
||||
if isinstance(element, (Text, Title, NarrativeText)):
|
||||
text = str(element).strip()
|
||||
if text:
|
||||
all_text.append(text)
|
||||
raw_text.append(text)
|
||||
|
||||
# 打印出原始文本,用于调试
|
||||
print("原始文本前10行:")
|
||||
for i, text in enumerate(raw_text[:10]):
|
||||
print(f"{i}: {text}")
|
||||
|
||||
# 1. 尝试查找连续的标题片段并合并它们
|
||||
i = 0
|
||||
while i < len(all_text) - 1:
|
||||
current = all_text[i]
|
||||
next_text = all_text[i + 1]
|
||||
|
||||
# 检查是否存在标题分割情况:一行以冒号结尾,下一行像是标题的延续
|
||||
if current.endswith(':') and len(current) < 50 and len(next_text) > 5 and next_text[0].isupper():
|
||||
# 合并这两行文本
|
||||
combined_title = f"{current} {next_text}"
|
||||
# 查找合并前的文本并替换
|
||||
all_text[i] = combined_title
|
||||
all_text.pop(i + 1)
|
||||
# 给合并后的标题很高的分数
|
||||
title_candidates.append((combined_title, 15, i))
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# 2. 首先尝试从标题元素中查找
|
||||
for i, element in enumerate(elements[:15]): # 只检查前15个元素
|
||||
if isinstance(element, Title):
|
||||
title_text = str(element).strip()
|
||||
# 排除常见的非标题内容
|
||||
if title_text.lower() not in ['abstract', '摘要', 'introduction', '引言']:
|
||||
# 计算标题分数(越高越可能是真正的标题)
|
||||
score = self._evaluate_title_candidate(title_text, i, element)
|
||||
title_candidates.append((title_text, score, i))
|
||||
|
||||
# 3. 特别处理常见的论文标题格式
|
||||
for i, text in enumerate(all_text[:15]):
|
||||
# 特别检查"KIMI K1.5:"类型的前缀标题
|
||||
if re.match(r'^[A-Z][A-Z0-9\s\.]+(\s+K\d+(\.\d+)?)?:', text):
|
||||
score = 12 # 给予很高的分数
|
||||
title_candidates.append((text, score, i))
|
||||
|
||||
# 如果下一行也是全大写,很可能是标题的延续
|
||||
if i+1 < len(all_text) and all_text[i+1].isupper() and len(all_text[i+1]) > 10:
|
||||
combined_title = f"{text} {all_text[i+1]}"
|
||||
title_candidates.append((combined_title, 15, i)) # 给合并标题更高分数
|
||||
|
||||
# 匹配全大写的标题行
|
||||
elif text.isupper() and len(text) > 10 and len(text) < 100:
|
||||
score = 10 - i * 0.5 # 越靠前越可能是标题
|
||||
title_candidates.append((text, score, i))
|
||||
|
||||
# 对标题候选按分数排序并选取最佳候选
|
||||
if title_candidates:
|
||||
title_candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
metadata.title = title_candidates[0][0]
|
||||
title_position = title_candidates[0][2]
|
||||
print(f"所有标题候选: {title_candidates[:3]}")
|
||||
else:
|
||||
# 如果没有找到合适的标题,使用一个备选策略
|
||||
for text in all_text[:10]:
|
||||
if text.isupper() and len(text) > 10 and len(text) < 200: # 大写且适当长度的文本
|
||||
metadata.title = text
|
||||
break
|
||||
title_position = 0
|
||||
|
||||
# 提取作者信息 - 改进后的作者提取逻辑
|
||||
author_candidates = []
|
||||
|
||||
# 1. 特别处理"TECHNICAL REPORT OF"之后的行,通常是作者或团队
|
||||
for i, text in enumerate(all_text):
|
||||
if "TECHNICAL REPORT" in text.upper() and i+1 < len(all_text):
|
||||
team_text = all_text[i+1].strip()
|
||||
if re.search(r'\b(team|group|lab)\b', team_text, re.IGNORECASE):
|
||||
author_candidates.append((team_text, 15))
|
||||
|
||||
# 2. 查找包含Team的文本
|
||||
for text in all_text[:20]:
|
||||
if "Team" in text and len(text) < 30:
|
||||
# 这很可能是团队名
|
||||
author_candidates.append((text, 12))
|
||||
|
||||
# 添加作者到元数据
|
||||
if author_candidates:
|
||||
# 按分数排序
|
||||
author_candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# 去重
|
||||
seen_authors = set()
|
||||
for author, _ in author_candidates:
|
||||
if author.lower() not in seen_authors and not author.isdigit():
|
||||
seen_authors.add(author.lower())
|
||||
metadata.authors.append(author)
|
||||
|
||||
# 如果没有找到作者,尝试查找隶属机构信息中的团队名称
|
||||
if not metadata.authors:
|
||||
for text in all_text[:20]:
|
||||
if re.search(r'\b(team|group|lab|laboratory|研究组|团队)\b', text, re.IGNORECASE):
|
||||
if len(text) < 50: # 避免太长的文本
|
||||
metadata.authors.append(text.strip())
|
||||
break
|
||||
|
||||
# 提取隶属机构信息
|
||||
for i, element in enumerate(elements[:30]):
|
||||
element_text = str(element).strip()
|
||||
if re.search(r'(university|institute|department|school|laboratory|college|center|centre|\d{5,}|^[a-zA-Z]+@|学院|大学|研究所|研究院)', element_text, re.IGNORECASE):
|
||||
# 可能是隶属机构
|
||||
if element_text not in metadata.affiliations and len(element_text) > 10:
|
||||
metadata.affiliations.append(element_text)
|
||||
|
||||
def _evaluate_title_candidate(self, text, position, element):
|
||||
"""评估标题候选项的可能性分数"""
|
||||
score = 0
|
||||
|
||||
# 位置因素:越靠前越可能是标题
|
||||
score += max(0, 10 - position) * 0.5
|
||||
|
||||
# 长度因素:标题通常不会太短也不会太长
|
||||
if 10 <= len(text) <= 150:
|
||||
score += 3
|
||||
elif len(text) < 10:
|
||||
score -= 2
|
||||
elif len(text) > 150:
|
||||
score -= 3
|
||||
|
||||
# 格式因素
|
||||
if text.isupper(): # 全大写可能是标题
|
||||
score += 2
|
||||
if re.match(r'^[A-Z]', text): # 首字母大写
|
||||
score += 1
|
||||
if ':' in text: # 标题常包含冒号
|
||||
score += 1.5
|
||||
|
||||
# 内容因素
|
||||
if re.search(r'\b(scaling|learning|model|approach|method|system|framework|analysis)\b', text.lower()):
|
||||
score += 2 # 包含常见的学术论文关键词
|
||||
|
||||
# 避免误判
|
||||
if re.match(r'^\d+$', text): # 纯数字
|
||||
score -= 10
|
||||
if re.search(r'^(http|www|doi)', text.lower()): # URL或DOI
|
||||
score -= 5
|
||||
if len(text.split()) <= 2 and len(text) < 15: # 太短的短语
|
||||
score -= 3
|
||||
|
||||
# 元数据因素(如果有)
|
||||
if hasattr(element, 'metadata') and element.metadata:
|
||||
# 修复:正确处理ElementMetadata对象
|
||||
try:
|
||||
# 尝试通过getattr安全地获取属性
|
||||
font_size = getattr(element.metadata, 'font_size', None)
|
||||
if font_size is not None and font_size > 14: # 假设标准字体大小是12
|
||||
score += 3
|
||||
|
||||
font_weight = getattr(element.metadata, 'font_weight', None)
|
||||
if font_weight == 'bold':
|
||||
score += 2 # 粗体加分
|
||||
except (AttributeError, TypeError):
|
||||
# 如果metadata的访问方式不正确,尝试其他可能的访问方式
|
||||
try:
|
||||
metadata_dict = element.metadata.__dict__ if hasattr(element.metadata, '__dict__') else {}
|
||||
if 'font_size' in metadata_dict and metadata_dict['font_size'] > 14:
|
||||
score += 3
|
||||
if 'font_weight' in metadata_dict and metadata_dict['font_weight'] == 'bold':
|
||||
score += 2
|
||||
except Exception:
|
||||
# 如果所有尝试都失败,忽略元数据处理
|
||||
pass
|
||||
|
||||
return score
|
||||
|
||||
def _extract_abstract_and_keywords(self, elements, metadata: PaperMetadata) -> None:
|
||||
"""从文档中提取摘要和关键词"""
|
||||
abstract_found = False
|
||||
keywords_found = False
|
||||
abstract_text = []
|
||||
|
||||
for i, element in enumerate(elements):
|
||||
element_text = str(element).strip().lower()
|
||||
|
||||
# 寻找摘要部分
|
||||
if not abstract_found and (
|
||||
isinstance(element, Title) and
|
||||
re.search(self.SECTION_PATTERNS['abstract'], element_text, re.IGNORECASE)
|
||||
):
|
||||
abstract_found = True
|
||||
continue
|
||||
|
||||
# 如果找到摘要部分,收集内容直到遇到关键词部分或新章节
|
||||
if abstract_found and not keywords_found:
|
||||
# 检查是否遇到关键词部分或新章节
|
||||
if (
|
||||
isinstance(element, Title) or
|
||||
re.search(self.SECTION_PATTERNS['keywords'], element_text, re.IGNORECASE) or
|
||||
re.match(r'\b(introduction|引言|method|方法)\b', element_text, re.IGNORECASE)
|
||||
):
|
||||
keywords_found = re.search(self.SECTION_PATTERNS['keywords'], element_text, re.IGNORECASE)
|
||||
abstract_found = False # 停止收集摘要
|
||||
else:
|
||||
# 收集摘要文本
|
||||
if isinstance(element, (Text, NarrativeText)) and element_text:
|
||||
abstract_text.append(element_text)
|
||||
|
||||
# 如果找到关键词部分,提取关键词
|
||||
if keywords_found and not abstract_found and not metadata.keywords:
|
||||
if isinstance(element, (Text, NarrativeText)):
|
||||
# 清除可能的"关键词:"/"Keywords:"前缀
|
||||
cleaned_text = re.sub(r'^\s*(关键词|keywords|key\s+words)\s*[::]\s*', '', element_text, flags=re.IGNORECASE)
|
||||
|
||||
# 尝试按不同分隔符分割
|
||||
for separator in [';', ';', ',', ',']:
|
||||
if separator in cleaned_text:
|
||||
metadata.keywords = [k.strip() for k in cleaned_text.split(separator) if k.strip()]
|
||||
break
|
||||
|
||||
# 如果未能分割,将整个文本作为一个关键词
|
||||
if not metadata.keywords and cleaned_text:
|
||||
metadata.keywords = [cleaned_text]
|
||||
|
||||
keywords_found = False # 已提取关键词,停止处理
|
||||
|
||||
# 设置摘要文本
|
||||
if abstract_text:
|
||||
metadata.abstract = self.config.paragraph_separator.join(abstract_text)
|
||||
|
||||
def _extract_additional_metadata(self, elements, metadata: PaperMetadata) -> None:
|
||||
"""提取其他元数据信息"""
|
||||
for element in elements[:30]: # 只检查文档前部分
|
||||
element_text = str(element).strip()
|
||||
|
||||
# 尝试匹配DOI
|
||||
doi_match = re.search(r'(doi|DOI):\s*(10\.\d{4,}\/[a-zA-Z0-9.-]+)', element_text)
|
||||
if doi_match and not metadata.doi:
|
||||
metadata.doi = doi_match.group(2)
|
||||
|
||||
# 尝试匹配日期
|
||||
date_match = re.search(r'(published|received|accepted|submitted):\s*(\d{1,2}\s+[a-zA-Z]+\s+\d{4}|\d{4}[-/]\d{1,2}[-/]\d{1,2})', element_text, re.IGNORECASE)
|
||||
if date_match and not metadata.date:
|
||||
metadata.date = date_match.group(2)
|
||||
|
||||
# 尝试匹配年份
|
||||
year_match = re.search(r'\b(19|20)\d{2}\b', element_text)
|
||||
if year_match and not metadata.year:
|
||||
metadata.year = year_match.group(0)
|
||||
|
||||
# 尝试匹配期刊/会议名称
|
||||
journal_match = re.search(r'(journal|conference):\s*([^,;.]+)', element_text, re.IGNORECASE)
|
||||
if journal_match:
|
||||
if "journal" in journal_match.group(1).lower() and not metadata.journal:
|
||||
metadata.journal = journal_match.group(2).strip()
|
||||
elif not metadata.conference:
|
||||
metadata.conference = journal_match.group(2).strip()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:演示用法"""
|
||||
# 创建提取器
|
||||
extractor = PaperMetadataExtractor()
|
||||
|
||||
# 使用示例
|
||||
try:
|
||||
# 替换为实际的文件路径
|
||||
sample_file = '/Users/boyin.liu/Documents/示例文档/论文/3.pdf'
|
||||
if Path(sample_file).exists():
|
||||
metadata = extractor.extract_metadata(sample_file)
|
||||
print("提取的元数据:")
|
||||
print(f"标题: {metadata.title}")
|
||||
print(f"作者: {', '.join(metadata.authors)}")
|
||||
print(f"机构: {', '.join(metadata.affiliations)}")
|
||||
print(f"摘要: {metadata.abstract[:200]}...")
|
||||
print(f"关键词: {', '.join(metadata.keywords)}")
|
||||
print(f"DOI: {metadata.doi}")
|
||||
print(f"日期: {metadata.date}")
|
||||
print(f"年份: {metadata.year}")
|
||||
print(f"期刊: {metadata.journal}")
|
||||
print(f"会议: {metadata.conference}")
|
||||
else:
|
||||
print(f"示例文件 {sample_file} 不存在")
|
||||
|
||||
print("\n支持的格式:", extractor.get_supported_formats())
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
from pathlib import Path
|
||||
from crazy_functions.doc_fns.read_fns.unstructured_all.paper_structure_extractor import PaperStructureExtractor
|
||||
|
||||
def extract_and_save_as_markdown(paper_path, output_path=None):
|
||||
"""
|
||||
提取论文结构并保存为Markdown格式
|
||||
|
||||
参数:
|
||||
paper_path: 论文文件路径
|
||||
output_path: 输出的Markdown文件路径,如果不指定,将使用与输入相同的文件名但扩展名为.md
|
||||
|
||||
返回:
|
||||
保存的Markdown文件路径
|
||||
"""
|
||||
# 创建提取器
|
||||
extractor = PaperStructureExtractor()
|
||||
|
||||
# 解析文件路径
|
||||
paper_path = Path(paper_path)
|
||||
|
||||
# 如果未指定输出路径,使用相同文件名但扩展名为.md
|
||||
if output_path is None:
|
||||
output_path = paper_path.with_suffix('.md')
|
||||
else:
|
||||
output_path = Path(output_path)
|
||||
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"正在处理论文: {paper_path}")
|
||||
|
||||
try:
|
||||
# 提取论文结构
|
||||
paper = extractor.extract_paper_structure(paper_path)
|
||||
|
||||
# 生成Markdown内容
|
||||
markdown_content = extractor.generate_markdown(paper)
|
||||
|
||||
# 保存到文件
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"已成功保存Markdown文件: {output_path}")
|
||||
|
||||
# 打印摘要信息
|
||||
print("\n论文摘要信息:")
|
||||
print(f"标题: {paper.metadata.title}")
|
||||
print(f"作者: {', '.join(paper.metadata.authors)}")
|
||||
print(f"关键词: {', '.join(paper.keywords)}")
|
||||
print(f"章节数: {len(paper.sections)}")
|
||||
print(f"图表数: {len(paper.figures)}")
|
||||
print(f"表格数: {len(paper.tables)}")
|
||||
print(f"公式数: {len(paper.formulas)}")
|
||||
print(f"参考文献数: {len(paper.references)}")
|
||||
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理论文时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 替换为实际的论文文件路径
|
||||
sample_paper = "crazy_functions/doc_fns/read_fns/paper/2501.12599v1.pdf"
|
||||
|
||||
# 可以指定输出路径,也可以使用默认路径
|
||||
# output_file = "/path/to/output/paper_structure.md"
|
||||
# extract_and_save_as_markdown(sample_paper, output_file)
|
||||
|
||||
# 使用默认输出路径(与输入文件同名但扩展名为.md)
|
||||
extract_and_save_as_markdown(sample_paper)
|
||||
|
||||
# # 批量处理多个论文的示例
|
||||
# paper_dir = Path("/path/to/papers/folder")
|
||||
# output_dir = Path("/path/to/output/folder")
|
||||
#
|
||||
# # 确保输出目录存在
|
||||
# output_dir.mkdir(parents=True, exist_ok=True)
|
||||
#
|
||||
# # 处理目录中的所有PDF文件
|
||||
# for paper_file in paper_dir.glob("*.pdf"):
|
||||
# output_file = output_dir / f"{paper_file.stem}.md"
|
||||
# extract_and_save_as_markdown(paper_file, output_file)
|
||||
@@ -0,0 +1,275 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set, Dict, Union, List
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import os
|
||||
|
||||
from unstructured.partition.auto import partition
|
||||
from unstructured.documents.elements import (
|
||||
Text, Title, NarrativeText, ListItem, Table,
|
||||
Footer, Header, PageBreak, Image, Address
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextExtractorConfig:
|
||||
"""通用文档提取器配置类
|
||||
|
||||
Attributes:
|
||||
extract_headers_footers: 是否提取页眉页脚
|
||||
extract_tables: 是否提取表格内容
|
||||
extract_lists: 是否提取列表内容
|
||||
extract_titles: 是否提取标题
|
||||
paragraph_separator: 段落之间的分隔符
|
||||
text_cleanup: 文本清理选项字典
|
||||
"""
|
||||
extract_headers_footers: bool = False
|
||||
extract_tables: bool = True
|
||||
extract_lists: bool = True
|
||||
extract_titles: bool = True
|
||||
paragraph_separator: str = '\n\n'
|
||||
text_cleanup: Dict[str, bool] = field(default_factory=lambda: {
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
})
|
||||
|
||||
|
||||
class UnstructuredTextExtractor:
|
||||
"""通用文档文本内容提取器
|
||||
|
||||
使用 unstructured 库支持多种文档格式的文本提取,提供统一的接口和配置选项。
|
||||
"""
|
||||
|
||||
SUPPORTED_EXTENSIONS: Set[str] = {
|
||||
# 文档格式
|
||||
'.pdf', '.docx', '.doc', '.txt',
|
||||
# 演示文稿
|
||||
'.ppt', '.pptx',
|
||||
# 电子表格
|
||||
'.xlsx', '.xls', '.csv',
|
||||
# 图片
|
||||
'.png', '.jpg', '.jpeg', '.tiff',
|
||||
# 邮件
|
||||
'.eml', '.msg', '.p7s',
|
||||
# Markdown
|
||||
".md",
|
||||
# Org Mode
|
||||
".org",
|
||||
# Open Office
|
||||
".odt",
|
||||
# reStructured Text
|
||||
".rst",
|
||||
# Rich Text
|
||||
".rtf",
|
||||
# TSV
|
||||
".tsv",
|
||||
# EPUB
|
||||
'.epub',
|
||||
# 其他格式
|
||||
'.html', '.xml', '.json',
|
||||
}
|
||||
|
||||
def __init__(self, config: Optional[TextExtractorConfig] = None):
|
||||
"""初始化提取器
|
||||
|
||||
Args:
|
||||
config: 提取器配置对象,如果为None则使用默认配置
|
||||
"""
|
||||
self.config = config or TextExtractorConfig()
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录器"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 添加文件处理器
|
||||
fh = logging.FileHandler('text_extractor.log')
|
||||
fh.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
def _validate_file(self, file_path: Union[str, Path], max_size_mb: int = 100) -> Path:
|
||||
"""验证文件
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
max_size_mb: 允许的最大文件大小(MB)
|
||||
|
||||
Returns:
|
||||
Path: 验证后的Path对象
|
||||
|
||||
Raises:
|
||||
ValueError: 文件不存在、格式不支持或大小超限
|
||||
PermissionError: 没有读取权限
|
||||
"""
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"File not found: {path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Not a file: {path}")
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
raise PermissionError(f"No read permission: {path}")
|
||||
|
||||
file_size_mb = path.stat().st_size / (1024 * 1024)
|
||||
if file_size_mb > max_size_mb:
|
||||
raise ValueError(
|
||||
f"File size ({file_size_mb:.1f}MB) exceeds limit of {max_size_mb}MB"
|
||||
)
|
||||
|
||||
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(
|
||||
f"Unsupported format: {path.suffix}. "
|
||||
f"Supported: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}"
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def _cleanup_text(self, text: str) -> str:
|
||||
"""清理文本
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
|
||||
Returns:
|
||||
str: 清理后的文本
|
||||
"""
|
||||
if self.config.text_cleanup['remove_extra_spaces']:
|
||||
text = ' '.join(text.split())
|
||||
|
||||
if self.config.text_cleanup['normalize_whitespace']:
|
||||
text = text.replace('\t', ' ').replace('\r', '\n')
|
||||
|
||||
if self.config.text_cleanup['lowercase']:
|
||||
text = text.lower()
|
||||
|
||||
return text.strip()
|
||||
|
||||
def _should_extract_element(self, element) -> bool:
|
||||
"""判断是否应该提取某个元素
|
||||
|
||||
Args:
|
||||
element: 文档元素
|
||||
|
||||
Returns:
|
||||
bool: 是否应该提取
|
||||
"""
|
||||
if isinstance(element, (Text, NarrativeText)):
|
||||
return True
|
||||
|
||||
if isinstance(element, Title) and self.config.extract_titles:
|
||||
return True
|
||||
|
||||
if isinstance(element, ListItem) and self.config.extract_lists:
|
||||
return True
|
||||
|
||||
if isinstance(element, Table) and self.config.extract_tables:
|
||||
return True
|
||||
|
||||
if isinstance(element, (Header, Footer)) and self.config.extract_headers_footers:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_supported_formats() -> List[str]:
|
||||
"""获取支持的文件格式列表"""
|
||||
return sorted(UnstructuredTextExtractor.SUPPORTED_EXTENSIONS)
|
||||
|
||||
def extract_text(
|
||||
self,
|
||||
file_path: Union[str, Path],
|
||||
strategy: str = "fast"
|
||||
) -> str:
|
||||
"""提取文本
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
strategy: 提取策略 ("fast" 或 "accurate")
|
||||
|
||||
Returns:
|
||||
str: 提取的文本内容
|
||||
|
||||
Raises:
|
||||
Exception: 提取过程中的错误
|
||||
"""
|
||||
try:
|
||||
path = self._validate_file(file_path)
|
||||
self.logger.info(f"Processing: {path}")
|
||||
|
||||
# 修改这里:添加 nlp=False 参数来禁用 NLTK
|
||||
elements = partition(
|
||||
str(path),
|
||||
strategy=strategy,
|
||||
include_metadata=True,
|
||||
nlp=True,
|
||||
)
|
||||
|
||||
# 其余代码保持不变
|
||||
text_parts = []
|
||||
for element in elements:
|
||||
if self._should_extract_element(element):
|
||||
text = str(element)
|
||||
cleaned_text = self._cleanup_text(text)
|
||||
if cleaned_text:
|
||||
if isinstance(element, (Header, Footer)):
|
||||
prefix = "[Header] " if isinstance(element, Header) else "[Footer] "
|
||||
text_parts.append(f"{prefix}{cleaned_text}")
|
||||
else:
|
||||
text_parts.append(cleaned_text)
|
||||
|
||||
return self.config.paragraph_separator.join(text_parts)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Extraction failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:演示用法"""
|
||||
# 配置
|
||||
config = TextExtractorConfig(
|
||||
extract_headers_footers=True,
|
||||
extract_tables=True,
|
||||
extract_lists=True,
|
||||
extract_titles=True,
|
||||
text_cleanup={
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
}
|
||||
)
|
||||
|
||||
# 创建提取器
|
||||
extractor = UnstructuredTextExtractor(config)
|
||||
|
||||
# 使用示例
|
||||
try:
|
||||
# 替换为实际的文件路径
|
||||
sample_file = './crazy_functions/doc_fns/read_fns/paper/2501.12599v1.pdf'
|
||||
if Path(sample_file).exists() or True:
|
||||
text = extractor.extract_text(sample_file)
|
||||
print("提取的文本:")
|
||||
print(text)
|
||||
else:
|
||||
print(f"示例文件 {sample_file} 不存在")
|
||||
|
||||
print("\n支持的格式:", extractor.get_supported_formats())
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
219
crazy_functions/doc_fns/read_fns/web_reader.py
Normal file
219
crazy_functions/doc_fns/read_fns/web_reader.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
import logging
|
||||
import trafilatura
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebExtractorConfig:
|
||||
"""网页内容提取器配置类
|
||||
|
||||
Attributes:
|
||||
extract_comments: 是否提取评论
|
||||
extract_tables: 是否提取表格
|
||||
extract_links: 是否保留链接信息
|
||||
paragraph_separator: 段落分隔符
|
||||
timeout: 网络请求超时时间(秒)
|
||||
max_retries: 最大重试次数
|
||||
user_agent: 自定义User-Agent
|
||||
text_cleanup: 文本清理选项
|
||||
"""
|
||||
extract_comments: bool = False
|
||||
extract_tables: bool = True
|
||||
extract_links: bool = False
|
||||
paragraph_separator: str = '\n\n'
|
||||
timeout: int = 10
|
||||
max_retries: int = 3
|
||||
user_agent: str = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
text_cleanup: Dict[str, bool] = field(default_factory=lambda: {
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
})
|
||||
|
||||
|
||||
class WebTextExtractor:
|
||||
"""网页文本内容提取器
|
||||
|
||||
使用trafilatura库提取网页中的主要文本内容,去除广告、导航等无关内容。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[WebExtractorConfig] = None):
|
||||
"""初始化提取器
|
||||
|
||||
Args:
|
||||
config: 提取器配置对象,如果为None则使用默认配置
|
||||
"""
|
||||
self.config = config or WebExtractorConfig()
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录器"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 添加文件处理器
|
||||
fh = logging.FileHandler('web_extractor.log')
|
||||
fh.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""验证URL格式是否有效
|
||||
|
||||
Args:
|
||||
url: 网页URL
|
||||
|
||||
Returns:
|
||||
bool: URL是否有效
|
||||
"""
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _download_webpage(self, url: str) -> Optional[str]:
|
||||
"""下载网页内容
|
||||
|
||||
Args:
|
||||
url: 网页URL
|
||||
|
||||
Returns:
|
||||
Optional[str]: 网页HTML内容,失败返回None
|
||||
|
||||
Raises:
|
||||
Exception: 下载失败时抛出异常
|
||||
"""
|
||||
headers = {'User-Agent': self.config.user_agent}
|
||||
|
||||
for attempt in range(self.config.max_retries):
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=self.config.timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(f"Attempt {attempt + 1} failed: {e}")
|
||||
if attempt == self.config.max_retries - 1:
|
||||
raise Exception(f"Failed to download webpage after {self.config.max_retries} attempts: {e}")
|
||||
return None
|
||||
|
||||
def _cleanup_text(self, text: str) -> str:
|
||||
"""清理文本
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
|
||||
Returns:
|
||||
str: 清理后的文本
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
if self.config.text_cleanup['remove_extra_spaces']:
|
||||
text = ' '.join(text.split())
|
||||
|
||||
if self.config.text_cleanup['normalize_whitespace']:
|
||||
text = text.replace('\t', ' ').replace('\r', '\n')
|
||||
|
||||
if self.config.text_cleanup['lowercase']:
|
||||
text = text.lower()
|
||||
|
||||
return text.strip()
|
||||
|
||||
def extract_text(self, url: str) -> str:
|
||||
"""提取网页文本内容
|
||||
|
||||
Args:
|
||||
url: 网页URL
|
||||
|
||||
Returns:
|
||||
str: 提取的文本内容
|
||||
|
||||
Raises:
|
||||
ValueError: URL无效时抛出
|
||||
Exception: 提取失败时抛出
|
||||
"""
|
||||
try:
|
||||
if not self._validate_url(url):
|
||||
raise ValueError(f"Invalid URL: {url}")
|
||||
|
||||
self.logger.info(f"Processing URL: {url}")
|
||||
|
||||
# 下载网页
|
||||
html_content = self._download_webpage(url)
|
||||
if not html_content:
|
||||
raise Exception("Failed to download webpage")
|
||||
|
||||
# 配置trafilatura提取选项
|
||||
extract_config = {
|
||||
'include_comments': self.config.extract_comments,
|
||||
'include_tables': self.config.extract_tables,
|
||||
'include_links': self.config.extract_links,
|
||||
'no_fallback': False, # 允许使用后备提取器
|
||||
}
|
||||
|
||||
# 提取文本
|
||||
extracted_text = trafilatura.extract(
|
||||
html_content,
|
||||
**extract_config
|
||||
)
|
||||
|
||||
if not extracted_text:
|
||||
raise Exception("No content could be extracted")
|
||||
|
||||
# 清理文本
|
||||
cleaned_text = self._cleanup_text(extracted_text)
|
||||
|
||||
return cleaned_text
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Extraction failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:演示用法"""
|
||||
# 配置
|
||||
config = WebExtractorConfig(
|
||||
extract_comments=False,
|
||||
extract_tables=True,
|
||||
extract_links=False,
|
||||
timeout=10,
|
||||
text_cleanup={
|
||||
'remove_extra_spaces': True,
|
||||
'normalize_whitespace': True,
|
||||
'remove_special_chars': False,
|
||||
'lowercase': False
|
||||
}
|
||||
)
|
||||
|
||||
# 创建提取器
|
||||
extractor = WebTextExtractor(config)
|
||||
|
||||
# 使用示例
|
||||
try:
|
||||
# 替换为实际的URL
|
||||
sample_url = 'https://arxiv.org/abs/2412.00036'
|
||||
text = extractor.extract_text(sample_url)
|
||||
print("提取的文本:")
|
||||
print(text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,354 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class SectionFragment:
|
||||
"""Arxiv论文片段数据类"""
|
||||
title: str
|
||||
authors: str
|
||||
abstract: str
|
||||
catalogs: str
|
||||
arxiv_id: str = ""
|
||||
current_section: str = "Introduction"
|
||||
content: str = ''
|
||||
bibliography: str = ''
|
||||
|
||||
|
||||
class PaperHtmlFormatter:
|
||||
"""HTML格式论文文档生成器"""
|
||||
|
||||
def __init__(self, fragments: List[SectionFragment], output_dir: Path):
|
||||
self.fragments = fragments
|
||||
self.output_dir = output_dir
|
||||
self.css_styles = """
|
||||
:root {
|
||||
--primary-color: #1a73e8;
|
||||
--secondary-color: #34495e;
|
||||
--background-color: #f8f9fa;
|
||||
--text-color: #2c3e50;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg-color: #f6f8fa;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Source Serif Pro", "Times New Roman", serif;
|
||||
line-height: 1.8;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
font-size: 2.2em;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.8em;
|
||||
margin-top: 2rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-color);
|
||||
font-size: 1.5em;
|
||||
margin-top: 1.5rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.authors {
|
||||
text-align: center;
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.1em;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
|
||||
.abstract-container {
|
||||
background: var(--background-color);
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.abstract-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.abstract-content {
|
||||
font-style: italic;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.toc-title {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
margin: 1.5rem 0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.fragment {
|
||||
margin: 2rem 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
.fragment:hover {
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bibliography {
|
||||
background: var(--code-bg-color);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-bg-color);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
.paper-info {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.arxiv-id {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
.container {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def _sanitize_html(self, text: str) -> str:
|
||||
"""清理HTML特殊字符"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
replacements = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
}
|
||||
|
||||
for old, new in replacements.items():
|
||||
text = text.replace(old, new)
|
||||
return text
|
||||
|
||||
def _create_section_id(self, section: str) -> str:
|
||||
"""创建section的ID"""
|
||||
section = section.strip() or "uncategorized"
|
||||
# 移除特殊字符,转换为小写并用连字符替换空格
|
||||
section_id = re.sub(r'[^\w\s-]', '', section.lower())
|
||||
return section_id.replace(' ', '-')
|
||||
|
||||
def format_paper_info(self) -> str:
|
||||
"""格式化论文基本信息"""
|
||||
if not self.fragments:
|
||||
return ""
|
||||
|
||||
first_fragment = self.fragments[0]
|
||||
paper_info = ['<div class="paper-info">']
|
||||
|
||||
# 添加标题
|
||||
if first_fragment.title:
|
||||
paper_info.append(f'<h1>{self._sanitize_html(first_fragment.title)}</h1>')
|
||||
|
||||
# 添加arXiv ID
|
||||
if first_fragment.arxiv_id:
|
||||
paper_info.append(f'<div class="arxiv-id">arXiv: {self._sanitize_html(first_fragment.arxiv_id)}</div>')
|
||||
|
||||
# 添加作者
|
||||
if first_fragment.authors:
|
||||
paper_info.append(f'<div class="authors">{self._sanitize_html(first_fragment.authors)}</div>')
|
||||
|
||||
# 添加摘要
|
||||
if first_fragment.abstract:
|
||||
paper_info.append('<div class="abstract-container">')
|
||||
paper_info.append('<div class="abstract-title">Abstract</div>')
|
||||
paper_info.append(f'<div class="abstract-content">{self._sanitize_html(first_fragment.abstract)}</div>')
|
||||
paper_info.append('</div>')
|
||||
|
||||
# 添加目录结构
|
||||
if first_fragment.catalogs:
|
||||
paper_info.append('<h2>Document Structure</h2>')
|
||||
paper_info.append('<pre>')
|
||||
paper_info.append(self._sanitize_html(first_fragment.catalogs))
|
||||
paper_info.append('</pre>')
|
||||
|
||||
paper_info.append('</div>')
|
||||
return '\n'.join(paper_info)
|
||||
|
||||
def format_table_of_contents(self, sections: Dict[str, List[SectionFragment]]) -> str:
|
||||
"""生成目录"""
|
||||
toc = ['<div class="toc">']
|
||||
toc.append('<div class="toc-title">Table of Contents</div>')
|
||||
toc.append('<nav>')
|
||||
|
||||
for section in sections:
|
||||
section_id = self._create_section_id(section)
|
||||
clean_section = section.strip() or "Uncategorized"
|
||||
toc.append(f'<div><a href="#{section_id}">{self._sanitize_html(clean_section)} '
|
||||
f'</a></div>')
|
||||
|
||||
toc.append('</nav>')
|
||||
toc.append('</div>')
|
||||
return '\n'.join(toc)
|
||||
|
||||
def format_sections(self) -> str:
|
||||
"""格式化论文各部分内容"""
|
||||
sections = {}
|
||||
for fragment in self.fragments:
|
||||
section = fragment.current_section or "Uncategorized"
|
||||
if section not in sections:
|
||||
sections[section] = []
|
||||
sections[section].append(fragment)
|
||||
|
||||
formatted_html = ['<div class="content">']
|
||||
formatted_html.append(self.format_table_of_contents(sections))
|
||||
|
||||
# 生成各部分内容
|
||||
for section, fragments in sections.items():
|
||||
section_id = self._create_section_id(section)
|
||||
formatted_html.append(f'<h2 id="{section_id}">')
|
||||
formatted_html.append(f'<span class="section-title">')
|
||||
formatted_html.append(f'<span class="section-icon">§</span>')
|
||||
formatted_html.append(f'{self._sanitize_html(section)}')
|
||||
formatted_html.append('</span>')
|
||||
formatted_html.append('</h2>')
|
||||
|
||||
formatted_html.append('<div class="section-content">')
|
||||
|
||||
for i, fragment in enumerate(fragments, 1):
|
||||
formatted_html.append('<div class="fragment">')
|
||||
|
||||
# 添加内容
|
||||
if fragment.content:
|
||||
formatted_html.append(
|
||||
f'<div class="fragment-content">{self._sanitize_html(fragment.content)}</div>'
|
||||
)
|
||||
|
||||
# 添加参考文献
|
||||
if fragment.bibliography:
|
||||
formatted_html.append('<div class="bibliography">')
|
||||
formatted_html.append(f'{self._sanitize_html(fragment.bibliography)}')
|
||||
formatted_html.append('</div>')
|
||||
|
||||
formatted_html.append('</div>')
|
||||
|
||||
formatted_html.append('</div>')
|
||||
|
||||
formatted_html.append('</div>')
|
||||
return '\n'.join(formatted_html)
|
||||
|
||||
def save_html(self) -> Path:
|
||||
"""保存HTML文档"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"paper_content_{timestamp}.html"
|
||||
file_path = self.output_dir / filename
|
||||
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{self._sanitize_html(self.fragments[0].title if self.fragments else 'Paper Content')}</title>
|
||||
<style>
|
||||
{self.css_styles}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{self.format_paper_info()}
|
||||
{self.format_sections()}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
print(f"HTML document saved to: {file_path}")
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving HTML document: {str(e)}")
|
||||
raise
|
||||
|
||||
# 使用示例:
|
||||
# formatter = PaperHtmlFormatter(fragments, output_dir)
|
||||
# output_path = formatter.save_html()
|
||||
@@ -1,4 +1,4 @@
|
||||
from toolbox import CatchException, update_ui, update_ui_lastest_msg
|
||||
from toolbox import CatchException, update_ui, update_ui_latest_msg
|
||||
from crazy_functions.multi_stage.multi_stage_utils import GptAcademicGameBaseState
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
@@ -13,7 +13,7 @@ class MiniGame_ASCII_Art(GptAcademicGameBaseState):
|
||||
else:
|
||||
if prompt.strip() == 'exit':
|
||||
self.delete_game = True
|
||||
yield from update_ui_lastest_msg(lastmsg=f"谜底是{self.obj},游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=f"谜底是{self.obj},游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||
return
|
||||
chatbot.append([prompt, ""])
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
@@ -31,12 +31,12 @@ class MiniGame_ASCII_Art(GptAcademicGameBaseState):
|
||||
self.cur_task = 'identify user guess'
|
||||
res = get_code_block(raw_res)
|
||||
history += ['', f'the answer is {self.obj}', inputs, res]
|
||||
yield from update_ui_lastest_msg(lastmsg=res, chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=res, chatbot=chatbot, history=history, delay=0.)
|
||||
|
||||
elif self.cur_task == 'identify user guess':
|
||||
if is_same_thing(self.obj, prompt, self.llm_kwargs):
|
||||
self.delete_game = True
|
||||
yield from update_ui_lastest_msg(lastmsg="你猜对了!", chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg="你猜对了!", chatbot=chatbot, history=history, delay=0.)
|
||||
else:
|
||||
self.cur_task = 'identify user guess'
|
||||
yield from update_ui_lastest_msg(lastmsg="猜错了,再试试,输入“exit”获取答案。", chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg="猜错了,再试试,输入“exit”获取答案。", chatbot=chatbot, history=history, delay=0.)
|
||||
@@ -63,7 +63,7 @@ prompts_terminate = """小说的前文回顾:
|
||||
"""
|
||||
|
||||
|
||||
from toolbox import CatchException, update_ui, update_ui_lastest_msg
|
||||
from toolbox import CatchException, update_ui, update_ui_latest_msg
|
||||
from crazy_functions.multi_stage.multi_stage_utils import GptAcademicGameBaseState
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
@@ -112,7 +112,7 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
||||
if prompt.strip() == 'exit' or prompt.strip() == '结束剧情':
|
||||
# should we terminate game here?
|
||||
self.delete_game = True
|
||||
yield from update_ui_lastest_msg(lastmsg=f"游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=f"游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||
return
|
||||
if '剧情收尾' in prompt:
|
||||
self.cur_task = 'story_terminate'
|
||||
@@ -137,8 +137,8 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
||||
)
|
||||
self.story.append(story_paragraph)
|
||||
# # 配图
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
|
||||
# # 构建后续剧情引导
|
||||
previously_on_story = ""
|
||||
@@ -171,8 +171,8 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
||||
)
|
||||
self.story.append(story_paragraph)
|
||||
# # 配图
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
|
||||
# # 构建后续剧情引导
|
||||
previously_on_story = ""
|
||||
@@ -204,8 +204,8 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
||||
chatbot, history_, self.sys_prompt_
|
||||
)
|
||||
# # 配图
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||
|
||||
# terminate game
|
||||
self.delete_game = True
|
||||
|
||||
@@ -2,7 +2,7 @@ import time
|
||||
import importlib
|
||||
from toolbox import trimmed_format_exc, gen_time_str, get_log_folder
|
||||
from toolbox import CatchException, update_ui, gen_time_str, trimmed_format_exc, is_the_upload_folder
|
||||
from toolbox import promote_file_to_downloadzone, get_log_folder, update_ui_lastest_msg
|
||||
from toolbox import promote_file_to_downloadzone, get_log_folder, update_ui_latest_msg
|
||||
import multiprocessing
|
||||
|
||||
def get_class_name(class_string):
|
||||
|
||||
@@ -102,10 +102,10 @@ class GptJsonIO():
|
||||
logging.info(f'Repairing json:{response}')
|
||||
repair_prompt = self.generate_repair_prompt(broken_json = response, error=repr(e))
|
||||
result = self.generate_output(gpt_gen_fn(repair_prompt, self.format_instructions))
|
||||
logging.info('Repaire json success.')
|
||||
logging.info('Repair json success.')
|
||||
except Exception as e:
|
||||
# 没辙了,放弃治疗
|
||||
logging.info('Repaire json fail.')
|
||||
logging.info('Repair json fail.')
|
||||
raise JsonStringError('Cannot repair json.', str(e))
|
||||
return result
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import re
|
||||
import shutil
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
from toolbox import update_ui, update_ui_lastest_msg, get_log_folder, gen_time_str
|
||||
from toolbox import update_ui, update_ui_latest_msg, get_log_folder, gen_time_str
|
||||
from toolbox import get_conf, promote_file_to_downloadzone
|
||||
from crazy_functions.latex_fns.latex_toolbox import PRESERVE, TRANSFORM
|
||||
from crazy_functions.latex_fns.latex_toolbox import set_forbidden_text, set_forbidden_text_begin_end, set_forbidden_text_careful_brace
|
||||
@@ -20,7 +20,7 @@ def split_subprocess(txt, project_folder, return_dict, opts):
|
||||
"""
|
||||
break down latex file to a linked list,
|
||||
each node use a preserve flag to indicate whether it should
|
||||
be proccessed by GPT.
|
||||
be processed by GPT.
|
||||
"""
|
||||
text = txt
|
||||
mask = np.zeros(len(txt), dtype=np.uint8) + TRANSFORM
|
||||
@@ -85,14 +85,14 @@ class LatexPaperSplit():
|
||||
"""
|
||||
break down latex file to a linked list,
|
||||
each node use a preserve flag to indicate whether it should
|
||||
be proccessed by GPT.
|
||||
be processed by GPT.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.nodes = None
|
||||
self.msg = "*{\\scriptsize\\textbf{警告:该PDF由GPT-Academic开源项目调用大语言模型+Latex翻译插件一键生成," + \
|
||||
"版权归原文作者所有。翻译内容可靠性无保障,请仔细鉴别并以原文为准。" + \
|
||||
"项目Github地址 \\url{https://github.com/binary-husky/gpt_academic/}。"
|
||||
# 请您不要删除或修改这行警告,除非您是论文的原作者(如果您是论文原作者,欢迎加REAME中的QQ联系开发者)
|
||||
# 请您不要删除或修改这行警告,除非您是论文的原作者(如果您是论文原作者,欢迎加README中的QQ联系开发者)
|
||||
self.msg_declare = "为了防止大语言模型的意外谬误产生扩散影响,禁止移除或修改此警告。}}\\\\"
|
||||
self.title = "unknown"
|
||||
self.abstract = "unknown"
|
||||
@@ -151,7 +151,7 @@ class LatexPaperSplit():
|
||||
"""
|
||||
break down latex file to a linked list,
|
||||
each node use a preserve flag to indicate whether it should
|
||||
be proccessed by GPT.
|
||||
be processed by GPT.
|
||||
P.S. use multiprocessing to avoid timeout error
|
||||
"""
|
||||
import multiprocessing
|
||||
@@ -300,7 +300,8 @@ def Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin
|
||||
write_html(pfg.sp_file_contents, pfg.sp_file_result, chatbot=chatbot, project_folder=project_folder)
|
||||
|
||||
# <-------- 写出文件 ---------->
|
||||
msg = f"当前大语言模型: {llm_kwargs['llm_model']},当前语言模型温度设定: {llm_kwargs['temperature']}。"
|
||||
model_name = llm_kwargs['llm_model'].replace('_', '\\_') # 替换LLM模型名称中的下划线为转义字符
|
||||
msg = f"当前大语言模型: {model_name},当前语言模型温度设定: {llm_kwargs['temperature']}。"
|
||||
final_tex = lps.merge_result(pfg.file_result, mode, msg)
|
||||
objdump((lps, pfg.file_result, mode, msg), file=pj(project_folder,'merge_result.pkl'))
|
||||
|
||||
@@ -350,7 +351,42 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
||||
max_try = 32
|
||||
chatbot.append([f"正在编译PDF文档", f'编译已经开始。当前工作路径为{work_folder},如果程序停顿5分钟以上,请直接去该路径下取回翻译结果,或者重启之后再度尝试 ...']); yield from update_ui(chatbot=chatbot, history=history)
|
||||
chatbot.append([f"正在编译PDF文档", '...']); yield from update_ui(chatbot=chatbot, history=history); time.sleep(1); chatbot[-1] = list(chatbot[-1]) # 刷新界面
|
||||
yield from update_ui_lastest_msg('编译已经开始...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg('编译已经开始...', chatbot, history) # 刷新Gradio前端界面
|
||||
# 检查是否需要使用xelatex
|
||||
def check_if_need_xelatex(tex_path):
|
||||
try:
|
||||
with open(tex_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read(5000)
|
||||
# 检查是否有使用xelatex的宏包
|
||||
need_xelatex = any(
|
||||
pkg in content
|
||||
for pkg in ['fontspec', 'xeCJK', 'xetex', 'unicode-math', 'xltxtra', 'xunicode']
|
||||
)
|
||||
if need_xelatex:
|
||||
logger.info(f"检测到宏包需要xelatex编译, 切换至xelatex编译")
|
||||
else:
|
||||
logger.info(f"未检测到宏包需要xelatex编译, 使用pdflatex编译")
|
||||
return need_xelatex
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# 根据编译器类型返回编译命令
|
||||
def get_compile_command(compiler, filename):
|
||||
compile_command = f'{compiler} -interaction=batchmode -file-line-error {filename}.tex'
|
||||
logger.info('Latex 编译指令: ' + compile_command)
|
||||
return compile_command
|
||||
|
||||
# 确定使用的编译器
|
||||
compiler = 'pdflatex'
|
||||
if check_if_need_xelatex(pj(work_folder_modified, f'{main_file_modified}.tex')):
|
||||
logger.info("检测到宏包需要xelatex编译,切换至xelatex编译")
|
||||
# Check if xelatex is installed
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run(['xelatex', '--version'], capture_output=True, check=True)
|
||||
compiler = 'xelatex'
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise RuntimeError("检测到需要使用xelatex编译,但系统中未安装xelatex。请先安装texlive或其他提供xelatex的LaTeX发行版。")
|
||||
|
||||
while True:
|
||||
import os
|
||||
@@ -360,36 +396,36 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
||||
shutil.copyfile(may_exist_bbl, target_bbl)
|
||||
|
||||
# https://stackoverflow.com/questions/738755/dont-make-me-manually-abort-a-latex-compile-when-theres-an-error
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译原始PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_original}.tex', work_folder_original)
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译原始PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_original), work_folder_original)
|
||||
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译转化后的PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_modified}.tex', work_folder_modified)
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译转化后的PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_modified), work_folder_modified)
|
||||
|
||||
if ok and os.path.exists(pj(work_folder_modified, f'{main_file_modified}.pdf')):
|
||||
# 只有第二步成功,才能继续下面的步骤
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译BibTex ...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译BibTex ...', chatbot, history) # 刷新Gradio前端界面
|
||||
if not os.path.exists(pj(work_folder_original, f'{main_file_original}.bbl')):
|
||||
ok = compile_latex_with_timeout(f'bibtex {main_file_original}.aux', work_folder_original)
|
||||
if not os.path.exists(pj(work_folder_modified, f'{main_file_modified}.bbl')):
|
||||
ok = compile_latex_with_timeout(f'bibtex {main_file_modified}.aux', work_folder_modified)
|
||||
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译文献交叉引用 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_original}.tex', work_folder_original)
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_modified}.tex', work_folder_modified)
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_original}.tex', work_folder_original)
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_modified}.tex', work_folder_modified)
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译文献交叉引用 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_original), work_folder_original)
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_modified), work_folder_modified)
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_original), work_folder_original)
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_modified), work_folder_modified)
|
||||
|
||||
if mode!='translate_zh':
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 使用latexdiff生成论文转化前后对比 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 使用latexdiff生成论文转化前后对比 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
logger.info( f'latexdiff --encoding=utf8 --append-safecmd=subfile {work_folder_original}/{main_file_original}.tex {work_folder_modified}/{main_file_modified}.tex --flatten > {work_folder}/merge_diff.tex')
|
||||
ok = compile_latex_with_timeout(f'latexdiff --encoding=utf8 --append-safecmd=subfile {work_folder_original}/{main_file_original}.tex {work_folder_modified}/{main_file_modified}.tex --flatten > {work_folder}/merge_diff.tex', os.getcwd())
|
||||
|
||||
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 正在编译对比PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error merge_diff.tex', work_folder)
|
||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 正在编译对比PDF ...', chatbot, history) # 刷新Gradio前端界面
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, 'merge_diff'), work_folder)
|
||||
ok = compile_latex_with_timeout(f'bibtex merge_diff.aux', work_folder)
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error merge_diff.tex', work_folder)
|
||||
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error merge_diff.tex', work_folder)
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, 'merge_diff'), work_folder)
|
||||
ok = compile_latex_with_timeout(get_compile_command(compiler, 'merge_diff'), work_folder)
|
||||
|
||||
# <---------- 检查结果 ----------->
|
||||
results_ = ""
|
||||
@@ -399,13 +435,13 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
||||
results_ += f"原始PDF编译是否成功: {original_pdf_success};"
|
||||
results_ += f"转化PDF编译是否成功: {modified_pdf_success};"
|
||||
results_ += f"对比PDF编译是否成功: {diff_pdf_success};"
|
||||
yield from update_ui_lastest_msg(f'第{n_fix}编译结束:<br/>{results_}...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg(f'第{n_fix}编译结束:<br/>{results_}...', chatbot, history) # 刷新Gradio前端界面
|
||||
|
||||
if diff_pdf_success:
|
||||
result_pdf = pj(work_folder_modified, f'merge_diff.pdf') # get pdf path
|
||||
promote_file_to_downloadzone(result_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
||||
if modified_pdf_success:
|
||||
yield from update_ui_lastest_msg(f'转化PDF编译已经成功, 正在尝试生成对比PDF, 请稍候 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg(f'转化PDF编译已经成功, 正在尝试生成对比PDF, 请稍候 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
result_pdf = pj(work_folder_modified, f'{main_file_modified}.pdf') # get pdf path
|
||||
origin_pdf = pj(work_folder_original, f'{main_file_original}.pdf') # get pdf path
|
||||
if os.path.exists(pj(work_folder, '..', 'translation')):
|
||||
@@ -436,7 +472,7 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
||||
work_folder_modified=work_folder_modified,
|
||||
fixed_line=fixed_line
|
||||
)
|
||||
yield from update_ui_lastest_msg(f'由于最为关键的转化PDF编译失败, 将根据报错信息修正tex源文件并重试, 当前报错的latex代码处于第{buggy_lines}行 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
yield from update_ui_latest_msg(f'由于最为关键的转化PDF编译失败, 将根据报错信息修正tex源文件并重试, 当前报错的latex代码处于第{buggy_lines}行 ...', chatbot, history) # 刷新Gradio前端界面
|
||||
if not can_retry: break
|
||||
|
||||
return False # 失败啦
|
||||
|
||||
@@ -168,7 +168,7 @@ def set_forbidden_text(text, mask, pattern, flags=0):
|
||||
def reverse_forbidden_text(text, mask, pattern, flags=0, forbid_wrapper=True):
|
||||
"""
|
||||
Move area out of preserve area (make text editable for GPT)
|
||||
count the number of the braces so as to catch compelete text area.
|
||||
count the number of the braces so as to catch complete text area.
|
||||
e.g.
|
||||
\begin{abstract} blablablablablabla. \end{abstract}
|
||||
"""
|
||||
@@ -188,7 +188,7 @@ def reverse_forbidden_text(text, mask, pattern, flags=0, forbid_wrapper=True):
|
||||
def set_forbidden_text_careful_brace(text, mask, pattern, flags=0):
|
||||
"""
|
||||
Add a preserve text area in this paper (text become untouchable for GPT).
|
||||
count the number of the braces so as to catch compelete text area.
|
||||
count the number of the braces so as to catch complete text area.
|
||||
e.g.
|
||||
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
||||
"""
|
||||
@@ -214,7 +214,7 @@ def reverse_forbidden_text_careful_brace(
|
||||
):
|
||||
"""
|
||||
Move area out of preserve area (make text editable for GPT)
|
||||
count the number of the braces so as to catch compelete text area.
|
||||
count the number of the braces so as to catch complete text area.
|
||||
e.g.
|
||||
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
||||
"""
|
||||
@@ -287,23 +287,23 @@ def find_main_tex_file(file_manifest, mode):
|
||||
在多Tex文档中,寻找主文件,必须包含documentclass,返回找到的第一个。
|
||||
P.S. 但愿没人把latex模板放在里面传进来 (6.25 加入判定latex模板的代码)
|
||||
"""
|
||||
canidates = []
|
||||
candidates = []
|
||||
for texf in file_manifest:
|
||||
if os.path.basename(texf).startswith("merge"):
|
||||
continue
|
||||
with open(texf, "r", encoding="utf8", errors="ignore") as f:
|
||||
file_content = f.read()
|
||||
if r"\documentclass" in file_content:
|
||||
canidates.append(texf)
|
||||
candidates.append(texf)
|
||||
else:
|
||||
continue
|
||||
|
||||
if len(canidates) == 0:
|
||||
if len(candidates) == 0:
|
||||
raise RuntimeError("无法找到一个主Tex文件(包含documentclass关键字)")
|
||||
elif len(canidates) == 1:
|
||||
return canidates[0]
|
||||
else: # if len(canidates) >= 2 通过一些Latex模板中常见(但通常不会出现在正文)的单词,对不同latex源文件扣分,取评分最高者返回
|
||||
canidates_score = []
|
||||
elif len(candidates) == 1:
|
||||
return candidates[0]
|
||||
else: # if len(candidates) >= 2 通过一些Latex模板中常见(但通常不会出现在正文)的单词,对不同latex源文件扣分,取评分最高者返回
|
||||
candidates_score = []
|
||||
# 给出一些判定模板文档的词作为扣分项
|
||||
unexpected_words = [
|
||||
"\\LaTeX",
|
||||
@@ -316,19 +316,19 @@ def find_main_tex_file(file_manifest, mode):
|
||||
"reviewers",
|
||||
]
|
||||
expected_words = ["\\input", "\\ref", "\\cite"]
|
||||
for texf in canidates:
|
||||
canidates_score.append(0)
|
||||
for texf in candidates:
|
||||
candidates_score.append(0)
|
||||
with open(texf, "r", encoding="utf8", errors="ignore") as f:
|
||||
file_content = f.read()
|
||||
file_content = rm_comments(file_content)
|
||||
for uw in unexpected_words:
|
||||
if uw in file_content:
|
||||
canidates_score[-1] -= 1
|
||||
candidates_score[-1] -= 1
|
||||
for uw in expected_words:
|
||||
if uw in file_content:
|
||||
canidates_score[-1] += 1
|
||||
select = np.argmax(canidates_score) # 取评分最高者返回
|
||||
return canidates[select]
|
||||
candidates_score[-1] += 1
|
||||
select = np.argmax(candidates_score) # 取评分最高者返回
|
||||
return candidates[select]
|
||||
|
||||
|
||||
def rm_comments(main_file):
|
||||
@@ -374,7 +374,7 @@ def find_tex_file_ignore_case(fp):
|
||||
|
||||
def merge_tex_files_(project_foler, main_file, mode):
|
||||
"""
|
||||
Merge Tex project recrusively
|
||||
Merge Tex project recursively
|
||||
"""
|
||||
main_file = rm_comments(main_file)
|
||||
for s in reversed([q for q in re.finditer(r"\\input\{(.*?)\}", main_file, re.M)]):
|
||||
@@ -429,7 +429,7 @@ def find_title_and_abs(main_file):
|
||||
|
||||
def merge_tex_files(project_foler, main_file, mode):
|
||||
"""
|
||||
Merge Tex project recrusively
|
||||
Merge Tex project recursively
|
||||
P.S. 顺便把CTEX塞进去以支持中文
|
||||
P.S. 顺便把Latex的注释去除
|
||||
"""
|
||||
|
||||
43
crazy_functions/media_fns/get_media.py
Normal file
43
crazy_functions/media_fns/get_media.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from toolbox import update_ui, get_conf, promote_file_to_downloadzone, update_ui_latest_msg, generate_file_link
|
||||
from shared_utils.docker_as_service_api import stream_daas
|
||||
from shared_utils.docker_as_service_api import DockerServiceApiComModel
|
||||
import random
|
||||
|
||||
def download_video(video_id, only_audio, user_name, chatbot, history):
|
||||
from toolbox import get_log_folder
|
||||
chatbot.append([None, "Processing..."])
|
||||
yield from update_ui(chatbot, history)
|
||||
client_command = f'{video_id} --audio-only' if only_audio else video_id
|
||||
server_urls = get_conf('DAAS_SERVER_URLS')
|
||||
server_url = random.choice(server_urls)
|
||||
docker_service_api_com_model = DockerServiceApiComModel(client_command=client_command)
|
||||
save_file_dir = get_log_folder(user_name, plugin_name='media_downloader')
|
||||
for output_manifest in stream_daas(docker_service_api_com_model, server_url, save_file_dir):
|
||||
status_buf = ""
|
||||
status_buf += "DaaS message: \n\n"
|
||||
status_buf += output_manifest['server_message'].replace('\n', '<br/>')
|
||||
status_buf += "\n\n"
|
||||
status_buf += "DaaS standard error: \n\n"
|
||||
status_buf += output_manifest['server_std_err'].replace('\n', '<br/>')
|
||||
status_buf += "\n\n"
|
||||
status_buf += "DaaS standard output: \n\n"
|
||||
status_buf += output_manifest['server_std_out'].replace('\n', '<br/>')
|
||||
status_buf += "\n\n"
|
||||
status_buf += "DaaS file attach: \n\n"
|
||||
status_buf += str(output_manifest['server_file_attach'])
|
||||
yield from update_ui_latest_msg(status_buf, chatbot, history)
|
||||
|
||||
return output_manifest['server_file_attach']
|
||||
|
||||
|
||||
def search_videos(keywords):
|
||||
from toolbox import get_log_folder
|
||||
client_command = keywords
|
||||
server_urls = get_conf('DAAS_SERVER_URLS')
|
||||
server_url = random.choice(server_urls)
|
||||
server_url = server_url.replace('stream', 'search')
|
||||
docker_service_api_com_model = DockerServiceApiComModel(client_command=client_command)
|
||||
save_file_dir = get_log_folder("default_user", plugin_name='media_downloader')
|
||||
for output_manifest in stream_daas(docker_service_api_com_model, server_url, save_file_dir):
|
||||
return output_manifest['server_message']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from toolbox import update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui_latest_msg, disable_auto_promotion
|
||||
from toolbox import CatchException, update_ui, get_conf, select_api_key, get_log_folder
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
||||
|
||||
@@ -113,7 +113,7 @@ def translate_pdf(article_dict, llm_kwargs, chatbot, fp, generated_conclusion_fi
|
||||
return [txt]
|
||||
else:
|
||||
# raw_token_num > TOKEN_LIMIT_PER_FRAGMENT
|
||||
# find a smooth token limit to achieve even seperation
|
||||
# find a smooth token limit to achieve even separation
|
||||
count = int(math.ceil(raw_token_num / TOKEN_LIMIT_PER_FRAGMENT))
|
||||
token_limit_smooth = raw_token_num // count + count
|
||||
return breakdown_text_to_satisfy_token_limit(txt, limit=token_limit_smooth, llm_model=llm_kwargs['llm_model'])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from toolbox import CatchException, report_exception, get_log_folder, gen_time_str, check_packages
|
||||
from toolbox import update_ui, promote_file_to_downloadzone, update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui, promote_file_to_downloadzone, update_ui_latest_msg, disable_auto_promotion
|
||||
from toolbox import write_history_to_file, promote_file_to_downloadzone, get_conf, extract_archive
|
||||
from crazy_functions.pdf_fns.parse_pdf import parse_pdf, translate_pdf
|
||||
|
||||
|
||||
@@ -6,75 +6,128 @@ from crazy_functions.crazy_utils import get_files_from_everything
|
||||
from shared_utils.colorful import *
|
||||
from loguru import logger
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
|
||||
def refresh_key(doc2x_api_key):
|
||||
import requests, json
|
||||
url = "https://api.doc2x.noedgeai.com/api/token/refresh"
|
||||
res = requests.post(
|
||||
url,
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key}
|
||||
)
|
||||
res_json = []
|
||||
if res.status_code == 200:
|
||||
decoded = res.content.decode("utf-8")
|
||||
res_json = json.loads(decoded)
|
||||
doc2x_api_key = res_json['data']['token']
|
||||
else:
|
||||
raise RuntimeError(format("[ERROR] status code: %d, body: %s" % (res.status_code, res.text)))
|
||||
return doc2x_api_key
|
||||
|
||||
def retry_request(max_retries=3, delay=3):
|
||||
"""
|
||||
Decorator for retrying HTTP requests
|
||||
Args:
|
||||
max_retries: Maximum number of retry attempts
|
||||
delay: Delay between retries in seconds
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
logger.error(
|
||||
f"Request failed, retrying... ({attempt + 1}/{max_retries}) Error: {e}"
|
||||
)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
raise e
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@retry_request()
|
||||
def make_request(method, url, **kwargs):
|
||||
"""
|
||||
Make HTTP request with retry mechanism
|
||||
"""
|
||||
return requests.request(method, url, **kwargs)
|
||||
|
||||
|
||||
def doc2x_api_response_status(response, uid=""):
|
||||
"""
|
||||
Check the status of Doc2x API response
|
||||
Args:
|
||||
response_data: Response object from Doc2x API
|
||||
"""
|
||||
response_json = response.json()
|
||||
response_data = response_json.get("data", {})
|
||||
code = response_json.get("code", "Unknown")
|
||||
meg = response_data.get("message", response_json)
|
||||
trace_id = response.headers.get("trace-id", "Failed to get trace-id")
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Doc2x return an error:\nTrace ID: {trace_id} {uid}\n{response.status_code} - {response_json}"
|
||||
)
|
||||
if code in ["parse_page_limit_exceeded", "parse_concurrency_limit"]:
|
||||
raise RuntimeError(
|
||||
f"Reached the limit of Doc2x:\nTrace ID: {trace_id} {uid}\n{code} - {meg}"
|
||||
)
|
||||
if code not in ["ok", "success"]:
|
||||
raise RuntimeError(
|
||||
f"Doc2x return an error:\nTrace ID: {trace_id} {uid}\n{code} - {meg}"
|
||||
)
|
||||
return response_data
|
||||
|
||||
|
||||
def 解析PDF_DOC2X_转Latex(pdf_file_path):
|
||||
zip_file_path, unzipped_folder = 解析PDF_DOC2X(pdf_file_path, format='tex')
|
||||
zip_file_path, unzipped_folder = 解析PDF_DOC2X(pdf_file_path, format="tex")
|
||||
return unzipped_folder
|
||||
|
||||
|
||||
def 解析PDF_DOC2X(pdf_file_path, format='tex'):
|
||||
def 解析PDF_DOC2X(pdf_file_path, format="tex"):
|
||||
"""
|
||||
format: 'tex', 'md', 'docx'
|
||||
format: 'tex', 'md', 'docx'
|
||||
"""
|
||||
import requests, json, os
|
||||
DOC2X_API_KEY = get_conf('DOC2X_API_KEY')
|
||||
|
||||
DOC2X_API_KEY = get_conf("DOC2X_API_KEY")
|
||||
latex_dir = get_log_folder(plugin_name="pdf_ocr_latex")
|
||||
markdown_dir = get_log_folder(plugin_name="pdf_ocr")
|
||||
doc2x_api_key = DOC2X_API_KEY
|
||||
|
||||
# < ------ 第1步:预上传获取URL,然后上传文件 ------ >
|
||||
logger.info("Doc2x 上传文件:预上传获取URL")
|
||||
res = make_request(
|
||||
"POST",
|
||||
"https://v2.doc2x.noedgeai.com/api/v2/parse/preupload",
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key},
|
||||
timeout=15,
|
||||
)
|
||||
res_data = doc2x_api_response_status(res)
|
||||
upload_url = res_data["url"]
|
||||
uuid = res_data["uid"]
|
||||
|
||||
# < ------ 第1步:上传 ------ >
|
||||
logger.info("Doc2x 第1步:上传")
|
||||
with open(pdf_file_path, 'rb') as file:
|
||||
res = requests.post(
|
||||
"https://v2.doc2x.noedgeai.com/api/v2/parse/pdf",
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key},
|
||||
data=file
|
||||
)
|
||||
# res_json = []
|
||||
if res.status_code == 200:
|
||||
res_json = res.json()
|
||||
else:
|
||||
raise RuntimeError(f"Doc2x return an error: {res.json()}")
|
||||
uuid = res_json['data']['uid']
|
||||
logger.info("Doc2x 上传文件:上传文件")
|
||||
with open(pdf_file_path, "rb") as file:
|
||||
res = make_request("PUT", upload_url, data=file, timeout=60)
|
||||
res.raise_for_status()
|
||||
|
||||
# < ------ 第2步:轮询等待 ------ >
|
||||
logger.info("Doc2x 第2步:轮询等待")
|
||||
params = {'uid': uuid}
|
||||
while True:
|
||||
res = requests.get(
|
||||
'https://v2.doc2x.noedgeai.com/api/v2/parse/status',
|
||||
logger.info("Doc2x 处理文件中:轮询等待")
|
||||
params = {"uid": uuid}
|
||||
max_attempts = 60
|
||||
attempt = 0
|
||||
while attempt < max_attempts:
|
||||
res = make_request(
|
||||
"GET",
|
||||
"https://v2.doc2x.noedgeai.com/api/v2/parse/status",
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key},
|
||||
params=params
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
res_json = res.json()
|
||||
if res_json['data']['status'] == "success":
|
||||
res_data = doc2x_api_response_status(res)
|
||||
if res_data["status"] == "success":
|
||||
break
|
||||
elif res_json['data']['status'] == "processing":
|
||||
time.sleep(3)
|
||||
logger.info(f"Doc2x is processing at {res_json['data']['progress']}%")
|
||||
elif res_json['data']['status'] == "failed":
|
||||
raise RuntimeError(f"Doc2x return an error: {res_json}")
|
||||
|
||||
elif res_data["status"] == "processing":
|
||||
time.sleep(5)
|
||||
logger.info(f"Doc2x is processing at {res_data['progress']}%")
|
||||
attempt += 1
|
||||
else:
|
||||
raise RuntimeError(f"Doc2x return an error: {res_data}")
|
||||
if attempt >= max_attempts:
|
||||
raise RuntimeError("Doc2x processing timeout after maximum attempts")
|
||||
|
||||
# < ------ 第3步:提交转化 ------ >
|
||||
logger.info("Doc2x 第3步:提交转化")
|
||||
@@ -84,42 +137,44 @@ def 解析PDF_DOC2X(pdf_file_path, format='tex'):
|
||||
"formula_mode": "dollar",
|
||||
"filename": "output"
|
||||
}
|
||||
res = requests.post(
|
||||
'https://v2.doc2x.noedgeai.com/api/v2/convert/parse',
|
||||
res = make_request(
|
||||
"POST",
|
||||
"https://v2.doc2x.noedgeai.com/api/v2/convert/parse",
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key},
|
||||
json=data
|
||||
json=data,
|
||||
timeout=15,
|
||||
)
|
||||
if res.status_code == 200:
|
||||
res_json = res.json()
|
||||
else:
|
||||
raise RuntimeError(f"Doc2x return an error: {res.json()}")
|
||||
|
||||
doc2x_api_response_status(res, uid=f"uid: {uuid}")
|
||||
|
||||
# < ------ 第4步:等待结果 ------ >
|
||||
logger.info("Doc2x 第4步:等待结果")
|
||||
params = {'uid': uuid}
|
||||
while True:
|
||||
res = requests.get(
|
||||
'https://v2.doc2x.noedgeai.com/api/v2/convert/parse/result',
|
||||
params = {"uid": uuid}
|
||||
max_attempts = 36
|
||||
attempt = 0
|
||||
while attempt < max_attempts:
|
||||
res = make_request(
|
||||
"GET",
|
||||
"https://v2.doc2x.noedgeai.com/api/v2/convert/parse/result",
|
||||
headers={"Authorization": "Bearer " + doc2x_api_key},
|
||||
params=params
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
res_json = res.json()
|
||||
if res_json['data']['status'] == "success":
|
||||
res_data = doc2x_api_response_status(res, uid=f"uid: {uuid}")
|
||||
if res_data["status"] == "success":
|
||||
break
|
||||
elif res_json['data']['status'] == "processing":
|
||||
elif res_data["status"] == "processing":
|
||||
time.sleep(3)
|
||||
logger.info(f"Doc2x still processing")
|
||||
elif res_json['data']['status'] == "failed":
|
||||
raise RuntimeError(f"Doc2x return an error: {res_json}")
|
||||
|
||||
logger.info("Doc2x still processing to convert file")
|
||||
attempt += 1
|
||||
if attempt >= max_attempts:
|
||||
raise RuntimeError("Doc2x conversion timeout after maximum attempts")
|
||||
|
||||
# < ------ 第5步:最后的处理 ------ >
|
||||
logger.info("Doc2x 第5步:最后的处理")
|
||||
logger.info("Doc2x 第5步:下载转换后的文件")
|
||||
|
||||
if format=='tex':
|
||||
if format == "tex":
|
||||
target_path = latex_dir
|
||||
if format=='md':
|
||||
if format == "md":
|
||||
target_path = markdown_dir
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
|
||||
@@ -127,17 +182,18 @@ def 解析PDF_DOC2X(pdf_file_path, format='tex'):
|
||||
# < ------ 下载 ------ >
|
||||
for attempt in range(max_attempt):
|
||||
try:
|
||||
result_url = res_json['data']['url']
|
||||
res = requests.get(result_url)
|
||||
zip_path = os.path.join(target_path, gen_time_str() + '.zip')
|
||||
result_url = res_data["url"]
|
||||
res = make_request("GET", result_url, timeout=60)
|
||||
zip_path = os.path.join(target_path, gen_time_str() + ".zip")
|
||||
unzip_path = os.path.join(target_path, gen_time_str())
|
||||
if res.status_code == 200:
|
||||
with open(zip_path, "wb") as f: f.write(res.content)
|
||||
with open(zip_path, "wb") as f:
|
||||
f.write(res.content)
|
||||
else:
|
||||
raise RuntimeError(f"Doc2x return an error: {res.json()}")
|
||||
except Exception as e:
|
||||
if attempt < max_attempt - 1:
|
||||
logger.error(f"Failed to download latex file, retrying... {e}")
|
||||
logger.error(f"Failed to download uid = {uuid} file, retrying... {e}")
|
||||
time.sleep(3)
|
||||
continue
|
||||
else:
|
||||
@@ -145,22 +201,31 @@ def 解析PDF_DOC2X(pdf_file_path, format='tex'):
|
||||
|
||||
# < ------ 解压 ------ >
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
zip_ref.extractall(unzip_path)
|
||||
return zip_path, unzip_path
|
||||
|
||||
|
||||
def 解析PDF_DOC2X_单文件(fp, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, DOC2X_API_KEY, user_request):
|
||||
|
||||
def 解析PDF_DOC2X_单文件(
|
||||
fp,
|
||||
project_folder,
|
||||
llm_kwargs,
|
||||
plugin_kwargs,
|
||||
chatbot,
|
||||
history,
|
||||
system_prompt,
|
||||
DOC2X_API_KEY,
|
||||
user_request,
|
||||
):
|
||||
def pdf2markdown(filepath):
|
||||
chatbot.append((None, f"Doc2x 解析中"))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
md_zip_path, unzipped_folder = 解析PDF_DOC2X(filepath, format='md')
|
||||
md_zip_path, unzipped_folder = 解析PDF_DOC2X(filepath, format="md")
|
||||
|
||||
promote_file_to_downloadzone(md_zip_path, chatbot=chatbot)
|
||||
chatbot.append((None, f"完成解析 {md_zip_path} ..."))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
return md_zip_path
|
||||
|
||||
def deliver_to_markdown_plugin(md_zip_path, user_request):
|
||||
@@ -174,77 +239,97 @@ def 解析PDF_DOC2X_单文件(fp, project_folder, llm_kwargs, plugin_kwargs, cha
|
||||
os.makedirs(target_path_base, exist_ok=True)
|
||||
shutil.copyfile(md_zip_path, this_file_path)
|
||||
ex_folder = this_file_path + ".extract"
|
||||
extract_archive(
|
||||
file_path=this_file_path, dest_dir=ex_folder
|
||||
)
|
||||
extract_archive(file_path=this_file_path, dest_dir=ex_folder)
|
||||
|
||||
# edit markdown files
|
||||
success, file_manifest, project_folder = get_files_from_everything(ex_folder, type='.md')
|
||||
success, file_manifest, project_folder = get_files_from_everything(
|
||||
ex_folder, type=".md"
|
||||
)
|
||||
for generated_fp in file_manifest:
|
||||
# 修正一些公式问题
|
||||
with open(generated_fp, 'r', encoding='utf8') as f:
|
||||
with open(generated_fp, "r", encoding="utf8") as f:
|
||||
content = f.read()
|
||||
# 将公式中的\[ \]替换成$$
|
||||
content = content.replace(r'\[', r'$$').replace(r'\]', r'$$')
|
||||
content = content.replace(r"\[", r"$$").replace(r"\]", r"$$")
|
||||
# 将公式中的\( \)替换成$
|
||||
content = content.replace(r'\(', r'$').replace(r'\)', r'$')
|
||||
content = content.replace('```markdown', '\n').replace('```', '\n')
|
||||
with open(generated_fp, 'w', encoding='utf8') as f:
|
||||
content = content.replace(r"\(", r"$").replace(r"\)", r"$")
|
||||
content = content.replace("```markdown", "\n").replace("```", "\n")
|
||||
with open(generated_fp, "w", encoding="utf8") as f:
|
||||
f.write(content)
|
||||
promote_file_to_downloadzone(generated_fp, chatbot=chatbot)
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
# 生成在线预览html
|
||||
file_name = '在线预览翻译(原文)' + gen_time_str() + '.html'
|
||||
file_name = "在线预览翻译(原文)" + gen_time_str() + ".html"
|
||||
preview_fp = os.path.join(ex_folder, file_name)
|
||||
from shared_utils.advanced_markdown_format import markdown_convertion_for_file
|
||||
from shared_utils.advanced_markdown_format import (
|
||||
markdown_convertion_for_file,
|
||||
)
|
||||
|
||||
with open(generated_fp, "r", encoding="utf-8") as f:
|
||||
md = f.read()
|
||||
# # Markdown中使用不标准的表格,需要在表格前加上一个emoji,以便公式渲染
|
||||
# md = re.sub(r'^<table>', r'.<table>', md, flags=re.MULTILINE)
|
||||
html = markdown_convertion_for_file(md)
|
||||
with open(preview_fp, "w", encoding="utf-8") as f: f.write(html)
|
||||
with open(preview_fp, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
chatbot.append([None, f"生成在线预览:{generate_file_link([preview_fp])}"])
|
||||
promote_file_to_downloadzone(preview_fp, chatbot=chatbot)
|
||||
|
||||
|
||||
|
||||
chatbot.append((None, f"调用Markdown插件 {ex_folder} ..."))
|
||||
plugin_kwargs['markdown_expected_output_dir'] = ex_folder
|
||||
plugin_kwargs["markdown_expected_output_dir"] = ex_folder
|
||||
|
||||
translated_f_name = 'translated_markdown.md'
|
||||
generated_fp = plugin_kwargs['markdown_expected_output_path'] = os.path.join(ex_folder, translated_f_name)
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from Markdown英译中(ex_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
||||
translated_f_name = "translated_markdown.md"
|
||||
generated_fp = plugin_kwargs["markdown_expected_output_path"] = os.path.join(
|
||||
ex_folder, translated_f_name
|
||||
)
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from Markdown英译中(
|
||||
ex_folder,
|
||||
llm_kwargs,
|
||||
plugin_kwargs,
|
||||
chatbot,
|
||||
history,
|
||||
system_prompt,
|
||||
user_request,
|
||||
)
|
||||
if os.path.exists(generated_fp):
|
||||
# 修正一些公式问题
|
||||
with open(generated_fp, 'r', encoding='utf8') as f: content = f.read()
|
||||
content = content.replace('```markdown', '\n').replace('```', '\n')
|
||||
with open(generated_fp, "r", encoding="utf8") as f:
|
||||
content = f.read()
|
||||
content = content.replace("```markdown", "\n").replace("```", "\n")
|
||||
# Markdown中使用不标准的表格,需要在表格前加上一个emoji,以便公式渲染
|
||||
# content = re.sub(r'^<table>', r'.<table>', content, flags=re.MULTILINE)
|
||||
with open(generated_fp, 'w', encoding='utf8') as f: f.write(content)
|
||||
with open(generated_fp, "w", encoding="utf8") as f:
|
||||
f.write(content)
|
||||
# 生成在线预览html
|
||||
file_name = '在线预览翻译' + gen_time_str() + '.html'
|
||||
file_name = "在线预览翻译" + gen_time_str() + ".html"
|
||||
preview_fp = os.path.join(ex_folder, file_name)
|
||||
from shared_utils.advanced_markdown_format import markdown_convertion_for_file
|
||||
from shared_utils.advanced_markdown_format import (
|
||||
markdown_convertion_for_file,
|
||||
)
|
||||
|
||||
with open(generated_fp, "r", encoding="utf-8") as f:
|
||||
md = f.read()
|
||||
html = markdown_convertion_for_file(md)
|
||||
with open(preview_fp, "w", encoding="utf-8") as f: f.write(html)
|
||||
with open(preview_fp, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
promote_file_to_downloadzone(preview_fp, chatbot=chatbot)
|
||||
# 生成包含图片的压缩包
|
||||
dest_folder = get_log_folder(chatbot.get_user())
|
||||
zip_name = '翻译后的带图文档.zip'
|
||||
zip_folder(source_folder=ex_folder, dest_folder=dest_folder, zip_name=zip_name)
|
||||
zip_name = "翻译后的带图文档.zip"
|
||||
zip_folder(
|
||||
source_folder=ex_folder, dest_folder=dest_folder, zip_name=zip_name
|
||||
)
|
||||
zip_fp = os.path.join(dest_folder, zip_name)
|
||||
promote_file_to_downloadzone(zip_fp, chatbot=chatbot)
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
md_zip_path = yield from pdf2markdown(fp)
|
||||
yield from deliver_to_markdown_plugin(md_zip_path, user_request)
|
||||
|
||||
|
||||
def 解析PDF_基于DOC2X(file_manifest, *args):
|
||||
for index, fp in enumerate(file_manifest):
|
||||
yield from 解析PDF_DOC2X_单文件(fp, *args)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -14,17 +14,17 @@ def extract_text_from_files(txt, chatbot, history):
|
||||
final_result(list):文本内容
|
||||
page_one(list):第一页内容/摘要
|
||||
file_manifest(list):文件路径
|
||||
excption(string):需要用户手动处理的信息,如没出错则保持为空
|
||||
exception(string):需要用户手动处理的信息,如没出错则保持为空
|
||||
"""
|
||||
|
||||
final_result = []
|
||||
page_one = []
|
||||
file_manifest = []
|
||||
excption = ""
|
||||
exception = ""
|
||||
|
||||
if txt == "":
|
||||
final_result.append(txt)
|
||||
return False, final_result, page_one, file_manifest, excption #如输入区内容不是文件则直接返回输入区内容
|
||||
return False, final_result, page_one, file_manifest, exception #如输入区内容不是文件则直接返回输入区内容
|
||||
|
||||
#查找输入区内容中的文件
|
||||
file_pdf,pdf_manifest,folder_pdf = get_files_from_everything(txt, '.pdf')
|
||||
@@ -33,20 +33,20 @@ def extract_text_from_files(txt, chatbot, history):
|
||||
file_doc,doc_manifest,folder_doc = get_files_from_everything(txt, '.doc')
|
||||
|
||||
if file_doc:
|
||||
excption = "word"
|
||||
return False, final_result, page_one, file_manifest, excption
|
||||
exception = "word"
|
||||
return False, final_result, page_one, file_manifest, exception
|
||||
|
||||
file_num = len(pdf_manifest) + len(md_manifest) + len(word_manifest)
|
||||
if file_num == 0:
|
||||
final_result.append(txt)
|
||||
return False, final_result, page_one, file_manifest, excption #如输入区内容不是文件则直接返回输入区内容
|
||||
return False, final_result, page_one, file_manifest, exception #如输入区内容不是文件则直接返回输入区内容
|
||||
|
||||
if file_pdf:
|
||||
try: # 尝试导入依赖,如果缺少依赖,则给出安装建议
|
||||
import fitz
|
||||
except:
|
||||
excption = "pdf"
|
||||
return False, final_result, page_one, file_manifest, excption
|
||||
exception = "pdf"
|
||||
return False, final_result, page_one, file_manifest, exception
|
||||
for index, fp in enumerate(pdf_manifest):
|
||||
file_content, pdf_one = read_and_clean_pdf_text(fp) # (尝试)按照章节切割PDF
|
||||
file_content = file_content.encode('utf-8', 'ignore').decode() # avoid reading non-utf8 chars
|
||||
@@ -72,8 +72,8 @@ def extract_text_from_files(txt, chatbot, history):
|
||||
try: # 尝试导入依赖,如果缺少依赖,则给出安装建议
|
||||
from docx import Document
|
||||
except:
|
||||
excption = "word_pip"
|
||||
return False, final_result, page_one, file_manifest, excption
|
||||
exception = "word_pip"
|
||||
return False, final_result, page_one, file_manifest, exception
|
||||
for index, fp in enumerate(word_manifest):
|
||||
doc = Document(fp)
|
||||
file_content = '\n'.join([p.text for p in doc.paragraphs])
|
||||
@@ -82,4 +82,4 @@ def extract_text_from_files(txt, chatbot, history):
|
||||
final_result.append(file_content)
|
||||
file_manifest.append(os.path.relpath(fp, folder_word))
|
||||
|
||||
return True, final_result, page_one, file_manifest, excption
|
||||
return True, final_result, page_one, file_manifest, exception
|
||||
@@ -1,115 +0,0 @@
|
||||
import logging
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class ArxivDownloader:
|
||||
"""用于下载arXiv论文源码的下载器"""
|
||||
|
||||
def __init__(self, root_dir: str = "./papers", proxies: Optional[Dict[str, str]] = None):
|
||||
"""
|
||||
初始化下载器
|
||||
|
||||
Args:
|
||||
root_dir: 保存下载文件的根目录
|
||||
proxies: 代理服务器设置,例如 {"http": "http://proxy:port", "https": "https://proxy:port"}
|
||||
"""
|
||||
self.root_dir = Path(root_dir)
|
||||
self.root_dir.mkdir(exist_ok=True)
|
||||
self.proxies = proxies
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
def _download_and_extract(self, arxiv_id: str) -> str:
|
||||
"""
|
||||
下载并解压arxiv论文源码
|
||||
|
||||
Args:
|
||||
arxiv_id: arXiv论文ID,例如"2103.00020"
|
||||
|
||||
Returns:
|
||||
str: 解压后的文件目录路径
|
||||
|
||||
Raises:
|
||||
RuntimeError: 当下载失败时抛出
|
||||
"""
|
||||
paper_dir = self.root_dir / arxiv_id
|
||||
tar_path = paper_dir / f"{arxiv_id}.tar.gz"
|
||||
|
||||
# 检查缓存
|
||||
if paper_dir.exists() and any(paper_dir.iterdir()):
|
||||
logging.info(f"Using cached version for {arxiv_id}")
|
||||
return str(paper_dir)
|
||||
|
||||
paper_dir.mkdir(exist_ok=True)
|
||||
|
||||
urls = [
|
||||
f"https://arxiv.org/src/{arxiv_id}",
|
||||
f"https://arxiv.org/e-print/{arxiv_id}"
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
logging.info(f"Downloading from {url}")
|
||||
response = requests.get(url, proxies=self.proxies)
|
||||
if response.status_code == 200:
|
||||
tar_path.write_bytes(response.content)
|
||||
with tarfile.open(tar_path, 'r:gz') as tar:
|
||||
tar.extractall(path=paper_dir)
|
||||
return str(paper_dir)
|
||||
except Exception as e:
|
||||
logging.warning(f"Download failed for {url}: {e}")
|
||||
continue
|
||||
|
||||
raise RuntimeError(f"Failed to download paper {arxiv_id}")
|
||||
|
||||
def download_paper(self, arxiv_id: str) -> str:
|
||||
"""
|
||||
下载指定的arXiv论文
|
||||
|
||||
Args:
|
||||
arxiv_id: arXiv论文ID
|
||||
|
||||
Returns:
|
||||
str: 论文文件所在的目录路径
|
||||
"""
|
||||
return self._download_and_extract(arxiv_id)
|
||||
|
||||
|
||||
def main():
|
||||
"""测试下载功能"""
|
||||
# 配置代理(如果需要)
|
||||
proxies = {
|
||||
"http": "http://your-proxy:port",
|
||||
"https": "https://your-proxy:port"
|
||||
}
|
||||
|
||||
# 创建下载器实例(如果不需要代理,可以不传入proxies参数)
|
||||
downloader = ArxivDownloader(root_dir="./downloaded_papers", proxies=None)
|
||||
|
||||
# 测试下载一篇论文(这里使用一个示例ID)
|
||||
try:
|
||||
paper_id = "2103.00020" # 这是一个示例ID
|
||||
paper_dir = downloader.download_paper(paper_id)
|
||||
print(f"Successfully downloaded paper to: {paper_dir}")
|
||||
|
||||
# 检查下载的文件
|
||||
paper_path = Path(paper_dir)
|
||||
if paper_path.exists():
|
||||
print("Downloaded files:")
|
||||
for file in paper_path.rglob("*"):
|
||||
if file.is_file():
|
||||
print(f"- {file.relative_to(paper_path)}")
|
||||
except Exception as e:
|
||||
print(f"Error downloading paper: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,836 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import tarfile
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Set
|
||||
|
||||
import aiohttp
|
||||
|
||||
from crazy_functions.rag_fns.arxiv_fns.author_extractor import LatexAuthorExtractor
|
||||
from crazy_functions.rag_fns.arxiv_fns.essay_structure import EssayStructureParser, DocumentStructure, read_tex_file
|
||||
from crazy_functions.rag_fns.arxiv_fns.section_extractor import Section
|
||||
from crazy_functions.rag_fns.arxiv_fns.section_fragment import SectionFragment
|
||||
from crazy_functions.rag_fns.arxiv_fns.tex_utils import TexUtils
|
||||
from crazy_functions.doc_fns.content_folder import PaperContentFormatter, PaperMetadata
|
||||
|
||||
|
||||
def save_fragments_to_file(fragments: List[SectionFragment], output_dir: Path ) -> Path:
|
||||
"""
|
||||
Save all fragments to a single structured markdown file.
|
||||
|
||||
Args:
|
||||
fragments: List of SectionFragment objects
|
||||
output_dir: Output directory path
|
||||
|
||||
Returns:
|
||||
Path: Path to the generated markdown file
|
||||
"""
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Create output directory
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename
|
||||
filename = f"paper_latex_content_{timestamp}.md"
|
||||
file_path = output_path/ filename
|
||||
|
||||
# Group fragments by section
|
||||
sections = {}
|
||||
for fragment in fragments:
|
||||
section = fragment.current_section or "Uncategorized"
|
||||
if section not in sections:
|
||||
sections[section] = []
|
||||
sections[section].append(fragment)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
# Write document header
|
||||
f.write("# Document Fragments Analysis\n\n")
|
||||
f.write("## Overview\n")
|
||||
f.write(f"- Total Fragments: {len(fragments)}\n")
|
||||
f.write(f"- Generated Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
|
||||
# Add paper information if available
|
||||
if fragments and (fragments[0].title or fragments[0].abstract):
|
||||
f.write("\n## Paper Information\n")
|
||||
if fragments[0].title:
|
||||
f.write(f"### Title\n{fragments[0].title}\n")
|
||||
if fragments[0].authors:
|
||||
f.write(f"\n### Authors\n{fragments[0].authors}\n")
|
||||
if fragments[0].abstract:
|
||||
f.write(f"\n### Abstract\n{fragments[0].abstract}\n")
|
||||
|
||||
# Write section tree if available
|
||||
if fragments and fragments[0].catalogs:
|
||||
f.write("\n## Section Tree\n")
|
||||
f.write("```\n") # 添加代码块开始标记
|
||||
f.write(fragments[0].catalogs)
|
||||
f.write("\n```") # 添加代码块结束标记
|
||||
|
||||
# Generate table of contents
|
||||
f.write("\n## Table of Contents\n")
|
||||
for section in sections:
|
||||
clean_section = section.strip() or "Uncategorized"
|
||||
fragment_count = len(sections[section])
|
||||
f.write(f"- [{clean_section}](#{clean_section.lower().replace(' ', '-')}) "
|
||||
f"({fragment_count} fragments)\n")
|
||||
|
||||
# Write content sections
|
||||
f.write("\n## Content\n")
|
||||
for section, section_fragments in sections.items():
|
||||
clean_section = section.strip() or "Uncategorized"
|
||||
f.write(f"\n### {clean_section}\n")
|
||||
|
||||
# Write each fragment
|
||||
for i, fragment in enumerate(section_fragments, 1):
|
||||
f.write(f"\n#### Fragment {i}\n")
|
||||
|
||||
# Metadata
|
||||
f.write("**Metadata:**\n")
|
||||
metadata = [
|
||||
f"- Section: {fragment.current_section}",
|
||||
f"- Length: {len(fragment.content)} chars",
|
||||
f"- ArXiv ID: {fragment.arxiv_id}" if fragment.arxiv_id else None
|
||||
]
|
||||
f.write("\n".join(filter(None, metadata)) + "\n")
|
||||
|
||||
# Content
|
||||
f.write("\n**Content:**\n")
|
||||
f.write("\n")
|
||||
f.write(fragment.content)
|
||||
f.write("\n")
|
||||
|
||||
# Bibliography if exists
|
||||
if fragment.bibliography:
|
||||
f.write("\n**Bibliography:**\n")
|
||||
f.write("```bibtex\n")
|
||||
f.write(fragment.bibliography)
|
||||
f.write("\n```\n")
|
||||
|
||||
# Add separator
|
||||
if i < len(section_fragments):
|
||||
f.write("\n---\n")
|
||||
|
||||
# Add statistics
|
||||
f.write("\n## Statistics\n")
|
||||
|
||||
# Length distribution
|
||||
lengths = [len(f.content) for f in fragments]
|
||||
f.write("\n### Length Distribution\n")
|
||||
f.write(f"- Minimum: {min(lengths)} chars\n")
|
||||
f.write(f"- Maximum: {max(lengths)} chars\n")
|
||||
f.write(f"- Average: {sum(lengths) / len(lengths):.1f} chars\n")
|
||||
|
||||
# Section distribution
|
||||
f.write("\n### Section Distribution\n")
|
||||
for section, section_fragments in sections.items():
|
||||
percentage = (len(section_fragments) / len(fragments)) * 100
|
||||
f.write(f"- {section}: {len(section_fragments)} ({percentage:.1f}%)\n")
|
||||
|
||||
print(f"Fragments saved to: {file_path}")
|
||||
return file_path
|
||||
|
||||
|
||||
# 定义各种引用命令的模式
|
||||
CITATION_PATTERNS = [
|
||||
# 基本的 \cite{} 格式
|
||||
r'\\cite(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
# natbib 格式
|
||||
r'\\citep(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\citet(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\citeauthor(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\citeyear(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\citealt(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\citealp(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
# biblatex 格式
|
||||
r'\\textcite(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\parencite(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
r'\\autocite(?:\*)?(?:\[[^\]]*\])?{([^}]+)}',
|
||||
# 自定义 [cite:...] 格式
|
||||
r'\[cite:([^\]]+)\]',
|
||||
]
|
||||
|
||||
# 编译所有模式
|
||||
COMPILED_PATTERNS = [re.compile(pattern) for pattern in CITATION_PATTERNS]
|
||||
|
||||
|
||||
class ArxivSplitter:
|
||||
"""Arxiv论文智能分割器"""
|
||||
|
||||
def __init__(self,
|
||||
root_dir: str = "gpt_log/arxiv_cache",
|
||||
proxies: Optional[Dict[str, str]] = None,
|
||||
cache_ttl: int = 7 * 24 * 60 * 60):
|
||||
"""
|
||||
初始化分割器
|
||||
|
||||
Args:
|
||||
char_range: 字符数范围(最小值, 最大值)
|
||||
root_dir: 缓存根目录
|
||||
proxies: 代理设置
|
||||
cache_ttl: 缓存过期时间(秒)
|
||||
"""
|
||||
self.root_dir = Path(root_dir)
|
||||
self.root_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.proxies = proxies or {}
|
||||
self.cache_ttl = cache_ttl
|
||||
|
||||
# 动态计算最优线程数
|
||||
import multiprocessing
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
# 根据CPU核心数动态设置,但设置上限防止过度并发
|
||||
self.document_structure = DocumentStructure()
|
||||
self.document_parser = EssayStructureParser()
|
||||
|
||||
self.max_workers = min(32, cpu_count * 2)
|
||||
|
||||
# 初始化TeX处理器
|
||||
self.tex_processor = TexUtils()
|
||||
|
||||
# 配置日志
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""配置日志"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _normalize_arxiv_id(self, input_str: str) -> str:
|
||||
"""规范化ArXiv ID"""
|
||||
if 'arxiv.org/' in input_str.lower():
|
||||
# 处理URL格式
|
||||
if '/pdf/' in input_str:
|
||||
arxiv_id = input_str.split('/pdf/')[-1]
|
||||
else:
|
||||
arxiv_id = input_str.split('/abs/')[-1]
|
||||
# 移除版本号和其他后缀
|
||||
return arxiv_id.split('v')[0].strip()
|
||||
return input_str.split('v')[0].strip()
|
||||
|
||||
def _check_cache(self, paper_dir: Path) -> bool:
|
||||
"""
|
||||
检查缓存是否有效,包括文件完整性检查
|
||||
|
||||
Args:
|
||||
paper_dir: 论文目录路径
|
||||
|
||||
Returns:
|
||||
bool: 如果缓存有效返回True,否则返回False
|
||||
"""
|
||||
if not paper_dir.exists():
|
||||
return False
|
||||
|
||||
# 检查目录中是否存在必要文件
|
||||
has_tex_files = False
|
||||
has_main_tex = False
|
||||
|
||||
for file_path in paper_dir.rglob("*"):
|
||||
if file_path.suffix == '.tex':
|
||||
has_tex_files = True
|
||||
content = self.tex_processor.read_file(str(file_path))
|
||||
if content and r'\documentclass' in content:
|
||||
has_main_tex = True
|
||||
break
|
||||
|
||||
if not (has_tex_files and has_main_tex):
|
||||
return False
|
||||
|
||||
# 检查缓存时间
|
||||
cache_time = paper_dir.stat().st_mtime
|
||||
if (time.time() - cache_time) < self.cache_ttl:
|
||||
self.logger.info(f"Using valid cache for {paper_dir.name}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def download_paper(self, arxiv_id: str, paper_dir: Path) -> bool:
|
||||
"""
|
||||
异步下载论文,包含重试机制和临时文件处理
|
||||
|
||||
Args:
|
||||
arxiv_id: ArXiv论文ID
|
||||
paper_dir: 目标目录路径
|
||||
|
||||
Returns:
|
||||
bool: 下载成功返回True,否则返回False
|
||||
"""
|
||||
from crazy_functions.rag_fns.arxiv_fns.arxiv_downloader import ArxivDownloader
|
||||
temp_tar_path = paper_dir / f"{arxiv_id}_temp.tar.gz"
|
||||
final_tar_path = paper_dir / f"{arxiv_id}.tar.gz"
|
||||
|
||||
# 确保目录存在
|
||||
paper_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 尝试使用 ArxivDownloader 下载
|
||||
try:
|
||||
downloader = ArxivDownloader(root_dir=str(paper_dir), proxies=self.proxies)
|
||||
downloaded_dir = downloader.download_paper(arxiv_id)
|
||||
if downloaded_dir:
|
||||
self.logger.info(f"Successfully downloaded using ArxivDownloader to {downloaded_dir}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.warning(f"ArxivDownloader failed: {str(e)}. Falling back to direct download.")
|
||||
|
||||
# 如果 ArxivDownloader 失败,使用原有的下载方式作为备选
|
||||
urls = [
|
||||
f"https://arxiv.org/src/{arxiv_id}",
|
||||
f"https://arxiv.org/e-print/{arxiv_id}"
|
||||
]
|
||||
|
||||
max_retries = 3
|
||||
retry_delay = 1 # 初始重试延迟(秒)
|
||||
|
||||
for url in urls:
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
self.logger.info(f"Downloading from {url} (attempt {attempt + 1}/{max_retries})")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, proxy=self.proxies.get('http')) as response:
|
||||
if response.status == 200:
|
||||
content = await response.read()
|
||||
|
||||
# 写入临时文件
|
||||
temp_tar_path.write_bytes(content)
|
||||
|
||||
try:
|
||||
# 验证tar文件完整性并解压
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, self._process_tar_file, temp_tar_path, paper_dir)
|
||||
|
||||
# 下载成功后移动临时文件到最终位置
|
||||
temp_tar_path.rename(final_tar_path)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Invalid tar file: {str(e)}")
|
||||
if temp_tar_path.exists():
|
||||
temp_tar_path.unlink()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Download attempt {attempt + 1} failed from {url}: {str(e)}")
|
||||
await asyncio.sleep(retry_delay * (attempt + 1)) # 指数退避
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def _process_tar_file(self, tar_path: Path, extract_path: Path):
|
||||
"""处理tar文件的同步操作"""
|
||||
with tarfile.open(tar_path, 'r:gz') as tar:
|
||||
tar.testall() # 验证文件完整性
|
||||
tar.extractall(path=extract_path) # 解压文件
|
||||
|
||||
def process_references(self, doc_structure: DocumentStructure, ref_bib: str) -> DocumentStructure:
|
||||
"""
|
||||
Process citations in document structure and add referenced literature for each section
|
||||
|
||||
Args:
|
||||
doc_structure: DocumentStructure object
|
||||
ref_bib: String containing references separated by newlines
|
||||
|
||||
Returns:
|
||||
Updated DocumentStructure object
|
||||
"""
|
||||
try:
|
||||
# Create a copy to avoid modifying the original
|
||||
doc = deepcopy(doc_structure)
|
||||
|
||||
# Parse references into a mapping
|
||||
ref_map = self._parse_references(ref_bib)
|
||||
if not ref_map:
|
||||
self.logger.warning("No valid references found in ref_bib")
|
||||
return doc
|
||||
|
||||
# Process all sections recursively
|
||||
self._process_section_references(doc.toc, ref_map)
|
||||
|
||||
return doc
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing references: {str(e)}")
|
||||
return doc_structure # Return original if processing fails
|
||||
|
||||
def _process_section_references(self, sections: List[Section], ref_map: Dict[str, str]) -> None:
|
||||
"""
|
||||
Recursively process sections to add references
|
||||
|
||||
Args:
|
||||
sections: List of Section objects
|
||||
ref_map: Mapping of citation keys to full references
|
||||
"""
|
||||
for section in sections:
|
||||
if section.content:
|
||||
# Find citations in current section
|
||||
cited_refs = self.find_citations(section.content)
|
||||
|
||||
if cited_refs:
|
||||
# Get full references for citations
|
||||
full_refs = []
|
||||
for ref_key in cited_refs:
|
||||
ref_text = ref_map.get(ref_key)
|
||||
if ref_text:
|
||||
full_refs.append(ref_text)
|
||||
else:
|
||||
self.logger.warning(f"Reference not found for citation key: {ref_key}")
|
||||
|
||||
# Add references to section content
|
||||
if full_refs:
|
||||
section.bibliography = "\n\n".join(full_refs)
|
||||
|
||||
# Process subsections recursively
|
||||
if section.subsections:
|
||||
self._process_section_references(section.subsections, ref_map)
|
||||
|
||||
def _parse_references(self, ref_bib: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parse reference string into a mapping of citation keys to full references
|
||||
|
||||
Args:
|
||||
ref_bib: Reference string with references separated by newlines
|
||||
|
||||
Returns:
|
||||
Dict mapping citation keys to full reference text
|
||||
"""
|
||||
ref_map = {}
|
||||
current_ref = []
|
||||
current_key = None
|
||||
|
||||
try:
|
||||
for line in ref_bib.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# New reference entry
|
||||
if line.startswith('@'):
|
||||
# Save previous reference if exists
|
||||
if current_key and current_ref:
|
||||
ref_map[current_key] = '\n'.join(current_ref)
|
||||
current_ref = []
|
||||
|
||||
# Extract key from new reference
|
||||
key_match = re.search(r'{(.*?),', line)
|
||||
if key_match:
|
||||
current_key = key_match.group(1)
|
||||
current_ref.append(line)
|
||||
else:
|
||||
if current_ref is not None:
|
||||
current_ref.append(line)
|
||||
|
||||
# Save last reference
|
||||
if current_key and current_ref:
|
||||
ref_map[current_key] = '\n'.join(current_ref)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing references: {str(e)}")
|
||||
|
||||
return ref_map
|
||||
|
||||
# 编译一次正则表达式以提高效率
|
||||
|
||||
@staticmethod
|
||||
def _clean_citation_key(key: str) -> str:
|
||||
"""Clean individual citation key."""
|
||||
return key.strip().strip(',').strip()
|
||||
|
||||
def _extract_keys_from_group(self, keys_str: str) -> Set[str]:
|
||||
"""Extract and clean individual citation keys from a group."""
|
||||
try:
|
||||
# 分割多个引用键(支持逗号和分号分隔)
|
||||
separators = '[,;]'
|
||||
keys = re.split(separators, keys_str)
|
||||
# 清理并过滤空键
|
||||
return {self._clean_citation_key(k) for k in keys if self._clean_citation_key(k)}
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error processing citation group '{keys_str}': {e}")
|
||||
return set()
|
||||
|
||||
def find_citations(self, content: str) -> Set[str]:
|
||||
"""
|
||||
Find citation keys in text content in various formats.
|
||||
|
||||
Args:
|
||||
content: Text content to search for citations
|
||||
|
||||
Returns:
|
||||
Set of unique citation keys
|
||||
|
||||
Examples:
|
||||
Supported formats include:
|
||||
- \cite{key1,key2}
|
||||
- \cite[p. 1]{key}
|
||||
- \citep{key}
|
||||
- \citet{key}
|
||||
- [cite:key1, key2]
|
||||
- And many other variants
|
||||
"""
|
||||
citations = set()
|
||||
|
||||
if not content:
|
||||
return citations
|
||||
|
||||
try:
|
||||
# 对每个编译好的模式进行搜索
|
||||
for pattern in COMPILED_PATTERNS:
|
||||
matches = pattern.finditer(content)
|
||||
for match in matches:
|
||||
# 获取捕获组中的引用键
|
||||
keys_str = match.group(1)
|
||||
if keys_str:
|
||||
# 提取并添加所有引用键
|
||||
new_keys = self._extract_keys_from_group(keys_str)
|
||||
citations.update(new_keys)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finding citations: {str(e)}")
|
||||
|
||||
# 移除明显无效的键
|
||||
citations = {key for key in citations
|
||||
if key and not key.startswith(('\\', '{', '}', '[', ']'))}
|
||||
|
||||
return citations
|
||||
|
||||
def get_citation_contexts(self, content: str, context_chars: int = 100) -> dict:
|
||||
"""
|
||||
Find citations and their surrounding context.
|
||||
|
||||
Args:
|
||||
content: Text content to search for citations
|
||||
context_chars: Number of characters of context to include before/after
|
||||
|
||||
Returns:
|
||||
Dict mapping citation keys to lists of context strings
|
||||
"""
|
||||
contexts = {}
|
||||
|
||||
if not content:
|
||||
return contexts
|
||||
|
||||
try:
|
||||
for pattern in COMPILED_PATTERNS:
|
||||
matches = pattern.finditer(content)
|
||||
for match in matches:
|
||||
# 获取匹配的位置
|
||||
start = max(0, match.start() - context_chars)
|
||||
end = min(len(content), match.end() + context_chars)
|
||||
|
||||
# 获取上下文
|
||||
context = content[start:end]
|
||||
|
||||
# 获取并处理引用键
|
||||
keys_str = match.group(1)
|
||||
keys = self._extract_keys_from_group(keys_str)
|
||||
|
||||
# 为每个键添加上下文
|
||||
for key in keys:
|
||||
if key not in contexts:
|
||||
contexts[key] = []
|
||||
contexts[key].append(context)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finding citation contexts: {str(e)}")
|
||||
|
||||
return contexts
|
||||
|
||||
async def process(self, arxiv_id_or_url: str) -> List[SectionFragment]:
|
||||
"""
|
||||
Process ArXiv paper and convert to list of SectionFragments.
|
||||
Each fragment represents the smallest section unit.
|
||||
|
||||
Args:
|
||||
arxiv_id_or_url: ArXiv paper ID or URL
|
||||
|
||||
Returns:
|
||||
List[SectionFragment]: List of processed paper fragments
|
||||
"""
|
||||
try:
|
||||
arxiv_id = self._normalize_arxiv_id(arxiv_id_or_url)
|
||||
paper_dir = self.root_dir / arxiv_id
|
||||
|
||||
# Check if paper directory exists, if not, try to download
|
||||
if not paper_dir.exists():
|
||||
self.logger.info(f"Downloading paper {arxiv_id}")
|
||||
await self.download_paper(arxiv_id, paper_dir)
|
||||
|
||||
# Find main TeX file
|
||||
main_tex = self.tex_processor.find_main_tex_file(str(paper_dir))
|
||||
if not main_tex:
|
||||
raise RuntimeError(f"No main TeX file found in {paper_dir}")
|
||||
|
||||
# 读取主 TeX 文件内容
|
||||
main_tex_content = read_tex_file(main_tex)
|
||||
|
||||
# Get all related TeX files and references
|
||||
tex_files = self.tex_processor.resolve_includes(main_tex)
|
||||
ref_bib = self.tex_processor.resolve_references(main_tex, paper_dir)
|
||||
|
||||
if not tex_files:
|
||||
raise RuntimeError(f"No valid TeX files found for {arxiv_id}")
|
||||
|
||||
# Reset document structure for new processing
|
||||
self.document_structure = DocumentStructure()
|
||||
|
||||
# 提取作者信息
|
||||
author_extractor = LatexAuthorExtractor()
|
||||
authors = author_extractor.extract_authors(main_tex_content)
|
||||
self.document_structure.authors = authors # 保存到文档结构中
|
||||
|
||||
# Process each TeX file
|
||||
for file_path in tex_files:
|
||||
self.logger.info(f"Processing TeX file: {file_path}")
|
||||
tex_content = read_tex_file(file_path)
|
||||
if tex_content:
|
||||
additional_doc = self.document_parser.parse(tex_content)
|
||||
self.document_structure = self.document_structure.merge(additional_doc)
|
||||
|
||||
# Process references if available
|
||||
if ref_bib:
|
||||
self.document_structure = self.process_references(self.document_structure, ref_bib)
|
||||
self.logger.info("Successfully processed references")
|
||||
else:
|
||||
self.logger.info("No references found to process")
|
||||
|
||||
# Generate table of contents once
|
||||
section_tree = self.document_structure.generate_toc_tree()
|
||||
|
||||
# Convert DocumentStructure to SectionFragments
|
||||
fragments = self._convert_to_fragments(
|
||||
doc_structure=self.document_structure,
|
||||
arxiv_id=arxiv_id,
|
||||
section_tree=section_tree
|
||||
)
|
||||
|
||||
return fragments
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to process {arxiv_id_or_url}: {str(e)}")
|
||||
raise
|
||||
|
||||
def _convert_to_fragments(self,
|
||||
doc_structure: DocumentStructure,
|
||||
arxiv_id: str,
|
||||
section_tree: str) -> List[SectionFragment]:
|
||||
"""
|
||||
Convert DocumentStructure to list of SectionFragments.
|
||||
Creates a fragment for each leaf section in the document hierarchy.
|
||||
|
||||
Args:
|
||||
doc_structure: Source DocumentStructure
|
||||
arxiv_id: ArXiv paper ID
|
||||
section_tree: Pre-generated table of contents tree
|
||||
|
||||
Returns:
|
||||
List[SectionFragment]: List of paper fragments
|
||||
"""
|
||||
fragments = []
|
||||
|
||||
# Create a base template for all fragments to avoid repetitive assignments
|
||||
base_fragment_template = {
|
||||
'title': doc_structure.title,
|
||||
'authors': doc_structure.authors,
|
||||
'abstract': doc_structure.abstract,
|
||||
'catalogs': section_tree,
|
||||
'arxiv_id': arxiv_id
|
||||
}
|
||||
|
||||
def get_leaf_sections(section: Section, path: List[str] = None) -> None:
|
||||
"""
|
||||
Recursively find all leaf sections and create fragments.
|
||||
A leaf section is one that has content but no subsections, or has neither.
|
||||
|
||||
Args:
|
||||
section: Current section being processed
|
||||
path: List of section titles forming the path to current section
|
||||
"""
|
||||
if path is None:
|
||||
path = []
|
||||
|
||||
current_path = path + [section.title]
|
||||
|
||||
if not section.subsections:
|
||||
# This is a leaf section, create a fragment if it has content
|
||||
if section.content or section.bibliography:
|
||||
fragment = SectionFragment(
|
||||
**base_fragment_template,
|
||||
current_section="/".join(current_path),
|
||||
content=self._clean_content(section.content),
|
||||
bibliography=section.bibliography
|
||||
)
|
||||
if self._validate_fragment(fragment):
|
||||
fragments.append(fragment)
|
||||
else:
|
||||
# Process each subsection
|
||||
for subsection in section.subsections:
|
||||
get_leaf_sections(subsection, current_path)
|
||||
|
||||
# Process all top-level sections
|
||||
for section in doc_structure.toc:
|
||||
get_leaf_sections(section)
|
||||
|
||||
# Add a fragment for the abstract if it exists
|
||||
if doc_structure.abstract:
|
||||
abstract_fragment = SectionFragment(
|
||||
**base_fragment_template,
|
||||
current_section="Abstract",
|
||||
content=self._clean_content(doc_structure.abstract)
|
||||
)
|
||||
if self._validate_fragment(abstract_fragment):
|
||||
fragments.insert(0, abstract_fragment)
|
||||
|
||||
self.logger.info(f"Created {len(fragments)} fragments")
|
||||
return fragments
|
||||
|
||||
def _validate_fragment(self, fragment: SectionFragment) -> bool:
|
||||
"""
|
||||
Validate if the fragment has all required fields with meaningful content.
|
||||
|
||||
Args:
|
||||
fragment: SectionFragment to validate
|
||||
|
||||
Returns:
|
||||
bool: True if fragment is valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
return all([
|
||||
fragment.title.strip(),
|
||||
fragment.catalogs.strip(),
|
||||
fragment.current_section.strip(),
|
||||
fragment.content.strip() or fragment.bibliography.strip()
|
||||
])
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def _clean_content(self, content: str) -> str:
|
||||
"""
|
||||
Clean and normalize content text.
|
||||
|
||||
Args:
|
||||
content: Raw content text
|
||||
|
||||
Returns:
|
||||
str: Cleaned content text
|
||||
"""
|
||||
if not content:
|
||||
return ""
|
||||
|
||||
# Remove excessive whitespace
|
||||
content = re.sub(r'\s+', ' ', content)
|
||||
|
||||
# Remove remaining LaTeX artifacts
|
||||
content = re.sub(r'\\item\s*', '• ', content) # Convert \item to bullet points
|
||||
content = re.sub(r'\\[a-zA-Z]+\{([^}]*)\}', r'\1', content) # Remove simple LaTeX commands
|
||||
|
||||
# Clean special characters
|
||||
content = content.replace('\\\\', '\n') # Convert LaTeX newlines to actual newlines
|
||||
content = re.sub(r'\s*\n\s*', '\n', content) # Clean up newlines
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
||||
def process_arxiv_sync(splitter: ArxivSplitter, arxiv_id: str) -> tuple[List[SectionFragment], str, List[Path]]:
|
||||
"""
|
||||
同步处理 ArXiv 文档并返回分割后的片段
|
||||
|
||||
Args:
|
||||
splitter: ArxivSplitter 实例
|
||||
arxiv_id: ArXiv 文档ID
|
||||
|
||||
Returns:
|
||||
list: 分割后的文档片段列表
|
||||
"""
|
||||
try:
|
||||
from crazy_functions.doc_fns.tex_html_formatter import PaperHtmlFormatter
|
||||
# 创建一个异步函数来执行异步操作
|
||||
async def _process():
|
||||
return await splitter.process(arxiv_id)
|
||||
|
||||
# 使用 asyncio.run() 运行异步函数
|
||||
output_files=[]
|
||||
fragments = asyncio.run(_process())
|
||||
file_save_path = splitter.root_dir / "arxiv_fragments"
|
||||
# 保存片段到文件
|
||||
try:
|
||||
md_output_dir = save_fragments_to_file(
|
||||
fragments,
|
||||
output_dir = file_save_path
|
||||
)
|
||||
output_files.append(md_output_dir)
|
||||
except:
|
||||
pass
|
||||
# 创建论文格式化器
|
||||
formatter = PaperContentFormatter()
|
||||
|
||||
# 准备元数据
|
||||
# 创建格式化选项
|
||||
|
||||
metadata = PaperMetadata(
|
||||
title=fragments[0].title,
|
||||
authors=fragments[0].authors,
|
||||
abstract=fragments[0].abstract,
|
||||
catalogs=fragments[0].catalogs,
|
||||
arxiv_id=fragments[0].arxiv_id
|
||||
)
|
||||
|
||||
# 格式化内容
|
||||
formatted_content = formatter.format(fragments, metadata)
|
||||
|
||||
try:
|
||||
html_formatter = PaperHtmlFormatter(fragments, file_save_path)
|
||||
html_output_dir = html_formatter.save_html()
|
||||
output_files.append(html_output_dir)
|
||||
except:
|
||||
pass
|
||||
return fragments, formatted_content, output_files
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Processing failed for {arxiv_id}: {str(e)}")
|
||||
raise
|
||||
def test_arxiv_splitter():
|
||||
"""测试ArXiv分割器的功能"""
|
||||
|
||||
# 测试配置
|
||||
test_cases = [
|
||||
{
|
||||
"arxiv_id": "2411.03663",
|
||||
"expected_title": "Large Language Models and Simple Scripts",
|
||||
"min_fragments": 10,
|
||||
},
|
||||
# {
|
||||
# "arxiv_id": "1805.10988",
|
||||
# "expected_title": "RAG vs Fine-tuning",
|
||||
# "min_fragments": 15,
|
||||
# }
|
||||
]
|
||||
|
||||
# 创建分割器实例
|
||||
splitter = ArxivSplitter(
|
||||
root_dir="private_upload/default_user"
|
||||
)
|
||||
|
||||
for case in test_cases:
|
||||
print(f"\nTesting paper: {case['arxiv_id']}")
|
||||
try:
|
||||
# fragments = await splitter.process(case['arxiv_id'])
|
||||
fragments, formatted_content, output_dir = process_arxiv_sync(splitter, case['arxiv_id'])
|
||||
# 保存fragments
|
||||
for fragment in fragments:
|
||||
# 长度检查
|
||||
print((fragment.content))
|
||||
print(len(fragment.content))
|
||||
# 类型检查
|
||||
print(output_dir)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Test failed for {case['arxiv_id']}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_arxiv_splitter()
|
||||
@@ -1,177 +0,0 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LatexAuthorExtractor:
|
||||
def __init__(self):
|
||||
# Patterns for matching author blocks with balanced braces
|
||||
self.author_block_patterns = [
|
||||
# Standard LaTeX patterns with optional arguments
|
||||
r'\\author(?:\s*\[[^\]]*\])?\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\(?:title)?author[s]?\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\name[s]?\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\Author[s]?\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\AUTHOR[S]?\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
# Conference and journal specific patterns
|
||||
r'\\addauthor\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\IEEEauthor\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\speaker\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\authorrunning\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
# Academic publisher specific patterns
|
||||
r'\\alignauthor\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\spauthor\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
r'\\authors\s*\{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)\}',
|
||||
]
|
||||
|
||||
# Cleaning patterns for LaTeX commands and formatting
|
||||
self.cleaning_patterns = [
|
||||
# Text formatting commands - preserve content
|
||||
(r'\\textbf\{([^}]+)\}', r'\1'),
|
||||
(r'\\textit\{([^}]+)\}', r'\1'),
|
||||
(r'\\emph\{([^}]+)\}', r'\1'),
|
||||
(r'\\texttt\{([^}]+)\}', r'\1'),
|
||||
(r'\\textrm\{([^}]+)\}', r'\1'),
|
||||
(r'\\text\{([^}]+)\}', r'\1'),
|
||||
|
||||
# Affiliation and footnote markers
|
||||
(r'\$\^{[^}]+}\$', ''),
|
||||
(r'\^{[^}]+}', ''),
|
||||
(r'\\thanks\{[^}]+\}', ''),
|
||||
(r'\\footnote\{[^}]+\}', ''),
|
||||
|
||||
# Email and contact formatting
|
||||
(r'\\email\{([^}]+)\}', r'\1'),
|
||||
(r'\\href\{[^}]+\}\{([^}]+)\}', r'\1'),
|
||||
|
||||
# Institution formatting
|
||||
(r'\\inst\{[^}]+\}', ''),
|
||||
(r'\\affil\{[^}]+\}', ''),
|
||||
|
||||
# Special characters and symbols
|
||||
(r'\\&', '&'),
|
||||
(r'\\\\\s*', ' '),
|
||||
(r'\\,', ' '),
|
||||
(r'\\;', ' '),
|
||||
(r'\\quad', ' '),
|
||||
(r'\\qquad', ' '),
|
||||
|
||||
# Math mode content
|
||||
(r'\$[^$]+\$', ''),
|
||||
|
||||
# Common symbols
|
||||
(r'\\dagger', '†'),
|
||||
(r'\\ddagger', '‡'),
|
||||
(r'\\ast', '*'),
|
||||
(r'\\star', '★'),
|
||||
|
||||
# Remove remaining LaTeX commands
|
||||
(r'\\[a-zA-Z]+', ''),
|
||||
|
||||
# Clean up remaining special characters
|
||||
(r'[\\{}]', '')
|
||||
]
|
||||
|
||||
def extract_author_block(self, text: str) -> Optional[str]:
|
||||
"""
|
||||
Extract the complete author block from LaTeX text.
|
||||
|
||||
Args:
|
||||
text (str): Input LaTeX text
|
||||
|
||||
Returns:
|
||||
Optional[str]: Extracted author block or None if not found
|
||||
"""
|
||||
try:
|
||||
if not text:
|
||||
return None
|
||||
|
||||
for pattern in self.author_block_patterns:
|
||||
match = re.search(pattern, text, re.DOTALL | re.MULTILINE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
|
||||
except (AttributeError, IndexError) as e:
|
||||
print(f"Error extracting author block: {e}")
|
||||
return None
|
||||
|
||||
def clean_tex_commands(self, text: str) -> str:
|
||||
"""
|
||||
Remove LaTeX commands and formatting from text while preserving content.
|
||||
|
||||
Args:
|
||||
text (str): Text containing LaTeX commands
|
||||
|
||||
Returns:
|
||||
str: Cleaned text with commands removed
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
cleaned_text = text
|
||||
|
||||
# Apply cleaning patterns
|
||||
for pattern, replacement in self.cleaning_patterns:
|
||||
cleaned_text = re.sub(pattern, replacement, cleaned_text)
|
||||
|
||||
# Clean up whitespace
|
||||
cleaned_text = re.sub(r'\s+', ' ', cleaned_text)
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
return cleaned_text
|
||||
|
||||
def extract_authors(self, text: str) -> Optional[str]:
|
||||
"""
|
||||
Extract and clean author information from LaTeX text.
|
||||
|
||||
Args:
|
||||
text (str): Input LaTeX text
|
||||
|
||||
Returns:
|
||||
Optional[str]: Cleaned author information or None if extraction fails
|
||||
"""
|
||||
try:
|
||||
if not text:
|
||||
return None
|
||||
|
||||
# Extract author block
|
||||
author_block = self.extract_author_block(text)
|
||||
if not author_block:
|
||||
return None
|
||||
|
||||
# Clean LaTeX commands
|
||||
cleaned_authors = self.clean_tex_commands(author_block)
|
||||
return cleaned_authors or None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing text: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def test_author_extractor():
|
||||
"""Test the LatexAuthorExtractor with sample inputs."""
|
||||
test_cases = [
|
||||
# Basic test case
|
||||
(r"\author{John Doe}", "John Doe"),
|
||||
|
||||
# Test with multiple authors
|
||||
(r"\author{Alice Smith \and Bob Jones}", "Alice Smith and Bob Jones"),
|
||||
|
||||
# Test with affiliations
|
||||
(r"\author[1]{John Smith}\affil[1]{University}", "John Smith"),
|
||||
|
||||
]
|
||||
|
||||
extractor = LatexAuthorExtractor()
|
||||
|
||||
for i, (input_tex, expected) in enumerate(test_cases, 1):
|
||||
result = extractor.extract_authors(input_tex)
|
||||
print(f"\nTest case {i}:")
|
||||
print(f"Input: {input_tex[:50]}...")
|
||||
print(f"Expected: {expected[:50]}...")
|
||||
print(f"Got: {result[:50]}...")
|
||||
print(f"Pass: {bool(result and result.strip() == expected.strip())}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_author_extractor()
|
||||
@@ -1,290 +0,0 @@
|
||||
"""
|
||||
LaTeX Document Parser
|
||||
|
||||
This module provides functionality for parsing and extracting structured information from LaTeX documents,
|
||||
including metadata, document structure, and content. It uses modular design and clean architecture principles.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict
|
||||
|
||||
from crazy_functions.rag_fns.arxiv_fns.latex_cleaner import clean_latex_commands
|
||||
from crazy_functions.rag_fns.arxiv_fns.section_extractor import Section, EnhancedSectionExtractor
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_tex_file(file_path):
|
||||
encodings = ['utf-8', 'latin1', 'gbk', 'gb2312', 'ascii']
|
||||
for encoding in encodings:
|
||||
try:
|
||||
with open(file_path, 'r', encoding=encoding) as f:
|
||||
return f.read()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentStructure:
|
||||
title: str = ''
|
||||
authors: str = ''
|
||||
abstract: str = ''
|
||||
toc: List[Section] = field(default_factory=list)
|
||||
metadata: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def merge(self, other: 'DocumentStructure', strategy: str = 'smart') -> 'DocumentStructure':
|
||||
"""
|
||||
Merge this document structure with another one.
|
||||
|
||||
Args:
|
||||
other: Another DocumentStructure to merge with
|
||||
strategy: Merge strategy - 'smart' (default) or 'append'
|
||||
'smart' - Intelligently merge sections with same titles
|
||||
'append' - Simply append sections from other document
|
||||
"""
|
||||
merged = deepcopy(self)
|
||||
|
||||
# Merge title if needed
|
||||
if not merged.title and other.title:
|
||||
merged.title = other.title
|
||||
|
||||
# Merge abstract
|
||||
merged.abstract = self._merge_abstract(merged.abstract, other.abstract)
|
||||
|
||||
# Merge metadata
|
||||
merged.metadata.update(other.metadata)
|
||||
|
||||
if strategy == 'append':
|
||||
merged.toc.extend(deepcopy(other.toc))
|
||||
else: # smart merge
|
||||
# Create sections lookup for efficient merging
|
||||
sections_map = {s.title: s for s in merged.toc}
|
||||
|
||||
for other_section in other.toc:
|
||||
if other_section.title in sections_map:
|
||||
# Merge existing section
|
||||
idx = next(i for i, s in enumerate(merged.toc)
|
||||
if s.title == other_section.title)
|
||||
merged.toc[idx] = merged.toc[idx].merge(other_section)
|
||||
else:
|
||||
# Add new section
|
||||
merged.toc.append(deepcopy(other_section))
|
||||
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _merge_abstract(abstract1: str, abstract2: str) -> str:
|
||||
"""Merge abstracts intelligently."""
|
||||
if not abstract1:
|
||||
return abstract2
|
||||
if not abstract2:
|
||||
return abstract1
|
||||
# Combine non-empty abstracts with a separator
|
||||
return f"{abstract1}\n\n{abstract2}"
|
||||
|
||||
def generate_toc_tree(self, indent_char: str = " ", abstract_preview_length: int = 0) -> str:
|
||||
"""
|
||||
Generate a tree-like string representation of the table of contents including abstract.
|
||||
|
||||
Args:
|
||||
indent_char: Character(s) used for indentation. Default is two spaces.
|
||||
abstract_preview_length: Maximum length of abstract preview. Default is 200 characters.
|
||||
|
||||
Returns:
|
||||
str: A formatted string showing the hierarchical document structure with abstract
|
||||
"""
|
||||
|
||||
def _format_section(section: Section, level: int = 0) -> str:
|
||||
# Create the current section line with proper indentation
|
||||
current_line = f"{indent_char * level}{'•' if level > 0 else '○'} {section.title}\n"
|
||||
|
||||
# Recursively process subsections
|
||||
subsections = ""
|
||||
if section.subsections:
|
||||
subsections = "".join(_format_section(subsec, level + 1)
|
||||
for subsec in section.subsections)
|
||||
|
||||
return current_line + subsections
|
||||
|
||||
result = []
|
||||
|
||||
# Add document title if it exists
|
||||
if self.title:
|
||||
result.append(f"《{self.title}》\n")
|
||||
|
||||
# Add abstract if it exists
|
||||
if self.abstract:
|
||||
result.append("\n□ Abstract:")
|
||||
# Format abstract content with word wrap
|
||||
abstract_preview = self.abstract[:abstract_preview_length]
|
||||
if len(self.abstract) > abstract_preview_length:
|
||||
abstract_preview += "..."
|
||||
|
||||
# Split abstract into lines and indent them
|
||||
wrapped_lines = []
|
||||
current_line = ""
|
||||
for word in abstract_preview.split():
|
||||
if len(current_line) + len(word) + 1 <= 80: # 80 characters per line
|
||||
current_line = (current_line + " " + word).strip()
|
||||
else:
|
||||
wrapped_lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
wrapped_lines.append(current_line)
|
||||
|
||||
# Add formatted abstract lines
|
||||
for line in wrapped_lines:
|
||||
result.append(f"\n{indent_char}{line}")
|
||||
result.append("\n") # Add extra newline after abstract
|
||||
|
||||
# Add table of contents header if there are sections
|
||||
if self.toc:
|
||||
result.append("\n◈ Table of Contents:\n")
|
||||
|
||||
# Add all top-level sections and their subsections
|
||||
result.extend(_format_section(section, 0) for section in self.toc)
|
||||
|
||||
return "".join(result)
|
||||
|
||||
|
||||
class BaseExtractor(ABC):
|
||||
"""Base class for LaTeX content extractors."""
|
||||
|
||||
@abstractmethod
|
||||
def extract(self, content: str) -> str:
|
||||
"""Extract specific content from LaTeX document."""
|
||||
pass
|
||||
|
||||
|
||||
class TitleExtractor(BaseExtractor):
|
||||
"""Extracts title from LaTeX document."""
|
||||
|
||||
PATTERNS = [
|
||||
r'\\title{(.+?)}',
|
||||
r'\\title\[.*?\]{(.+?)}',
|
||||
r'\\Title{(.+?)}',
|
||||
r'\\TITLE{(.+?)}',
|
||||
r'\\begin{document}\s*\\section[*]?{(.+?)}',
|
||||
r'\\maketitle\s*\\section[*]?{(.+?)}',
|
||||
r'\\chapter[*]?{(.+?)}'
|
||||
]
|
||||
|
||||
def extract(self, content: str) -> str:
|
||||
"""Extract title using defined patterns."""
|
||||
for pattern in self.PATTERNS:
|
||||
matches = list(re.finditer(pattern, content, re.IGNORECASE | re.DOTALL))
|
||||
for match in matches:
|
||||
title = match.group(1).strip()
|
||||
if title:
|
||||
return clean_latex_commands(title)
|
||||
return ''
|
||||
|
||||
|
||||
class AbstractExtractor(BaseExtractor):
|
||||
"""Extracts abstract from LaTeX document."""
|
||||
|
||||
PATTERNS = [
|
||||
r'\\begin{abstract}(.*?)\\end{abstract}',
|
||||
r'\\abstract{(.*?)}',
|
||||
r'\\ABSTRACT{(.*?)}',
|
||||
r'\\Abstract{(.*?)}',
|
||||
r'\\begin{Abstract}(.*?)\\end{Abstract}',
|
||||
r'\\section[*]?{(?:Abstract|ABSTRACT)}\s*(.*?)(?:\\section|\Z)',
|
||||
r'\\chapter[*]?{(?:Abstract|ABSTRACT)}\s*(.*?)(?:\\chapter|\Z)'
|
||||
]
|
||||
|
||||
def extract(self, content: str) -> str:
|
||||
"""Extract abstract using defined patterns."""
|
||||
for pattern in self.PATTERNS:
|
||||
matches = list(re.finditer(pattern, content, re.IGNORECASE | re.DOTALL))
|
||||
for match in matches:
|
||||
abstract = match.group(1).strip()
|
||||
if abstract:
|
||||
return clean_latex_commands(abstract)
|
||||
return ''
|
||||
|
||||
|
||||
class EssayStructureParser:
|
||||
"""Main class for parsing LaTeX documents."""
|
||||
|
||||
def __init__(self):
|
||||
self.title_extractor = TitleExtractor()
|
||||
self.abstract_extractor = AbstractExtractor()
|
||||
self.section_extractor = EnhancedSectionExtractor() # Using the enhanced extractor
|
||||
|
||||
def parse(self, content: str) -> DocumentStructure:
|
||||
"""Parse LaTeX document and extract structured information."""
|
||||
try:
|
||||
content = self._preprocess_content(content)
|
||||
|
||||
return DocumentStructure(
|
||||
title=self.title_extractor.extract(content),
|
||||
abstract=self.abstract_extractor.extract(content),
|
||||
toc=self.section_extractor.extract(content)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing LaTeX document: {str(e)}")
|
||||
raise
|
||||
|
||||
def _preprocess_content(self, content: str) -> str:
|
||||
"""Preprocess LaTeX content for parsing."""
|
||||
# Remove comments
|
||||
content = re.sub(r'(?<!\\)%.*$', '', content, flags=re.MULTILINE)
|
||||
return content
|
||||
|
||||
|
||||
def pretty_print_structure(doc: DocumentStructure, max_content_length: int = 100):
|
||||
"""Print document structure in a readable format."""
|
||||
print(f"Title: {doc.title}\n")
|
||||
print(f"Abstract: {doc.abstract}\n")
|
||||
print("Table of Contents:")
|
||||
|
||||
def print_section(section: Section, indent: int = 0):
|
||||
print(" " * indent + f"- {section.title}")
|
||||
if section.content:
|
||||
preview = section.content[:max_content_length]
|
||||
if len(section.content) > max_content_length:
|
||||
preview += "..."
|
||||
print(" " * (indent + 1) + f"Content: {preview}")
|
||||
for subsection in section.subsections:
|
||||
print_section(subsection, indent + 1)
|
||||
|
||||
for section in doc.toc:
|
||||
print_section(section)
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Test with a file
|
||||
file_path = 'test_cache/2411.03663/neurips_2024.tex'
|
||||
main_tex = read_tex_file(file_path)
|
||||
|
||||
# Parse main file
|
||||
parser = EssayStructureParser()
|
||||
main_doc = parser.parse(main_tex)
|
||||
|
||||
# Merge other documents
|
||||
file_path_list = [
|
||||
"test_cache/2411.03663/1_intro.tex",
|
||||
"test_cache/2411.03663/0_abstract.tex",
|
||||
"test_cache/2411.03663/2_pre.tex",
|
||||
"test_cache/2411.03663/3_method.tex",
|
||||
"test_cache/2411.03663/4_experiment.tex",
|
||||
"test_cache/2411.03663/5_related_work.tex",
|
||||
"test_cache/2411.03663/6_conclu.tex",
|
||||
"test_cache/2411.03663/reference.bib"
|
||||
]
|
||||
for file_path in file_path_list:
|
||||
tex_content = read_tex_file(file_path)
|
||||
additional_doc = parser.parse(tex_content)
|
||||
main_doc = main_doc.merge(additional_doc)
|
||||
|
||||
tree = main_doc.generate_toc_tree()
|
||||
pretty_print_structure(main_doc)
|
||||
@@ -1,329 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from typing import Set, Dict, Pattern, Optional, List, Tuple
|
||||
|
||||
|
||||
class EnvType(Enum):
|
||||
"""Environment classification types."""
|
||||
PRESERVE = "preserve" # Preserve complete environment including commands
|
||||
REMOVE = "remove" # Remove environment completely
|
||||
EXTRACT = "extract" # Extract and clean content
|
||||
|
||||
|
||||
@dataclass
|
||||
class LatexConfig:
|
||||
"""Configuration for LaTeX processing."""
|
||||
preserve_envs: Set[str] = field(default_factory=lambda: {
|
||||
# Math environments - preserve complete content
|
||||
'equation', 'equation*', 'align', 'align*', 'displaymath',
|
||||
'math', 'eqnarray', 'eqnarray*', 'gather', 'gather*',
|
||||
'multline', 'multline*', 'flalign', 'flalign*',
|
||||
'alignat', 'alignat*', 'cases', 'split', 'aligned',
|
||||
# Tables and figures - preserve structure and content
|
||||
'table', 'table*', 'tabular', 'tabularx', 'array', 'matrix',
|
||||
'figure', 'figure*', 'subfigure', 'wrapfigure',
|
||||
'minipage', 'tabbing', 'verbatim', 'longtable',
|
||||
'sidewaystable', 'sidewaysfigure', 'floatrow',
|
||||
# Arrays and matrices
|
||||
'pmatrix', 'bmatrix', 'Bmatrix', 'vmatrix', 'Vmatrix',
|
||||
'smallmatrix', 'array', 'matrix*', 'pmatrix*', 'bmatrix*',
|
||||
# Algorithms and code
|
||||
'algorithm', 'algorithmic', 'lstlisting', 'verbatim',
|
||||
'minted', 'listing', 'algorithmic*', 'algorithm2e',
|
||||
# Theorems and proofs
|
||||
'theorem', 'proof', 'definition', 'lemma', 'corollary',
|
||||
'proposition', 'example', 'remark', 'note', 'claim',
|
||||
'axiom', 'property', 'assumption', 'conjecture', 'observation',
|
||||
# Bibliography
|
||||
'thebibliography', 'bibliography', 'references'
|
||||
})
|
||||
|
||||
# 引用类命令的特殊处理配置
|
||||
citation_commands: Set[str] = field(default_factory=lambda: {
|
||||
# Basic citations
|
||||
'cite', 'citep', 'citet', 'citeyear', 'citeauthor',
|
||||
'citeyearpar', 'citetext', 'citenum',
|
||||
# Natbib citations
|
||||
'citefullauthor', 'citealp', 'citealt', 'citename',
|
||||
'citepalias', 'citetalias', 'citetext',
|
||||
# Cross-references
|
||||
'ref', 'eqref', 'pageref', 'autoref', 'nameref', 'cref',
|
||||
'Cref', 'vref', 'Vref', 'fref', 'pref',
|
||||
# Hyperref
|
||||
'hyperref', 'href', 'url',
|
||||
# Labels
|
||||
'label', 'tag'
|
||||
})
|
||||
|
||||
preserve_commands: Set[str] = field(default_factory=lambda: {
|
||||
# Text formatting
|
||||
'emph', 'textbf', 'textit', 'underline', 'texttt', 'footnote',
|
||||
'section', 'subsection', 'subsubsection', 'paragraph', 'part',
|
||||
'chapter', 'title', 'author', 'date', 'thanks',
|
||||
# Math operators and symbols
|
||||
'frac', 'sum', 'int', 'prod', 'lim', 'sup', 'inf',
|
||||
'partial', 'nabla', 'implies', 'iff', 'therefore',
|
||||
'exists', 'forall', 'in', 'subset', 'subseteq',
|
||||
# Greek letters and math symbols
|
||||
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta',
|
||||
'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu',
|
||||
'nu', 'xi', 'pi', 'rho', 'sigma', 'tau',
|
||||
'upsilon', 'phi', 'chi', 'psi', 'omega',
|
||||
'Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', 'Pi',
|
||||
'Sigma', 'Upsilon', 'Phi', 'Psi', 'Omega',
|
||||
# Math commands
|
||||
'left', 'right', 'big', 'Big', 'bigg', 'Bigg',
|
||||
'mathbf', 'mathit', 'mathsf', 'mathtt', 'mathbb',
|
||||
'mathcal', 'mathfrak', 'mathscr', 'mathrm', 'mathop',
|
||||
'operatorname', 'overline', 'underline', 'overbrace',
|
||||
'underbrace', 'overset', 'underset', 'stackrel',
|
||||
# Spacing and alignment
|
||||
'quad', 'qquad', 'hspace', 'vspace', 'medskip',
|
||||
'bigskip', 'smallskip', 'hfill', 'vfill', 'centering',
|
||||
'raggedright', 'raggedleft'
|
||||
})
|
||||
|
||||
remove_commands: Set[str] = field(default_factory=lambda: {
|
||||
# Document setup
|
||||
'documentclass', 'usepackage', 'input', 'include', 'includeonly',
|
||||
'bibliographystyle', 'frontmatter', 'mainmatter',
|
||||
'newtheorem', 'theoremstyle', 'proofname',
|
||||
'newcommand', 'renewcommand', 'providecommand', 'DeclareMathOperator',
|
||||
'newenvironment',
|
||||
# Layout and spacing
|
||||
'pagestyle', 'thispagestyle', 'newpage', 'clearpage',
|
||||
'pagebreak', 'linebreak', 'newline', 'setlength',
|
||||
'setcounter', 'addtocounter', 'makeatletter',
|
||||
'makeatother', 'pagenumbering'
|
||||
})
|
||||
|
||||
latex_chars: Dict[str, str] = field(default_factory=lambda: {
|
||||
'~': ' ', '\\&': '&', '\\%': '%', '\\_': '_', '\\$': '$',
|
||||
'\\#': '#', '\\{': '{', '\\}': '}', '``': '"', "''": '"',
|
||||
'\\textbackslash': '\\', '\\ldots': '...', '\\dots': '...',
|
||||
'\\textasciitilde': '~', '\\textasciicircum': '^'
|
||||
})
|
||||
|
||||
# 保留原始格式的特殊命令模式
|
||||
special_command_patterns: List[Tuple[str, str]] = field(default_factory=lambda: [
|
||||
(r'\\cite\*?(?:\[[^\]]*\])?{([^}]+)}', r'\\cite{\1}'),
|
||||
(r'\\ref\*?{([^}]+)}', r'\\ref{\1}'),
|
||||
(r'\\label{([^}]+)}', r'\\label{\1}'),
|
||||
(r'\\eqref{([^}]+)}', r'\\eqref{\1}'),
|
||||
(r'\\autoref{([^}]+)}', r'\\autoref{\1}'),
|
||||
(r'\\url{([^}]+)}', r'\\url{\1}'),
|
||||
(r'\\href{([^}]+)}{([^}]+)}', r'\\href{\1}{\2}')
|
||||
])
|
||||
|
||||
|
||||
class LatexCleaner:
|
||||
"""Enhanced LaTeX text cleaner that preserves mathematical content and citations."""
|
||||
|
||||
def __init__(self, config: Optional[LatexConfig] = None):
|
||||
self.config = config or LatexConfig()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
# 初始化正则表达式缓存
|
||||
self._regex_cache = {}
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _get_env_pattern(self, env_name: str) -> Pattern:
|
||||
"""Get cached regex pattern for environment matching."""
|
||||
return re.compile(fr'\\begin{{{env_name}}}(.*?)\\end{{{env_name}}}', re.DOTALL)
|
||||
|
||||
def _get_env_type(self, env_name: str) -> EnvType:
|
||||
"""Determine environment processing type."""
|
||||
if env_name.rstrip('*') in {name.rstrip('*') for name in self.config.preserve_envs}:
|
||||
return EnvType.PRESERVE
|
||||
elif env_name in {'comment'}:
|
||||
return EnvType.REMOVE
|
||||
return EnvType.EXTRACT
|
||||
|
||||
def _preserve_special_commands(self, text: str) -> str:
|
||||
"""Preserve special commands like citations and references with their complete structure."""
|
||||
for pattern, replacement in self.config.special_command_patterns:
|
||||
if pattern not in self._regex_cache:
|
||||
self._regex_cache[pattern] = re.compile(pattern)
|
||||
|
||||
def replace_func(match):
|
||||
# 保持原始命令格式
|
||||
return match.group(0)
|
||||
|
||||
text = self._regex_cache[pattern].sub(replace_func, text)
|
||||
return text
|
||||
|
||||
def _process_environment(self, match: re.Match) -> str:
|
||||
"""Process LaTeX environments while preserving complete content for special environments."""
|
||||
try:
|
||||
env_name = match.group(1)
|
||||
content = match.group(2)
|
||||
env_type = self._get_env_type(env_name)
|
||||
|
||||
if env_type == EnvType.PRESERVE:
|
||||
# 完整保留环境内容
|
||||
complete_env = match.group(0)
|
||||
return f"\n[BEGIN_{env_name}]\n{complete_env}\n[END_{env_name}]\n"
|
||||
elif env_type == EnvType.REMOVE:
|
||||
return ' '
|
||||
else:
|
||||
# 处理嵌套环境
|
||||
return self._clean_nested_environments(content)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing environment {match.group(1) if match else 'unknown'}: {e}")
|
||||
return match.group(0)
|
||||
|
||||
def _preserve_inline_math(self, text: str) -> str:
|
||||
"""Preserve complete inline math content."""
|
||||
|
||||
def preserve_math(match):
|
||||
return f" {match.group(0)} "
|
||||
|
||||
patterns = [
|
||||
(r'\$[^$]+\$', preserve_math),
|
||||
(r'\\[\(\[].*?\\[\)\]]', preserve_math),
|
||||
(r'\\begin{math}.*?\\end{math}', preserve_math)
|
||||
]
|
||||
|
||||
for pattern, handler in patterns:
|
||||
if pattern not in self._regex_cache:
|
||||
self._regex_cache[pattern] = re.compile(pattern, re.DOTALL)
|
||||
text = self._regex_cache[pattern].sub(handler, text)
|
||||
|
||||
return text
|
||||
|
||||
def _clean_nested_environments(self, text: str) -> str:
|
||||
"""Process nested environments recursively."""
|
||||
pattern = r'\\begin{(\w+)}(.*?)\\end{\1}'
|
||||
if pattern not in self._regex_cache:
|
||||
self._regex_cache[pattern] = re.compile(pattern, re.DOTALL)
|
||||
|
||||
return self._regex_cache[pattern].sub(self._process_environment, text)
|
||||
|
||||
def _clean_commands(self, text: str) -> str:
|
||||
"""Clean LaTeX commands while preserving important content."""
|
||||
# 首先处理特殊命令
|
||||
text = self._preserve_special_commands(text)
|
||||
|
||||
# 保留内联数学
|
||||
text = self._preserve_inline_math(text)
|
||||
|
||||
# 移除指定的命令
|
||||
for cmd in self.config.remove_commands:
|
||||
if cmd not in self._regex_cache:
|
||||
self._regex_cache[cmd] = re.compile(
|
||||
fr'\\{cmd}\*?(?:\[.*?\])?(?:{{.*?}})*'
|
||||
)
|
||||
text = self._regex_cache[cmd].sub('', text)
|
||||
|
||||
# 处理带内容的命令
|
||||
def handle_command(match: re.Match) -> str:
|
||||
cmd = match.group(1).rstrip('*')
|
||||
if cmd in self.config.preserve_commands or cmd in self.config.citation_commands:
|
||||
return match.group(0) # 完整保留命令和内容
|
||||
return ' '
|
||||
|
||||
if 'command_pattern' not in self._regex_cache:
|
||||
self._regex_cache['command_pattern'] = re.compile(
|
||||
r'\\(\w+)\*?(?:\[.*?\])?{(.*?)}'
|
||||
)
|
||||
|
||||
text = self._regex_cache['command_pattern'].sub(handle_command, text)
|
||||
return text
|
||||
|
||||
def _normalize_text(self, text: str) -> str:
|
||||
"""Normalize text while preserving special content markers."""
|
||||
# 替换特殊字符
|
||||
for char, replacement in self.config.latex_chars.items():
|
||||
text = text.replace(char, replacement)
|
||||
|
||||
# 清理空白字符,同时保留环境标记
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
text = re.sub(r'\s*\[BEGIN_(\w+)\]\s*', r'\n[BEGIN_\1]\n', text)
|
||||
text = re.sub(r'\s*\[END_(\w+)\]\s*', r'\n[END_\1]\n', text)
|
||||
|
||||
# 保持块级环境之间的分隔
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
def clean_text(self, text: str) -> str:
|
||||
"""Clean LaTeX text while preserving mathematical content, citations, and special environments."""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
try:
|
||||
# 移除注释
|
||||
text = re.sub(r'(?<!\\)%.*?(?=\n|$)', '', text, flags=re.MULTILINE)
|
||||
|
||||
# 处理环境
|
||||
text = self._clean_nested_environments(text)
|
||||
|
||||
# 清理命令并规范化
|
||||
text = self._clean_commands(text)
|
||||
text = self._normalize_text(text)
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cleaning text: {e}")
|
||||
return text # 发生错误时返回原始文本
|
||||
|
||||
|
||||
def clean_latex_commands(text: str) -> str:
|
||||
"""Convenience function for quick text cleaning with default config."""
|
||||
cleaner = LatexCleaner()
|
||||
return cleaner.clean_text(text)
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
text = r"""
|
||||
\documentclass{article}
|
||||
\begin{document}
|
||||
|
||||
\section{Introduction}
|
||||
This is a reference to \cite{smith2020} and equation \eqref{eq:main}.
|
||||
|
||||
\begin{equation}\label{eq:main}
|
||||
E = mc^2 \times \sum_{i=1}^{n} x_i
|
||||
\end{equation}
|
||||
|
||||
See Figure \ref{fig:example} for details.
|
||||
|
||||
\begin{figure}
|
||||
\includegraphics{image.png}
|
||||
\caption{Example figure\label
|
||||
\textbf{Important} result: $E=mc^2$ and
|
||||
\begin{equation}
|
||||
F = ma
|
||||
\end{equation}
|
||||
\label{sec:intro}
|
||||
"""
|
||||
|
||||
# Custom configuration
|
||||
config = LatexConfig(
|
||||
preserve_envs={},
|
||||
preserve_commands={'textbf', 'emph'},
|
||||
latex_chars={'~': ' ', '\\&': '&'}
|
||||
)
|
||||
|
||||
|
||||
def read_tex_file(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
return content
|
||||
except FileNotFoundError:
|
||||
return "文件未找到,请检查路径是否正确。"
|
||||
except Exception as e:
|
||||
return f"读取文件时发生错误: {e}"
|
||||
|
||||
|
||||
# 使用函数
|
||||
file_path = 'test_cache/2411.03663/neurips_2024.tex'
|
||||
content = read_tex_file(file_path)
|
||||
cleaner = LatexCleaner(config)
|
||||
text = cleaner.clean_text(text)
|
||||
print(text)
|
||||
@@ -1,396 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class LaTeXPatterns:
|
||||
"""LaTeX模式存储类,用于集中管理所有LaTeX相关的正则表达式模式"""
|
||||
special_envs = {
|
||||
'math': [
|
||||
# 基础数学环境
|
||||
r'\\begin{(equation|align|gather|eqnarray|multline|flalign|alignat)\*?}.*?\\end{\1\*?}',
|
||||
r'\$\$.*?\$\$',
|
||||
r'\$[^$]+\$',
|
||||
# 矩阵环境
|
||||
r'\\begin{(matrix|pmatrix|bmatrix|Bmatrix|vmatrix|Vmatrix|smallmatrix)\*?}.*?\\end{\1\*?}',
|
||||
# 数组环境
|
||||
r'\\begin{(array|cases|aligned|gathered|split)\*?}.*?\\end{\1\*?}',
|
||||
# 其他数学环境
|
||||
r'\\begin{(subequations|math|displaymath)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'table': [
|
||||
# 基础表格环境
|
||||
r'\\begin{(table|tabular|tabularx|tabulary|longtable)\*?}.*?\\end{\1\*?}',
|
||||
# 复杂表格环境
|
||||
r'\\begin{(tabu|supertabular|xtabular|mpsupertabular)\*?}.*?\\end{\1\*?}',
|
||||
# 自定义表格环境
|
||||
r'\\begin{(threeparttable|tablefootnote)\*?}.*?\\end{\1\*?}',
|
||||
# 表格注释环境
|
||||
r'\\begin{(tablenotes)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'figure': [
|
||||
# 图片环境
|
||||
r'\\begin{figure\*?}.*?\\end{figure\*?}',
|
||||
r'\\begin{(subfigure|wrapfigure)\*?}.*?\\end{\1\*?}',
|
||||
# 图片插入命令
|
||||
r'\\includegraphics(\[.*?\])?\{.*?\}',
|
||||
# tikz 图形环境
|
||||
r'\\begin{(tikzpicture|pgfpicture)\*?}.*?\\end{\1\*?}',
|
||||
# 其他图形环境
|
||||
r'\\begin{(picture|pspicture)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'algorithm': [
|
||||
# 算法环境
|
||||
r'\\begin{(algorithm|algorithmic|algorithm2e|algorithmicx)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(lstlisting|verbatim|minted|listing)\*?}.*?\\end{\1\*?}',
|
||||
# 代码块环境
|
||||
r'\\begin{(code|verbatimtab|verbatimwrite)\*?}.*?\\end{\1\*?}',
|
||||
# 伪代码环境
|
||||
r'\\begin{(pseudocode|procedure)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'list': [
|
||||
# 列表环境
|
||||
r'\\begin{(itemize|enumerate|description)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(list|compactlist|bulletlist)\*?}.*?\\end{\1\*?}',
|
||||
# 自定义列表环境
|
||||
r'\\begin{(tasks|todolist)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'theorem': [
|
||||
# 定理类环境
|
||||
r'\\begin{(theorem|lemma|proposition|corollary)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(definition|example|proof|remark)\*?}.*?\\end{\1\*?}',
|
||||
# 其他证明环境
|
||||
r'\\begin{(axiom|property|assumption|conjecture)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'box': [
|
||||
# 文本框环境
|
||||
r'\\begin{(tcolorbox|mdframed|framed|shaded)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(boxedminipage|shadowbox)\*?}.*?\\end{\1\*?}',
|
||||
# 强调环境
|
||||
r'\\begin{(important|warning|info|note)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'quote': [
|
||||
# 引用环境
|
||||
r'\\begin{(quote|quotation|verse|abstract)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(excerpt|epigraph)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'bibliography': [
|
||||
# 参考文献环境
|
||||
r'\\begin{(thebibliography|bibliography)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(biblist|citelist)\*?}.*?\\end{\1\*?}'
|
||||
],
|
||||
|
||||
'index': [
|
||||
# 索引环境
|
||||
r'\\begin{(theindex|printindex)\*?}.*?\\end{\1\*?}',
|
||||
r'\\begin{(glossary|acronym)\*?}.*?\\end{\1\*?}'
|
||||
]
|
||||
}
|
||||
# 章节模式
|
||||
section_patterns = [
|
||||
# 基础章节命令
|
||||
r'\\chapter\{([^}]+)\}',
|
||||
r'\\section\{([^}]+)\}',
|
||||
r'\\subsection\{([^}]+)\}',
|
||||
r'\\subsubsection\{([^}]+)\}',
|
||||
r'\\paragraph\{([^}]+)\}',
|
||||
r'\\subparagraph\{([^}]+)\}',
|
||||
|
||||
# 带星号的变体(不编号)
|
||||
r'\\chapter\*\{([^}]+)\}',
|
||||
r'\\section\*\{([^}]+)\}',
|
||||
r'\\subsection\*\{([^}]+)\}',
|
||||
r'\\subsubsection\*\{([^}]+)\}',
|
||||
r'\\paragraph\*\{([^}]+)\}',
|
||||
r'\\subparagraph\*\{([^}]+)\}',
|
||||
|
||||
# 特殊章节
|
||||
r'\\part\{([^}]+)\}',
|
||||
r'\\part\*\{([^}]+)\}',
|
||||
r'\\appendix\{([^}]+)\}',
|
||||
|
||||
# 前言部分
|
||||
r'\\frontmatter\{([^}]+)\}',
|
||||
r'\\mainmatter\{([^}]+)\}',
|
||||
r'\\backmatter\{([^}]+)\}',
|
||||
|
||||
# 目录相关
|
||||
r'\\tableofcontents',
|
||||
r'\\listoffigures',
|
||||
r'\\listoftables',
|
||||
|
||||
# 自定义章节命令
|
||||
r'\\addchap\{([^}]+)\}', # KOMA-Script类
|
||||
r'\\addsec\{([^}]+)\}', # KOMA-Script类
|
||||
r'\\minisec\{([^}]+)\}', # KOMA-Script类
|
||||
|
||||
# 带可选参数的章节命令
|
||||
r'\\chapter\[([^]]+)\]\{([^}]+)\}',
|
||||
r'\\section\[([^]]+)\]\{([^}]+)\}',
|
||||
r'\\subsection\[([^]]+)\]\{([^}]+)\}'
|
||||
]
|
||||
|
||||
# 包含模式
|
||||
include_patterns = [
|
||||
r'\\(input|include|subfile)\{([^}]+)\}'
|
||||
]
|
||||
|
||||
metadata_patterns = {
|
||||
# 标题相关
|
||||
'title': [
|
||||
r'\\title\{([^}]+)\}',
|
||||
r'\\Title\{([^}]+)\}',
|
||||
r'\\doctitle\{([^}]+)\}',
|
||||
r'\\subtitle\{([^}]+)\}',
|
||||
r'\\chapter\*?\{([^}]+)\}', # 第一章可能作为标题
|
||||
r'\\maketitle\s*\\section\*?\{([^}]+)\}' # 第一节可能作为标题
|
||||
],
|
||||
|
||||
# 摘要相关
|
||||
'abstract': [
|
||||
r'\\begin{abstract}(.*?)\\end{abstract}',
|
||||
r'\\abstract\{([^}]+)\}',
|
||||
r'\\begin{摘要}(.*?)\\end{摘要}',
|
||||
r'\\begin{Summary}(.*?)\\end{Summary}',
|
||||
r'\\begin{synopsis}(.*?)\\end{synopsis}',
|
||||
r'\\begin{abstracten}(.*?)\\end{abstracten}' # 英文摘要
|
||||
],
|
||||
|
||||
# 作者信息
|
||||
'author': [
|
||||
r'\\author\{([^}]+)\}',
|
||||
r'\\Author\{([^}]+)\}',
|
||||
r'\\authorinfo\{([^}]+)\}',
|
||||
r'\\authors\{([^}]+)\}',
|
||||
r'\\author\[([^]]+)\]\{([^}]+)\}', # 带附加信息的作者
|
||||
r'\\begin{authors}(.*?)\\end{authors}'
|
||||
],
|
||||
|
||||
# 日期相关
|
||||
'date': [
|
||||
r'\\date\{([^}]+)\}',
|
||||
r'\\Date\{([^}]+)\}',
|
||||
r'\\submitdate\{([^}]+)\}',
|
||||
r'\\publishdate\{([^}]+)\}',
|
||||
r'\\revisiondate\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 关键词
|
||||
'keywords': [
|
||||
r'\\keywords\{([^}]+)\}',
|
||||
r'\\Keywords\{([^}]+)\}',
|
||||
r'\\begin{keywords}(.*?)\\end{keywords}',
|
||||
r'\\key\{([^}]+)\}',
|
||||
r'\\begin{关键词}(.*?)\\end{关键词}'
|
||||
],
|
||||
|
||||
# 机构/单位
|
||||
'institution': [
|
||||
r'\\institute\{([^}]+)\}',
|
||||
r'\\institution\{([^}]+)\}',
|
||||
r'\\affiliation\{([^}]+)\}',
|
||||
r'\\organization\{([^}]+)\}',
|
||||
r'\\department\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 学科/主题
|
||||
'subject': [
|
||||
r'\\subject\{([^}]+)\}',
|
||||
r'\\Subject\{([^}]+)\}',
|
||||
r'\\field\{([^}]+)\}',
|
||||
r'\\discipline\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 版本信息
|
||||
'version': [
|
||||
r'\\version\{([^}]+)\}',
|
||||
r'\\revision\{([^}]+)\}',
|
||||
r'\\release\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 许可证/版权
|
||||
'license': [
|
||||
r'\\license\{([^}]+)\}',
|
||||
r'\\copyright\{([^}]+)\}',
|
||||
r'\\begin{license}(.*?)\\end{license}'
|
||||
],
|
||||
|
||||
# 联系方式
|
||||
'contact': [
|
||||
r'\\email\{([^}]+)\}',
|
||||
r'\\phone\{([^}]+)\}',
|
||||
r'\\address\{([^}]+)\}',
|
||||
r'\\contact\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 致谢
|
||||
'acknowledgments': [
|
||||
r'\\begin{acknowledgments}(.*?)\\end{acknowledgments}',
|
||||
r'\\acknowledgments\{([^}]+)\}',
|
||||
r'\\thanks\{([^}]+)\}',
|
||||
r'\\begin{致谢}(.*?)\\end{致谢}'
|
||||
],
|
||||
|
||||
# 项目/基金
|
||||
'funding': [
|
||||
r'\\funding\{([^}]+)\}',
|
||||
r'\\grant\{([^}]+)\}',
|
||||
r'\\project\{([^}]+)\}',
|
||||
r'\\support\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 分类号/编号
|
||||
'classification': [
|
||||
r'\\classification\{([^}]+)\}',
|
||||
r'\\serialnumber\{([^}]+)\}',
|
||||
r'\\id\{([^}]+)\}',
|
||||
r'\\doi\{([^}]+)\}'
|
||||
],
|
||||
|
||||
# 语言
|
||||
'language': [
|
||||
r'\\documentlanguage\{([^}]+)\}',
|
||||
r'\\lang\{([^}]+)\}',
|
||||
r'\\language\{([^}]+)\}'
|
||||
]
|
||||
}
|
||||
latex_only_patterns = {
|
||||
# 文档类和包引入
|
||||
r'\\documentclass(\[.*?\])?\{.*?\}',
|
||||
r'\\usepackage(\[.*?\])?\{.*?\}',
|
||||
# 常见的文档设置命令
|
||||
r'\\setlength\{.*?\}\{.*?\}',
|
||||
r'\\newcommand\{.*?\}(\[.*?\])?\{.*?\}',
|
||||
r'\\renewcommand\{.*?\}(\[.*?\])?\{.*?\}',
|
||||
r'\\definecolor\{.*?\}\{.*?\}\{.*?\}',
|
||||
# 页面设置相关
|
||||
r'\\pagestyle\{.*?\}',
|
||||
r'\\thispagestyle\{.*?\}',
|
||||
# 其他常见的设置命令
|
||||
r'\\bibliographystyle\{.*?\}',
|
||||
r'\\bibliography\{.*?\}',
|
||||
r'\\setcounter\{.*?\}\{.*?\}',
|
||||
# 字体和文本设置命令
|
||||
r'\\makeFNbottom',
|
||||
r'\\@setfontsize\\[A-Z]+\{.*?\}\{.*?\}', # 匹配字体大小设置
|
||||
r'\\renewcommand\\[A-Z]+\{\\@setfontsize\\[A-Z]+\{.*?\}\{.*?\}\}',
|
||||
r'\\renewcommand\{?\\thefootnote\}?\{\\fnsymbol\{footnote\}\}',
|
||||
r'\\renewcommand\\footnoterule\{.*?\}',
|
||||
r'\\color\{.*?\}',
|
||||
|
||||
# 页面和节标题设置
|
||||
r'\\setcounter\{secnumdepth\}\{.*?\}',
|
||||
r'\\renewcommand\\@biblabel\[.*?\]\{.*?\}',
|
||||
r'\\renewcommand\\@makefntext\[.*?\](\{.*?\})*',
|
||||
r'\\renewcommand\{?\\figurename\}?\{.*?\}',
|
||||
|
||||
# 字体样式设置
|
||||
r'\\sectionfont\{.*?\}',
|
||||
r'\\subsectionfont\{.*?\}',
|
||||
r'\\subsubsectionfont\{.*?\}',
|
||||
|
||||
# 间距和布局设置
|
||||
r'\\setstretch\{.*?\}',
|
||||
r'\\setlength\{\\skip\\footins\}\{.*?\}',
|
||||
r'\\setlength\{\\footnotesep\}\{.*?\}',
|
||||
r'\\setlength\{\\jot\}\{.*?\}',
|
||||
r'\\hrule\s+width\s+.*?\s+height\s+.*?',
|
||||
|
||||
# makeatletter 和 makeatother
|
||||
r'\\makeatletter\s*',
|
||||
r'\\makeatother\s*',
|
||||
r'\\footnotetext\{[^}]*\$\^{[^}]*}\$[^}]*\}', # 带有上标的脚注
|
||||
# r'\\footnotetext\{[^}]*\}', # 普通脚注
|
||||
# r'\\footnotetext\{.*?(?:\$\^{.*?}\$)?.*?(?:email\s*:\s*[^}]*)?.*?\}', # 带有邮箱的脚注
|
||||
# r'\\footnotetext\{.*?(?:ESI|DOI).*?\}', # 带有 DOI 或 ESI 引用的脚注
|
||||
# 文档结构命令
|
||||
r'\\begin\{document\}',
|
||||
r'\\end\{document\}',
|
||||
r'\\maketitle',
|
||||
r'\\printbibliography',
|
||||
r'\\newpage',
|
||||
|
||||
# 输入文件命令
|
||||
r'\\input\{[^}]*\}',
|
||||
r'\\input\{.*?\.tex\}', # 特别匹配 .tex 后缀的输入
|
||||
|
||||
# 脚注相关
|
||||
# r'\\footnotetext\[\d+\]\{[^}]*\}', # 带编号的脚注
|
||||
|
||||
# 致谢环境
|
||||
r'\\begin\{ack\}',
|
||||
r'\\end\{ack\}',
|
||||
r'\\begin\{ack\}[^\n]*(?:\n.*?)*?\\end\{ack\}', # 匹配整个致谢环境及其内容
|
||||
|
||||
# 其他文档控制命令
|
||||
r'\\renewcommand\{\\thefootnote\}\{\\fnsymbol\{footnote\}\}',
|
||||
}
|
||||
math_envs = [
|
||||
# 基础数学环境
|
||||
(r'\\begin{equation\*?}.*?\\end{equation\*?}', 'equation'), # 单行公式
|
||||
(r'\\begin{align\*?}.*?\\end{align\*?}', 'align'), # 多行对齐公式
|
||||
(r'\\begin{gather\*?}.*?\\end{gather\*?}', 'gather'), # 多行居中公式
|
||||
(r'\$\$.*?\$\$', 'display'), # 行间公式
|
||||
(r'\$.*?\$', 'inline'), # 行内公式
|
||||
|
||||
# 矩阵环境
|
||||
(r'\\begin{matrix}.*?\\end{matrix}', 'matrix'), # 基础矩阵
|
||||
(r'\\begin{pmatrix}.*?\\end{pmatrix}', 'pmatrix'), # 圆括号矩阵
|
||||
(r'\\begin{bmatrix}.*?\\end{bmatrix}', 'bmatrix'), # 方括号矩阵
|
||||
(r'\\begin{vmatrix}.*?\\end{vmatrix}', 'vmatrix'), # 竖线矩阵
|
||||
(r'\\begin{Vmatrix}.*?\\end{Vmatrix}', 'Vmatrix'), # 双竖线矩阵
|
||||
(r'\\begin{smallmatrix}.*?\\end{smallmatrix}', 'smallmatrix'), # 小号矩阵
|
||||
|
||||
# 数组环境
|
||||
(r'\\begin{array}.*?\\end{array}', 'array'), # 数组
|
||||
(r'\\begin{cases}.*?\\end{cases}', 'cases'), # 分段函数
|
||||
|
||||
# 多行公式环境
|
||||
(r'\\begin{multline\*?}.*?\\end{multline\*?}', 'multline'), # 多行单个公式
|
||||
(r'\\begin{split}.*?\\end{split}', 'split'), # 拆分长公式
|
||||
(r'\\begin{alignat\*?}.*?\\end{alignat\*?}', 'alignat'), # 对齐环境带间距控制
|
||||
(r'\\begin{flalign\*?}.*?\\end{flalign\*?}', 'flalign'), # 完全左对齐
|
||||
|
||||
# 特殊数学环境
|
||||
(r'\\begin{subequations}.*?\\end{subequations}', 'subequations'), # 子公式编号
|
||||
(r'\\begin{gathered}.*?\\end{gathered}', 'gathered'), # 居中对齐组
|
||||
(r'\\begin{aligned}.*?\\end{aligned}', 'aligned'), # 内部对齐组
|
||||
|
||||
# 定理类环境
|
||||
(r'\\begin{theorem}.*?\\end{theorem}', 'theorem'), # 定理
|
||||
(r'\\begin{lemma}.*?\\end{lemma}', 'lemma'), # 引理
|
||||
(r'\\begin{proof}.*?\\end{proof}', 'proof'), # 证明
|
||||
|
||||
# 数学模式中的表格环境
|
||||
(r'\\begin{tabular}.*?\\end{tabular}', 'tabular'), # 表格
|
||||
(r'\\begin{array}.*?\\end{array}', 'array'), # 数组
|
||||
|
||||
# 其他专业数学环境
|
||||
(r'\\begin{CD}.*?\\end{CD}', 'CD'), # 交换图
|
||||
(r'\\begin{boxed}.*?\\end{boxed}', 'boxed'), # 带框公式
|
||||
(r'\\begin{empheq}.*?\\end{empheq}', 'empheq'), # 强调公式
|
||||
|
||||
# 化学方程式环境 (需要加载 mhchem 包)
|
||||
(r'\\begin{reaction}.*?\\end{reaction}', 'reaction'), # 化学反应式
|
||||
(r'\\ce\{.*?\}', 'chemequation'), # 化学方程式
|
||||
|
||||
# 物理单位环境 (需要加载 siunitx 包)
|
||||
(r'\\SI\{.*?\}\{.*?\}', 'SI'), # 物理单位
|
||||
(r'\\si\{.*?\}', 'si'), # 单位
|
||||
|
||||
# 补充环境
|
||||
(r'\\begin{equation\+}.*?\\end{equation\+}', 'equation+'), # breqn包的自动换行公式
|
||||
(r'\\begin{dmath\*?}.*?\\end{dmath\*?}', 'dmath'), # breqn包的显示数学模式
|
||||
(r'\\begin{dgroup\*?}.*?\\end{dgroup\*?}', 'dgroup'), # breqn包的公式组
|
||||
]
|
||||
|
||||
# 示例使用函数
|
||||
|
||||
# 使用示例
|
||||
@@ -1,416 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SectionLevel(Enum):
|
||||
CHAPTER = 0
|
||||
SECTION = 1
|
||||
SUBSECTION = 2
|
||||
SUBSUBSECTION = 3
|
||||
PARAGRAPH = 4
|
||||
SUBPARAGRAPH = 5
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, SectionLevel):
|
||||
return NotImplemented
|
||||
return self.value < other.value
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, SectionLevel):
|
||||
return NotImplemented
|
||||
return self.value <= other.value
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, SectionLevel):
|
||||
return NotImplemented
|
||||
return self.value > other.value
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, SectionLevel):
|
||||
return NotImplemented
|
||||
return self.value >= other.value
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
level: SectionLevel
|
||||
title: str
|
||||
content: str = ''
|
||||
bibliography: str = ''
|
||||
subsections: List['Section'] = field(default_factory=list)
|
||||
|
||||
def merge(self, other: 'Section') -> 'Section':
|
||||
"""Merge this section with another section."""
|
||||
if self.title != other.title or self.level != other.level:
|
||||
raise ValueError("Can only merge sections with same title and level")
|
||||
|
||||
merged = deepcopy(self)
|
||||
merged.content = self._merge_content(self.content, other.content)
|
||||
|
||||
# Create subsections lookup for efficient merging
|
||||
subsections_map = {s.title: s for s in merged.subsections}
|
||||
|
||||
for other_subsection in other.subsections:
|
||||
if other_subsection.title in subsections_map:
|
||||
# Merge existing subsection
|
||||
idx = next(i for i, s in enumerate(merged.subsections)
|
||||
if s.title == other_subsection.title)
|
||||
merged.subsections[idx] = merged.subsections[idx].merge(other_subsection)
|
||||
else:
|
||||
# Add new subsection
|
||||
merged.subsections.append(deepcopy(other_subsection))
|
||||
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _merge_content(content1: str, content2: str) -> str:
|
||||
"""Merge content strings intelligently."""
|
||||
if not content1:
|
||||
return content2
|
||||
if not content2:
|
||||
return content1
|
||||
# Combine non-empty contents with a separator
|
||||
return f"{content1}\n\n{content2}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LatexEnvironment:
|
||||
"""表示LaTeX环境的数据类"""
|
||||
name: str
|
||||
start: int
|
||||
end: int
|
||||
content: str
|
||||
raw: str
|
||||
|
||||
|
||||
class EnhancedSectionExtractor:
|
||||
"""Enhanced section extractor with comprehensive content handling and hierarchy management."""
|
||||
|
||||
def __init__(self, preserve_environments: bool = True):
|
||||
"""
|
||||
初始化Section提取器
|
||||
|
||||
Args:
|
||||
preserve_environments: 是否保留特定环境(如equation, figure等)的原始LaTeX代码
|
||||
"""
|
||||
self.preserve_environments = preserve_environments
|
||||
|
||||
# Section级别定义
|
||||
self.section_levels = {
|
||||
'chapter': SectionLevel.CHAPTER,
|
||||
'section': SectionLevel.SECTION,
|
||||
'subsection': SectionLevel.SUBSECTION,
|
||||
'subsubsection': SectionLevel.SUBSUBSECTION,
|
||||
'paragraph': SectionLevel.PARAGRAPH,
|
||||
'subparagraph': SectionLevel.SUBPARAGRAPH
|
||||
}
|
||||
|
||||
# 需要保留的环境类型
|
||||
self.important_environments = {
|
||||
'equation', 'equation*', 'align', 'align*',
|
||||
'figure', 'table', 'algorithm', 'algorithmic',
|
||||
'definition', 'theorem', 'lemma', 'proof',
|
||||
'itemize', 'enumerate', 'description'
|
||||
}
|
||||
|
||||
# 改进的section pattern
|
||||
self.section_pattern = (
|
||||
r'\\(?P<type>chapter|section|subsection|subsubsection|paragraph|subparagraph)'
|
||||
r'\*?' # Optional star
|
||||
r'(?:\[(?P<short>.*?)\])?' # Optional short title
|
||||
r'{(?P<title>(?:[^{}]|\{[^{}]*\})*?)}' # Main title with nested braces support
|
||||
)
|
||||
|
||||
# 环境匹配模式
|
||||
self.environment_pattern = (
|
||||
r'\\begin{(?P<env_name>[^}]+)}'
|
||||
r'(?P<env_content>.*?)'
|
||||
r'\\end{(?P=env_name)}'
|
||||
)
|
||||
|
||||
def _find_environments(self, content: str) -> List[LatexEnvironment]:
|
||||
"""
|
||||
查找文档中的所有LaTeX环境。
|
||||
支持嵌套环境的处理。
|
||||
"""
|
||||
environments = []
|
||||
stack = []
|
||||
|
||||
# 使用正则表达式查找所有begin和end标记
|
||||
begin_pattern = r'\\begin{([^}]+)}'
|
||||
end_pattern = r'\\end{([^}]+)}'
|
||||
|
||||
# 组合模式来同时匹配begin和end
|
||||
tokens = []
|
||||
for match in re.finditer(fr'({begin_pattern})|({end_pattern})', content):
|
||||
if match.group(1): # begin标记
|
||||
tokens.append(('begin', match.group(1), match.start()))
|
||||
else: # end标记
|
||||
tokens.append(('end', match.group(2), match.start()))
|
||||
|
||||
# 处理环境嵌套
|
||||
for token_type, env_name, pos in tokens:
|
||||
if token_type == 'begin':
|
||||
stack.append((env_name, pos))
|
||||
elif token_type == 'end' and stack:
|
||||
if stack[-1][0] == env_name:
|
||||
start_env_name, start_pos = stack.pop()
|
||||
env_content = content[start_pos:pos]
|
||||
raw_content = content[start_pos:pos + len('\\end{' + env_name + '}')]
|
||||
|
||||
if start_env_name in self.important_environments:
|
||||
environments.append(LatexEnvironment(
|
||||
name=start_env_name,
|
||||
start=start_pos,
|
||||
end=pos + len('\\end{' + env_name + '}'),
|
||||
content=env_content,
|
||||
raw=raw_content
|
||||
))
|
||||
|
||||
return sorted(environments, key=lambda x: x.start)
|
||||
|
||||
def _protect_environments(self, content: str) -> Tuple[str, Dict[str, str]]:
|
||||
"""
|
||||
保护重要的LaTeX环境,用占位符替换它们。
|
||||
返回处理后的内容和恢复映射。
|
||||
"""
|
||||
environments = self._find_environments(content)
|
||||
replacements = {}
|
||||
|
||||
# 从后向前替换,避免位置改变的问题
|
||||
for env in reversed(environments):
|
||||
if env.name in self.important_environments:
|
||||
placeholder = f'__ENV_{len(replacements)}__'
|
||||
replacements[placeholder] = env.raw
|
||||
content = content[:env.start] + placeholder + content[env.end:]
|
||||
|
||||
return content, replacements
|
||||
|
||||
def _restore_environments(self, content: str, replacements: Dict[str, str]) -> str:
|
||||
"""
|
||||
恢复之前保护的环境。
|
||||
"""
|
||||
for placeholder, original in replacements.items():
|
||||
content = content.replace(placeholder, original)
|
||||
return content
|
||||
|
||||
def extract(self, content: str) -> List[Section]:
|
||||
"""
|
||||
从LaTeX文档中提取sections及其内容。
|
||||
|
||||
Args:
|
||||
content: LaTeX文档内容
|
||||
|
||||
Returns:
|
||||
List[Section]: 提取的section列表,包含层次结构
|
||||
"""
|
||||
try:
|
||||
# 预处理:保护重要环境
|
||||
if self.preserve_environments:
|
||||
content, env_replacements = self._protect_environments(content)
|
||||
|
||||
# 查找所有sections
|
||||
sections = self._find_all_sections(content)
|
||||
if not sections:
|
||||
return []
|
||||
|
||||
# 处理sections
|
||||
root_sections = self._process_sections(content, sections)
|
||||
|
||||
# 如果需要,恢复环境
|
||||
if self.preserve_environments:
|
||||
for section in self._traverse_sections(root_sections):
|
||||
section.content = self._restore_environments(section.content, env_replacements)
|
||||
|
||||
return root_sections
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting sections: {str(e)}")
|
||||
raise
|
||||
|
||||
def _find_all_sections(self, content: str) -> List[dict]:
|
||||
"""查找所有section命令及其位置。"""
|
||||
sections = []
|
||||
|
||||
for match in re.finditer(self.section_pattern, content, re.DOTALL | re.MULTILINE):
|
||||
section_type = match.group('type').lower()
|
||||
if section_type not in self.section_levels:
|
||||
continue
|
||||
|
||||
section = {
|
||||
'type': section_type,
|
||||
'level': self.section_levels[section_type],
|
||||
'title': self._clean_title(match.group('title')),
|
||||
'start': match.start(),
|
||||
'command_end': match.end(),
|
||||
}
|
||||
sections.append(section)
|
||||
|
||||
return sorted(sections, key=lambda x: x['start'])
|
||||
|
||||
def _process_sections(self, content: str, sections: List[dict]) -> List[Section]:
|
||||
"""处理sections以构建层次结构和提取内容。"""
|
||||
# 计算content范围
|
||||
self._calculate_content_ranges(content, sections)
|
||||
|
||||
# 构建层次结构
|
||||
root_sections = []
|
||||
section_stack = []
|
||||
|
||||
for section_info in sections:
|
||||
new_section = Section(
|
||||
level=section_info['level'],
|
||||
title=section_info['title'],
|
||||
content=self._extract_clean_content(content, section_info),
|
||||
subsections=[]
|
||||
)
|
||||
|
||||
# 调整堆栈以找到正确的父section
|
||||
while section_stack and section_stack[-1].level.value >= new_section.level.value:
|
||||
section_stack.pop()
|
||||
|
||||
if section_stack:
|
||||
section_stack[-1].subsections.append(new_section)
|
||||
else:
|
||||
root_sections.append(new_section)
|
||||
|
||||
section_stack.append(new_section)
|
||||
|
||||
return root_sections
|
||||
|
||||
def _calculate_content_ranges(self, content: str, sections: List[dict]):
|
||||
for i, current in enumerate(sections):
|
||||
content_start = current['command_end']
|
||||
|
||||
# 找到下一个section(无论什么级别)
|
||||
content_end = len(content)
|
||||
for next_section in sections[i + 1:]:
|
||||
content_end = next_section['start']
|
||||
break
|
||||
|
||||
current['content_range'] = (content_start, content_end)
|
||||
|
||||
def _calculate_content_ranges_with_subsection_content(self, content: str, sections: List[dict]):
|
||||
"""为每个section计算内容范围。"""
|
||||
for i, current in enumerate(sections):
|
||||
content_start = current['command_end']
|
||||
|
||||
# 找到下一个同级或更高级的section
|
||||
content_end = len(content)
|
||||
for next_section in sections[i + 1:]:
|
||||
if next_section['level'] <= current['level']:
|
||||
content_end = next_section['start']
|
||||
break
|
||||
|
||||
current['content_range'] = (content_start, content_end)
|
||||
|
||||
def _extract_clean_content(self, content: str, section_info: dict) -> str:
|
||||
"""提取并清理section内容。"""
|
||||
start, end = section_info['content_range']
|
||||
raw_content = content[start:end]
|
||||
|
||||
# 清理内容
|
||||
clean_content = self._clean_content(raw_content)
|
||||
return clean_content
|
||||
|
||||
def _clean_content(self, content: str) -> str:
|
||||
"""清理LaTeX内容同时保留重要信息。"""
|
||||
# 移除注释
|
||||
content = re.sub(r'(?<!\\)%.*?\n', '\n', content)
|
||||
|
||||
# LaTeX命令处理规则
|
||||
replacements = [
|
||||
# 保留引用
|
||||
(r'\\cite(?:\[.*?\])?{(.*?)}', r'[cite:\1]'),
|
||||
# 保留脚注
|
||||
(r'\\footnote{(.*?)}', r'[footnote:\1]'),
|
||||
# 处理引用
|
||||
(r'\\ref{(.*?)}', r'[ref:\1]'),
|
||||
# 保留URL
|
||||
(r'\\url{(.*?)}', r'[url:\1]'),
|
||||
# 保留超链接
|
||||
(r'\\href{(.*?)}{(.*?)}', r'[\2](\1)'),
|
||||
# 处理文本格式命令
|
||||
(r'\\(?:textbf|textit|emph){(.*?)}', r'\1'),
|
||||
# 保留特殊字符
|
||||
(r'\\([&%$#_{}])', r'\1'),
|
||||
]
|
||||
|
||||
# 应用所有替换规则
|
||||
for pattern, replacement in replacements:
|
||||
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
|
||||
# 清理多余的空白
|
||||
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||
return content.strip()
|
||||
|
||||
def _clean_title(self, title: str) -> str:
|
||||
"""清理section标题。"""
|
||||
# 处理嵌套的花括号
|
||||
while '{' in title:
|
||||
title = re.sub(r'{([^{}]*)}', r'\1', title)
|
||||
|
||||
# 处理LaTeX命令
|
||||
title = re.sub(r'\\[a-zA-Z]+(?:\[.*?\])?{(.*?)}', r'\1', title)
|
||||
title = re.sub(r'\\([&%$#_{}])', r'\1', title)
|
||||
|
||||
return title.strip()
|
||||
|
||||
def _traverse_sections(self, sections: List[Section]) -> List[Section]:
|
||||
"""遍历所有sections(包括子sections)。"""
|
||||
result = []
|
||||
for section in sections:
|
||||
result.append(section)
|
||||
result.extend(self._traverse_sections(section.subsections))
|
||||
return result
|
||||
|
||||
|
||||
def test_enhanced_extractor():
|
||||
"""使用复杂的测试用例测试提取器。"""
|
||||
test_content = r"""
|
||||
\section{Complex Examples}
|
||||
Here's a complex section with various environments.
|
||||
|
||||
\begin{equation}
|
||||
E = mc^2
|
||||
\end{equation}
|
||||
|
||||
\subsection{Nested Environments}
|
||||
This subsection has nested environments.
|
||||
|
||||
\begin{figure}
|
||||
\begin{equation*}
|
||||
f(x) = \int_0^x g(t) dt
|
||||
\end{equation*}
|
||||
\caption{A nested equation in a figure}
|
||||
\end{figure}
|
||||
|
||||
"""
|
||||
|
||||
extractor = EnhancedSectionExtractor()
|
||||
sections = extractor.extract(test_content)
|
||||
|
||||
def print_section(section, level=0):
|
||||
print("\n" + " " * level + f"[{section.level.name}] {section.title}")
|
||||
if section.content:
|
||||
content_preview = section.content[:150] + "..." if len(section.content) > 150 else section.content
|
||||
print(" " * (level + 1) + f"Content: {content_preview}")
|
||||
for subsection in section.subsections:
|
||||
print_section(subsection, level + 1)
|
||||
|
||||
print("\nExtracted Section Structure:")
|
||||
for section in sections:
|
||||
print_section(section)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_enhanced_extractor()
|
||||
@@ -1,14 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SectionFragment:
|
||||
"""Arxiv论文片段数据类"""
|
||||
title: str # 论文标题
|
||||
authors: str
|
||||
abstract: str # 论文摘要
|
||||
catalogs: str # 文章各章节的目录结构
|
||||
arxiv_id: str = "" # 添加 arxiv_id 属性
|
||||
current_section: str = "Introduction" # 当前片段所属的section或者subsection或者孙subsubsection名字
|
||||
content: str = '' # 当前片段的内容
|
||||
bibliography: str = '' # 当前片段的参考文献
|
||||
@@ -1,266 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Set, Optional
|
||||
|
||||
from crazy_functions.rag_fns.arxiv_fns.latex_patterns import LaTeXPatterns
|
||||
|
||||
|
||||
class TexUtils:
|
||||
"""TeX文档处理器类"""
|
||||
|
||||
def __init__(self, ):
|
||||
"""
|
||||
初始化TeX处理器
|
||||
|
||||
Args:
|
||||
char_range: 字符数范围(最小值, 最大值)
|
||||
"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化LaTeX环境和命令模式
|
||||
self._init_patterns()
|
||||
self.latex_only_patterns = LaTeXPatterns.latex_only_patterns
|
||||
|
||||
def _init_patterns(self):
|
||||
"""初始化LaTeX模式匹配规则"""
|
||||
# 特殊环境模式
|
||||
self.special_envs = LaTeXPatterns.special_envs
|
||||
# 章节模式
|
||||
self.section_patterns = LaTeXPatterns.section_patterns
|
||||
# 包含模式
|
||||
self.include_patterns = LaTeXPatterns.include_patterns
|
||||
# 元数据模式
|
||||
self.metadata_patterns = LaTeXPatterns.metadata_patterns
|
||||
|
||||
def read_file(self, file_path: str) -> Optional[str]:
|
||||
"""
|
||||
读取TeX文件内容,支持多种编码
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
Optional[str]: 文件内容或None
|
||||
"""
|
||||
encodings = ['utf-8', 'latin1', 'gbk', 'gb2312', 'ascii']
|
||||
for encoding in encodings:
|
||||
try:
|
||||
with open(file_path, 'r', encoding=encoding) as f:
|
||||
return f.read()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
self.logger.warning(f"Failed to read {file_path} with all encodings")
|
||||
return None
|
||||
|
||||
def find_main_tex_file(self, directory: str) -> Optional[str]:
|
||||
"""
|
||||
查找主TeX文件
|
||||
|
||||
Args:
|
||||
directory: 目录路径
|
||||
|
||||
Returns:
|
||||
Optional[str]: 主文件路径或None
|
||||
"""
|
||||
tex_files = list(Path(directory).rglob("*.tex"))
|
||||
if not tex_files:
|
||||
return None
|
||||
|
||||
# 按优先级查找
|
||||
for tex_file in tex_files:
|
||||
content = self.read_file(str(tex_file))
|
||||
if content:
|
||||
if r'\documentclass' in content:
|
||||
return str(tex_file)
|
||||
if tex_file.name.lower() == 'main.tex':
|
||||
return str(tex_file)
|
||||
|
||||
# 返回最大的tex文件
|
||||
return str(max(tex_files, key=lambda x: x.stat().st_size))
|
||||
|
||||
def resolve_includes(self, tex_file: str, processed: Set[str] = None) -> List[str]:
|
||||
"""
|
||||
解析TeX文件中的include引用
|
||||
|
||||
Args:
|
||||
tex_file: TeX文件路径
|
||||
processed: 已处理的文件集合
|
||||
|
||||
Returns:
|
||||
List[str]: 相关文件路径列表
|
||||
"""
|
||||
if processed is None:
|
||||
processed = set()
|
||||
|
||||
if tex_file in processed:
|
||||
return []
|
||||
|
||||
processed.add(tex_file)
|
||||
result = [tex_file]
|
||||
content = self.read_file(tex_file)
|
||||
|
||||
if not content:
|
||||
return result
|
||||
|
||||
base_dir = Path(tex_file).parent
|
||||
for pattern in self.include_patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
included_file = match.group(2)
|
||||
if not included_file.endswith('.tex'):
|
||||
included_file += '.tex'
|
||||
|
||||
full_path = str(base_dir / included_file)
|
||||
if os.path.exists(full_path) and full_path not in processed:
|
||||
result.extend(self.resolve_includes(full_path, processed))
|
||||
|
||||
return result
|
||||
|
||||
def resolve_references(self, tex_file: str, path_dir: str = None) -> str:
|
||||
"""
|
||||
解析TeX文件中的参考文献引用,返回所有引用文献的内容,只保留title、author和journal字段。
|
||||
如果在tex_file目录下没找到bib文件,会在path_dir中查找。
|
||||
|
||||
Args:
|
||||
tex_file: TeX文件路径
|
||||
path_dir: 额外的参考文献搜索路径
|
||||
|
||||
Returns:
|
||||
str: 所有参考文献内容的字符串,只包含特定字段,不同参考文献之间用空行分隔
|
||||
"""
|
||||
all_references = [] # 存储所有参考文献内容
|
||||
content = self.read_file(tex_file)
|
||||
|
||||
if not content:
|
||||
return ""
|
||||
|
||||
# 扩展参考文献引用的模式
|
||||
bib_patterns = [
|
||||
r'\\bibliography\{([^}]+)\}',
|
||||
r'\\addbibresource\{([^}]+)\}',
|
||||
r'\\bibliographyfile\{([^}]+)\}',
|
||||
r'\\begin\{thebibliography\}',
|
||||
r'\\bibinput\{([^}]+)\}',
|
||||
r'\\newrefsection\{([^}]+)\}'
|
||||
]
|
||||
|
||||
base_dir = Path(tex_file).parent
|
||||
found_in_tex_dir = False
|
||||
|
||||
# 首先在tex文件目录下查找显式引用的bib文件
|
||||
for pattern in bib_patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
if not match.groups():
|
||||
continue
|
||||
|
||||
bib_files = match.group(1).split(',')
|
||||
for bib_file in bib_files:
|
||||
bib_file = bib_file.strip()
|
||||
if not bib_file.endswith('.bib'):
|
||||
bib_file += '.bib'
|
||||
|
||||
full_path = str(base_dir / bib_file)
|
||||
if os.path.exists(full_path):
|
||||
found_in_tex_dir = True
|
||||
bib_content = self.read_file(full_path)
|
||||
if bib_content:
|
||||
processed_refs = self._process_bib_content(bib_content)
|
||||
all_references.extend(processed_refs)
|
||||
|
||||
# 如果在tex文件目录下没找到bib文件,且提供了额外搜索路径
|
||||
if not found_in_tex_dir and path_dir:
|
||||
search_dir = Path(path_dir)
|
||||
try:
|
||||
for bib_path in search_dir.glob('**/*.bib'):
|
||||
bib_content = self.read_file(str(bib_path))
|
||||
if bib_content:
|
||||
processed_refs = self._process_bib_content(bib_content)
|
||||
all_references.extend(processed_refs)
|
||||
except Exception as e:
|
||||
print(f"Error searching in path_dir: {e}")
|
||||
|
||||
# 合并所有参考文献内容,用空行分隔
|
||||
return "\n\n".join(all_references)
|
||||
|
||||
def _process_bib_content(self, content: str) -> List[str]:
|
||||
"""
|
||||
处理bib文件内容,提取每个参考文献的特定字段
|
||||
|
||||
Args:
|
||||
content: bib文件内容
|
||||
|
||||
Returns:
|
||||
List[str]: 处理后的参考文献列表
|
||||
"""
|
||||
processed_refs = []
|
||||
# 匹配完整的参考文献条目
|
||||
ref_pattern = r'@\w+\{[^@]*\}'
|
||||
# 匹配参考文献类型和键值
|
||||
entry_start_pattern = r'@(\w+)\{([^,]*?),'
|
||||
# 匹配字段
|
||||
field_pattern = r'(\w+)\s*=\s*\{([^}]*)\}'
|
||||
|
||||
# 查找所有参考文献条目
|
||||
for ref_match in re.finditer(ref_pattern, content, re.DOTALL):
|
||||
ref_content = ref_match.group(0)
|
||||
|
||||
# 获取参考文献类型和键值
|
||||
entry_match = re.match(entry_start_pattern, ref_content)
|
||||
if not entry_match:
|
||||
continue
|
||||
|
||||
entry_type, cite_key = entry_match.groups()
|
||||
|
||||
# 提取需要的字段
|
||||
needed_fields = {'title': None, 'author': None, 'journal': None}
|
||||
for field_match in re.finditer(field_pattern, ref_content):
|
||||
field_name, field_value = field_match.groups()
|
||||
field_name = field_name.lower()
|
||||
if field_name in needed_fields:
|
||||
needed_fields[field_name] = field_value.strip()
|
||||
|
||||
# 构建新的参考文献条目
|
||||
if any(needed_fields.values()): # 如果至少有一个需要的字段
|
||||
ref_lines = [f"@{entry_type}{{{cite_key},"]
|
||||
for field_name, field_value in needed_fields.items():
|
||||
if field_value:
|
||||
ref_lines.append(f" {field_name}={{{field_value}}},")
|
||||
ref_lines[-1] = ref_lines[-1][:-1] # 移除最后一个逗号
|
||||
ref_lines.append("}")
|
||||
|
||||
processed_refs.append("\n".join(ref_lines))
|
||||
|
||||
return processed_refs
|
||||
|
||||
def _extract_inline_references(self, content: str) -> str:
|
||||
"""
|
||||
从tex文件内容中提取直接写在文件中的参考文献
|
||||
|
||||
Args:
|
||||
content: tex文件内容
|
||||
|
||||
Returns:
|
||||
str: 提取的参考文献内容,如果没有找到则返回空字符串
|
||||
"""
|
||||
# 查找参考文献环境
|
||||
bib_start = r'\\begin\{thebibliography\}'
|
||||
bib_end = r'\\end\{thebibliography\}'
|
||||
|
||||
start_match = re.search(bib_start, content)
|
||||
end_match = re.search(bib_end, content)
|
||||
|
||||
if start_match and end_match:
|
||||
return content[start_match.start():end_match.end()]
|
||||
|
||||
return ""
|
||||
|
||||
def _preprocess_content(self, content: str) -> str:
|
||||
"""预处理TeX内容"""
|
||||
# 移除注释
|
||||
content = re.sub(r'(?m)%.*$', '', content)
|
||||
# 规范化空白字符
|
||||
# content = re.sub(r'\s+', ' ', content)
|
||||
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||
return content.strip()
|
||||
@@ -1,10 +1,10 @@
|
||||
import atexit
|
||||
import os
|
||||
from loguru import logger
|
||||
from typing import List
|
||||
|
||||
from llama_index.core import Document
|
||||
from llama_index.core.ingestion import run_transformations
|
||||
from llama_index.core.schema import TextNode
|
||||
from loguru import logger
|
||||
|
||||
from crazy_functions.rag_fns.vector_store_index import GptacVectorStoreIndex
|
||||
from request_llms.embed_models.openai_embed import OpenAiEmbeddingModel
|
||||
@@ -59,7 +59,7 @@ class SaveLoad():
|
||||
def purge(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.checkpoint_dir, ignore_errors=True)
|
||||
self.vs_index = self.create_new_vs()
|
||||
self.vs_index = self.create_new_vs(self.checkpoint_dir)
|
||||
|
||||
|
||||
class LlamaIndexRagWorker(SaveLoad):
|
||||
@@ -68,60 +68,11 @@ class LlamaIndexRagWorker(SaveLoad):
|
||||
self.embed_model = OpenAiEmbeddingModel(llm_kwargs)
|
||||
self.user_name = user_name
|
||||
self.checkpoint_dir = checkpoint_dir
|
||||
|
||||
# 确保checkpoint_dir存在
|
||||
if checkpoint_dir:
|
||||
os.makedirs(checkpoint_dir, exist_ok=True)
|
||||
|
||||
logger.info(f"Initializing LlamaIndexRagWorker with checkpoint_dir: {checkpoint_dir}")
|
||||
|
||||
# 初始化向量存储
|
||||
if auto_load_checkpoint and self.does_checkpoint_exist():
|
||||
logger.info("Loading existing vector store from checkpoint")
|
||||
self.vs_index = self.load_from_checkpoint()
|
||||
if auto_load_checkpoint:
|
||||
self.vs_index = self.load_from_checkpoint(checkpoint_dir)
|
||||
else:
|
||||
logger.info("Creating new vector store")
|
||||
self.vs_index = self.create_new_vs()
|
||||
|
||||
# 注册退出时保存
|
||||
atexit.register(self.save_to_checkpoint)
|
||||
|
||||
def add_text_to_vector_store(self, text: str) -> None:
|
||||
"""添加文本到向量存储"""
|
||||
try:
|
||||
logger.info(f"Adding text to vector store (first 100 chars): {text[:100]}...")
|
||||
node = TextNode(text=text)
|
||||
nodes = run_transformations(
|
||||
[node],
|
||||
self.vs_index._transformations,
|
||||
show_progress=True
|
||||
)
|
||||
self.vs_index.insert_nodes(nodes)
|
||||
|
||||
# 立即保存
|
||||
self.save_to_checkpoint()
|
||||
|
||||
if self.debug_mode:
|
||||
self.inspect_vector_store()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding text to vector store: {str(e)}")
|
||||
raise
|
||||
|
||||
def save_to_checkpoint(self, checkpoint_dir=None):
|
||||
"""保存向量存储到检查点"""
|
||||
try:
|
||||
if checkpoint_dir is None:
|
||||
checkpoint_dir = self.checkpoint_dir
|
||||
logger.info(f'Saving vector store to: {checkpoint_dir}')
|
||||
if checkpoint_dir:
|
||||
self.vs_index.storage_context.persist(persist_dir=checkpoint_dir)
|
||||
logger.info('Vector store saved successfully')
|
||||
else:
|
||||
logger.warning('No checkpoint directory specified, skipping save')
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving checkpoint: {str(e)}")
|
||||
raise
|
||||
atexit.register(lambda: self.save_to_checkpoint(checkpoint_dir))
|
||||
|
||||
def assign_embedding_model(self):
|
||||
pass
|
||||
@@ -130,28 +81,44 @@ class LlamaIndexRagWorker(SaveLoad):
|
||||
# This function is for debugging
|
||||
self.vs_index.storage_context.index_store.to_dict()
|
||||
docstore = self.vs_index.storage_context.docstore.docs
|
||||
vector_store_preview = "\n".join([f"{_id} | {tn.text}" for _id, tn in docstore.items()])
|
||||
vector_store_preview = "\n".join([ f"{_id} | {tn.text}" for _id, tn in docstore.items() ])
|
||||
logger.info('\n++ --------inspect_vector_store begin--------')
|
||||
logger.info(vector_store_preview)
|
||||
logger.info('oo --------inspect_vector_store end--------')
|
||||
return vector_store_preview
|
||||
|
||||
def add_documents_to_vector_store(self, document_list):
|
||||
documents = [Document(text=t) for t in document_list]
|
||||
def add_documents_to_vector_store(self, document_list: List[Document]):
|
||||
"""
|
||||
Adds a list of Document objects to the vector store after processing.
|
||||
"""
|
||||
documents = document_list
|
||||
documents_nodes = run_transformations(
|
||||
documents, # type: ignore
|
||||
self.vs_index._transformations,
|
||||
show_progress=True
|
||||
)
|
||||
self.vs_index.insert_nodes(documents_nodes)
|
||||
if self.debug_mode: self.inspect_vector_store()
|
||||
if self.debug_mode:
|
||||
self.inspect_vector_store()
|
||||
|
||||
def add_text_to_vector_store(self, text: str):
|
||||
node = TextNode(text=text)
|
||||
documents_nodes = run_transformations(
|
||||
[node],
|
||||
self.vs_index._transformations,
|
||||
show_progress=True
|
||||
)
|
||||
self.vs_index.insert_nodes(documents_nodes)
|
||||
if self.debug_mode:
|
||||
self.inspect_vector_store()
|
||||
|
||||
def remember_qa(self, question, answer):
|
||||
formatted_str = QUESTION_ANSWER_RECORD.format(question=question, answer=answer)
|
||||
self.add_text_to_vector_store(formatted_str)
|
||||
|
||||
def retrieve_from_store_with_query(self, query):
|
||||
if self.debug_mode: self.inspect_vector_store()
|
||||
if self.debug_mode:
|
||||
self.inspect_vector_store()
|
||||
retriever = self.vs_index.as_retriever()
|
||||
return retriever.retrieve(query)
|
||||
|
||||
@@ -160,6 +127,12 @@ class LlamaIndexRagWorker(SaveLoad):
|
||||
return DEFAULT_QUERY_GENERATION_PROMPT.format(context_str=context_str, query_str=query)
|
||||
|
||||
def generate_node_array_preview(self, nodes):
|
||||
buf = "\n".join(([f"(No.{i + 1} | score {n.score:.3f}): {n.text}" for i, n in enumerate(nodes)]))
|
||||
buf = "\n".join(([f"(No.{i+1} | score {n.score:.3f}): {n.text}" for i, n in enumerate(nodes)]))
|
||||
if self.debug_mode: logger.info(buf)
|
||||
return buf
|
||||
|
||||
def purge_vector_store(self):
|
||||
"""
|
||||
Purges the current vector store and creates a new one.
|
||||
"""
|
||||
self.purge()
|
||||
@@ -1,14 +1,20 @@
|
||||
import atexit
|
||||
import llama_index
|
||||
import os
|
||||
import atexit
|
||||
from typing import List
|
||||
|
||||
from loguru import logger
|
||||
from llama_index.core import Document
|
||||
from llama_index.core.schema import TextNode
|
||||
from request_llms.embed_models.openai_embed import OpenAiEmbeddingModel
|
||||
from shared_utils.connect_void_terminal import get_chat_default_kwargs
|
||||
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
|
||||
from crazy_functions.rag_fns.vector_store_index import GptacVectorStoreIndex
|
||||
from llama_index.core.ingestion import run_transformations
|
||||
from llama_index.core import PromptTemplate
|
||||
from llama_index.core.response_synthesizers import TreeSummarize
|
||||
from llama_index.core import StorageContext
|
||||
from llama_index.vector_stores.milvus import MilvusVectorStore
|
||||
from loguru import logger
|
||||
|
||||
from crazy_functions.rag_fns.llama_index_worker import LlamaIndexRagWorker
|
||||
from crazy_functions.rag_fns.vector_store_index import GptacVectorStoreIndex
|
||||
from request_llms.embed_models.openai_embed import OpenAiEmbeddingModel
|
||||
|
||||
DEFAULT_QUERY_GENERATION_PROMPT = """\
|
||||
Now, you have context information as below:
|
||||
@@ -59,19 +65,17 @@ class MilvusSaveLoad():
|
||||
|
||||
def create_new_vs(self, checkpoint_dir, overwrite=False):
|
||||
vector_store = MilvusVectorStore(
|
||||
uri=os.path.join(checkpoint_dir, "milvus_demo.db"),
|
||||
uri=os.path.join(checkpoint_dir, "milvus_demo.db"),
|
||||
dim=self.embed_model.embedding_dimension(),
|
||||
overwrite=overwrite
|
||||
)
|
||||
storage_context = StorageContext.from_defaults(vector_store=vector_store)
|
||||
index = GptacVectorStoreIndex.default_vector_store(storage_context=storage_context,
|
||||
embed_model=self.embed_model)
|
||||
index = GptacVectorStoreIndex.default_vector_store(storage_context=storage_context, embed_model=self.embed_model)
|
||||
return index
|
||||
|
||||
def purge(self):
|
||||
self.vs_index = self.create_new_vs(self.checkpoint_dir, overwrite=True)
|
||||
|
||||
|
||||
class MilvusRagWorker(MilvusSaveLoad, LlamaIndexRagWorker):
|
||||
|
||||
def __init__(self, user_name, llm_kwargs, auto_load_checkpoint=True, checkpoint_dir=None) -> None:
|
||||
@@ -92,7 +96,7 @@ class MilvusRagWorker(MilvusSaveLoad, LlamaIndexRagWorker):
|
||||
docstore = self.vs_index.storage_context.docstore.docs
|
||||
if not docstore.items():
|
||||
raise ValueError("cannot inspect")
|
||||
vector_store_preview = "\n".join([f"{_id} | {tn.text}" for _id, tn in docstore.items()])
|
||||
vector_store_preview = "\n".join([ f"{_id} | {tn.text}" for _id, tn in docstore.items() ])
|
||||
except:
|
||||
dummy_retrieve_res: List["NodeWithScore"] = self.vs_index.as_retriever().retrieve(' ')
|
||||
vector_store_preview = "\n".join(
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
import os
|
||||
from llama_index.core import SimpleDirectoryReader
|
||||
|
||||
supports_format = ['.csv', '.docx', '.doc', '.epub', '.ipynb', '.mbox', '.md', '.pdf', '.txt', '.ppt',
|
||||
'.pptm', '.pptx', '.py', '.xls', '.xlsx', '.html', '.json', '.xml', '.yaml', '.yml', '.m']
|
||||
|
||||
|
||||
def read_docx_doc(file_path):
|
||||
if file_path.split(".")[-1] == "docx":
|
||||
from docx import Document
|
||||
doc = Document(file_path)
|
||||
file_content = "\n".join([para.text for para in doc.paragraphs])
|
||||
else:
|
||||
try:
|
||||
import win32com.client
|
||||
word = win32com.client.Dispatch("Word.Application")
|
||||
word.visible = False
|
||||
# 打开文件
|
||||
doc = word.Documents.Open(os.getcwd() + '/' + file_path)
|
||||
# file_content = doc.Content.Text
|
||||
doc = word.ActiveDocument
|
||||
file_content = doc.Range().Text
|
||||
doc.Close()
|
||||
word.Quit()
|
||||
except:
|
||||
raise RuntimeError('请先将.doc文档转换为.docx文档。')
|
||||
return file_content
|
||||
supports_format = ['.csv', '.docx', '.epub', '.ipynb', '.mbox', '.md', '.pdf', '.txt', '.ppt',
|
||||
'.pptm', '.pptx']
|
||||
|
||||
|
||||
# 修改后的 extract_text 函数,结合 SimpleDirectoryReader 和自定义解析逻辑
|
||||
import os
|
||||
|
||||
|
||||
def extract_text(file_path):
|
||||
_, ext = os.path.splitext(file_path.lower())
|
||||
|
||||
# 使用 SimpleDirectoryReader 处理它支持的文件格式
|
||||
if ext in ['.docx', '.doc']:
|
||||
return read_docx_doc(file_path)
|
||||
try:
|
||||
reader = SimpleDirectoryReader(input_files=[file_path])
|
||||
documents = reader.load_data()
|
||||
if len(documents) > 0:
|
||||
return documents[0].text
|
||||
except Exception as e:
|
||||
pass
|
||||
if ext in supports_format:
|
||||
try:
|
||||
reader = SimpleDirectoryReader(input_files=[file_path])
|
||||
documents = reader.load_data()
|
||||
if len(documents) > 0:
|
||||
return documents[0].text
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from llama_index.core.callbacks.base import CallbackManager
|
||||
from llama_index.core.schema import TransformComponent
|
||||
from llama_index.core.service_context import ServiceContext
|
||||
@@ -13,18 +13,18 @@ from llama_index.core.storage.storage_context import StorageContext
|
||||
|
||||
|
||||
class GptacVectorStoreIndex(VectorStoreIndex):
|
||||
|
||||
|
||||
@classmethod
|
||||
def default_vector_store(
|
||||
cls,
|
||||
storage_context: Optional[StorageContext] = None,
|
||||
show_progress: bool = False,
|
||||
callback_manager: Optional[CallbackManager] = None,
|
||||
transformations: Optional[List[TransformComponent]] = None,
|
||||
# deprecated
|
||||
service_context: Optional[ServiceContext] = None,
|
||||
embed_model=None,
|
||||
**kwargs: Any,
|
||||
cls,
|
||||
storage_context: Optional[StorageContext] = None,
|
||||
show_progress: bool = False,
|
||||
callback_manager: Optional[CallbackManager] = None,
|
||||
transformations: Optional[List[TransformComponent]] = None,
|
||||
# deprecated
|
||||
service_context: Optional[ServiceContext] = None,
|
||||
embed_model = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Create index from documents.
|
||||
|
||||
@@ -36,14 +36,15 @@ class GptacVectorStoreIndex(VectorStoreIndex):
|
||||
storage_context = storage_context or StorageContext.from_defaults()
|
||||
docstore = storage_context.docstore
|
||||
callback_manager = (
|
||||
callback_manager
|
||||
or callback_manager_from_settings_or_context(Settings, service_context)
|
||||
callback_manager
|
||||
or callback_manager_from_settings_or_context(Settings, service_context)
|
||||
)
|
||||
transformations = transformations or transformations_from_settings_or_context(
|
||||
Settings, service_context
|
||||
)
|
||||
|
||||
with callback_manager.as_trace("index_construction"):
|
||||
|
||||
return cls(
|
||||
nodes=[],
|
||||
storage_context=storage_context,
|
||||
@@ -54,3 +55,4 @@ class GptacVectorStoreIndex(VectorStoreIndex):
|
||||
embed_model=embed_model,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ def similarity_search_with_score_by_vector(
|
||||
self, embedding: List[float], k: int = 4
|
||||
) -> List[Tuple[Document, float]]:
|
||||
|
||||
def seperate_list(ls: List[int]) -> List[List[int]]:
|
||||
def separate_list(ls: List[int]) -> List[List[int]]:
|
||||
lists = []
|
||||
ls1 = [ls[0]]
|
||||
for i in range(1, len(ls)):
|
||||
@@ -82,7 +82,7 @@ def similarity_search_with_score_by_vector(
|
||||
continue
|
||||
_id = self.index_to_docstore_id[i]
|
||||
doc = self.docstore.search(_id)
|
||||
if not self.chunk_conent:
|
||||
if not self.chunk_content:
|
||||
if not isinstance(doc, Document):
|
||||
raise ValueError(f"Could not find document for id {_id}, got {doc}")
|
||||
doc.metadata["score"] = int(scores[0][j])
|
||||
@@ -104,12 +104,12 @@ def similarity_search_with_score_by_vector(
|
||||
id_set.add(l)
|
||||
if break_flag:
|
||||
break
|
||||
if not self.chunk_conent:
|
||||
if not self.chunk_content:
|
||||
return docs
|
||||
if len(id_set) == 0 and self.score_threshold > 0:
|
||||
return []
|
||||
id_list = sorted(list(id_set))
|
||||
id_lists = seperate_list(id_list)
|
||||
id_lists = separate_list(id_list)
|
||||
for id_seq in id_lists:
|
||||
for id in id_seq:
|
||||
if id == id_seq[0]:
|
||||
@@ -132,7 +132,7 @@ class LocalDocQA:
|
||||
embeddings: object = None
|
||||
top_k: int = VECTOR_SEARCH_TOP_K
|
||||
chunk_size: int = CHUNK_SIZE
|
||||
chunk_conent: bool = True
|
||||
chunk_content: bool = True
|
||||
score_threshold: int = VECTOR_SEARCH_SCORE_THRESHOLD
|
||||
|
||||
def init_cfg(self,
|
||||
@@ -209,16 +209,16 @@ class LocalDocQA:
|
||||
|
||||
# query 查询内容
|
||||
# vs_path 知识库路径
|
||||
# chunk_conent 是否启用上下文关联
|
||||
# chunk_content 是否启用上下文关联
|
||||
# score_threshold 搜索匹配score阈值
|
||||
# vector_search_top_k 搜索知识库内容条数,默认搜索5条结果
|
||||
# chunk_sizes 匹配单段内容的连接上下文长度
|
||||
def get_knowledge_based_conent_test(self, query, vs_path, chunk_conent,
|
||||
def get_knowledge_based_content_test(self, query, vs_path, chunk_content,
|
||||
score_threshold=VECTOR_SEARCH_SCORE_THRESHOLD,
|
||||
vector_search_top_k=VECTOR_SEARCH_TOP_K, chunk_size=CHUNK_SIZE,
|
||||
text2vec=None):
|
||||
self.vector_store = FAISS.load_local(vs_path, text2vec)
|
||||
self.vector_store.chunk_conent = chunk_conent
|
||||
self.vector_store.chunk_content = chunk_content
|
||||
self.vector_store.score_threshold = score_threshold
|
||||
self.vector_store.chunk_size = chunk_size
|
||||
|
||||
@@ -241,7 +241,7 @@ class LocalDocQA:
|
||||
|
||||
|
||||
|
||||
def construct_vector_store(vs_id, vs_path, files, sentence_size, history, one_conent, one_content_segmentation, text2vec):
|
||||
def construct_vector_store(vs_id, vs_path, files, sentence_size, history, one_content, one_content_segmentation, text2vec):
|
||||
for file in files:
|
||||
assert os.path.exists(file), "输入文件不存在:" + file
|
||||
import nltk
|
||||
@@ -297,7 +297,7 @@ class knowledge_archive_interface():
|
||||
files=file_manifest,
|
||||
sentence_size=100,
|
||||
history=[],
|
||||
one_conent="",
|
||||
one_content="",
|
||||
one_content_segmentation="",
|
||||
text2vec = self.get_chinese_text2vec(),
|
||||
)
|
||||
@@ -319,19 +319,19 @@ class knowledge_archive_interface():
|
||||
files=[],
|
||||
sentence_size=100,
|
||||
history=[],
|
||||
one_conent="",
|
||||
one_content="",
|
||||
one_content_segmentation="",
|
||||
text2vec = self.get_chinese_text2vec(),
|
||||
)
|
||||
VECTOR_SEARCH_SCORE_THRESHOLD = 0
|
||||
VECTOR_SEARCH_TOP_K = 4
|
||||
CHUNK_SIZE = 512
|
||||
resp, prompt = self.qa_handle.get_knowledge_based_conent_test(
|
||||
resp, prompt = self.qa_handle.get_knowledge_based_content_test(
|
||||
query = txt,
|
||||
vs_path = self.kai_path,
|
||||
score_threshold=VECTOR_SEARCH_SCORE_THRESHOLD,
|
||||
vector_search_top_k=VECTOR_SEARCH_TOP_K,
|
||||
chunk_conent=True,
|
||||
chunk_content=True,
|
||||
chunk_size=CHUNK_SIZE,
|
||||
text2vec = self.get_chinese_text2vec(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from toolbox import update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui_latest_msg, disable_auto_promotion
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
||||
import copy, json, pickle, os, sys, time
|
||||
@@ -9,14 +9,14 @@ import copy, json, pickle, os, sys, time
|
||||
def read_avail_plugin_enum():
|
||||
from crazy_functional import get_crazy_functions
|
||||
plugin_arr = get_crazy_functions()
|
||||
# remove plugins with out explaination
|
||||
# remove plugins with out explanation
|
||||
plugin_arr = {k:v for k, v in plugin_arr.items() if ('Info' in v) and ('Function' in v)}
|
||||
plugin_arr_info = {"F_{:04d}".format(i):v["Info"] for i, v in enumerate(plugin_arr.values(), start=1)}
|
||||
plugin_arr_dict = {"F_{:04d}".format(i):v for i, v in enumerate(plugin_arr.values(), start=1)}
|
||||
plugin_arr_dict_parse = {"F_{:04d}".format(i):v for i, v in enumerate(plugin_arr.values(), start=1)}
|
||||
plugin_arr_dict_parse.update({f"F_{i}":v for i, v in enumerate(plugin_arr.values(), start=1)})
|
||||
prompt = json.dumps(plugin_arr_info, ensure_ascii=False, indent=2)
|
||||
prompt = "\n\nThe defination of PluginEnum:\nPluginEnum=" + prompt
|
||||
prompt = "\n\nThe definition of PluginEnum:\nPluginEnum=" + prompt
|
||||
return prompt, plugin_arr_dict, plugin_arr_dict_parse
|
||||
|
||||
def wrap_code(txt):
|
||||
@@ -55,7 +55,7 @@ def execute_plugin(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prom
|
||||
plugin_selection: str = Field(description="The most related plugin from one of the PluginEnum.", default="F_0000")
|
||||
reason_of_selection: str = Field(description="The reason why you should select this plugin.", default="This plugin satisfy user requirement most")
|
||||
# ⭐ ⭐ ⭐ 选择插件
|
||||
yield from update_ui_lastest_msg(lastmsg=f"正在执行任务: {txt}\n\n查找可用插件中...", chatbot=chatbot, history=history, delay=0)
|
||||
yield from update_ui_latest_msg(lastmsg=f"正在执行任务: {txt}\n\n查找可用插件中...", chatbot=chatbot, history=history, delay=0)
|
||||
gpt_json_io = GptJsonIO(Plugin)
|
||||
gpt_json_io.format_instructions = "The format of your output should be a json that can be parsed by json.loads.\n"
|
||||
gpt_json_io.format_instructions += """Output example: {"plugin_selection":"F_1234", "reason_of_selection":"F_1234 plugin satisfy user requirement most"}\n"""
|
||||
@@ -74,13 +74,13 @@ def execute_plugin(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prom
|
||||
msg += "请求的Prompt为:\n" + wrap_code(get_inputs_show_user(inputs, plugin_arr_enum_prompt))
|
||||
msg += "语言模型回复为:\n" + wrap_code(gpt_reply)
|
||||
msg += "\n但您可以尝试再试一次\n"
|
||||
yield from update_ui_lastest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
yield from update_ui_latest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
return
|
||||
if plugin_sel.plugin_selection not in plugin_arr_dict_parse:
|
||||
msg = f"抱歉, 找不到合适插件执行该任务, 或者{llm_kwargs['llm_model']}无法理解您的需求。"
|
||||
msg += f"语言模型{llm_kwargs['llm_model']}选择了不存在的插件:\n" + wrap_code(gpt_reply)
|
||||
msg += "\n但您可以尝试再试一次\n"
|
||||
yield from update_ui_lastest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
yield from update_ui_latest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
return
|
||||
|
||||
# ⭐ ⭐ ⭐ 确认插件参数
|
||||
@@ -90,7 +90,7 @@ def execute_plugin(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prom
|
||||
appendix_info = get_recent_file_prompt_support(chatbot)
|
||||
|
||||
plugin = plugin_arr_dict_parse[plugin_sel.plugin_selection]
|
||||
yield from update_ui_lastest_msg(lastmsg=f"正在执行任务: {txt}\n\n提取插件参数...", chatbot=chatbot, history=history, delay=0)
|
||||
yield from update_ui_latest_msg(lastmsg=f"正在执行任务: {txt}\n\n提取插件参数...", chatbot=chatbot, history=history, delay=0)
|
||||
class PluginExplicit(BaseModel):
|
||||
plugin_selection: str = plugin_sel.plugin_selection
|
||||
plugin_arg: str = Field(description="The argument of the plugin.", default="")
|
||||
@@ -109,6 +109,6 @@ def execute_plugin(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prom
|
||||
fn = plugin['Function']
|
||||
fn_name = fn.__name__
|
||||
msg = f'{llm_kwargs["llm_model"]}为您选择了插件: `{fn_name}`\n\n插件说明:{plugin["Info"]}\n\n插件参数:{plugin_sel.plugin_arg}\n\n假如偏离了您的要求,按停止键终止。'
|
||||
yield from update_ui_lastest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
yield from update_ui_latest_msg(lastmsg=msg, chatbot=chatbot, history=history, delay=2)
|
||||
yield from fn(plugin_sel.plugin_arg, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, -1)
|
||||
return
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from toolbox import update_ui_lastest_msg, get_conf
|
||||
from toolbox import update_ui_latest_msg, get_conf
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO
|
||||
import copy, json, pickle, os, sys
|
||||
@@ -9,7 +9,7 @@ import copy, json, pickle, os, sys
|
||||
def modify_configuration_hot(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_intention):
|
||||
ALLOW_RESET_CONFIG = get_conf('ALLOW_RESET_CONFIG')
|
||||
if not ALLOW_RESET_CONFIG:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"当前配置不允许被修改!如需激活本功能,请在config.py中设置ALLOW_RESET_CONFIG=True后重启软件。",
|
||||
chatbot=chatbot, history=history, delay=2
|
||||
)
|
||||
@@ -30,7 +30,7 @@ def modify_configuration_hot(txt, llm_kwargs, plugin_kwargs, chatbot, history, s
|
||||
new_option_value: str = Field(description="the new value of the option", default=None)
|
||||
|
||||
# ⭐ ⭐ ⭐ 分析用户意图
|
||||
yield from update_ui_lastest_msg(lastmsg=f"正在执行任务: {txt}\n\n读取新配置中", chatbot=chatbot, history=history, delay=0)
|
||||
yield from update_ui_latest_msg(lastmsg=f"正在执行任务: {txt}\n\n读取新配置中", chatbot=chatbot, history=history, delay=0)
|
||||
gpt_json_io = GptJsonIO(ModifyConfigurationIntention)
|
||||
inputs = "Analyze how to change configuration according to following user input, answer me with json: \n\n" + \
|
||||
">> " + txt.rstrip('\n').replace('\n','\n>> ') + '\n\n' + \
|
||||
@@ -44,11 +44,11 @@ def modify_configuration_hot(txt, llm_kwargs, plugin_kwargs, chatbot, history, s
|
||||
|
||||
ok = (explicit_conf in txt)
|
||||
if ok:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n新配置{explicit_conf}={user_intention.new_option_value}",
|
||||
chatbot=chatbot, history=history, delay=1
|
||||
)
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n新配置{explicit_conf}={user_intention.new_option_value}\n\n正在修改配置中",
|
||||
chatbot=chatbot, history=history, delay=2
|
||||
)
|
||||
@@ -57,25 +57,25 @@ def modify_configuration_hot(txt, llm_kwargs, plugin_kwargs, chatbot, history, s
|
||||
from toolbox import set_conf
|
||||
set_conf(explicit_conf, user_intention.new_option_value)
|
||||
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n配置修改完成,重新页面即可生效。", chatbot=chatbot, history=history, delay=1
|
||||
)
|
||||
else:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"失败,如果需要配置{explicit_conf},您需要明确说明并在指令中提到它。", chatbot=chatbot, history=history, delay=5
|
||||
)
|
||||
|
||||
def modify_configuration_reboot(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_intention):
|
||||
ALLOW_RESET_CONFIG = get_conf('ALLOW_RESET_CONFIG')
|
||||
if not ALLOW_RESET_CONFIG:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"当前配置不允许被修改!如需激活本功能,请在config.py中设置ALLOW_RESET_CONFIG=True后重启软件。",
|
||||
chatbot=chatbot, history=history, delay=2
|
||||
)
|
||||
return
|
||||
|
||||
yield from modify_configuration_hot(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_intention)
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n配置修改完成,五秒后即将重启!若出现报错请无视即可。", chatbot=chatbot, history=history, delay=5
|
||||
)
|
||||
os.execl(sys.executable, sys.executable, *sys.argv)
|
||||
|
||||
@@ -5,7 +5,7 @@ class VoidTerminalState():
|
||||
self.reset_state()
|
||||
|
||||
def reset_state(self):
|
||||
self.has_provided_explaination = False
|
||||
self.has_provided_explanation = False
|
||||
|
||||
def lock_plugin(self, chatbot):
|
||||
chatbot._cookies['lock_plugin'] = 'crazy_functions.虚空终端->虚空终端'
|
||||
|
||||
2706
crazy_functions/word_dfa/dfa_algo.py
Normal file
2706
crazy_functions/word_dfa/dfa_algo.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
from toolbox import CatchException, update_ui, update_ui_lastest_msg
|
||||
from toolbox import CatchException, update_ui, update_ui_latest_msg
|
||||
from crazy_functions.multi_stage.multi_stage_utils import GptAcademicGameBaseState
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
|
||||
@@ -15,7 +15,7 @@ Testing:
|
||||
|
||||
|
||||
from toolbox import CatchException, update_ui, gen_time_str, trimmed_format_exc, is_the_upload_folder
|
||||
from toolbox import promote_file_to_downloadzone, get_log_folder, update_ui_lastest_msg
|
||||
from toolbox import promote_file_to_downloadzone, get_log_folder, update_ui_latest_msg
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive, get_plugin_arg
|
||||
from crazy_functions.crazy_utils import input_clipping, try_install_deps
|
||||
from crazy_functions.gen_fns.gen_fns_shared import is_function_successfully_generated
|
||||
@@ -27,7 +27,7 @@ import time
|
||||
import glob
|
||||
import multiprocessing
|
||||
|
||||
templete = """
|
||||
template = """
|
||||
```python
|
||||
import ... # Put dependencies here, e.g. import numpy as np.
|
||||
|
||||
@@ -77,10 +77,10 @@ def gpt_interact_multi_step(txt, file_type, llm_kwargs, chatbot, history):
|
||||
|
||||
# 第二步
|
||||
prompt_compose = [
|
||||
"If previous stage is successful, rewrite the function you have just written to satisfy following templete: \n",
|
||||
templete
|
||||
"If previous stage is successful, rewrite the function you have just written to satisfy following template: \n",
|
||||
template
|
||||
]
|
||||
i_say = "".join(prompt_compose); inputs_show_user = "If previous stage is successful, rewrite the function you have just written to satisfy executable templete. "
|
||||
i_say = "".join(prompt_compose); inputs_show_user = "If previous stage is successful, rewrite the function you have just written to satisfy executable template. "
|
||||
gpt_say = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||
inputs=i_say, inputs_show_user=inputs_show_user,
|
||||
llm_kwargs=llm_kwargs, chatbot=chatbot, history=history,
|
||||
@@ -164,18 +164,18 @@ def 函数动态生成(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_
|
||||
if get_plugin_arg(plugin_kwargs, key="file_path_arg", default=False):
|
||||
file_path = get_plugin_arg(plugin_kwargs, key="file_path_arg", default=None)
|
||||
file_list.append(file_path)
|
||||
yield from update_ui_lastest_msg(f"当前文件: {file_path}", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg(f"当前文件: {file_path}", chatbot, history, 1)
|
||||
elif have_any_recent_upload_files(chatbot):
|
||||
file_dir = get_recent_file_prompt_support(chatbot)
|
||||
file_list = glob.glob(os.path.join(file_dir, '**/*'), recursive=True)
|
||||
yield from update_ui_lastest_msg(f"当前文件处理列表: {file_list}", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg(f"当前文件处理列表: {file_list}", chatbot, history, 1)
|
||||
else:
|
||||
chatbot.append(["文件检索", "没有发现任何近期上传的文件。"])
|
||||
yield from update_ui_lastest_msg("没有发现任何近期上传的文件。", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg("没有发现任何近期上传的文件。", chatbot, history, 1)
|
||||
return # 2. 如果没有文件
|
||||
if len(file_list) == 0:
|
||||
chatbot.append(["文件检索", "没有发现任何近期上传的文件。"])
|
||||
yield from update_ui_lastest_msg("没有发现任何近期上传的文件。", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg("没有发现任何近期上传的文件。", chatbot, history, 1)
|
||||
return # 2. 如果没有文件
|
||||
|
||||
# 读取文件
|
||||
@@ -183,7 +183,7 @@ def 函数动态生成(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_
|
||||
|
||||
# 粗心检查
|
||||
if is_the_upload_folder(txt):
|
||||
yield from update_ui_lastest_msg(f"请在输入框内填写需求, 然后再次点击该插件! 至于您的文件,不用担心, 文件路径 {txt} 已经被记忆. ", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg(f"请在输入框内填写需求, 然后再次点击该插件! 至于您的文件,不用担心, 文件路径 {txt} 已经被记忆. ", chatbot, history, 1)
|
||||
return
|
||||
|
||||
# 开始干正事
|
||||
@@ -195,7 +195,7 @@ def 函数动态生成(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_
|
||||
code, installation_advance, txt, file_type, llm_kwargs, chatbot, history = \
|
||||
yield from gpt_interact_multi_step(txt, file_type, llm_kwargs, chatbot, history)
|
||||
chatbot.append(["代码生成阶段结束", ""])
|
||||
yield from update_ui_lastest_msg(f"正在验证上述代码的有效性 ...", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg(f"正在验证上述代码的有效性 ...", chatbot, history, 1)
|
||||
# ⭐ 分离代码块
|
||||
code = get_code_block(code)
|
||||
# ⭐ 检查模块
|
||||
@@ -206,11 +206,11 @@ def 函数动态生成(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_
|
||||
if not traceback: traceback = trimmed_format_exc()
|
||||
# 处理异常
|
||||
if not traceback: traceback = trimmed_format_exc()
|
||||
yield from update_ui_lastest_msg(f"第 {j+1}/{MAX_TRY} 次代码生成尝试, 失败了~ 别担心, 我们5秒后再试一次... \n\n此次我们的错误追踪是\n```\n{traceback}\n```\n", chatbot, history, 5)
|
||||
yield from update_ui_latest_msg(f"第 {j+1}/{MAX_TRY} 次代码生成尝试, 失败了~ 别担心, 我们5秒后再试一次... \n\n此次我们的错误追踪是\n```\n{traceback}\n```\n", chatbot, history, 5)
|
||||
|
||||
# 代码生成结束, 开始执行
|
||||
TIME_LIMIT = 15
|
||||
yield from update_ui_lastest_msg(f"开始创建新进程并执行代码! 时间限制 {TIME_LIMIT} 秒. 请等待任务完成... ", chatbot, history, 1)
|
||||
yield from update_ui_latest_msg(f"开始创建新进程并执行代码! 时间限制 {TIME_LIMIT} 秒. 请等待任务完成... ", chatbot, history, 1)
|
||||
manager = multiprocessing.Manager()
|
||||
return_dict = manager.dict()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import time
|
||||
from toolbox import CatchException, update_ui, gen_time_str, trimmed_format_exc, ProxyNetworkActivate
|
||||
from toolbox import get_conf, select_api_key, update_ui_lastest_msg, Singleton
|
||||
from toolbox import get_conf, select_api_key, update_ui_latest_msg, Singleton
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive, get_plugin_arg
|
||||
from crazy_functions.crazy_utils import input_clipping, try_install_deps
|
||||
from crazy_functions.agent_fns.persistent import GradioMultiuserManagerForPersistentClasses
|
||||
|
||||
127
crazy_functions/总结word文档.py
Normal file
127
crazy_functions/总结word文档.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from toolbox import update_ui
|
||||
from toolbox import CatchException, report_exception
|
||||
from toolbox import write_history_to_file, promote_file_to_downloadzone
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
fast_debug = False
|
||||
|
||||
|
||||
def 解析docx(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt):
|
||||
import time, os
|
||||
# pip install python-docx 用于docx格式,跨平台
|
||||
# pip install pywin32 用于doc格式,仅支持Win平台
|
||||
for index, fp in enumerate(file_manifest):
|
||||
if fp.split(".")[-1] == "docx":
|
||||
from docx import Document
|
||||
doc = Document(fp)
|
||||
file_content = "\n".join([para.text for para in doc.paragraphs])
|
||||
else:
|
||||
try:
|
||||
import win32com.client
|
||||
word = win32com.client.Dispatch("Word.Application")
|
||||
word.visible = False
|
||||
# 打开文件
|
||||
doc = word.Documents.Open(os.getcwd() + '/' + fp)
|
||||
# file_content = doc.Content.Text
|
||||
doc = word.ActiveDocument
|
||||
file_content = doc.Range().Text
|
||||
doc.Close()
|
||||
word.Quit()
|
||||
except:
|
||||
raise RuntimeError('请先将.doc文档转换为.docx文档。')
|
||||
|
||||
# private_upload里面的文件名在解压zip后容易出现乱码(rar和7z格式正常),故可以只分析文章内容,不输入文件名
|
||||
from crazy_functions.pdf_fns.breakdown_txt import breakdown_text_to_satisfy_token_limit
|
||||
from request_llms.bridge_all import model_info
|
||||
max_token = model_info[llm_kwargs['llm_model']]['max_token']
|
||||
TOKEN_LIMIT_PER_FRAGMENT = max_token * 3 // 4
|
||||
paper_fragments = breakdown_text_to_satisfy_token_limit(txt=file_content, limit=TOKEN_LIMIT_PER_FRAGMENT, llm_model=llm_kwargs['llm_model'])
|
||||
this_paper_history = []
|
||||
for i, paper_frag in enumerate(paper_fragments):
|
||||
i_say = f'请对下面的文章片段用中文做概述,文件名是{os.path.relpath(fp, project_folder)},文章内容是 ```{paper_frag}```'
|
||||
i_say_show_user = f'请对下面的文章片段做概述: {os.path.abspath(fp)}的第{i+1}/{len(paper_fragments)}个片段。'
|
||||
gpt_say = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||
inputs=i_say,
|
||||
inputs_show_user=i_say_show_user,
|
||||
llm_kwargs=llm_kwargs,
|
||||
chatbot=chatbot,
|
||||
history=[],
|
||||
sys_prompt="总结文章。"
|
||||
)
|
||||
|
||||
chatbot[-1] = (i_say_show_user, gpt_say)
|
||||
history.extend([i_say_show_user,gpt_say])
|
||||
this_paper_history.extend([i_say_show_user,gpt_say])
|
||||
|
||||
# 已经对该文章的所有片段总结完毕,如果文章被切分了,
|
||||
if len(paper_fragments) > 1:
|
||||
i_say = f"根据以上的对话,总结文章{os.path.abspath(fp)}的主要内容。"
|
||||
gpt_say = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||
inputs=i_say,
|
||||
inputs_show_user=i_say,
|
||||
llm_kwargs=llm_kwargs,
|
||||
chatbot=chatbot,
|
||||
history=this_paper_history,
|
||||
sys_prompt="总结文章。"
|
||||
)
|
||||
|
||||
history.extend([i_say,gpt_say])
|
||||
this_paper_history.extend([i_say,gpt_say])
|
||||
|
||||
res = write_history_to_file(history)
|
||||
promote_file_to_downloadzone(res, chatbot=chatbot)
|
||||
chatbot.append(("完成了吗?", res))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
res = write_history_to_file(history)
|
||||
promote_file_to_downloadzone(res, chatbot=chatbot)
|
||||
chatbot.append(("所有文件都总结完成了吗?", res))
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
|
||||
@CatchException
|
||||
def 总结word文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
||||
import glob, os
|
||||
|
||||
# 基本信息:功能、贡献者
|
||||
chatbot.append([
|
||||
"函数插件功能?",
|
||||
"批量总结Word文档。函数插件贡献者: JasonGuo1。注意, 如果是.doc文件, 请先转化为.docx格式。"])
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
|
||||
# 尝试导入依赖,如果缺少依赖,则给出安装建议
|
||||
try:
|
||||
from docx import Document
|
||||
except:
|
||||
report_exception(chatbot, history,
|
||||
a=f"解析项目: {txt}",
|
||||
b=f"导入软件依赖失败。使用该模块需要额外依赖,安装方法```pip install --upgrade python-docx pywin32```。")
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
return
|
||||
|
||||
# 清空历史,以免输入溢出
|
||||
history = []
|
||||
|
||||
# 检测输入参数,如没有给定输入参数,直接退出
|
||||
if os.path.exists(txt):
|
||||
project_folder = txt
|
||||
else:
|
||||
if txt == "": txt = '空空如也的输入栏'
|
||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"找不到本地项目或无权访问: {txt}")
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
return
|
||||
|
||||
# 搜索需要处理的文件清单
|
||||
if txt.endswith('.docx') or txt.endswith('.doc'):
|
||||
file_manifest = [txt]
|
||||
else:
|
||||
file_manifest = [f for f in glob.glob(f'{project_folder}/**/*.docx', recursive=True)] + \
|
||||
[f for f in glob.glob(f'{project_folder}/**/*.doc', recursive=True)]
|
||||
|
||||
# 如果没找到任何文件
|
||||
if len(file_manifest) == 0:
|
||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"找不到任何.docx或doc文件: {txt}")
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
return
|
||||
|
||||
# 开始正式执行任务
|
||||
yield from 解析docx(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
||||
@@ -1,496 +0,0 @@
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple, Dict, Generator
|
||||
|
||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||
from crazy_functions.pdf_fns.breakdown_txt import breakdown_text_to_satisfy_token_limit
|
||||
from crazy_functions.rag_fns.rag_file_support import extract_text
|
||||
from request_llms.bridge_all import model_info
|
||||
from toolbox import update_ui, CatchException, report_exception
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileFragment:
|
||||
"""文件片段数据类,用于组织处理单元"""
|
||||
file_path: str
|
||||
content: str
|
||||
rel_path: str
|
||||
fragment_index: int
|
||||
total_fragments: int
|
||||
|
||||
|
||||
class BatchDocumentSummarizer:
|
||||
"""优化的文档总结器 - 批处理版本"""
|
||||
|
||||
def __init__(self, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List, history: List, system_prompt: str):
|
||||
"""初始化总结器"""
|
||||
self.llm_kwargs = llm_kwargs
|
||||
self.plugin_kwargs = plugin_kwargs
|
||||
self.chatbot = chatbot
|
||||
self.history = history
|
||||
self.system_prompt = system_prompt
|
||||
self.failed_files = []
|
||||
self.file_summaries_map = {}
|
||||
|
||||
def _get_token_limit(self) -> int:
|
||||
"""获取模型token限制"""
|
||||
max_token = model_info[self.llm_kwargs['llm_model']]['max_token']
|
||||
return max_token * 3 // 4
|
||||
|
||||
def _create_batch_inputs(self, fragments: List[FileFragment]) -> Tuple[List, List, List]:
|
||||
"""创建批处理输入"""
|
||||
inputs_array = []
|
||||
inputs_show_user_array = []
|
||||
history_array = []
|
||||
|
||||
for frag in fragments:
|
||||
if self.plugin_kwargs.get("advanced_arg"):
|
||||
i_say = (f'请按照用户要求对文件内容进行处理,文件名为{os.path.basename(frag.file_path)},'
|
||||
f'用户要求为:{self.plugin_kwargs["advanced_arg"]}:'
|
||||
f'文件内容是 ```{frag.content}```')
|
||||
i_say_show_user = (f'正在处理 {frag.rel_path} (片段 {frag.fragment_index + 1}/{frag.total_fragments})')
|
||||
else:
|
||||
i_say = (f'请对下面的内容用中文做总结,不超过500字,文件名是{os.path.basename(frag.file_path)},'
|
||||
f'内容是 ```{frag.content}```')
|
||||
i_say_show_user = f'正在处理 {frag.rel_path} (片段 {frag.fragment_index + 1}/{frag.total_fragments})'
|
||||
|
||||
inputs_array.append(i_say)
|
||||
inputs_show_user_array.append(i_say_show_user)
|
||||
history_array.append([])
|
||||
|
||||
return inputs_array, inputs_show_user_array, history_array
|
||||
|
||||
def _process_single_file_with_timeout(self, file_info: Tuple[str, str], mutable_status: List) -> List[FileFragment]:
|
||||
"""包装了超时控制的文件处理函数"""
|
||||
|
||||
def timeout_handler():
|
||||
thread = threading.current_thread()
|
||||
if hasattr(thread, '_timeout_occurred'):
|
||||
thread._timeout_occurred = True
|
||||
|
||||
# 设置超时标记
|
||||
thread = threading.current_thread()
|
||||
thread._timeout_occurred = False
|
||||
|
||||
# 设置超时定时器
|
||||
timer = threading.Timer(self.watch_dog_patience, timeout_handler)
|
||||
timer.start()
|
||||
|
||||
try:
|
||||
fp, project_folder = file_info
|
||||
fragments = []
|
||||
|
||||
# 定期检查是否超时
|
||||
def check_timeout():
|
||||
if hasattr(thread, '_timeout_occurred') and thread._timeout_occurred:
|
||||
raise TimeoutError("处理超时")
|
||||
|
||||
# 更新状态
|
||||
mutable_status[0] = "检查文件大小"
|
||||
mutable_status[1] = time.time()
|
||||
check_timeout()
|
||||
|
||||
# 文件大小检查
|
||||
if os.path.getsize(fp) > self.max_file_size:
|
||||
self.failed_files.append((fp, f"文件过大:超过{self.max_file_size / 1024 / 1024}MB"))
|
||||
mutable_status[2] = "文件过大"
|
||||
return fragments
|
||||
|
||||
check_timeout()
|
||||
|
||||
# 更新状态
|
||||
mutable_status[0] = "提取文件内容"
|
||||
mutable_status[1] = time.time()
|
||||
|
||||
# 提取内容
|
||||
content = extract_text(fp)
|
||||
if content is None:
|
||||
self.failed_files.append((fp, "文件解析失败:不支持的格式或文件损坏"))
|
||||
mutable_status[2] = "格式不支持"
|
||||
return fragments
|
||||
elif not content.strip():
|
||||
self.failed_files.append((fp, "文件内容为空"))
|
||||
mutable_status[2] = "内容为空"
|
||||
return fragments
|
||||
|
||||
check_timeout()
|
||||
|
||||
# 更新状态
|
||||
mutable_status[0] = "分割文本"
|
||||
mutable_status[1] = time.time()
|
||||
|
||||
# 分割文本
|
||||
try:
|
||||
paper_fragments = breakdown_text_to_satisfy_token_limit(
|
||||
txt=content,
|
||||
limit=self._get_token_limit(),
|
||||
llm_model=self.llm_kwargs['llm_model']
|
||||
)
|
||||
except Exception as e:
|
||||
self.failed_files.append((fp, f"文本分割失败:{str(e)}"))
|
||||
mutable_status[2] = "分割失败"
|
||||
return fragments
|
||||
|
||||
check_timeout()
|
||||
|
||||
# 处理片段
|
||||
rel_path = os.path.relpath(fp, project_folder)
|
||||
for i, frag in enumerate(paper_fragments):
|
||||
if frag.strip():
|
||||
fragments.append(FileFragment(
|
||||
file_path=fp,
|
||||
content=frag,
|
||||
rel_path=rel_path,
|
||||
fragment_index=i,
|
||||
total_fragments=len(paper_fragments)
|
||||
))
|
||||
|
||||
mutable_status[2] = "处理完成"
|
||||
return fragments
|
||||
|
||||
except TimeoutError as e:
|
||||
self.failed_files.append((fp, "处理超时"))
|
||||
mutable_status[2] = "处理超时"
|
||||
return []
|
||||
except Exception as e:
|
||||
self.failed_files.append((fp, f"处理失败:{str(e)}"))
|
||||
mutable_status[2] = "处理异常"
|
||||
return []
|
||||
finally:
|
||||
timer.cancel()
|
||||
|
||||
def prepare_fragments(self, project_folder: str, file_paths: List[str]) -> Generator:
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Generator, List
|
||||
"""并行准备所有文件的处理片段"""
|
||||
all_fragments = []
|
||||
total_files = len(file_paths)
|
||||
|
||||
# 配置参数
|
||||
self.refresh_interval = 0.2 # UI刷新间隔
|
||||
self.watch_dog_patience = 5 # 看门狗超时时间
|
||||
self.max_file_size = 10 * 1024 * 1024 # 10MB限制
|
||||
self.max_workers = min(32, len(file_paths)) # 最多32个线程
|
||||
|
||||
# 创建有超时控制的线程池
|
||||
executor = ThreadPoolExecutor(max_workers=self.max_workers)
|
||||
|
||||
# 用于跨线程状态传递的可变列表 - 增加文件名信息
|
||||
mutable_status_array = [["等待中", time.time(), "pending", file_path] for file_path in file_paths]
|
||||
|
||||
# 创建文件处理任务
|
||||
file_infos = [(fp, project_folder) for fp in file_paths]
|
||||
|
||||
# 提交所有任务,使用带超时控制的处理函数
|
||||
futures = [
|
||||
executor.submit(
|
||||
self._process_single_file_with_timeout,
|
||||
file_info,
|
||||
mutable_status_array[i]
|
||||
) for i, file_info in enumerate(file_infos)
|
||||
]
|
||||
|
||||
# 更新UI的计数器
|
||||
cnt = 0
|
||||
|
||||
try:
|
||||
# 监控任务执行
|
||||
while True:
|
||||
time.sleep(self.refresh_interval)
|
||||
cnt += 1
|
||||
|
||||
# 检查任务完成状态
|
||||
worker_done = [f.done() for f in futures]
|
||||
|
||||
# 更新状态显示
|
||||
status_str = ""
|
||||
for i, (status, timestamp, desc, file_path) in enumerate(mutable_status_array):
|
||||
# 获取文件名(去掉路径)
|
||||
file_name = os.path.basename(file_path)
|
||||
if worker_done[i]:
|
||||
status_str += f"文件 {file_name}: {desc}\n"
|
||||
else:
|
||||
status_str += f"文件 {file_name}: {status} {desc}\n"
|
||||
|
||||
# 更新UI
|
||||
self.chatbot[-1] = [
|
||||
"处理进度",
|
||||
f"正在处理文件...\n\n{status_str}" + "." * (cnt % 10 + 1)
|
||||
]
|
||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||
|
||||
# 检查是否所有任务完成
|
||||
if all(worker_done):
|
||||
break
|
||||
|
||||
finally:
|
||||
# 确保线程池正确关闭
|
||||
executor.shutdown(wait=False)
|
||||
|
||||
# 收集结果
|
||||
processed_files = 0
|
||||
for future in futures:
|
||||
try:
|
||||
fragments = future.result(timeout=0.1) # 给予一个短暂的超时时间来获取结果
|
||||
all_fragments.extend(fragments)
|
||||
processed_files += 1
|
||||
except concurrent.futures.TimeoutError:
|
||||
# 处理获取结果超时
|
||||
file_index = futures.index(future)
|
||||
self.failed_files.append((file_paths[file_index], "结果获取超时"))
|
||||
continue
|
||||
except Exception as e:
|
||||
# 处理其他异常
|
||||
file_index = futures.index(future)
|
||||
self.failed_files.append((file_paths[file_index], f"未知错误:{str(e)}"))
|
||||
continue
|
||||
|
||||
# 最终进度更新
|
||||
self.chatbot.append([
|
||||
"文件处理完成",
|
||||
f"成功处理 {len(all_fragments)} 个片段,失败 {len(self.failed_files)} 个文件"
|
||||
])
|
||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||
|
||||
return all_fragments
|
||||
|
||||
def _process_fragments_batch(self, fragments: List[FileFragment]) -> Generator:
|
||||
"""批量处理文件片段"""
|
||||
from collections import defaultdict
|
||||
batch_size = 64 # 每批处理的片段数
|
||||
max_retries = 3 # 最大重试次数
|
||||
retry_delay = 5 # 重试延迟(秒)
|
||||
results = defaultdict(list)
|
||||
|
||||
# 按批次处理
|
||||
for i in range(0, len(fragments), batch_size):
|
||||
batch = fragments[i:i + batch_size]
|
||||
|
||||
inputs_array, inputs_show_user_array, history_array = self._create_batch_inputs(batch)
|
||||
sys_prompt_array = ["请总结以下内容:"] * len(batch)
|
||||
|
||||
# 添加重试机制
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
response_collection = yield from request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
inputs_array=inputs_array,
|
||||
inputs_show_user_array=inputs_show_user_array,
|
||||
llm_kwargs=self.llm_kwargs,
|
||||
chatbot=self.chatbot,
|
||||
history_array=history_array,
|
||||
sys_prompt_array=sys_prompt_array,
|
||||
)
|
||||
|
||||
# 处理响应
|
||||
for j, frag in enumerate(batch):
|
||||
summary = response_collection[j * 2 + 1]
|
||||
if summary and summary.strip():
|
||||
results[frag.rel_path].append({
|
||||
'index': frag.fragment_index,
|
||||
'summary': summary,
|
||||
'total': frag.total_fragments
|
||||
})
|
||||
break # 成功处理,跳出重试循环
|
||||
|
||||
except Exception as e:
|
||||
if retry == max_retries - 1: # 最后一次重试失败
|
||||
for frag in batch:
|
||||
self.failed_files.append((frag.file_path, f"处理失败:{str(e)}"))
|
||||
else:
|
||||
yield from update_ui(self.chatbot.append([f"批次处理失败,{retry_delay}秒后重试...", str(e)]))
|
||||
time.sleep(retry_delay)
|
||||
|
||||
return results
|
||||
|
||||
def _generate_final_summary_request(self) -> Tuple[List, List, List]:
|
||||
"""准备最终总结请求"""
|
||||
if not self.file_summaries_map:
|
||||
return (["无可用的文件总结"], ["生成最终总结"], [[]])
|
||||
|
||||
summaries = list(self.file_summaries_map.values())
|
||||
if all(not summary for summary in summaries):
|
||||
return (["所有文件处理均失败"], ["生成最终总结"], [[]])
|
||||
|
||||
if self.plugin_kwargs.get("advanced_arg"):
|
||||
i_say = "根据以上所有文件的处理结果,按要求进行综合处理:" + self.plugin_kwargs['advanced_arg']
|
||||
else:
|
||||
i_say = "请根据以上所有文件的处理结果,生成最终的总结,不超过1000字。"
|
||||
|
||||
return ([i_say], [i_say], [summaries])
|
||||
|
||||
def process_files(self, project_folder: str, file_paths: List[str]) -> Generator:
|
||||
"""处理所有文件"""
|
||||
total_files = len(file_paths)
|
||||
self.chatbot.append([f"开始处理", f"总计 {total_files} 个文件"])
|
||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||
|
||||
# 1. 准备所有文件片段
|
||||
# 在 process_files 函数中:
|
||||
fragments = yield from self.prepare_fragments(project_folder, file_paths)
|
||||
if not fragments:
|
||||
self.chatbot.append(["处理失败", "没有可处理的文件内容"])
|
||||
return "没有可处理的文件内容"
|
||||
|
||||
# 2. 批量处理所有文件片段
|
||||
self.chatbot.append([f"文件分析", f"共计 {len(fragments)} 个处理单元"])
|
||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||
|
||||
try:
|
||||
file_summaries = yield from self._process_fragments_batch(fragments)
|
||||
except Exception as e:
|
||||
self.chatbot.append(["处理错误", f"批处理过程失败:{str(e)}"])
|
||||
return "处理过程发生错误"
|
||||
|
||||
# 3. 为每个文件生成整体总结
|
||||
self.chatbot.append(["生成总结", "正在汇总文件内容..."])
|
||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||
|
||||
# 处理每个文件的总结
|
||||
for rel_path, summaries in file_summaries.items():
|
||||
if len(summaries) > 1: # 多片段文件需要生成整体总结
|
||||
sorted_summaries = sorted(summaries, key=lambda x: x['index'])
|
||||
if self.plugin_kwargs.get("advanced_arg"):
|
||||
|
||||
i_say = f'请按照用户要求对文件内容进行处理,用户要求为:{self.plugin_kwargs["advanced_arg"]}:'
|
||||
else:
|
||||
i_say = f"请总结文件 {os.path.basename(rel_path)} 的主要内容,不超过500字。"
|
||||
|
||||
try:
|
||||
summary_texts = [s['summary'] for s in sorted_summaries]
|
||||
response_collection = yield from request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
inputs_array=[i_say],
|
||||
inputs_show_user_array=[f"生成 {rel_path} 的处理结果"],
|
||||
llm_kwargs=self.llm_kwargs,
|
||||
chatbot=self.chatbot,
|
||||
history_array=[summary_texts],
|
||||
sys_prompt_array=["你是一个优秀的助手,"],
|
||||
)
|
||||
self.file_summaries_map[rel_path] = response_collection[1]
|
||||
except Exception as e:
|
||||
self.chatbot.append(["警告", f"文件 {rel_path} 总结生成失败:{str(e)}"])
|
||||
self.file_summaries_map[rel_path] = "总结生成失败"
|
||||
else: # 单片段文件直接使用其唯一的总结
|
||||
self.file_summaries_map[rel_path] = summaries[0]['summary']
|
||||
|
||||
# 4. 生成最终总结
|
||||
if total_files ==1:
|
||||
return "文件数为1,此时不调用总结模块"
|
||||
else:
|
||||
try:
|
||||
# 收集所有文件的总结用于生成最终总结
|
||||
file_summaries_for_final = []
|
||||
for rel_path, summary in self.file_summaries_map.items():
|
||||
file_summaries_for_final.append(f"文件 {rel_path} 的总结:\n{summary}")
|
||||
|
||||
if self.plugin_kwargs.get("advanced_arg"):
|
||||
final_summary_prompt = ("根据以下所有文件的总结内容,按要求进行综合处理:" +
|
||||
self.plugin_kwargs['advanced_arg'])
|
||||
else:
|
||||
final_summary_prompt = "请根据以下所有文件的总结内容,生成最终的总结报告。"
|
||||
|
||||
response_collection = yield from request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||
inputs_array=[final_summary_prompt],
|
||||
inputs_show_user_array=["生成最终总结报告"],
|
||||
llm_kwargs=self.llm_kwargs,
|
||||
chatbot=self.chatbot,
|
||||
history_array=[file_summaries_for_final],
|
||||
sys_prompt_array=["总结所有文件内容。"],
|
||||
max_workers=1
|
||||
)
|
||||
|
||||
return response_collection[1] if len(response_collection) > 1 else "生成总结失败"
|
||||
except Exception as e:
|
||||
self.chatbot.append(["错误", f"最终总结生成失败:{str(e)}"])
|
||||
return "生成总结失败"
|
||||
|
||||
def save_results(self, final_summary: str):
|
||||
"""保存结果到文件"""
|
||||
from toolbox import promote_file_to_downloadzone, write_history_to_file
|
||||
from crazy_functions.doc_fns.batch_file_query_doc import MarkdownFormatter, HtmlFormatter, WordFormatter
|
||||
import os
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 创建各种格式化器
|
||||
md_formatter = MarkdownFormatter(final_summary, self.file_summaries_map, self.failed_files)
|
||||
html_formatter = HtmlFormatter(final_summary, self.file_summaries_map, self.failed_files)
|
||||
word_formatter = WordFormatter(final_summary, self.file_summaries_map, self.failed_files)
|
||||
|
||||
result_files = []
|
||||
|
||||
# 保存 Markdown
|
||||
md_content = md_formatter.create_document()
|
||||
result_file_md = write_history_to_file(
|
||||
history=[md_content], # 直接传入内容列表
|
||||
file_basename=f"文档总结_{timestamp}.md"
|
||||
)
|
||||
result_files.append(result_file_md)
|
||||
|
||||
# 保存 HTML
|
||||
html_content = html_formatter.create_document()
|
||||
result_file_html = write_history_to_file(
|
||||
history=[html_content],
|
||||
file_basename=f"文档总结_{timestamp}.html"
|
||||
)
|
||||
result_files.append(result_file_html)
|
||||
|
||||
# 保存 Word
|
||||
doc = word_formatter.create_document()
|
||||
# 由于 Word 文档需要用 doc.save(),我们使用与 md 文件相同的目录
|
||||
result_file_docx = os.path.join(
|
||||
os.path.dirname(result_file_md),
|
||||
f"文档总结_{timestamp}.docx"
|
||||
)
|
||||
doc.save(result_file_docx)
|
||||
result_files.append(result_file_docx)
|
||||
|
||||
# 添加到下载区
|
||||
for file in result_files:
|
||||
promote_file_to_downloadzone(file, chatbot=self.chatbot)
|
||||
|
||||
self.chatbot.append(["处理完成", f"结果已保存至: {', '.join(result_files)}"])
|
||||
@CatchException
|
||||
def 批量文件询问(txt: str, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List,
|
||||
history: List, system_prompt: str, user_request: str):
|
||||
"""主函数 - 优化版本"""
|
||||
# 初始化
|
||||
import glob
|
||||
import re
|
||||
from crazy_functions.rag_fns.rag_file_support import supports_format
|
||||
from toolbox import report_exception
|
||||
|
||||
summarizer = BatchDocumentSummarizer(llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
||||
chatbot.append(["函数插件功能", f"作者:lbykkkk,批量总结文件。支持格式: {', '.join(supports_format)}等其他文本格式文件,如果长时间卡在文件处理过程,请查看处理进度,然后删除所有处于“pending”状态的文件,然后重新上传处理。"])
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
|
||||
# 验证输入路径
|
||||
if not os.path.exists(txt):
|
||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"找不到项目或无权访问: {txt}")
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
# 获取文件列表
|
||||
project_folder = txt
|
||||
extract_folder = next((d for d in glob.glob(f'{project_folder}/*')
|
||||
if os.path.isdir(d) and d.endswith('.extract')), project_folder)
|
||||
|
||||
exclude_patterns = r'/[^/]+\.(zip|rar|7z|tar|gz)$'
|
||||
file_manifest = [f for f in glob.glob(f'{extract_folder}/**', recursive=True)
|
||||
if os.path.isfile(f) and not re.search(exclude_patterns, f)]
|
||||
|
||||
if not file_manifest:
|
||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b="未找到支持的文件类型")
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
return
|
||||
|
||||
# 处理所有文件并生成总结
|
||||
final_summary = yield from summarizer.process_files(project_folder, file_manifest)
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
|
||||
# 保存结果
|
||||
summarizer.save_results(final_summary)
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
@@ -1,5 +1,5 @@
|
||||
from toolbox import CatchException, report_exception, get_log_folder, gen_time_str
|
||||
from toolbox import update_ui, promote_file_to_downloadzone, update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui, promote_file_to_downloadzone, update_ui_latest_msg, disable_auto_promotion
|
||||
from toolbox import write_history_to_file, promote_file_to_downloadzone
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||
|
||||
@@ -166,7 +166,7 @@ class PointWithTrace(Scene):
|
||||
|
||||
```
|
||||
|
||||
# do not use get_graph, this funciton is deprecated
|
||||
# do not use get_graph, this function is deprecated
|
||||
|
||||
class ExampleFunctionGraph(Scene):
|
||||
def construct(self):
|
||||
|
||||
@@ -324,16 +324,16 @@ def 生成多种Mermaid图表(
|
||||
if os.path.exists(txt): # 如输入区无内容则直接解析历史记录
|
||||
from crazy_functions.pdf_fns.parse_word import extract_text_from_files
|
||||
|
||||
file_exist, final_result, page_one, file_manifest, excption = (
|
||||
file_exist, final_result, page_one, file_manifest, exception = (
|
||||
extract_text_from_files(txt, chatbot, history)
|
||||
)
|
||||
else:
|
||||
file_exist = False
|
||||
excption = ""
|
||||
exception = ""
|
||||
file_manifest = []
|
||||
|
||||
if excption != "":
|
||||
if excption == "word":
|
||||
if exception != "":
|
||||
if exception == "word":
|
||||
report_exception(
|
||||
chatbot,
|
||||
history,
|
||||
@@ -341,7 +341,7 @@ def 生成多种Mermaid图表(
|
||||
b=f"找到了.doc文件,但是该文件格式不被支持,请先转化为.docx格式。",
|
||||
)
|
||||
|
||||
elif excption == "pdf":
|
||||
elif exception == "pdf":
|
||||
report_exception(
|
||||
chatbot,
|
||||
history,
|
||||
@@ -349,7 +349,7 @@ def 生成多种Mermaid图表(
|
||||
b=f"导入软件依赖失败。使用该模块需要额外依赖,安装方法```pip install --upgrade pymupdf```。",
|
||||
)
|
||||
|
||||
elif excption == "word_pip":
|
||||
elif exception == "word_pip":
|
||||
report_exception(
|
||||
chatbot,
|
||||
history,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from toolbox import CatchException, update_ui, ProxyNetworkActivate, update_ui_lastest_msg, get_log_folder, get_user
|
||||
from toolbox import CatchException, update_ui, ProxyNetworkActivate, update_ui_latest_msg, get_log_folder, get_user
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive, get_files_from_everything
|
||||
from loguru import logger
|
||||
install_msg ="""
|
||||
@@ -42,7 +42,7 @@ def 知识库文件注入(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
# from crazy_functions.crazy_utils import try_install_deps
|
||||
# try_install_deps(['zh_langchain==0.2.1', 'pypinyin'], reload_m=['pypinyin', 'zh_langchain'])
|
||||
# yield from update_ui_lastest_msg("安装完成,您可以再次重试。", chatbot, history)
|
||||
# yield from update_ui_latest_msg("安装完成,您可以再次重试。", chatbot, history)
|
||||
return
|
||||
|
||||
# < --------------------读取文件--------------- >
|
||||
@@ -95,7 +95,7 @@ def 读取知识库作答(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||
# from crazy_functions.crazy_utils import try_install_deps
|
||||
# try_install_deps(['zh_langchain==0.2.1', 'pypinyin'], reload_m=['pypinyin', 'zh_langchain'])
|
||||
# yield from update_ui_lastest_msg("安装完成,您可以再次重试。", chatbot, history)
|
||||
# yield from update_ui_latest_msg("安装完成,您可以再次重试。", chatbot, history)
|
||||
return
|
||||
|
||||
# < ------------------- --------------- >
|
||||
|
||||
@@ -47,7 +47,7 @@ explain_msg = """
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from toolbox import CatchException, update_ui, is_the_upload_folder
|
||||
from toolbox import update_ui_lastest_msg, disable_auto_promotion
|
||||
from toolbox import update_ui_latest_msg, disable_auto_promotion
|
||||
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from crazy_functions.crazy_utils import input_clipping
|
||||
@@ -113,19 +113,19 @@ def 虚空终端(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt
|
||||
# 用简单的关键词检测用户意图
|
||||
is_certain, _ = analyze_intention_with_simple_rules(txt)
|
||||
if is_the_upload_folder(txt):
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explaination', value=False)
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explanation', value=False)
|
||||
appendix_msg = "\n\n**很好,您已经上传了文件**,现在请您描述您的需求。"
|
||||
|
||||
if is_certain or (state.has_provided_explaination):
|
||||
if is_certain or (state.has_provided_explanation):
|
||||
# 如果意图明确,跳过提示环节
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explaination', value=True)
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explanation', value=True)
|
||||
state.unlock_plugin(chatbot=chatbot)
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
yield from 虚空终端主路由(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
||||
return
|
||||
else:
|
||||
# 如果意图模糊,提示
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explaination', value=True)
|
||||
state.set_state(chatbot=chatbot, key='has_provided_explanation', value=True)
|
||||
state.lock_plugin(chatbot=chatbot)
|
||||
chatbot.append(("虚空终端状态:", explain_msg+appendix_msg))
|
||||
yield from update_ui(chatbot=chatbot, history=history)
|
||||
@@ -141,7 +141,7 @@ def 虚空终端主路由(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
# ⭐ ⭐ ⭐ 分析用户意图
|
||||
is_certain, user_intention = analyze_intention_with_simple_rules(txt)
|
||||
if not is_certain:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n分析用户意图中", chatbot=chatbot, history=history, delay=0)
|
||||
gpt_json_io = GptJsonIO(UserIntention)
|
||||
rf_req = "\nchoose from ['ModifyConfiguration', 'ExecutePlugin', 'Chat']"
|
||||
@@ -154,13 +154,13 @@ def 虚空终端主路由(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
user_intention = gpt_json_io.generate_output_auto_repair(analyze_res, run_gpt_fn)
|
||||
lastmsg=f"正在执行任务: {txt}\n\n用户意图理解: 意图={explain_intention_to_user[user_intention.intention_type]}",
|
||||
except JsonStringError as e:
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n用户意图理解: 失败 当前语言模型({llm_kwargs['llm_model']})不能理解您的意图", chatbot=chatbot, history=history, delay=0)
|
||||
return
|
||||
else:
|
||||
pass
|
||||
|
||||
yield from update_ui_lastest_msg(
|
||||
yield from update_ui_latest_msg(
|
||||
lastmsg=f"正在执行任务: {txt}\n\n用户意图理解: 意图={explain_intention_to_user[user_intention.intention_type]}",
|
||||
chatbot=chatbot, history=history, delay=0)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class AsyncGptTask():
|
||||
MAX_TOKEN_ALLO = 2560
|
||||
i_say, history = input_clipping(i_say, history, max_token_limit=MAX_TOKEN_ALLO)
|
||||
gpt_say_partial = predict_no_ui_long_connection(inputs=i_say, llm_kwargs=llm_kwargs, history=history, sys_prompt=sys_prompt,
|
||||
observe_window=observe_window[index], console_slience=True)
|
||||
observe_window=observe_window[index], console_silence=True)
|
||||
except ConnectionAbortedError as token_exceed_err:
|
||||
logger.error('至少一个线程任务Token溢出而失败', e)
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||
from toolbox import CatchException, report_exception, promote_file_to_downloadzone
|
||||
from toolbox import update_ui, update_ui_lastest_msg, disable_auto_promotion, write_history_to_file
|
||||
from toolbox import update_ui, update_ui_latest_msg, disable_auto_promotion, write_history_to_file
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
@@ -156,7 +156,7 @@ def 谷歌检索小助手(txt, llm_kwargs, plugin_kwargs, chatbot, history, syst
|
||||
history = []
|
||||
meta_paper_info_list = yield from get_meta_information(txt, chatbot, history)
|
||||
if len(meta_paper_info_list) == 0:
|
||||
yield from update_ui_lastest_msg(lastmsg='获取文献失败,可能触发了google反爬虫机制。',chatbot=chatbot, history=history, delay=0)
|
||||
yield from update_ui_latest_msg(lastmsg='获取文献失败,可能触发了google反爬虫机制。',chatbot=chatbot, history=history, delay=0)
|
||||
return
|
||||
batchsize = 5
|
||||
for batch in range(math.ceil(len(meta_paper_info_list)/batchsize)):
|
||||
|
||||
@@ -5,6 +5,10 @@ FROM fuqingxu/11.3.1-runtime-ubuntu20.04-with-texlive:latest
|
||||
|
||||
# edge-tts需要的依赖,某些pip包所需的依赖
|
||||
RUN apt update && apt install ffmpeg build-essential -y
|
||||
RUN apt-get install -y fontconfig
|
||||
RUN ln -s /usr/local/texlive/2023/texmf-dist/fonts/truetype /usr/share/fonts/truetype/texlive
|
||||
RUN fc-cache -fv
|
||||
RUN apt-get clean
|
||||
|
||||
# use python3 as the system default python
|
||||
WORKDIR /gpt
|
||||
@@ -30,7 +34,7 @@ RUN python3 -m pip install -r request_llms/requirements_qwen.txt
|
||||
RUN python3 -m pip install -r request_llms/requirements_chatglm.txt
|
||||
RUN python3 -m pip install -r request_llms/requirements_newbing.txt
|
||||
RUN python3 -m pip install nougat-ocr
|
||||
|
||||
RUN python3 -m pip cache purge
|
||||
|
||||
# 预热Tiktoken模块
|
||||
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
|
||||
@@ -7,6 +7,7 @@ RUN apt-get install -y git python python3 python-dev python3-dev --fix-missing
|
||||
|
||||
# edge-tts需要的依赖,某些pip包所需的依赖
|
||||
RUN apt update && apt install ffmpeg build-essential -y
|
||||
RUN apt-get clean
|
||||
|
||||
# use python3 as the system default python
|
||||
RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.8
|
||||
@@ -22,6 +23,7 @@ RUN python3 -m pip install -r request_llms/requirements_moss.txt
|
||||
RUN python3 -m pip install -r request_llms/requirements_qwen.txt
|
||||
RUN python3 -m pip install -r request_llms/requirements_chatglm.txt
|
||||
RUN python3 -m pip install -r request_llms/requirements_newbing.txt
|
||||
RUN python3 -m pip cache purge
|
||||
|
||||
|
||||
# 预热Tiktoken模块
|
||||
|
||||
@@ -18,5 +18,7 @@ RUN apt update && apt install ffmpeg -y
|
||||
# 可选步骤,用于预热模块
|
||||
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
|
||||
RUN python3 -m pip cache purge && apt-get clean
|
||||
|
||||
# 启动
|
||||
CMD ["python3", "-u", "main.py"]
|
||||
|
||||
@@ -30,5 +30,7 @@ COPY --chown=gptuser:gptuser . .
|
||||
# 可选步骤,用于预热模块
|
||||
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
|
||||
RUN python3 -m pip cache purge
|
||||
|
||||
# 启动
|
||||
CMD ["python3", "-u", "main.py"]
|
||||
|
||||
@@ -24,6 +24,8 @@ RUN apt update && apt install ffmpeg -y
|
||||
|
||||
# 可选步骤,用于预热模块
|
||||
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||
RUN python3 -m pip cache purge && apt-get clean
|
||||
|
||||
|
||||
# 启动
|
||||
CMD ["python3", "-u", "main.py"]
|
||||
|
||||
26
docs/WindowsRun.bat
Normal file
26
docs/WindowsRun.bat
Normal file
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
:: 设置环境变量
|
||||
set ENV_NAME=gpt
|
||||
set ENV_PATH=%~dp0%ENV_NAME%
|
||||
set SCRIPT_PATH=%~dp0main.py
|
||||
|
||||
:: 判断环境是否已解压
|
||||
if not exist "%ENV_PATH%" (
|
||||
echo Extracting environment...
|
||||
mkdir "%ENV_PATH%"
|
||||
tar -xzf gpt.tar.gz -C "%ENV_PATH%"
|
||||
|
||||
:: 运行conda环境激活脚本
|
||||
call "%ENV_PATH%\Scripts\activate.bat"
|
||||
) else (
|
||||
:: 如果环境已存在,直接激活
|
||||
call "%ENV_PATH%\Scripts\activate.bat"
|
||||
)
|
||||
echo Start to run program:
|
||||
:: 运行Python脚本
|
||||
python "%SCRIPT_PATH%"
|
||||
|
||||
endlocal
|
||||
pause
|
||||
@@ -1141,7 +1141,7 @@
|
||||
"内容太长了都会触发token数量溢出的错误": "An error of token overflow will be triggered if the content is too long",
|
||||
"chatbot 为WebUI中显示的对话列表": "chatbot is the conversation list displayed in WebUI",
|
||||
"修改它": "Modify it",
|
||||
"然后yeild出去": "Then yield it out",
|
||||
"然后yield出去": "Then yield it out",
|
||||
"可以直接修改对话界面内容": "You can directly modify the conversation interface content",
|
||||
"additional_fn代表点击的哪个按钮": "additional_fn represents which button is clicked",
|
||||
"按钮见functional.py": "See functional.py for buttons",
|
||||
@@ -1732,7 +1732,7 @@
|
||||
"或者重启之后再度尝试": "Or try again after restarting",
|
||||
"免费": "Free",
|
||||
"仅在Windows系统进行了测试": "Tested only on Windows system",
|
||||
"欢迎加REAME中的QQ联系开发者": "Feel free to contact the developer via QQ in REAME",
|
||||
"欢迎加README中的QQ联系开发者": "Feel free to contact the developer via QQ in README",
|
||||
"当前知识库内的有效文件": "Valid files in the current knowledge base",
|
||||
"您可以到Github Issue区": "You can go to the Github Issue area",
|
||||
"刷新Gradio前端界面": "Refresh the Gradio frontend interface",
|
||||
@@ -1759,7 +1759,7 @@
|
||||
"报错信息如下. 如果是与网络相关的问题": "Error message as follows. If it is related to network issues",
|
||||
"功能描述": "Function description",
|
||||
"禁止移除或修改此警告": "Removal or modification of this warning is prohibited",
|
||||
"Arixv翻译": "Arixv translation",
|
||||
"ArXiv翻译": "ArXiv translation",
|
||||
"读取优先级": "Read priority",
|
||||
"包含documentclass关键字": "Contains the documentclass keyword",
|
||||
"根据文本使用GPT模型生成相应的图像": "Generate corresponding images using GPT model based on the text",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"开始最终总结": "Start final summary",
|
||||
"openai的官方KEY需要伴随组织编码": "Openai's official KEY needs to be accompanied by organizational code",
|
||||
"将子线程的gpt结果写入chatbot": "Write the GPT result of the sub-thread into the chatbot",
|
||||
"Arixv论文精细翻译": "Fine translation of Arixv paper",
|
||||
"ArXiv论文精细翻译": "Fine translation of ArXiv paper",
|
||||
"开始接收chatglmft的回复": "Start receiving replies from chatglmft",
|
||||
"请先将.doc文档转换为.docx文档": "Please convert .doc documents to .docx documents first",
|
||||
"避免多用户干扰": "Avoid multiple user interference",
|
||||
@@ -2360,7 +2360,7 @@
|
||||
"请在config.py中设置ALLOW_RESET_CONFIG=True后重启软件": "Please set ALLOW_RESET_CONFIG=True in config.py and restart the software",
|
||||
"按照自然语言描述生成一个动画 | 输入参数是一段话": "Generate an animation based on natural language description | Input parameter is a sentence",
|
||||
"你的hf用户名如qingxu98": "Your hf username is qingxu98",
|
||||
"Arixv论文精细翻译 | 输入参数arxiv论文的ID": "Fine translation of Arixv paper | Input parameter is the ID of arxiv paper",
|
||||
"ArXiv论文精细翻译 | 输入参数arxiv论文的ID": "Fine translation of ArXiv paper | Input parameter is the ID of arxiv paper",
|
||||
"无法获取 abstract": "Unable to retrieve abstract",
|
||||
"尽可能地仅用一行命令解决我的要求": "Try to solve my request using only one command",
|
||||
"提取插件参数": "Extract plugin parameters",
|
||||
|
||||
@@ -753,7 +753,7 @@
|
||||
"手动指定和筛选源代码文件类型": "ソースコードファイルタイプを手動で指定およびフィルタリングする",
|
||||
"更多函数插件": "その他の関数プラグイン",
|
||||
"看门狗的耐心": "監視犬の忍耐力",
|
||||
"然后yeild出去": "そして出力する",
|
||||
"然后yield出去": "そして出力する",
|
||||
"拆分过长的IPynb文件": "長すぎるIPynbファイルを分割する",
|
||||
"1. 把input的余量留出来": "1. 入力の余裕を残す",
|
||||
"请求超时": "リクエストがタイムアウトしました",
|
||||
@@ -1803,7 +1803,7 @@
|
||||
"默认值为1000": "デフォルト値は1000です",
|
||||
"写出文件": "ファイルに書き出す",
|
||||
"生成的视频文件路径": "生成されたビデオファイルのパス",
|
||||
"Arixv论文精细翻译": "Arixv論文の詳細な翻訳",
|
||||
"ArXiv论文精细翻译": "ArXiv論文の詳細な翻訳",
|
||||
"用latex编译为PDF对修正处做高亮": "LaTeXでコンパイルしてPDFに修正をハイライトする",
|
||||
"点击“停止”键可终止程序": "「停止」ボタンをクリックしてプログラムを終了できます",
|
||||
"否则将导致每个人的Claude问询历史互相渗透": "さもないと、各人のClaudeの問い合わせ履歴が相互に侵入します",
|
||||
@@ -1987,7 +1987,7 @@
|
||||
"前面是中文逗号": "前面是中文逗号",
|
||||
"的依赖": "的依赖",
|
||||
"材料如下": "材料如下",
|
||||
"欢迎加REAME中的QQ联系开发者": "欢迎加REAME中的QQ联系开发者",
|
||||
"欢迎加README中的QQ联系开发者": "欢迎加README中的QQ联系开发者",
|
||||
"开始下载": "開始ダウンロード",
|
||||
"100字以内": "100文字以内",
|
||||
"创建request": "リクエストの作成",
|
||||
|
||||
@@ -771,7 +771,7 @@
|
||||
"查询代理的地理位置": "查詢代理的地理位置",
|
||||
"是否在输入过长时": "是否在輸入過長時",
|
||||
"chatGPT分析报告": "chatGPT分析報告",
|
||||
"然后yeild出去": "然後yield出去",
|
||||
"然后yield出去": "然後yield出去",
|
||||
"用户取消了程序": "使用者取消了程式",
|
||||
"琥珀色": "琥珀色",
|
||||
"这里是特殊函数插件的高级参数输入区": "這裡是特殊函數插件的高級參數輸入區",
|
||||
@@ -1587,7 +1587,7 @@
|
||||
"否则将导致每个人的Claude问询历史互相渗透": "否則將導致每個人的Claude問詢歷史互相滲透",
|
||||
"提问吧! 但注意": "提問吧!但注意",
|
||||
"待处理的word文档路径": "待處理的word文檔路徑",
|
||||
"欢迎加REAME中的QQ联系开发者": "歡迎加REAME中的QQ聯繫開發者",
|
||||
"欢迎加README中的QQ联系开发者": "歡迎加README中的QQ聯繫開發者",
|
||||
"建议暂时不要使用": "建議暫時不要使用",
|
||||
"Latex没有安装": "Latex沒有安裝",
|
||||
"在这里放一些网上搜集的demo": "在這裡放一些網上搜集的demo",
|
||||
@@ -1989,7 +1989,7 @@
|
||||
"请耐心等待": "請耐心等待",
|
||||
"在执行完成之后": "在執行完成之後",
|
||||
"参数简单": "參數簡單",
|
||||
"Arixv论文精细翻译": "Arixv論文精細翻譯",
|
||||
"ArXiv论文精细翻译": "ArXiv論文精細翻譯",
|
||||
"备份和下载": "備份和下載",
|
||||
"当前报错的latex代码处于第": "當前報錯的latex代碼處於第",
|
||||
"Markdown翻译": "Markdown翻譯",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user