Compare commits
1 Commits
516f5af00a
...
binary-hus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7db2d46063 |
@@ -1,6 +0,0 @@
|
|||||||
.venv
|
|
||||||
.github
|
|
||||||
.vscode
|
|
||||||
gpt_log
|
|
||||||
tests
|
|
||||||
README.md
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
|
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
|
||||||
name: build-with-latex-arm
|
name: build-with-all-capacity-beta
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- 'master'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}_with_latex_arm
|
IMAGE_NAME: ${{ github.repository }}_with_all_capacity_beta
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
@@ -18,17 +18,11 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -41,11 +35,10 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/arm64
|
file: docs/GithubAction+AllCapacityBeta
|
||||||
file: docs/GithubAction+NoLocal+Latex
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
44
.github/workflows/build-with-chatglm.yml
vendored
Normal file
44
.github/workflows/build-with-chatglm.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
|
||||||
|
name: build-with-chatglm
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}_chatglm_moss
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
file: docs/GithubAction+ChatGLM+Moss
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
44
.github/workflows/build-with-jittorllms.yml
vendored
Normal file
44
.github/workflows/build-with-jittorllms.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
|
||||||
|
name: build-with-jittorllms
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}_jittorllms
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
file: docs/GithubAction+JittorLLMs
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
56
.github/workflows/conda-pack-windows.yml
vendored
56
.github/workflows/conda-pack-windows.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
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'
|
name: 'Close stale issues and PRs'
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '*/30 * * * *'
|
- cron: '*/5 * * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
@@ -19,6 +19,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
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 7 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 1 days.'
|
||||||
days-before-stale: 100
|
days-before-stale: 100
|
||||||
days-before-close: 7
|
days-before-close: 1
|
||||||
|
debug-only: true
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -131,9 +131,6 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# macOS files
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
@@ -156,12 +153,3 @@ media
|
|||||||
flagged
|
flagged
|
||||||
request_llms/ChatGLM-6b-onnx-u8s8
|
request_llms/ChatGLM-6b-onnx-u8s8
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
test.*
|
|
||||||
temp.*
|
|
||||||
objdump*
|
|
||||||
*.min.*.js
|
|
||||||
TODO
|
|
||||||
experimental_mods
|
|
||||||
search_results
|
|
||||||
gg.docx
|
|
||||||
unstructured_reader.py
|
|
||||||
|
|||||||
27
Dockerfile
27
Dockerfile
@@ -3,38 +3,33 @@
|
|||||||
# - 如何构建: 先修改 `config.py`, 然后 `docker build -t gpt-academic . `
|
# - 如何构建: 先修改 `config.py`, 然后 `docker build -t gpt-academic . `
|
||||||
# - 如何运行(Linux下): `docker run --rm -it --net=host 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 `
|
# - 如何运行(其他操作系统,选择任意一个固定端口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源 (以下三行,可以删除)
|
# 非必要步骤,更换pip源 (以下三行,可以删除)
|
||||||
RUN echo '[global]' > /etc/pip.conf && \
|
RUN echo '[global]' > /etc/pip.conf && \
|
||||||
echo 'index-url = https://mirrors.aliyun.com/pypi/simple/' >> /etc/pip.conf && \
|
echo 'index-url = https://mirrors.aliyun.com/pypi/simple/' >> /etc/pip.conf && \
|
||||||
echo 'trusted-host = mirrors.aliyun.com' >> /etc/pip.conf
|
echo 'trusted-host = mirrors.aliyun.com' >> /etc/pip.conf
|
||||||
|
|
||||||
# 语音输出功能(以下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
|
WORKDIR /gpt
|
||||||
|
|
||||||
# 安装大部分依赖,利用Docker缓存加速以后的构建 (以下两行,可以删除)
|
|
||||||
|
# 安装大部分依赖,利用Docker缓存加速以后的构建 (以下三行,可以删除)
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN uv venv --python=3.12 && uv pip install --verbose -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
COPY ./docs/gradio-3.32.6-py3-none-any.whl ./docs/gradio-3.32.6-py3-none-any.whl
|
||||||
ENV PATH="/gpt/.venv/bin:$PATH"
|
RUN pip3 install -r requirements.txt
|
||||||
RUN python -c 'import loguru'
|
|
||||||
|
|
||||||
# 装载项目文件,安装剩余依赖(必要)
|
# 装载项目文件,安装剩余依赖(必要)
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN uv pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
# # 非必要步骤,用于预热模块(可以删除)
|
|
||||||
RUN python -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
# 非必要步骤,用于预热模块(可以删除)
|
||||||
|
RUN python3 -c 'from check_proxy import warm_up_modules; warm_up_modules()'
|
||||||
|
|
||||||
|
|
||||||
# 启动(必要)
|
# 启动(必要)
|
||||||
CMD ["bash", "-c", "python main.py"]
|
CMD ["python3", "-u", "main.py"]
|
||||||
|
|||||||
103
README.md
103
README.md
@@ -1,13 +1,8 @@
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> `master主分支`最新动态(2025.8.23): Dockerfile构建效率大幅优化
|
> 2024.1.16: 恭迎GLM4,全力支持Qwen、GLM、DeepseekCoder等国内中文大语言基座模型!
|
||||||
> `master主分支`最新动态(2025.7.31): 新GUI前端,Coming Soon
|
>
|
||||||
>
|
> 2024.1.17: 某些依赖包尚不兼容python 3.12,推荐python 3.11。
|
||||||
> 2025.2.2: 三分钟快速接入最强qwen2.5-max[视频](https://www.bilibili.com/video/BV1LeFuerEG4)
|
> 2024.1.17: 安装依赖时,请选择`requirements.txt`中**指定的版本**。 安装命令:`pip install -r requirements.txt`。本项目完全开源免费,您可通过订阅[在线服务](https://github.com/binary-husky/gpt_academic/wiki/online)的方式鼓励本项目的发展。
|
||||||
> 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`。
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -60,10 +55,6 @@ Read this in [English](docs/README.English.md) | [日本語](docs/README.Japanes
|
|||||||
功能(⭐= 近期新增功能) | 描述
|
功能(⭐= 近期新增功能) | 描述
|
||||||
--- | ---
|
--- | ---
|
||||||
⭐[接入新模型](https://github.com/binary-husky/gpt_academic/wiki/%E5%A6%82%E4%BD%95%E5%88%87%E6%8D%A2%E6%A8%A1%E5%9E%8B) | 百度[千帆](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu)与文心一言, 通义千问[Qwen](https://modelscope.cn/models/qwen/Qwen-7B-Chat/summary),上海AI-Lab[书生](https://github.com/InternLM/InternLM),讯飞[星火](https://xinghuo.xfyun.cn/),[LLaMa2](https://huggingface.co/meta-llama/Llama-2-7b-chat-hf),[智谱GLM4](https://open.bigmodel.cn/),DALLE3, [DeepseekCoder](https://coder.deepseek.com/)
|
⭐[接入新模型](https://github.com/binary-husky/gpt_academic/wiki/%E5%A6%82%E4%BD%95%E5%88%87%E6%8D%A2%E6%A8%A1%E5%9E%8B) | 百度[千帆](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu)与文心一言, 通义千问[Qwen](https://modelscope.cn/models/qwen/Qwen-7B-Chat/summary),上海AI-Lab[书生](https://github.com/InternLM/InternLM),讯飞[星火](https://xinghuo.xfyun.cn/),[LLaMa2](https://huggingface.co/meta-llama/Llama-2-7b-chat-hf),[智谱GLM4](https://open.bigmodel.cn/),DALLE3, [DeepseekCoder](https://coder.deepseek.com/)
|
||||||
⭐支持mermaid图像渲染 | 支持让GPT生成[流程图](https://www.bilibili.com/video/BV18c41147H9/)、状态转移图、甘特图、饼状图、GitGraph等等(3.7版本)
|
|
||||||
⭐Arxiv论文精细翻译 ([Docker](https://github.com/binary-husky/gpt_academic/pkgs/container/gpt_academic_with_latex)) | [插件] 一键[以超高质量翻译arxiv论文](https://www.bilibili.com/video/BV1dz4y1v77A/),目前最好的论文翻译工具
|
|
||||||
⭐[实时语音对话输入](https://github.com/binary-husky/gpt_academic/blob/master/docs/use_audio.md) | [插件] 异步[监听音频](https://www.bilibili.com/video/BV1AV4y187Uy/),自动断句,自动寻找回答时机
|
|
||||||
⭐虚空终端插件 | [插件] 能够使用自然语言直接调度本项目其他插件
|
|
||||||
润色、翻译、代码解释 | 一键润色、翻译、查找论文语法错误、解释代码
|
润色、翻译、代码解释 | 一键润色、翻译、查找论文语法错误、解释代码
|
||||||
[自定义快捷键](https://www.bilibili.com/video/BV14s4y1E7jN) | 支持自定义快捷键
|
[自定义快捷键](https://www.bilibili.com/video/BV14s4y1E7jN) | 支持自定义快捷键
|
||||||
模块化设计 | 支持自定义强大的[插件](https://github.com/binary-husky/gpt_academic/tree/master/crazy_functions),插件支持[热更新](https://github.com/binary-husky/gpt_academic/wiki/%E5%87%BD%E6%95%B0%E6%8F%92%E4%BB%B6%E6%8C%87%E5%8D%97)
|
模块化设计 | 支持自定义强大的[插件](https://github.com/binary-husky/gpt_academic/tree/master/crazy_functions),插件支持[热更新](https://github.com/binary-husky/gpt_academic/wiki/%E5%87%BD%E6%95%B0%E6%8F%92%E4%BB%B6%E6%8C%87%E5%8D%97)
|
||||||
@@ -71,17 +62,22 @@ Read this in [English](docs/README.English.md) | [日本語](docs/README.Japanes
|
|||||||
读论文、[翻译](https://www.bilibili.com/video/BV1KT411x7Wn)论文 | [插件] 一键解读latex/pdf论文全文并生成摘要
|
读论文、[翻译](https://www.bilibili.com/video/BV1KT411x7Wn)论文 | [插件] 一键解读latex/pdf论文全文并生成摘要
|
||||||
Latex全文[翻译](https://www.bilibili.com/video/BV1nk4y1Y7Js/)、[润色](https://www.bilibili.com/video/BV1FT411H7c5/) | [插件] 一键翻译或润色latex论文
|
Latex全文[翻译](https://www.bilibili.com/video/BV1nk4y1Y7Js/)、[润色](https://www.bilibili.com/video/BV1FT411H7c5/) | [插件] 一键翻译或润色latex论文
|
||||||
批量注释生成 | [插件] 一键批量生成函数注释
|
批量注释生成 | [插件] 一键批量生成函数注释
|
||||||
Markdown[中英互译](https://www.bilibili.com/video/BV1yo4y157jV/) | [插件] 看到上面5种语言的[README](https://github.com/binary-husky/gpt_academic/blob/master/docs/README.English.md)了吗?就是出自他的手笔
|
Markdown[中英互译](https://www.bilibili.com/video/BV1yo4y157jV/) | [插件] 看到上面5种语言的[README](https://github.com/binary-husky/gpt_academic/blob/master/docs/README_EN.md)了吗?就是出自他的手笔
|
||||||
|
⭐支持mermaid图像渲染 | 支持让GPT生成[流程图](https://www.bilibili.com/video/BV18c41147H9/)、状态转移图、甘特图、饼状图、GitGraph等等(3.7版本)
|
||||||
[PDF论文全文翻译功能](https://www.bilibili.com/video/BV1KT411x7Wn) | [插件] PDF论文提取题目&摘要+翻译全文(多线程)
|
[PDF论文全文翻译功能](https://www.bilibili.com/video/BV1KT411x7Wn) | [插件] PDF论文提取题目&摘要+翻译全文(多线程)
|
||||||
[Arxiv小助手](https://www.bilibili.com/video/BV1LM4y1279X) | [插件] 输入arxiv文章url即可一键翻译摘要+下载PDF
|
[Arxiv小助手](https://www.bilibili.com/video/BV1LM4y1279X) | [插件] 输入arxiv文章url即可一键翻译摘要+下载PDF
|
||||||
Latex论文一键校对 | [插件] 仿Grammarly对Latex文章进行语法、拼写纠错+输出对照PDF
|
Latex论文一键校对 | [插件] 仿Grammarly对Latex文章进行语法、拼写纠错+输出对照PDF
|
||||||
[谷歌学术统合小助手](https://www.bilibili.com/video/BV19L411U7ia) | [插件] 给定任意谷歌学术搜索页面URL,让gpt帮你[写relatedworks](https://www.bilibili.com/video/BV1GP411U7Az/)
|
[谷歌学术统合小助手](https://www.bilibili.com/video/BV19L411U7ia) | [插件] 给定任意谷歌学术搜索页面URL,让gpt帮你[写relatedworks](https://www.bilibili.com/video/BV1GP411U7Az/)
|
||||||
互联网信息聚合+GPT | [插件] 一键[让GPT从互联网获取信息](https://www.bilibili.com/video/BV1om4y127ck)回答问题,让信息永不过时
|
互联网信息聚合+GPT | [插件] 一键[让GPT从互联网获取信息](https://www.bilibili.com/video/BV1om4y127ck)回答问题,让信息永不过时
|
||||||
|
⭐Arxiv论文精细翻译 ([Docker](https://github.com/binary-husky/gpt_academic/pkgs/container/gpt_academic_with_latex)) | [插件] 一键[以超高质量翻译arxiv论文](https://www.bilibili.com/video/BV1dz4y1v77A/),目前最好的论文翻译工具
|
||||||
|
⭐[实时语音对话输入](https://github.com/binary-husky/gpt_academic/blob/master/docs/use_audio.md) | [插件] 异步[监听音频](https://www.bilibili.com/video/BV1AV4y187Uy/),自动断句,自动寻找回答时机
|
||||||
公式/图片/表格显示 | 可以同时显示公式的[tex形式和渲染形式](https://user-images.githubusercontent.com/96192199/230598842-1d7fcddd-815d-40ee-af60-baf488a199df.png),支持公式、代码高亮
|
公式/图片/表格显示 | 可以同时显示公式的[tex形式和渲染形式](https://user-images.githubusercontent.com/96192199/230598842-1d7fcddd-815d-40ee-af60-baf488a199df.png),支持公式、代码高亮
|
||||||
|
⭐AutoGen多智能体插件 | [插件] 借助微软AutoGen,探索多Agent的智能涌现可能!
|
||||||
启动暗色[主题](https://github.com/binary-husky/gpt_academic/issues/173) | 在浏览器url后面添加```/?__theme=dark```可以切换dark主题
|
启动暗色[主题](https://github.com/binary-husky/gpt_academic/issues/173) | 在浏览器url后面添加```/?__theme=dark```可以切换dark主题
|
||||||
[多LLM模型](https://www.bilibili.com/video/BV1wT411p7yf)支持 | 同时被GPT3.5、GPT4、[清华ChatGLM2](https://github.com/THUDM/ChatGLM2-6B)、[复旦MOSS](https://github.com/OpenLMLab/MOSS)伺候的感觉一定会很不错吧?
|
[多LLM模型](https://www.bilibili.com/video/BV1wT411p7yf)支持 | 同时被GPT3.5、GPT4、[清华ChatGLM2](https://github.com/THUDM/ChatGLM2-6B)、[复旦MOSS](https://github.com/OpenLMLab/MOSS)伺候的感觉一定会很不错吧?
|
||||||
更多LLM模型接入,支持[huggingface部署](https://huggingface.co/spaces/qingxu98/gpt-academic) | 加入Newbing接口(新必应),引入清华[Jittorllms](https://github.com/Jittor/JittorLLMs)支持[LLaMA](https://github.com/facebookresearch/llama)和[盘古α](https://openi.org.cn/pangu/)
|
更多LLM模型接入,支持[huggingface部署](https://huggingface.co/spaces/qingxu98/gpt-academic) | 加入Newbing接口(新必应),引入清华[Jittorllms](https://github.com/Jittor/JittorLLMs)支持[LLaMA](https://github.com/facebookresearch/llama)和[盘古α](https://openi.org.cn/pangu/)
|
||||||
⭐[void-terminal](https://github.com/binary-husky/void-terminal) pip包 | 脱离GUI,在Python中直接调用本项目的所有函数插件(开发中)
|
⭐[void-terminal](https://github.com/binary-husky/void-terminal) pip包 | 脱离GUI,在Python中直接调用本项目的所有函数插件(开发中)
|
||||||
|
⭐虚空终端插件 | [插件] 能够使用自然语言直接调度本项目其他插件
|
||||||
更多新功能展示 (图像生成等) …… | 见本文档结尾处 ……
|
更多新功能展示 (图像生成等) …… | 见本文档结尾处 ……
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,10 +87,6 @@ Latex论文一键校对 | [插件] 仿Grammarly对Latex文章进行语法、拼
|
|||||||
<img src="https://user-images.githubusercontent.com/96192199/279702205-d81137c3-affd-4cd1-bb5e-b15610389762.gif" width="700" >
|
<img src="https://user-images.githubusercontent.com/96192199/279702205-d81137c3-affd-4cd1-bb5e-b15610389762.gif" width="700" >
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="https://github.com/binary-husky/gpt_academic/assets/96192199/70ff1ec5-e589-4561-a29e-b831079b37fb.gif" width="700" >
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
- 所有按钮都通过读取functional.py动态生成,可随意加自定义功能,解放剪贴板
|
- 所有按钮都通过读取functional.py动态生成,可随意加自定义功能,解放剪贴板
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -124,25 +116,6 @@ Latex论文一键校对 | [插件] 仿Grammarly对Latex文章进行语法、拼
|
|||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A{"安装方法"} --> W1("I 🔑直接运行 (Windows, Linux or MacOS)")
|
|
||||||
W1 --> W11["1 Python pip包管理依赖"]
|
|
||||||
W1 --> W12["2 Anaconda包管理依赖(推荐⭐)"]
|
|
||||||
|
|
||||||
A --> W2["II 🐳使用Docker (Windows, Linux or MacOS)"]
|
|
||||||
|
|
||||||
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 其他 ..."]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 安装方法I:直接运行 (Windows, Linux or MacOS)
|
### 安装方法I:直接运行 (Windows, Linux or MacOS)
|
||||||
|
|
||||||
1. 下载项目
|
1. 下载项目
|
||||||
@@ -156,7 +129,7 @@ flowchart TD
|
|||||||
|
|
||||||
在`config.py`中,配置API KEY等变量。[特殊网络环境设置方法](https://github.com/binary-husky/gpt_academic/issues/1)、[Wiki-项目配置说明](https://github.com/binary-husky/gpt_academic/wiki/项目配置说明)。
|
在`config.py`中,配置API KEY等变量。[特殊网络环境设置方法](https://github.com/binary-husky/gpt_academic/issues/1)、[Wiki-项目配置说明](https://github.com/binary-husky/gpt_academic/wiki/项目配置说明)。
|
||||||
|
|
||||||
「 程序会优先检查是否存在名为`config_private.py`的私密配置文件,并用其中的配置覆盖`config.py`的同名配置。如您能理解以上读取逻辑,我们强烈建议您在`config.py`同路径下创建一个名为`config_private.py`的新配置文件,并使用`config_private.py`配置项目,从而确保自动更新时不会丢失配置 」。
|
「 程序会优先检查是否存在名为`config_private.py`的私密配置文件,并用其中的配置覆盖`config.py`的同名配置。如您能理解以上读取逻辑,我们强烈建议您在`config.py`同路径下创建一个名为`config_private.py`的新配置文件,并使用`config_private.py`配置项目,以确保更新或其他用户无法轻易查看您的私有配置 」。
|
||||||
|
|
||||||
「 支持通过`环境变量`配置项目,环境变量的书写格式参考`docker-compose.yml`文件或者我们的[Wiki页面](https://github.com/binary-husky/gpt_academic/wiki/项目配置说明)。配置读取优先级: `环境变量` > `config_private.py` > `config.py` 」。
|
「 支持通过`环境变量`配置项目,环境变量的书写格式参考`docker-compose.yml`文件或者我们的[Wiki页面](https://github.com/binary-husky/gpt_academic/wiki/项目配置说明)。配置读取优先级: `环境变量` > `config_private.py` > `config.py` 」。
|
||||||
|
|
||||||
@@ -173,32 +146,26 @@ flowchart TD
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
<details><summary>如果需要支持清华ChatGLM系列/复旦MOSS/RWKV作为后端,请点击展开此处</summary>
|
<details><summary>如果需要支持清华ChatGLM2/复旦MOSS/RWKV作为后端,请点击展开此处</summary>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
【可选步骤】如果需要支持清华ChatGLM系列/复旦MOSS作为后端,需要额外安装更多依赖(前提条件:熟悉Python + 用过Pytorch + 电脑配置够强):
|
【可选步骤】如果需要支持清华ChatGLM3/复旦MOSS作为后端,需要额外安装更多依赖(前提条件:熟悉Python + 用过Pytorch + 电脑配置够强):
|
||||||
|
|
||||||
```sh
|
```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)
|
# 【可选步骤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
|
python -m pip install -r request_llms/requirements_chatglm.txt
|
||||||
|
|
||||||
# 【可选步骤II】支持清华ChatGLM4 注意:此模型至少需要24G显存
|
# 【可选步骤II】支持复旦MOSS
|
||||||
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
|
python -m pip install -r request_llms/requirements_moss.txt
|
||||||
git clone --depth=1 https://github.com/OpenLMLab/MOSS.git request_llms/moss # 注意执行此行代码时,必须处于项目根路径
|
git clone --depth=1 https://github.com/OpenLMLab/MOSS.git request_llms/moss # 注意执行此行代码时,必须处于项目根路径
|
||||||
|
|
||||||
# 【可选步骤IV】支持RWKV Runner
|
# 【可选步骤III】支持RWKV Runner
|
||||||
参考wiki:https://github.com/binary-husky/gpt_academic/wiki/%E9%80%82%E9%85%8DRWKV-Runner
|
参考wiki:https://github.com/binary-husky/gpt_academic/wiki/%E9%80%82%E9%85%8DRWKV-Runner
|
||||||
|
|
||||||
# 【可选步骤V】确保config.py配置文件的AVAIL_LLM_MODELS包含了期望的模型,目前支持的全部模型如下(jittorllms系列目前仅支持docker方案):
|
# 【可选步骤IV】确保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"]
|
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"]
|
||||||
|
|
||||||
# 【可选步骤VI】支持本地模型INT8,INT4量化(这里所指的模型本身不是量化版本,目前deepseek-coder支持,后面测试后会加入更多模型量化选择)
|
# 【可选步骤V】支持本地模型INT8,INT4量化(这里所指的模型本身不是量化版本,目前deepseek-coder支持,后面测试后会加入更多模型量化选择)
|
||||||
pip install bitsandbyte
|
pip install bitsandbyte
|
||||||
# windows用户安装bitsandbytes需要使用下面bitsandbytes-windows-webui
|
# windows用户安装bitsandbytes需要使用下面bitsandbytes-windows-webui
|
||||||
python -m pip install bitsandbytes --prefer-binary --extra-index-url=https://jllllll.github.io/bitsandbytes-windows-webui
|
python -m pip install bitsandbytes --prefer-binary --extra-index-url=https://jllllll.github.io/bitsandbytes-windows-webui
|
||||||
@@ -267,7 +234,8 @@ P.S. 如果需要依赖Latex的插件功能,请见Wiki。另外,您也可以
|
|||||||
# Advanced Usage
|
# Advanced Usage
|
||||||
### I:自定义新的便捷按钮(学术快捷键)
|
### I:自定义新的便捷按钮(学术快捷键)
|
||||||
|
|
||||||
现在已可以通过UI中的`界面外观`菜单中的`自定义菜单`添加新的便捷按钮。如果需要在代码中定义,请使用任意文本编辑器打开`core_functional.py`,添加如下条目即可:
|
任意文本编辑器打开`core_functional.py`,添加如下条目,然后重启程序。(如果按钮已存在,那么可以直接修改(前缀、后缀都已支持热修改),无需重启程序即可生效。)
|
||||||
|
例如
|
||||||
|
|
||||||
```python
|
```python
|
||||||
"超级英译中": {
|
"超级英译中": {
|
||||||
@@ -356,8 +324,8 @@ Tip:不指定文件直接点击 `载入对话历史存档` 可以查看历史h
|
|||||||
|
|
||||||
|
|
||||||
### II:版本:
|
### II:版本:
|
||||||
- version 3.80(TODO): 优化AutoGen插件主题并设计一系列衍生插件
|
|
||||||
- version 3.70: 引入Mermaid绘图,实现GPT画脑图等功能
|
- version 3.70(todo): 优化AutoGen插件主题并设计一系列衍生插件
|
||||||
- version 3.60: 引入AutoGen作为新一代插件的基石
|
- version 3.60: 引入AutoGen作为新一代插件的基石
|
||||||
- version 3.57: 支持GLM3,星火v3,文心一言v4,修复本地模型的并发BUG
|
- version 3.57: 支持GLM3,星火v3,文心一言v4,修复本地模型的并发BUG
|
||||||
- version 3.56: 支持动态追加基础功能按钮,新汇报PDF汇总页面
|
- version 3.56: 支持动态追加基础功能按钮,新汇报PDF汇总页面
|
||||||
@@ -390,32 +358,6 @@ GPT Academic开发者QQ群:`610599535`
|
|||||||
- 某些浏览器翻译插件干扰此软件前端的运行
|
- 某些浏览器翻译插件干扰此软件前端的运行
|
||||||
- 官方Gradio目前有很多兼容性问题,请**务必使用`requirement.txt`安装Gradio**
|
- 官方Gradio目前有很多兼容性问题,请**务必使用`requirement.txt`安装Gradio**
|
||||||
|
|
||||||
```mermaid
|
|
||||||
timeline LR
|
|
||||||
title GPT-Academic项目发展历程
|
|
||||||
section 2.x
|
|
||||||
1.0~2.2: 基础功能: 引入模块化函数插件: 可折叠式布局: 函数插件支持热重载
|
|
||||||
2.3~2.5: 增强多线程交互性: 新增PDF全文翻译功能: 新增输入区切换位置的功能: 自更新
|
|
||||||
2.6: 重构了插件结构: 提高了交互性: 加入更多插件
|
|
||||||
section 3.x
|
|
||||||
3.0~3.1: 对chatglm支持: 对其他小型llm支持: 支持同时问询多个gpt模型: 支持多个apikey负载均衡
|
|
||||||
3.2~3.3: 函数插件支持更多参数接口: 保存对话功能: 解读任意语言代码: 同时询问任意的LLM组合: 互联网信息综合功能
|
|
||||||
3.4: 加入arxiv论文翻译: 加入latex论文批改功能
|
|
||||||
3.44: 正式支持Azure: 优化界面易用性
|
|
||||||
3.46: 自定义ChatGLM2微调模型: 实时语音对话
|
|
||||||
3.49: 支持阿里达摩院通义千问: 上海AI-Lab书生: 讯飞星火: 支持百度千帆平台 & 文心一言
|
|
||||||
3.50: 虚空终端: 支持插件分类: 改进UI: 设计新主题
|
|
||||||
3.53: 动态选择不同界面主题: 提高稳定性: 解决多用户冲突问题
|
|
||||||
3.55: 动态代码解释器: 重构前端界面: 引入悬浮窗口与菜单栏
|
|
||||||
3.56: 动态追加基础功能按钮: 新汇报PDF汇总页面
|
|
||||||
3.57: GLM3, 星火v3: 支持文心一言v4: 修复本地模型的并发BUG
|
|
||||||
3.60: 引入AutoGen
|
|
||||||
3.70: 引入Mermaid绘图: 实现GPT画脑图等功能
|
|
||||||
3.80(TODO): 优化AutoGen插件主题: 设计衍生插件
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### III:主题
|
### III:主题
|
||||||
可以通过修改`THEME`选项(config.py)变更主题
|
可以通过修改`THEME`选项(config.py)变更主题
|
||||||
1. `Chuanhu-Small-and-Beautiful` [网址](https://github.com/GaiZhenbiao/ChuanhuChatGPT/)
|
1. `Chuanhu-Small-and-Beautiful` [网址](https://github.com/GaiZhenbiao/ChuanhuChatGPT/)
|
||||||
@@ -426,6 +368,7 @@ timeline LR
|
|||||||
1. `master` 分支: 主分支,稳定版
|
1. `master` 分支: 主分支,稳定版
|
||||||
2. `frontier` 分支: 开发分支,测试版
|
2. `frontier` 分支: 开发分支,测试版
|
||||||
3. 如何[接入其他大模型](request_llms/README.md)
|
3. 如何[接入其他大模型](request_llms/README.md)
|
||||||
|
4. 访问GPT-Academic的[在线服务并支持我们](https://github.com/binary-husky/gpt_academic/wiki/online)
|
||||||
|
|
||||||
### V:参考与学习
|
### V:参考与学习
|
||||||
|
|
||||||
|
|||||||
203
check_proxy.py
203
check_proxy.py
@@ -1,77 +1,37 @@
|
|||||||
from loguru import logger
|
|
||||||
|
|
||||||
def check_proxy(proxies, return_ip=False):
|
def check_proxy(proxies):
|
||||||
"""
|
|
||||||
检查代理配置并返回结果。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
proxies (dict): 包含http和https代理配置的字典。
|
|
||||||
return_ip (bool, optional): 是否返回代理的IP地址。默认为False。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str or None: 检查的结果信息或代理的IP地址(如果`return_ip`为True)。
|
|
||||||
"""
|
|
||||||
import requests
|
import requests
|
||||||
proxies_https = proxies['https'] if proxies is not None else '无'
|
proxies_https = proxies['https'] if proxies is not None else '无'
|
||||||
ip = None
|
|
||||||
try:
|
try:
|
||||||
response = requests.get("https://ipapi.co/json/", proxies=proxies, timeout=4) # ⭐ 执行GET请求以获取代理信息
|
response = requests.get("https://ipapi.co/json/", proxies=proxies, timeout=4)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if 'country_name' in data:
|
if 'country_name' in data:
|
||||||
country = data['country_name']
|
country = data['country_name']
|
||||||
result = f"代理配置 {proxies_https}, 代理所在地:{country}"
|
result = f"代理配置 {proxies_https}, 代理所在地:{country}"
|
||||||
if 'ip' in data:
|
|
||||||
ip = data['ip']
|
|
||||||
elif 'error' in data:
|
elif 'error' in data:
|
||||||
alternative, ip = _check_with_backup_source(proxies) # ⭐ 调用备用方法检查代理配置
|
alternative = _check_with_backup_source(proxies)
|
||||||
if alternative is None:
|
if alternative is None:
|
||||||
result = f"代理配置 {proxies_https}, 代理所在地:未知,IP查询频率受限"
|
result = f"代理配置 {proxies_https}, 代理所在地:未知,IP查询频率受限"
|
||||||
else:
|
else:
|
||||||
result = f"代理配置 {proxies_https}, 代理所在地:{alternative}"
|
result = f"代理配置 {proxies_https}, 代理所在地:{alternative}"
|
||||||
else:
|
else:
|
||||||
result = f"代理配置 {proxies_https}, 代理数据解析失败:{data}"
|
result = f"代理配置 {proxies_https}, 代理数据解析失败:{data}"
|
||||||
|
print(result)
|
||||||
if not return_ip:
|
return result
|
||||||
logger.warning(result)
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return ip
|
|
||||||
except:
|
except:
|
||||||
result = f"代理配置 {proxies_https}, 代理所在地查询超时,代理可能无效"
|
result = f"代理配置 {proxies_https}, 代理所在地查询超时,代理可能无效"
|
||||||
if not return_ip:
|
print(result)
|
||||||
logger.warning(result)
|
return result
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return ip
|
|
||||||
|
|
||||||
def _check_with_backup_source(proxies):
|
def _check_with_backup_source(proxies):
|
||||||
"""
|
|
||||||
通过备份源检查代理,并获取相应信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
proxies (dict): 包含代理信息的字典。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: 代理信息(geo)和IP地址(ip)的元组。
|
|
||||||
"""
|
|
||||||
import random, string, requests
|
import random, string, requests
|
||||||
random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||||
try:
|
try: return requests.get(f"http://{random_string}.edns.ip-api.com/json", proxies=proxies, timeout=4).json()['dns']['geo']
|
||||||
res_json = requests.get(f"http://{random_string}.edns.ip-api.com/json", proxies=proxies, timeout=4).json() # ⭐ 执行代理检查和备份源请求
|
except: return None
|
||||||
return res_json['dns']['geo'], res_json['dns']['ip']
|
|
||||||
except:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def backup_and_download(current_version, remote_version):
|
def backup_and_download(current_version, remote_version):
|
||||||
"""
|
"""
|
||||||
一键更新协议:备份当前版本,下载远程版本并解压缩。
|
一键更新协议:备份和下载
|
||||||
|
|
||||||
Args:
|
|
||||||
current_version (str): 当前版本号。
|
|
||||||
remote_version (str): 远程版本号。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 新版本目录的路径。
|
|
||||||
"""
|
"""
|
||||||
from toolbox import get_conf
|
from toolbox import get_conf
|
||||||
import shutil
|
import shutil
|
||||||
@@ -87,8 +47,8 @@ def backup_and_download(current_version, remote_version):
|
|||||||
shutil.copytree('./', backup_dir, ignore=lambda x, y: ['history'])
|
shutil.copytree('./', backup_dir, ignore=lambda x, y: ['history'])
|
||||||
proxies = get_conf('proxies')
|
proxies = get_conf('proxies')
|
||||||
try: r = requests.get('https://github.com/binary-husky/chatgpt_academic/archive/refs/heads/master.zip', proxies=proxies, stream=True)
|
try: r = requests.get('https://github.com/binary-husky/chatgpt_academic/archive/refs/heads/master.zip', proxies=proxies, stream=True)
|
||||||
except: r = requests.get('https://public.agent-matrix.com/publish/master.zip', proxies=proxies, stream=True)
|
except: r = requests.get('https://public.gpt-academic.top/publish/master.zip', proxies=proxies, stream=True)
|
||||||
zip_file_path = backup_dir+'/master.zip' # ⭐ 保存备份文件的路径
|
zip_file_path = backup_dir+'/master.zip'
|
||||||
with open(zip_file_path, 'wb+') as f:
|
with open(zip_file_path, 'wb+') as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
dst_path = new_version_dir
|
dst_path = new_version_dir
|
||||||
@@ -104,17 +64,6 @@ def backup_and_download(current_version, remote_version):
|
|||||||
def patch_and_restart(path):
|
def patch_and_restart(path):
|
||||||
"""
|
"""
|
||||||
一键更新协议:覆盖和重启
|
一键更新协议:覆盖和重启
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): 新版本代码所在的路径
|
|
||||||
|
|
||||||
注意事项:
|
|
||||||
如果您的程序没有使用config_private.py私密配置文件,则会将config.py重命名为config_private.py以避免配置丢失。
|
|
||||||
|
|
||||||
更新流程:
|
|
||||||
- 复制最新版本代码到当前目录
|
|
||||||
- 更新pip包依赖
|
|
||||||
- 如果更新失败,则提示手动安装依赖库并重启
|
|
||||||
"""
|
"""
|
||||||
from distutils import dir_util
|
from distutils import dir_util
|
||||||
import shutil
|
import shutil
|
||||||
@@ -122,44 +71,33 @@ def patch_and_restart(path):
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import glob
|
import glob
|
||||||
from shared_utils.colorful import log亮黄, log亮绿, log亮红
|
from colorful import print亮黄, print亮绿, print亮红
|
||||||
|
# if not using config_private, move origin config.py as config_private.py
|
||||||
if not os.path.exists('config_private.py'):
|
if not os.path.exists('config_private.py'):
|
||||||
log亮黄('由于您没有设置config_private.py私密配置,现将您的现有配置移动至config_private.py以防止配置丢失,',
|
print亮黄('由于您没有设置config_private.py私密配置,现将您的现有配置移动至config_private.py以防止配置丢失,',
|
||||||
'另外您可以随时在history子文件夹下找回旧版的程序。')
|
'另外您可以随时在history子文件夹下找回旧版的程序。')
|
||||||
shutil.copyfile('config.py', 'config_private.py')
|
shutil.copyfile('config.py', 'config_private.py')
|
||||||
|
|
||||||
path_new_version = glob.glob(path + '/*-master')[0]
|
path_new_version = glob.glob(path + '/*-master')[0]
|
||||||
dir_util.copy_tree(path_new_version, './') # ⭐ 将最新版本代码复制到当前目录
|
dir_util.copy_tree(path_new_version, './')
|
||||||
|
print亮绿('代码已经更新,即将更新pip包依赖……')
|
||||||
log亮绿('代码已经更新,即将更新pip包依赖……')
|
for i in reversed(range(5)): time.sleep(1); print(i)
|
||||||
for i in reversed(range(5)): time.sleep(1); log亮绿(i)
|
try:
|
||||||
|
|
||||||
try:
|
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
|
||||||
except:
|
except:
|
||||||
log亮红('pip包依赖安装出现问题,需要手动安装新增的依赖库 `python -m pip install -r requirements.txt`,然后在用常规的`python main.py`的方式启动。')
|
print亮红('pip包依赖安装出现问题,需要手动安装新增的依赖库 `python -m pip install -r requirements.txt`,然后在用常规的`python main.py`的方式启动。')
|
||||||
|
print亮绿('更新完成,您可以随时在history子文件夹下找回旧版的程序,5s之后重启')
|
||||||
log亮绿('更新完成,您可以随时在history子文件夹下找回旧版的程序,5s之后重启')
|
print亮红('假如重启失败,您可能需要手动安装新增的依赖库 `python -m pip install -r requirements.txt`,然后在用常规的`python main.py`的方式启动。')
|
||||||
log亮红('假如重启失败,您可能需要手动安装新增的依赖库 `python -m pip install -r requirements.txt`,然后在用常规的`python main.py`的方式启动。')
|
print(' ------------------------------ -----------------------------------')
|
||||||
log亮绿(' ------------------------------ -----------------------------------')
|
for i in reversed(range(8)): time.sleep(1); print(i)
|
||||||
|
os.execl(sys.executable, sys.executable, *sys.argv)
|
||||||
for i in reversed(range(8)): time.sleep(1); log亮绿(i)
|
|
||||||
os.execl(sys.executable, sys.executable, *sys.argv) # 重启程序
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_version():
|
def get_current_version():
|
||||||
"""
|
|
||||||
获取当前的版本号。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 当前的版本号。如果无法获取版本号,则返回空字符串。
|
|
||||||
"""
|
|
||||||
import json
|
import json
|
||||||
try:
|
try:
|
||||||
with open('./version', 'r', encoding='utf8') as f:
|
with open('./version', 'r', encoding='utf8') as f:
|
||||||
current_version = json.loads(f.read())['version'] # ⭐ 从读取的json数据中提取版本号
|
current_version = json.loads(f.read())['version']
|
||||||
except:
|
except:
|
||||||
current_version = ""
|
current_version = ""
|
||||||
return current_version
|
return current_version
|
||||||
@@ -168,12 +106,6 @@ def get_current_version():
|
|||||||
def auto_update(raise_error=False):
|
def auto_update(raise_error=False):
|
||||||
"""
|
"""
|
||||||
一键更新协议:查询版本和用户意见
|
一键更新协议:查询版本和用户意见
|
||||||
|
|
||||||
Args:
|
|
||||||
raise_error (bool, optional): 是否在出错时抛出错误。默认为 False。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from toolbox import get_conf
|
from toolbox import get_conf
|
||||||
@@ -181,7 +113,7 @@ def auto_update(raise_error=False):
|
|||||||
import json
|
import json
|
||||||
proxies = get_conf('proxies')
|
proxies = get_conf('proxies')
|
||||||
try: response = requests.get("https://raw.githubusercontent.com/binary-husky/chatgpt_academic/master/version", proxies=proxies, timeout=5)
|
try: response = requests.get("https://raw.githubusercontent.com/binary-husky/chatgpt_academic/master/version", proxies=proxies, timeout=5)
|
||||||
except: response = requests.get("https://public.agent-matrix.com/publish/version", proxies=proxies, timeout=5)
|
except: response = requests.get("https://public.gpt-academic.top/publish/version", proxies=proxies, timeout=5)
|
||||||
remote_json_data = json.loads(response.text)
|
remote_json_data = json.loads(response.text)
|
||||||
remote_version = remote_json_data['version']
|
remote_version = remote_json_data['version']
|
||||||
if remote_json_data["show_feature"]:
|
if remote_json_data["show_feature"]:
|
||||||
@@ -192,22 +124,22 @@ def auto_update(raise_error=False):
|
|||||||
current_version = f.read()
|
current_version = f.read()
|
||||||
current_version = json.loads(current_version)['version']
|
current_version = json.loads(current_version)['version']
|
||||||
if (remote_version - current_version) >= 0.01-1e-5:
|
if (remote_version - current_version) >= 0.01-1e-5:
|
||||||
from shared_utils.colorful import log亮黄
|
from colorful import print亮黄
|
||||||
log亮黄(f'\n新版本可用。新版本:{remote_version},当前版本:{current_version}。{new_feature}') # ⭐ 在控制台打印新版本信息
|
print亮黄(f'\n新版本可用。新版本:{remote_version},当前版本:{current_version}。{new_feature}')
|
||||||
logger.info('(1)Github更新地址:\nhttps://github.com/binary-husky/chatgpt_academic\n')
|
print('(1)Github更新地址:\nhttps://github.com/binary-husky/chatgpt_academic\n')
|
||||||
user_instruction = input('(2)是否一键更新代码(Y+回车=确认,输入其他/无输入+回车=不更新)?')
|
user_instruction = input('(2)是否一键更新代码(Y+回车=确认,输入其他/无输入+回车=不更新)?')
|
||||||
if user_instruction in ['Y', 'y']:
|
if user_instruction in ['Y', 'y']:
|
||||||
path = backup_and_download(current_version, remote_version) # ⭐ 备份并下载文件
|
path = backup_and_download(current_version, remote_version)
|
||||||
try:
|
try:
|
||||||
patch_and_restart(path) # ⭐ 执行覆盖并重启操作
|
patch_and_restart(path)
|
||||||
except:
|
except:
|
||||||
msg = '更新失败。'
|
msg = '更新失败。'
|
||||||
if raise_error:
|
if raise_error:
|
||||||
from toolbox import trimmed_format_exc
|
from toolbox import trimmed_format_exc
|
||||||
msg += trimmed_format_exc()
|
msg += trimmed_format_exc()
|
||||||
logger.warning(msg)
|
print(msg)
|
||||||
else:
|
else:
|
||||||
logger.info('自动更新程序:已禁用')
|
print('自动更新程序:已禁用')
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
@@ -216,13 +148,10 @@ def auto_update(raise_error=False):
|
|||||||
if raise_error:
|
if raise_error:
|
||||||
from toolbox import trimmed_format_exc
|
from toolbox import trimmed_format_exc
|
||||||
msg += trimmed_format_exc()
|
msg += trimmed_format_exc()
|
||||||
logger.info(msg)
|
print(msg)
|
||||||
|
|
||||||
def warm_up_modules():
|
def warm_up_modules():
|
||||||
"""
|
print('正在执行一些模块的预热 ...')
|
||||||
预热模块,加载特定模块并执行预热操作。
|
|
||||||
"""
|
|
||||||
logger.info('正在执行一些模块的预热 ...')
|
|
||||||
from toolbox import ProxyNetworkActivate
|
from toolbox import ProxyNetworkActivate
|
||||||
from request_llms.bridge_all import model_info
|
from request_llms.bridge_all import model_info
|
||||||
with ProxyNetworkActivate("Warmup_Modules"):
|
with ProxyNetworkActivate("Warmup_Modules"):
|
||||||
@@ -230,70 +159,18 @@ def warm_up_modules():
|
|||||||
enc.encode("模块预热", disallowed_special=())
|
enc.encode("模块预热", disallowed_special=())
|
||||||
enc = model_info["gpt-4"]['tokenizer']
|
enc = model_info["gpt-4"]['tokenizer']
|
||||||
enc.encode("模块预热", disallowed_special=())
|
enc.encode("模块预热", disallowed_special=())
|
||||||
try_warm_up_vectordb()
|
|
||||||
|
|
||||||
|
|
||||||
# def try_warm_up_vectordb():
|
|
||||||
# try:
|
|
||||||
# import os
|
|
||||||
# import nltk
|
|
||||||
# target = os.path.expanduser('~/nltk_data')
|
|
||||||
# logger.info(f'模块预热: nltk punkt (从Github下载部分文件到 {target})')
|
|
||||||
# nltk.data.path.append(target)
|
|
||||||
# nltk.download('punkt', download_dir=target)
|
|
||||||
# logger.info('模块预热完成: nltk punkt')
|
|
||||||
# except:
|
|
||||||
# logger.exception('模块预热: nltk punkt 失败,可能需要手动安装 nltk punkt')
|
|
||||||
# logger.error('模块预热: nltk punkt 失败,可能需要手动安装 nltk punkt')
|
|
||||||
|
|
||||||
|
|
||||||
def try_warm_up_vectordb():
|
|
||||||
import os
|
|
||||||
import nltk
|
|
||||||
target = os.path.expanduser('~/nltk_data')
|
|
||||||
nltk.data.path.append(target)
|
|
||||||
try:
|
|
||||||
# 尝试加载 punkt
|
|
||||||
logger.info(f'nltk模块预热')
|
|
||||||
nltk.data.find('tokenizers/punkt')
|
|
||||||
nltk.data.find('tokenizers/punkt_tab')
|
|
||||||
nltk.data.find('taggers/averaged_perceptron_tagger_eng')
|
|
||||||
logger.info('nltk模块预热完成(读取本地缓存)')
|
|
||||||
except:
|
|
||||||
# 如果找不到,则尝试下载
|
|
||||||
try:
|
|
||||||
logger.info(f'模块预热: nltk punkt (从 Github 下载部分文件到 {target})')
|
|
||||||
from shared_utils.nltk_downloader import Downloader
|
|
||||||
_downloader = Downloader()
|
|
||||||
_downloader.download('punkt', download_dir=target)
|
|
||||||
_downloader.download('punkt_tab', download_dir=target)
|
|
||||||
_downloader.download('averaged_perceptron_tagger_eng', download_dir=target)
|
|
||||||
logger.info('nltk模块预热完成')
|
|
||||||
except Exception:
|
|
||||||
logger.exception('模块预热: nltk punkt 失败,可能需要手动安装 nltk punkt')
|
|
||||||
|
|
||||||
|
|
||||||
def warm_up_vectordb():
|
def warm_up_vectordb():
|
||||||
"""
|
print('正在执行一些模块的预热 ...')
|
||||||
执行一些模块的预热操作。
|
|
||||||
|
|
||||||
本函数主要用于执行一些模块的预热操作,确保在后续的流程中能够顺利运行。
|
|
||||||
|
|
||||||
⭐ 关键作用:预热模块
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
logger.info('正在执行一些模块的预热 ...')
|
|
||||||
from toolbox import ProxyNetworkActivate
|
from toolbox import ProxyNetworkActivate
|
||||||
with ProxyNetworkActivate("Warmup_Modules"):
|
with ProxyNetworkActivate("Warmup_Modules"):
|
||||||
import nltk
|
import nltk
|
||||||
with ProxyNetworkActivate("Warmup_Modules"): nltk.download("punkt")
|
with ProxyNetworkActivate("Warmup_Modules"): nltk.download("punkt")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import os
|
import os
|
||||||
os.environ['no_proxy'] = '*' # 避免代理网络产生意外污染
|
os.environ['no_proxy'] = '*' # 避免代理网络产生意外污染
|
||||||
from toolbox import get_conf
|
from toolbox import get_conf
|
||||||
proxies = get_conf('proxies')
|
proxies = get_conf('proxies')
|
||||||
check_proxy(proxies)
|
check_proxy(proxies)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import platform
|
import platform
|
||||||
from sys import stdout
|
from sys import stdout
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
if platform.system()=="Linux":
|
if platform.system()=="Linux":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
from colorama import init
|
from colorama import init
|
||||||
init()
|
init()
|
||||||
|
|
||||||
@@ -60,29 +59,3 @@ def sprint亮紫(*kw):
|
|||||||
return "\033[1;35m"+' '.join(kw)+"\033[0m"
|
return "\033[1;35m"+' '.join(kw)+"\033[0m"
|
||||||
def sprint亮靛(*kw):
|
def sprint亮靛(*kw):
|
||||||
return "\033[1;36m"+' '.join(kw)+"\033[0m"
|
return "\033[1;36m"+' '.join(kw)+"\033[0m"
|
||||||
|
|
||||||
def log红(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint红(*kw))
|
|
||||||
def log绿(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint绿(*kw))
|
|
||||||
def log黄(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint黄(*kw))
|
|
||||||
def log蓝(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint蓝(*kw))
|
|
||||||
def log紫(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint紫(*kw))
|
|
||||||
def log靛(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint靛(*kw))
|
|
||||||
|
|
||||||
def log亮红(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮红(*kw))
|
|
||||||
def log亮绿(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮绿(*kw))
|
|
||||||
def log亮黄(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮黄(*kw))
|
|
||||||
def log亮蓝(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮蓝(*kw))
|
|
||||||
def log亮紫(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮紫(*kw))
|
|
||||||
def log亮靛(*kw,**kargs):
|
|
||||||
logger.opt(depth=1).info(sprint亮靛(*kw))
|
|
||||||
260
config.py
260
config.py
@@ -2,80 +2,45 @@
|
|||||||
以下所有配置也都支持利用环境变量覆写,环境变量配置格式见docker-compose.yml。
|
以下所有配置也都支持利用环境变量覆写,环境变量配置格式见docker-compose.yml。
|
||||||
读取优先级:环境变量 > config_private.py > config.py
|
读取优先级:环境变量 > config_private.py > config.py
|
||||||
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
All the following configurations also support using environment variables to override,
|
All the following configurations also support using environment variables to override,
|
||||||
and the environment variable configuration format can be seen in docker-compose.yml.
|
and the environment variable configuration format can be seen in docker-compose.yml.
|
||||||
Configuration reading priority: environment variable > config_private.py > config.py
|
Configuration reading priority: environment variable > config_private.py > config.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# [step 1-1]>> ( 接入OpenAI模型家族 ) API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
# [step 1]>> API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
||||||
API_KEY = "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" # 可同时填写多个API-KEY,用英文逗号分割,例如API_KEY = "sk-openaikey1,sk-openaikey2,fkxxxx-api2dkey3,azure-apikey4"
|
API_KEY = "此处填API密钥" # 可同时填写多个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 1-3]>> ( 接入 deepseek-reasoner, 即 deepseek-r1 ) 深度求索(DeepSeek) API KEY,默认请求地址为"https://api.deepseek.com/v1/chat/completions"
|
# [step 2]>> 改为True应用代理,如果直接在海外服务器部署,此处不修改;如果使用本地或无地域限制的大模型时,此处也不需要修改
|
||||||
DEEPSEEK_API_KEY = "sk-d99b8cc6b7414cc88a5d950a3ff7585e"
|
|
||||||
|
|
||||||
# [step 2]>> 改为True应用代理。如果使用本地或无地域限制的大模型时,此处不修改;如果直接在海外服务器部署,此处不修改
|
|
||||||
USE_PROXY = False
|
USE_PROXY = False
|
||||||
if USE_PROXY:
|
if USE_PROXY:
|
||||||
|
"""
|
||||||
|
代理网络的地址,打开你的代理软件查看代理协议(socks5h / http)、地址(localhost)和端口(11284)
|
||||||
|
填写格式是 [协议]:// [地址] :[端口],填写之前不要忘记把USE_PROXY改成True,如果直接在海外服务器部署,此处不修改
|
||||||
|
<配置教程&视频教程> https://github.com/binary-husky/gpt_academic/issues/1>
|
||||||
|
[协议] 常见协议无非socks5h/http; 例如 v2**y 和 ss* 的默认本地协议是socks5h; 而cl**h 的默认本地协议是http
|
||||||
|
[地址] 填localhost或者127.0.0.1(localhost意思是代理软件安装在本机上)
|
||||||
|
[端口] 在代理软件的设置里找。虽然不同的代理软件界面不一样,但端口号都应该在最显眼的位置上
|
||||||
|
"""
|
||||||
proxies = {
|
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",
|
"http": "socks5h://localhost:11284", # 再例如 "http": "http://127.0.0.1:7890",
|
||||||
|
"https": "socks5h://localhost:11284", # 再例如 "https": "http://127.0.0.1:7890",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
proxies = None
|
proxies = None
|
||||||
|
|
||||||
# [step 3]>> 模型选择是 (注意: LLM_MODEL是默认选中的模型, 它*必须*被包含在AVAIL_LLM_MODELS列表中 )
|
# ------------------------------------ 以下配置可以优化体验, 但大部分场合下并不需要修改 ------------------------------------
|
||||||
LLM_MODEL = "gpt-4" # 可选 ↓↓↓
|
|
||||||
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", "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"
|
|
||||||
|
|
||||||
# --- --- --- ---
|
|
||||||
# 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和对话隐私完全暴露给您设定的中间人!)
|
# 重新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": "在这里填写重定向的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 = {"https://api.openai.com/v1/chat/completions": "https://reverse-proxy-url/v1/chat/completions"}
|
||||||
API_URL_REDIRECT = {}
|
API_URL_REDIRECT = {}
|
||||||
|
|
||||||
|
|
||||||
# 多线程函数插件中,默认允许多少路线程同时访问OpenAI。Free trial users的限制是每分钟3次,Pay-as-you-go users的限制是每分钟3500次
|
# 多线程函数插件中,默认允许多少路线程同时访问OpenAI。Free trial users的限制是每分钟3次,Pay-as-you-go users的限制是每分钟3500次
|
||||||
# 一言以蔽之:免费(5刀)用户填3,OpenAI绑了信用卡的用户可以填 16 或者更高。提高限制请查询:https://platform.openai.com/docs/guides/rate-limits/overview
|
# 一言以蔽之:免费(5刀)用户填3,OpenAI绑了信用卡的用户可以填 16 或者更高。提高限制请查询:https://platform.openai.com/docs/guides/rate-limits/overview
|
||||||
DEFAULT_WORKER_NUM = 8
|
DEFAULT_WORKER_NUM = 3
|
||||||
|
|
||||||
|
|
||||||
# 色彩主题, 可选 ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast"]
|
# 色彩主题, 可选 ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast"]
|
||||||
@@ -83,31 +48,6 @@ DEFAULT_WORKER_NUM = 8
|
|||||||
THEME = "Default"
|
THEME = "Default"
|
||||||
AVAIL_THEMES = ["Default", "Chuanhu-Small-and-Beautiful", "High-Contrast", "Gstaff/Xkcd", "NoCrypt/Miku"]
|
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)
|
# 默认的系统提示词(system prompt)
|
||||||
INIT_SYS_PROMPT = "Serve me as a writing and programming assistant."
|
INIT_SYS_PROMPT = "Serve me as a writing and programming assistant."
|
||||||
@@ -126,7 +66,7 @@ LAYOUT = "LEFT-RIGHT" # "LEFT-RIGHT"(左右布局) # "TOP-DOWN"(上下
|
|||||||
|
|
||||||
|
|
||||||
# 暗色模式 / 亮色模式
|
# 暗色模式 / 亮色模式
|
||||||
DARK_MODE = True
|
DARK_MODE = True
|
||||||
|
|
||||||
|
|
||||||
# 发送请求到OpenAI后,等待多久判定为超时
|
# 发送请求到OpenAI后,等待多久判定为超时
|
||||||
@@ -134,20 +74,31 @@ TIMEOUT_SECONDS = 30
|
|||||||
|
|
||||||
|
|
||||||
# 网页的端口, -1代表随机端口
|
# 网页的端口, -1代表随机端口
|
||||||
WEB_PORT = 19998
|
WEB_PORT = -1
|
||||||
|
|
||||||
# 是否自动打开浏览器页面
|
|
||||||
AUTO_OPEN_BROWSER = True
|
|
||||||
|
|
||||||
|
|
||||||
# 如果OpenAI不响应(网络卡顿、代理失败、KEY失效),重试的次数限制
|
# 如果OpenAI不响应(网络卡顿、代理失败、KEY失效),重试的次数限制
|
||||||
MAX_RETRY = 3
|
MAX_RETRY = 2
|
||||||
|
|
||||||
|
|
||||||
# 插件分类默认选项
|
# 插件分类默认选项
|
||||||
DEFAULT_FN_GROUPS = ['对话', '编程', '学术', '智能体']
|
DEFAULT_FN_GROUPS = ['对话', '编程', '学术', '智能体']
|
||||||
|
|
||||||
|
|
||||||
|
# 模型选择是 (注意: LLM_MODEL是默认选中的模型, 它*必须*被包含在AVAIL_LLM_MODELS列表中 )
|
||||||
|
LLM_MODEL = "gpt-3.5-turbo" # 可选 ↓↓↓
|
||||||
|
AVAIL_LLM_MODELS = ["gpt-3.5-turbo-1106","gpt-4-1106-preview","gpt-4-vision-preview",
|
||||||
|
"gpt-3.5-turbo-16k", "gpt-3.5-turbo", "azure-gpt-3.5",
|
||||||
|
"gpt-4", "gpt-4-32k", "azure-gpt-4", "api2d-gpt-4",
|
||||||
|
"gemini-pro", "chatglm3", "moss", "claude-2"]
|
||||||
|
# P.S. 其他可用的模型还包括 [
|
||||||
|
# "qwen-turbo", "qwen-plus", "qwen-max"
|
||||||
|
# "zhipuai", "qianfan", "deepseekcoder", "llama2", "qwen-local", "gpt-3.5-turbo-0613",
|
||||||
|
# "gpt-3.5-turbo-16k-0613", "gpt-3.5-random", "api2d-gpt-3.5-turbo", 'api2d-gpt-3.5-turbo-16k',
|
||||||
|
# "spark", "sparkv2", "sparkv3", "chatglm_onnx", "claude-1-100k", "claude-2", "internlm", "jittorllms_pangualpha", "jittorllms_llama"
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
# 定义界面上“询问多个GPT模型”插件应该使用哪些模型,请从AVAIL_LLM_MODELS中选择,并在不同模型之间用`&`间隔,例如"gpt-3.5-turbo&chatglm3&azure-gpt-4"
|
# 定义界面上“询问多个GPT模型”插件应该使用哪些模型,请从AVAIL_LLM_MODELS中选择,并在不同模型之间用`&`间隔,例如"gpt-3.5-turbo&chatglm3&azure-gpt-4"
|
||||||
MULTI_QUERY_LLM_MODELS = "gpt-3.5-turbo&chatglm3"
|
MULTI_QUERY_LLM_MODELS = "gpt-3.5-turbo&chatglm3"
|
||||||
|
|
||||||
@@ -158,15 +109,16 @@ MULTI_QUERY_LLM_MODELS = "gpt-3.5-turbo&chatglm3"
|
|||||||
QWEN_LOCAL_MODEL_SELECTION = "Qwen/Qwen-1_8B-Chat-Int8"
|
QWEN_LOCAL_MODEL_SELECTION = "Qwen/Qwen-1_8B-Chat-Int8"
|
||||||
|
|
||||||
|
|
||||||
|
# 接入通义千问在线大模型 https://dashscope.console.aliyun.com/
|
||||||
|
DASHSCOPE_API_KEY = "" # 阿里灵积云API_KEY
|
||||||
|
|
||||||
|
|
||||||
# 百度千帆(LLM_MODEL="qianfan")
|
# 百度千帆(LLM_MODEL="qianfan")
|
||||||
BAIDU_CLOUD_API_KEY = ''
|
BAIDU_CLOUD_API_KEY = ''
|
||||||
BAIDU_CLOUD_SECRET_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"
|
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"
|
||||||
|
|
||||||
|
|
||||||
# 如果使用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",并在此处指定模型路径
|
# 如果使用ChatGLM2微调模型,请把 LLM_MODEL="chatglmft",并在此处指定模型路径
|
||||||
CHATGLM_PTUNING_CHECKPOINT = "" # 例如"/home/hmp/ChatGLM2-6B/ptuning/output/6b-pt-128-1e-2/checkpoint-100"
|
CHATGLM_PTUNING_CHECKPOINT = "" # 例如"/home/hmp/ChatGLM2-6B/ptuning/output/6b-pt-128-1e-2/checkpoint-100"
|
||||||
|
|
||||||
@@ -175,7 +127,6 @@ CHATGLM_PTUNING_CHECKPOINT = "" # 例如"/home/hmp/ChatGLM2-6B/ptuning/output/6b
|
|||||||
LOCAL_MODEL_DEVICE = "cpu" # 可选 "cuda"
|
LOCAL_MODEL_DEVICE = "cpu" # 可选 "cuda"
|
||||||
LOCAL_MODEL_QUANT = "FP16" # 默认 "FP16" "INT4" 启用量化INT4版本 "INT8" 启用量化INT8版本
|
LOCAL_MODEL_QUANT = "FP16" # 默认 "FP16" "INT4" 启用量化INT4版本 "INT8" 启用量化INT8版本
|
||||||
|
|
||||||
|
|
||||||
# 设置gradio的并行线程数(不需要修改)
|
# 设置gradio的并行线程数(不需要修改)
|
||||||
CONCURRENT_COUNT = 100
|
CONCURRENT_COUNT = 100
|
||||||
|
|
||||||
@@ -185,7 +136,7 @@ AUTO_CLEAR_TXT = False
|
|||||||
|
|
||||||
|
|
||||||
# 加一个live2d装饰
|
# 加一个live2d装饰
|
||||||
ADD_WAIFU = True
|
ADD_WAIFU = False
|
||||||
|
|
||||||
|
|
||||||
# 设置用户名和密码(不需要修改)(相关功能不稳定,与gradio版本和网络都相关,如果本地使用不建议加这个)
|
# 设置用户名和密码(不需要修改)(相关功能不稳定,与gradio版本和网络都相关,如果本地使用不建议加这个)
|
||||||
@@ -193,8 +144,7 @@ ADD_WAIFU = True
|
|||||||
AUTHENTICATION = []
|
AUTHENTICATION = []
|
||||||
|
|
||||||
|
|
||||||
# 如果需要在二级路径下运行(常规情况下,不要修改!!)
|
# 如果需要在二级路径下运行(常规情况下,不要修改!!)(需要配合修改main.py才能生效!)
|
||||||
# (举例 CUSTOM_PATH = "/gpt_academic",可以让软件运行在 http://ip:port/gpt_academic/ 下。)
|
|
||||||
CUSTOM_PATH = "/"
|
CUSTOM_PATH = "/"
|
||||||
|
|
||||||
|
|
||||||
@@ -208,7 +158,7 @@ API_ORG = ""
|
|||||||
|
|
||||||
|
|
||||||
# 如果需要使用Slack Claude,使用教程详情见 request_llms/README.md
|
# 如果需要使用Slack Claude,使用教程详情见 request_llms/README.md
|
||||||
SLACK_CLAUDE_BOT_ID = ''
|
SLACK_CLAUDE_BOT_ID = ''
|
||||||
SLACK_CLAUDE_USER_TOKEN = ''
|
SLACK_CLAUDE_USER_TOKEN = ''
|
||||||
|
|
||||||
|
|
||||||
@@ -222,8 +172,14 @@ AZURE_ENGINE = "填入你亲手写的部署名" # 读 docs\use_azure.
|
|||||||
AZURE_CFG_ARRAY = {}
|
AZURE_CFG_ARRAY = {}
|
||||||
|
|
||||||
|
|
||||||
# 阿里云实时语音识别 配置难度较高
|
# 使用Newbing (不推荐使用,未来将删除)
|
||||||
# 参考 https://github.com/binary-husky/gpt_academic/blob/master/docs/use_audio.md
|
NEWBING_STYLE = "creative" # ["creative", "balanced", "precise"]
|
||||||
|
NEWBING_COOKIES = """
|
||||||
|
put your new bing cookies here
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 阿里云实时语音识别 配置难度较高 仅建议高手用户使用 参考 https://github.com/binary-husky/gpt_academic/blob/master/docs/use_audio.md
|
||||||
ENABLE_AUDIO = False
|
ENABLE_AUDIO = False
|
||||||
ALIYUN_TOKEN="" # 例如 f37f30e0f9934c34a992f6f64f7eba4f
|
ALIYUN_TOKEN="" # 例如 f37f30e0f9934c34a992f6f64f7eba4f
|
||||||
ALIYUN_APPKEY="" # 例如 RoPlZrM88DnAFkZK
|
ALIYUN_APPKEY="" # 例如 RoPlZrM88DnAFkZK
|
||||||
@@ -231,12 +187,6 @@ ALIYUN_ACCESSKEY="" # (无需填写)
|
|||||||
ALIYUN_SECRET="" # (无需填写)
|
ALIYUN_SECRET="" # (无需填写)
|
||||||
|
|
||||||
|
|
||||||
# GPT-SOVITS 文本转语音服务的运行地址(将语言模型的生成文本朗读出来)
|
|
||||||
TTS_TYPE = "EDGE_TTS" # EDGE_TTS / LOCAL_SOVITS_API / DISABLE
|
|
||||||
GPT_SOVITS_URL = ""
|
|
||||||
EDGE_TTS_VOICE = "zh-CN-XiaoxiaoNeural"
|
|
||||||
|
|
||||||
|
|
||||||
# 接入讯飞星火大模型 https://console.xfyun.cn/services/iat
|
# 接入讯飞星火大模型 https://console.xfyun.cn/services/iat
|
||||||
XFYUN_APPID = "00000000"
|
XFYUN_APPID = "00000000"
|
||||||
XFYUN_API_SECRET = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
XFYUN_API_SECRET = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
@@ -245,40 +195,13 @@ XFYUN_API_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|||||||
|
|
||||||
# 接入智谱大模型
|
# 接入智谱大模型
|
||||||
ZHIPUAI_API_KEY = ""
|
ZHIPUAI_API_KEY = ""
|
||||||
ZHIPUAI_MODEL = "" # 此选项已废弃,不再需要填写
|
ZHIPUAI_MODEL = "glm-4" # 可选 "glm-3-turbo" "glm-4"
|
||||||
|
|
||||||
|
|
||||||
# Claude API KEY
|
# Claude API KEY
|
||||||
ANTHROPIC_API_KEY = ""
|
ANTHROPIC_API_KEY = ""
|
||||||
|
|
||||||
|
|
||||||
# 月之暗面 API KEY
|
|
||||||
MOONSHOT_API_KEY = ""
|
|
||||||
|
|
||||||
|
|
||||||
# 零一万物(Yi Model) API KEY
|
|
||||||
YIMODEL_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 = ""
|
|
||||||
MATHPIX_APPKEY = ""
|
|
||||||
|
|
||||||
|
|
||||||
# DOC2X的PDF解析服务,注册账号并获取API KEY: https://doc2x.noedgeai.com/login
|
|
||||||
DOC2X_API_KEY = ""
|
|
||||||
|
|
||||||
|
|
||||||
# 自定义API KEY格式
|
# 自定义API KEY格式
|
||||||
CUSTOM_API_KEY_PATTERN = ""
|
CUSTOM_API_KEY_PATTERN = ""
|
||||||
|
|
||||||
@@ -295,15 +218,11 @@ HUGGINGFACE_ACCESS_TOKEN = "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"
|
|||||||
# 获取方法:复制以下空间https://huggingface.co/spaces/qingxu98/grobid,设为public,然后GROBID_URL = "https://(你的hf用户名如qingxu98)-(你的填写的空间名如grobid).hf.space"
|
# 获取方法:复制以下空间https://huggingface.co/spaces/qingxu98/grobid,设为public,然后GROBID_URL = "https://(你的hf用户名如qingxu98)-(你的填写的空间名如grobid).hf.space"
|
||||||
GROBID_URLS = [
|
GROBID_URLS = [
|
||||||
"https://qingxu98-grobid.hf.space","https://qingxu98-grobid2.hf.space","https://qingxu98-grobid3.hf.space",
|
"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-grobid4.hf.space","https://qingxu98-grobid5.hf.space", "https://qingxu98-grobid6.hf.space",
|
||||||
"https://qingxu98-grobid7.hf.space", "https://qingxu98-grobid8.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
|
ALLOW_RESET_CONFIG = False
|
||||||
|
|
||||||
@@ -312,21 +231,21 @@ ALLOW_RESET_CONFIG = False
|
|||||||
AUTOGEN_USE_DOCKER = False
|
AUTOGEN_USE_DOCKER = False
|
||||||
|
|
||||||
|
|
||||||
# 临时的上传文件夹位置,请尽量不要修改
|
# 临时的上传文件夹位置,请勿修改
|
||||||
PATH_PRIVATE_UPLOAD = "private_upload"
|
PATH_PRIVATE_UPLOAD = "private_upload"
|
||||||
|
|
||||||
|
|
||||||
# 日志文件夹的位置,请尽量不要修改
|
# 日志文件夹的位置,请勿修改
|
||||||
PATH_LOGGING = "gpt_log"
|
PATH_LOGGING = "gpt_log"
|
||||||
|
|
||||||
|
|
||||||
# 存储翻译好的arxiv论文的路径,请尽量不要修改
|
# 除了连接OpenAI之外,还有哪些场合允许使用代理,请勿修改
|
||||||
ARXIV_CACHE_DIR = "gpt_log/arxiv_cache"
|
WHEN_TO_USE_PROXY = ["Download_LLM", "Download_Gradio_Theme", "Connect_Grobid",
|
||||||
|
"Warmup_Modules", "Nougat_Download", "AutoGen"]
|
||||||
|
|
||||||
|
|
||||||
# 除了连接OpenAI之外,还有哪些场合允许使用代理,请尽量不要修改
|
# *实验性功能*: 自动检测并屏蔽失效的KEY,请勿使用
|
||||||
WHEN_TO_USE_PROXY = ["Connect_OpenAI", "Download_LLM", "Download_Gradio_Theme", "Connect_Grobid",
|
BLOCK_INVALID_APIKEY = False
|
||||||
"Warmup_Modules", "Nougat_Download", "AutoGen", "Connect_OpenAI_Embedding"]
|
|
||||||
|
|
||||||
|
|
||||||
# 启用插件热加载
|
# 启用插件热加载
|
||||||
@@ -336,32 +255,7 @@ PLUGIN_HOT_RELOAD = False
|
|||||||
# 自定义按钮的最大数量限制
|
# 自定义按钮的最大数量限制
|
||||||
NUM_CUSTOM_BASIC_BTN = 4
|
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 = ""
|
|
||||||
|
|
||||||
|
|
||||||
# SEMANTIC SCHOLAR API KEY
|
|
||||||
SEMANTIC_SCHOLAR_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]
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
--------------- 配置关联关系说明 ---------------
|
|
||||||
|
|
||||||
在线大模型配置关联关系示意图
|
在线大模型配置关联关系示意图
|
||||||
│
|
│
|
||||||
├── "gpt-3.5-turbo" 等openai模型
|
├── "gpt-3.5-turbo" 等openai模型
|
||||||
@@ -385,7 +279,7 @@ AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0
|
|||||||
│ ├── XFYUN_API_SECRET
|
│ ├── XFYUN_API_SECRET
|
||||||
│ └── XFYUN_API_KEY
|
│ └── XFYUN_API_KEY
|
||||||
│
|
│
|
||||||
├── "claude-3-opus-20240229" 等claude模型
|
├── "claude-1-100k" 等claude模型
|
||||||
│ └── ANTHROPIC_API_KEY
|
│ └── ANTHROPIC_API_KEY
|
||||||
│
|
│
|
||||||
├── "stack-claude"
|
├── "stack-claude"
|
||||||
@@ -397,11 +291,9 @@ AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0
|
|||||||
│ ├── BAIDU_CLOUD_API_KEY
|
│ ├── BAIDU_CLOUD_API_KEY
|
||||||
│ └── BAIDU_CLOUD_SECRET_KEY
|
│ └── BAIDU_CLOUD_SECRET_KEY
|
||||||
│
|
│
|
||||||
├── "glm-4", "glm-3-turbo", "zhipuai" 智谱AI大模型
|
├── "zhipuai" 智谱AI大模型chatglm_turbo
|
||||||
│ └── ZHIPUAI_API_KEY
|
│ ├── ZHIPUAI_API_KEY
|
||||||
│
|
│ └── ZHIPUAI_MODEL
|
||||||
├── "yi-34b-chat-0205", "yi-34b-chat-200k" 等零一万物(Yi Model)大模型
|
|
||||||
│ └── YIMODEL_API_KEY
|
|
||||||
│
|
│
|
||||||
├── "qwen-turbo" 等通义千问大模型
|
├── "qwen-turbo" 等通义千问大模型
|
||||||
│ └── DASHSCOPE_API_KEY
|
│ └── DASHSCOPE_API_KEY
|
||||||
@@ -409,15 +301,13 @@ AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0
|
|||||||
├── "Gemini"
|
├── "Gemini"
|
||||||
│ └── GEMINI_API_KEY
|
│ └── GEMINI_API_KEY
|
||||||
│
|
│
|
||||||
└── "one-api-...(max_token=...)" 用一种更方便的方式接入one-api多模型管理界面
|
└── "newbing" Newbing接口不再稳定,不推荐使用
|
||||||
├── AVAIL_LLM_MODELS
|
├── NEWBING_STYLE
|
||||||
├── API_KEY
|
└── NEWBING_COOKIES
|
||||||
└── API_URL_REDIRECT
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
本地大模型示意图
|
本地大模型示意图
|
||||||
│
|
│
|
||||||
├── "chatglm4"
|
|
||||||
├── "chatglm3"
|
├── "chatglm3"
|
||||||
├── "chatglm"
|
├── "chatglm"
|
||||||
├── "chatglm_onnx"
|
├── "chatglm_onnx"
|
||||||
@@ -447,9 +337,6 @@ AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0
|
|||||||
|
|
||||||
插件在线服务配置依赖关系示意图
|
插件在线服务配置依赖关系示意图
|
||||||
│
|
│
|
||||||
├── 互联网检索
|
|
||||||
│ └── SEARXNG_URLS
|
|
||||||
│
|
|
||||||
├── 语音功能
|
├── 语音功能
|
||||||
│ ├── ENABLE_AUDIO
|
│ ├── ENABLE_AUDIO
|
||||||
│ ├── ALIYUN_TOKEN
|
│ ├── ALIYUN_TOKEN
|
||||||
@@ -458,9 +345,6 @@ AUTO_CONTEXT_MAX_CLIP_RATIO = [0.80, 0.60, 0.45, 0.25, 0.20, 0.18, 0.16, 0.14, 0
|
|||||||
│ └── ALIYUN_SECRET
|
│ └── ALIYUN_SECRET
|
||||||
│
|
│
|
||||||
└── PDF文档精准解析
|
└── PDF文档精准解析
|
||||||
├── GROBID_URLS
|
└── GROBID_URLS
|
||||||
├── MATHPIX_APPID
|
|
||||||
└── MATHPIX_APPKEY
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,466 +0,0 @@
|
|||||||
"""
|
|
||||||
以下所有配置也都支持利用环境变量覆写,环境变量配置格式见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]>> ( 接入OpenAI模型家族 ) API_KEY = "sk-123456789xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"。极少数情况下,还需要填写组织(格式如org-123456789abcdefghijklmno的),请向下翻,找 API_ORG 设置项
|
|
||||||
API_KEY = "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" # 可同时填写多个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 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 = False
|
|
||||||
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
|
|
||||||
|
|
||||||
# [step 3]>> 模型选择是 (注意: LLM_MODEL是默认选中的模型, 它*必须*被包含在AVAIL_LLM_MODELS列表中 )
|
|
||||||
LLM_MODEL = "gpt-4" # 可选 ↓↓↓
|
|
||||||
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", "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"
|
|
||||||
|
|
||||||
# --- --- --- ---
|
|
||||||
# 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 = 8
|
|
||||||
|
|
||||||
|
|
||||||
# 色彩主题, 可选 ["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)",
|
|
||||||
# 备注:以下字体需要网络支持,您可以自定义任意您喜欢的字体,如下所示,需要满足的格式为 "字体昵称(字体英文真名@字体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."
|
|
||||||
|
|
||||||
|
|
||||||
# 对话窗的高度 (仅在LAYOUT="TOP-DOWN"时生效)
|
|
||||||
CHATBOT_HEIGHT = 1115
|
|
||||||
|
|
||||||
|
|
||||||
# 代码高亮
|
|
||||||
CODE_HIGHLIGHT = True
|
|
||||||
|
|
||||||
|
|
||||||
# 窗口布局
|
|
||||||
LAYOUT = "LEFT-RIGHT" # "LEFT-RIGHT"(左右布局) # "TOP-DOWN"(上下布局)
|
|
||||||
|
|
||||||
|
|
||||||
# 暗色模式 / 亮色模式
|
|
||||||
DARK_MODE = True
|
|
||||||
|
|
||||||
|
|
||||||
# 发送请求到OpenAI后,等待多久判定为超时
|
|
||||||
TIMEOUT_SECONDS = 30
|
|
||||||
|
|
||||||
|
|
||||||
# 网页的端口, -1代表随机端口
|
|
||||||
WEB_PORT = 19998
|
|
||||||
|
|
||||||
# 是否自动打开浏览器页面
|
|
||||||
AUTO_OPEN_BROWSER = True
|
|
||||||
|
|
||||||
|
|
||||||
# 如果OpenAI不响应(网络卡顿、代理失败、KEY失效),重试的次数限制
|
|
||||||
MAX_RETRY = 3
|
|
||||||
|
|
||||||
|
|
||||||
# 插件分类默认选项
|
|
||||||
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 = True
|
|
||||||
|
|
||||||
|
|
||||||
# 设置用户名和密码(不需要修改)(相关功能不稳定,与gradio版本和网络都相关,如果本地使用不建议加这个)
|
|
||||||
# [("username", "password"), ("username2", "password2"), ...]
|
|
||||||
AUTHENTICATION = []
|
|
||||||
|
|
||||||
|
|
||||||
# 如果需要在二级路径下运行(常规情况下,不要修改!!)
|
|
||||||
# (举例 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 = "EDGE_TTS" # 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 = ""
|
|
||||||
|
|
||||||
|
|
||||||
# 接入火山引擎的在线大模型),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 = ""
|
|
||||||
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) ]
|
|
||||||
|
|
||||||
|
|
||||||
# 在互联网搜索组件中,负责将搜索结果整理成干净的Markdown
|
|
||||||
JINA_API_KEY = ""
|
|
||||||
|
|
||||||
|
|
||||||
# SEMANTIC SCHOLAR API KEY
|
|
||||||
SEMANTIC_SCHOLAR_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]
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
--------------- 配置关联关系说明 ---------------
|
|
||||||
|
|
||||||
在线大模型配置关联关系示意图
|
|
||||||
│
|
|
||||||
├── "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
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
@@ -3,71 +3,30 @@
|
|||||||
# 'stop' 颜色对应 theme.py 中的 color_er
|
# 'stop' 颜色对应 theme.py 中的 color_er
|
||||||
import importlib
|
import importlib
|
||||||
from toolbox import clear_line_break
|
from toolbox import clear_line_break
|
||||||
from toolbox import apply_gpt_academic_string_mask_langbased
|
|
||||||
from toolbox import build_gpt_academic_masked_string_langbased
|
|
||||||
from textwrap import dedent
|
|
||||||
|
|
||||||
def get_core_functions():
|
def get_core_functions():
|
||||||
return {
|
return {
|
||||||
|
"英语学术润色": {
|
||||||
"学术语料润色": {
|
|
||||||
# [1*] 前缀字符串,会被加在你的输入之前。例如,用来描述你的要求,例如翻译、解释代码、润色等等。
|
|
||||||
# 这里填一个提示词字符串就行了,这里为了区分中英文情景搞复杂了一点
|
|
||||||
"Prefix": build_gpt_academic_masked_string_langbased(
|
|
||||||
text_show_english=
|
|
||||||
r"Below is a paragraph from an academic paper. Polish the writing to meet the academic style, "
|
|
||||||
r"improve the spelling, grammar, clarity, concision and overall readability. When necessary, rewrite the whole sentence. "
|
|
||||||
r"Firstly, you should provide the polished paragraph (in English). "
|
|
||||||
r"Secondly, you should list all your modification and explain the reasons to do so in markdown table.",
|
|
||||||
text_show_chinese=
|
|
||||||
r"作为一名中文学术论文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,"
|
|
||||||
r"同时分解长句,减少重复,并提供改进建议。请先提供文本的更正版本,然后在markdown表格中列出修改的内容,并给出修改的理由:"
|
|
||||||
) + "\n\n",
|
|
||||||
# [2*] 后缀字符串,会被加在你的输入之后。例如,配合前缀可以把你的输入内容用引号圈起来
|
|
||||||
"Suffix": r"",
|
|
||||||
# [3] 按钮颜色 (可选参数,默认 secondary)
|
|
||||||
"Color": r"secondary",
|
|
||||||
# [4] 按钮是否可见 (可选参数,默认 True,即可见)
|
|
||||||
"Visible": True,
|
|
||||||
# [5] 是否在触发时清除历史 (可选参数,默认 False,即不处理之前的对话历史)
|
|
||||||
"AutoClearHistory": False,
|
|
||||||
# [6] 文本预处理 (可选参数,默认 None,举例:写个函数移除所有的换行符)
|
|
||||||
"PreProcess": None,
|
|
||||||
# [7] 模型选择 (可选参数。如不设置,则使用当前全局模型;如设置,则用指定模型覆盖全局模型。)
|
|
||||||
# "ModelOverride": "gpt-3.5-turbo", # 主要用途:强制点击此基础功能按钮时,使用指定的模型。
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
"总结绘制脑图": {
|
|
||||||
# 前缀,会被加在你的输入之前。例如,用来描述你的要求,例如翻译、解释代码、润色等等
|
# 前缀,会被加在你的输入之前。例如,用来描述你的要求,例如翻译、解释代码、润色等等
|
||||||
"Prefix": '''"""\n\n''',
|
"Prefix": r"Below is a paragraph from an academic paper. Polish the writing to meet the academic style, " +
|
||||||
|
r"improve the spelling, grammar, clarity, concision and overall readability. When necessary, rewrite the whole sentence. " +
|
||||||
|
r"Firstly, you should provide the polished paragraph. "
|
||||||
|
r"Secondly, you should list all your modification and explain the reasons to do so in markdown table." + "\n\n",
|
||||||
# 后缀,会被加在你的输入之后。例如,配合前缀可以把你的输入内容用引号圈起来
|
# 后缀,会被加在你的输入之后。例如,配合前缀可以把你的输入内容用引号圈起来
|
||||||
"Suffix":
|
"Suffix": r"",
|
||||||
# dedent() 函数用于去除多行字符串的缩进
|
# 按钮颜色 (默认 secondary)
|
||||||
dedent("\n\n"+r'''
|
"Color": r"secondary",
|
||||||
"""
|
# 按钮是否可见 (默认 True,即可见)
|
||||||
|
"Visible": True,
|
||||||
使用mermaid flowchart对以上文本进行总结,概括上述段落的内容以及内在逻辑关系,例如:
|
# 是否在触发时清除历史 (默认 False,即不处理之前的对话历史)
|
||||||
|
"AutoClearHistory": False
|
||||||
以下是对以上文本的总结,以mermaid flowchart的形式展示:
|
},
|
||||||
```mermaid
|
"中文学术润色": {
|
||||||
flowchart LR
|
"Prefix": r"作为一名中文学术论文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性," +
|
||||||
A["节点名1"] --> B("节点名2")
|
r"同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。请编辑以下文本" + "\n\n",
|
||||||
B --> C{"节点名3"}
|
"Suffix": r"",
|
||||||
C --> D["节点名4"]
|
|
||||||
C --> |"箭头名1"| E["节点名5"]
|
|
||||||
C --> |"箭头名2"| F["节点名6"]
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:
|
|
||||||
(1)使用中文
|
|
||||||
(2)节点名字使用引号包裹,如["Laptop"]
|
|
||||||
(3)`|` 和 `"`之间不要存在空格
|
|
||||||
(4)根据情况选择flowchart LR(从左到右)或者flowchart TD(从上到下)
|
|
||||||
'''),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"查找语法错误": {
|
"查找语法错误": {
|
||||||
"Prefix": r"Help me ensure that the grammar and the spelling is correct. "
|
"Prefix": r"Help me ensure that the grammar and the spelling is correct. "
|
||||||
r"Do not try to polish the text, if no mistake is found, tell me that this paragraph is good. "
|
r"Do not try to polish the text, if no mistake is found, tell me that this paragraph is good. "
|
||||||
@@ -87,61 +46,42 @@ def get_core_functions():
|
|||||||
"Suffix": r"",
|
"Suffix": r"",
|
||||||
"PreProcess": clear_line_break, # 预处理:清除换行符
|
"PreProcess": clear_line_break, # 预处理:清除换行符
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"中译英": {
|
"中译英": {
|
||||||
"Prefix": r"Please translate following sentence to English:" + "\n\n",
|
"Prefix": r"Please translate following sentence to English:" + "\n\n",
|
||||||
"Suffix": r"",
|
"Suffix": r"",
|
||||||
},
|
},
|
||||||
|
"学术中英互译": {
|
||||||
|
"Prefix": r"I want you to act as a scientific English-Chinese translator, " +
|
||||||
"学术英中互译": {
|
r"I will provide you with some paragraphs in one language " +
|
||||||
"Prefix": build_gpt_academic_masked_string_langbased(
|
r"and your task is to accurately and academically translate the paragraphs only into the other language. " +
|
||||||
text_show_chinese=
|
r"Do not repeat the original provided paragraphs after translation. " +
|
||||||
r"I want you to act as a scientific English-Chinese translator, "
|
r"You should use artificial intelligence tools, " +
|
||||||
r"I will provide you with some paragraphs in one language "
|
r"such as natural language processing, and rhetorical knowledge " +
|
||||||
r"and your task is to accurately and academically translate the paragraphs only into the other language. "
|
r"and experience about effective writing techniques to reply. " +
|
||||||
r"Do not repeat the original provided paragraphs after translation. "
|
r"I'll give you my paragraphs as follows, tell me what language it is written in, and then translate:" + "\n\n",
|
||||||
r"You should use artificial intelligence tools, "
|
"Suffix": "",
|
||||||
r"such as natural language processing, and rhetorical knowledge "
|
"Color": "secondary",
|
||||||
r"and experience about effective writing techniques to reply. "
|
|
||||||
r"I'll give you my paragraphs as follows, tell me what language it is written in, and then translate:",
|
|
||||||
text_show_english=
|
|
||||||
r"你是经验丰富的翻译,请把以下学术文章段落翻译成中文,"
|
|
||||||
r"并同时充分考虑中文的语法、清晰、简洁和整体可读性,"
|
|
||||||
r"必要时,你可以修改整个句子的顺序以确保翻译后的段落符合中文的语言习惯。"
|
|
||||||
r"你需要翻译的文本如下:"
|
|
||||||
) + "\n\n",
|
|
||||||
"Suffix": r"",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"英译中": {
|
"英译中": {
|
||||||
"Prefix": r"翻译成地道的中文:" + "\n\n",
|
"Prefix": r"翻译成地道的中文:" + "\n\n",
|
||||||
"Suffix": r"",
|
"Suffix": r"",
|
||||||
"Visible": False,
|
"Visible": False,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"找图片": {
|
"找图片": {
|
||||||
"Prefix": r"我需要你找一张网络图片。使用Unsplash API(https://source.unsplash.com/960x640/?<英语关键词>)获取图片URL,"
|
"Prefix": r"我需要你找一张网络图片。使用Unsplash API(https://source.unsplash.com/960x640/?<英语关键词>)获取图片URL," +
|
||||||
r"然后请使用Markdown格式封装,并且不要有反斜线,不要用代码块。现在,请按以下描述给我发送图片:" + "\n\n",
|
r"然后请使用Markdown格式封装,并且不要有反斜线,不要用代码块。现在,请按以下描述给我发送图片:" + "\n\n",
|
||||||
"Suffix": r"",
|
"Suffix": r"",
|
||||||
"Visible": False,
|
"Visible": False,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"解释代码": {
|
"解释代码": {
|
||||||
"Prefix": r"请解释以下代码:" + "\n```\n",
|
"Prefix": r"请解释以下代码:" + "\n```\n",
|
||||||
"Suffix": "\n```\n",
|
"Suffix": "\n```\n",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"参考文献转Bib": {
|
"参考文献转Bib": {
|
||||||
"Prefix": r"Here are some bibliography items, please transform them into bibtex style."
|
"Prefix": r"Here are some bibliography items, please transform them into bibtex style." +
|
||||||
r"Note that, reference styles maybe more than one kind, you should transform each item correctly."
|
r"Note that, reference styles maybe more than one kind, you should transform each item correctly." +
|
||||||
r"Items need to be transformed:" + "\n\n",
|
r"Items need to be transformed:",
|
||||||
"Visible": False,
|
"Visible": False,
|
||||||
"Suffix": r"",
|
"Suffix": r"",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,18 +98,8 @@ def handle_core_functionality(additional_fn, inputs, history, chatbot):
|
|||||||
return inputs, history
|
return inputs, history
|
||||||
else:
|
else:
|
||||||
# 预制功能
|
# 预制功能
|
||||||
if "PreProcess" in core_functional[additional_fn]:
|
if "PreProcess" in core_functional[additional_fn]: inputs = core_functional[additional_fn]["PreProcess"](inputs) # 获取预处理函数(如果有的话)
|
||||||
if core_functional[additional_fn]["PreProcess"] is not None:
|
inputs = core_functional[additional_fn]["Prefix"] + inputs + core_functional[additional_fn]["Suffix"]
|
||||||
inputs = core_functional[additional_fn]["PreProcess"](inputs) # 获取预处理函数(如果有的话)
|
|
||||||
# 为字符串加上上面定义的前缀和后缀。
|
|
||||||
inputs = apply_gpt_academic_string_mask_langbased(
|
|
||||||
string = core_functional[additional_fn]["Prefix"] + inputs + core_functional[additional_fn]["Suffix"],
|
|
||||||
lang_reference = inputs,
|
|
||||||
)
|
|
||||||
if core_functional[additional_fn].get("AutoClearHistory", False):
|
if core_functional[additional_fn].get("AutoClearHistory", False):
|
||||||
history = []
|
history = []
|
||||||
return inputs, history
|
return inputs, history
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
t = get_core_functions()["总结绘制脑图"]
|
|
||||||
print(t["Prefix"] + t["Suffix"])
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,290 +0,0 @@
|
|||||||
import re
|
|
||||||
import os
|
|
||||||
import asyncio
|
|
||||||
from typing import List, Dict, Tuple
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from textwrap import dedent
|
|
||||||
from toolbox import CatchException, get_conf, update_ui, promote_file_to_downloadzone, get_log_folder, get_user
|
|
||||||
from toolbox import update_ui, CatchException, report_exception, write_history_to_file
|
|
||||||
from crazy_functions.review_fns.data_sources.semantic_source import SemanticScholarSource
|
|
||||||
from crazy_functions.review_fns.data_sources.arxiv_source import ArxivSource
|
|
||||||
from crazy_functions.review_fns.query_analyzer import QueryAnalyzer
|
|
||||||
from crazy_functions.review_fns.handlers.review_handler import 文献综述功能
|
|
||||||
from crazy_functions.review_fns.handlers.recommend_handler import 论文推荐功能
|
|
||||||
from crazy_functions.review_fns.handlers.qa_handler import 学术问答功能
|
|
||||||
from crazy_functions.review_fns.handlers.paper_handler import 单篇论文分析功能
|
|
||||||
from crazy_functions.Conversation_To_File import write_chat_to_file
|
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
|
||||||
from crazy_functions.review_fns.handlers.latest_handler import Arxiv最新论文推荐功能
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def 学术对话(txt: str, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List,
|
|
||||||
history: List, system_prompt: str, user_request: str):
|
|
||||||
"""主函数"""
|
|
||||||
|
|
||||||
# 初始化数据源
|
|
||||||
arxiv_source = ArxivSource()
|
|
||||||
semantic_source = SemanticScholarSource(
|
|
||||||
api_key=get_conf("SEMANTIC_SCHOLAR_KEY")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 初始化处理器
|
|
||||||
handlers = {
|
|
||||||
"review": 文献综述功能(arxiv_source, semantic_source, llm_kwargs),
|
|
||||||
"recommend": 论文推荐功能(arxiv_source, semantic_source, llm_kwargs),
|
|
||||||
"qa": 学术问答功能(arxiv_source, semantic_source, llm_kwargs),
|
|
||||||
"paper": 单篇论文分析功能(arxiv_source, semantic_source, llm_kwargs),
|
|
||||||
"latest": Arxiv最新论文推荐功能(arxiv_source, semantic_source, llm_kwargs),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 分析查询意图
|
|
||||||
chatbot.append([None, "正在分析研究主题和查询要求..."])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
query_analyzer = QueryAnalyzer()
|
|
||||||
search_criteria = yield from query_analyzer.analyze_query(txt, chatbot, llm_kwargs)
|
|
||||||
handler = handlers.get(search_criteria.query_type)
|
|
||||||
if not handler:
|
|
||||||
handler = handlers["qa"] # 默认使用QA处理器
|
|
||||||
|
|
||||||
# 处理查询
|
|
||||||
chatbot.append([None, f"使用{handler.__class__.__name__}处理...,可能需要您耐心等待3~5分钟..."])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
final_prompt = asyncio.run(handler.handle(
|
|
||||||
criteria=search_criteria,
|
|
||||||
chatbot=chatbot,
|
|
||||||
history=history,
|
|
||||||
system_prompt=system_prompt,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
plugin_kwargs=plugin_kwargs
|
|
||||||
))
|
|
||||||
|
|
||||||
if final_prompt:
|
|
||||||
# 检查是否是道歉提示
|
|
||||||
if "很抱歉,我们未能找到" in final_prompt:
|
|
||||||
chatbot.append([txt, final_prompt])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
# 在 final_prompt 末尾添加用户原始查询要求
|
|
||||||
final_prompt += dedent(f"""
|
|
||||||
Original user query: "{txt}"
|
|
||||||
|
|
||||||
IMPORTANT NOTE :
|
|
||||||
- Your response must directly address the user's original user query above
|
|
||||||
- While following the previous guidelines, prioritize answering what the user specifically asked
|
|
||||||
- Make sure your response format and content align with the user's expectations
|
|
||||||
- Do not translate paper titles, keep them in their original language
|
|
||||||
- Do not generate a reference list in your response - references will be handled separately
|
|
||||||
""")
|
|
||||||
|
|
||||||
# 使用最终的prompt生成回答
|
|
||||||
response = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
|
||||||
inputs=final_prompt,
|
|
||||||
inputs_show_user=txt,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
chatbot=chatbot,
|
|
||||||
history=[],
|
|
||||||
sys_prompt=f"You are a helpful academic assistant. Response in Chinese by default unless specified language is required in the user's query."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. 获取文献列表
|
|
||||||
papers_list = handler.ranked_papers # 直接使用原始论文数据
|
|
||||||
|
|
||||||
# 在新的对话中添加格式化的参考文献列表
|
|
||||||
if papers_list:
|
|
||||||
references = ""
|
|
||||||
for idx, paper in enumerate(papers_list, 1):
|
|
||||||
# 构建作者列表
|
|
||||||
authors = paper.authors[:3]
|
|
||||||
if len(paper.authors) > 3:
|
|
||||||
authors.append("et al.")
|
|
||||||
authors_str = ", ".join(authors)
|
|
||||||
|
|
||||||
# 构建期刊指标信息
|
|
||||||
metrics = []
|
|
||||||
if hasattr(paper, 'if_factor') and paper.if_factor:
|
|
||||||
metrics.append(f"IF: {paper.if_factor}")
|
|
||||||
if hasattr(paper, 'jcr_division') and paper.jcr_division:
|
|
||||||
metrics.append(f"JCR: {paper.jcr_division}")
|
|
||||||
if hasattr(paper, 'cas_division') and paper.cas_division:
|
|
||||||
metrics.append(f"中科院分区: {paper.cas_division}")
|
|
||||||
metrics_str = f" [{', '.join(metrics)}]" if metrics else ""
|
|
||||||
|
|
||||||
# 构建DOI链接
|
|
||||||
doi_link = ""
|
|
||||||
if paper.doi:
|
|
||||||
if "arxiv.org" in str(paper.doi):
|
|
||||||
doi_url = paper.doi
|
|
||||||
else:
|
|
||||||
doi_url = f"https://doi.org/{paper.doi}"
|
|
||||||
doi_link = f" <a href='{doi_url}' target='_blank'>DOI: {paper.doi}</a>"
|
|
||||||
|
|
||||||
# 构建完整的引用
|
|
||||||
reference = f"[{idx}] {authors_str}. *{paper.title}*"
|
|
||||||
if paper.venue_name:
|
|
||||||
reference += f". {paper.venue_name}"
|
|
||||||
if paper.year:
|
|
||||||
reference += f", {paper.year}"
|
|
||||||
reference += metrics_str
|
|
||||||
if doi_link:
|
|
||||||
reference += f".{doi_link}"
|
|
||||||
reference += " \n"
|
|
||||||
|
|
||||||
references += reference
|
|
||||||
|
|
||||||
# 添加新的对话显示参考文献
|
|
||||||
chatbot.append(["参考文献如下:", references])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
|
|
||||||
# 2. 保存为不同格式
|
|
||||||
from .review_fns.conversation_doc.word_doc import WordFormatter
|
|
||||||
from .review_fns.conversation_doc.word2pdf import WordToPdfConverter
|
|
||||||
from .review_fns.conversation_doc.markdown_doc import MarkdownFormatter
|
|
||||||
from .review_fns.conversation_doc.html_doc import HtmlFormatter
|
|
||||||
|
|
||||||
# 创建保存目录
|
|
||||||
save_dir = get_log_folder(get_user(chatbot), plugin_name='chatscholar')
|
|
||||||
|
|
||||||
if not os.path.exists(save_dir):
|
|
||||||
os.makedirs(save_dir)
|
|
||||||
|
|
||||||
# 生成文件名
|
|
||||||
def get_safe_filename(txt, max_length=10):
|
|
||||||
# 获取文本前max_length个字符作为文件名
|
|
||||||
filename = txt[:max_length].strip()
|
|
||||||
# 移除不安全的文件名字符
|
|
||||||
filename = re.sub(r'[\\/:*?"<>|]', '', filename)
|
|
||||||
# 如果文件名为空,使用时间戳
|
|
||||||
if not filename:
|
|
||||||
filename = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
||||||
return filename
|
|
||||||
|
|
||||||
base_filename = get_safe_filename(txt)
|
|
||||||
|
|
||||||
result_files = [] # 收集所有生成的文件
|
|
||||||
pdf_path = None # 用于跟踪PDF是否成功生成
|
|
||||||
|
|
||||||
# 保存为Markdown
|
|
||||||
try:
|
|
||||||
md_formatter = MarkdownFormatter()
|
|
||||||
md_content = md_formatter.create_document(txt, response, papers_list)
|
|
||||||
result_file_md = write_history_to_file(
|
|
||||||
history=[md_content],
|
|
||||||
file_basename=f"markdown_{base_filename}.md"
|
|
||||||
)
|
|
||||||
result_files.append(result_file_md)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Markdown保存失败: {str(e)}")
|
|
||||||
|
|
||||||
# 保存为HTML
|
|
||||||
try:
|
|
||||||
html_formatter = HtmlFormatter()
|
|
||||||
html_content = html_formatter.create_document(txt, response, papers_list)
|
|
||||||
result_file_html = write_history_to_file(
|
|
||||||
history=[html_content],
|
|
||||||
file_basename=f"html_{base_filename}.html"
|
|
||||||
)
|
|
||||||
result_files.append(result_file_html)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"HTML保存失败: {str(e)}")
|
|
||||||
|
|
||||||
# 保存为Word
|
|
||||||
try:
|
|
||||||
word_formatter = WordFormatter()
|
|
||||||
try:
|
|
||||||
doc = word_formatter.create_document(txt, response, papers_list)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Word文档内容生成失败: {str(e)}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
try:
|
|
||||||
result_file_docx = os.path.join(
|
|
||||||
os.path.dirname(result_file_md) if result_file_md else save_dir,
|
|
||||||
f"docx_{base_filename}.docx"
|
|
||||||
)
|
|
||||||
doc.save(result_file_docx)
|
|
||||||
result_files.append(result_file_docx)
|
|
||||||
print(f"Word文档已保存到: {result_file_docx}")
|
|
||||||
|
|
||||||
# 转换为PDF
|
|
||||||
try:
|
|
||||||
pdf_path = WordToPdfConverter.convert_to_pdf(result_file_docx)
|
|
||||||
if pdf_path:
|
|
||||||
result_files.append(pdf_path)
|
|
||||||
print(f"PDF文档已生成: {pdf_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"PDF转换失败: {str(e)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Word文档保存失败: {str(e)}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Word格式化失败: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
print(f"详细错误信息: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
# 保存为BibTeX格式
|
|
||||||
try:
|
|
||||||
from .review_fns.conversation_doc.reference_formatter import ReferenceFormatter
|
|
||||||
ref_formatter = ReferenceFormatter()
|
|
||||||
bibtex_content = ref_formatter.create_document(papers_list)
|
|
||||||
|
|
||||||
# 在与其他文件相同目录下创建BibTeX文件
|
|
||||||
result_file_bib = os.path.join(
|
|
||||||
os.path.dirname(result_file_md) if result_file_md else save_dir,
|
|
||||||
f"references_{base_filename}.bib"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 直接写入文件
|
|
||||||
with open(result_file_bib, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(bibtex_content)
|
|
||||||
|
|
||||||
result_files.append(result_file_bib)
|
|
||||||
print(f"BibTeX文件已保存到: {result_file_bib}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"BibTeX格式保存失败: {str(e)}")
|
|
||||||
|
|
||||||
# 保存为EndNote格式
|
|
||||||
try:
|
|
||||||
from .review_fns.conversation_doc.endnote_doc import EndNoteFormatter
|
|
||||||
endnote_formatter = EndNoteFormatter()
|
|
||||||
endnote_content = endnote_formatter.create_document(papers_list)
|
|
||||||
|
|
||||||
# 在与其他文件相同目录下创建EndNote文件
|
|
||||||
result_file_enw = os.path.join(
|
|
||||||
os.path.dirname(result_file_md) if result_file_md else save_dir,
|
|
||||||
f"references_{base_filename}.enw"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 直接写入文件
|
|
||||||
with open(result_file_enw, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(endnote_content)
|
|
||||||
|
|
||||||
result_files.append(result_file_enw)
|
|
||||||
print(f"EndNote文件已保存到: {result_file_enw}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"EndNote格式保存失败: {str(e)}")
|
|
||||||
|
|
||||||
# 添加所有文件到下载区
|
|
||||||
success_files = []
|
|
||||||
for file in result_files:
|
|
||||||
try:
|
|
||||||
promote_file_to_downloadzone(file, chatbot=chatbot)
|
|
||||||
success_files.append(os.path.basename(file))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"文件添加到下载区失败: {str(e)}")
|
|
||||||
|
|
||||||
# 更新成功提示消息
|
|
||||||
if success_files:
|
|
||||||
chatbot.append(["保存对话记录成功,bib和enw文件支持导入到EndNote、Zotero、JabRef、Mendeley等文献管理软件,HTML文件支持在浏览器中打开,里面包含详细论文源信息", "对话已保存并添加到下载区,可以在下载区找到相关文件"])
|
|
||||||
else:
|
|
||||||
chatbot.append(["保存对话记录", "所有格式的保存都失败了,请检查错误日志。"])
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
else:
|
|
||||||
report_exception(chatbot, history, a=f"处理失败", b=f"请尝试其他查询")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
232
crazy_functions/CodeInterpreter.py
Normal file
232
crazy_functions/CodeInterpreter.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
from collections.abc import Callable, Iterable, Mapping
|
||||||
|
from typing import Any
|
||||||
|
from toolbox import CatchException, update_ui, gen_time_str, trimmed_format_exc
|
||||||
|
from toolbox import promote_file_to_downloadzone, get_log_folder
|
||||||
|
from .crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
||||||
|
from .crazy_utils import input_clipping, try_install_deps
|
||||||
|
from multiprocessing import Process, Pipe
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
templete = """
|
||||||
|
```python
|
||||||
|
import ... # Put dependencies here, e.g. import numpy as np
|
||||||
|
|
||||||
|
class TerminalFunction(object): # Do not change the name of the class, The name of the class must be `TerminalFunction`
|
||||||
|
|
||||||
|
def run(self, path): # The name of the function must be `run`, it takes only a positional argument.
|
||||||
|
# rewrite the function you have just written here
|
||||||
|
...
|
||||||
|
return generated_file_path
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inspect_dependency(chatbot, history):
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_code_block(reply):
|
||||||
|
import re
|
||||||
|
pattern = r"```([\s\S]*?)```" # regex pattern to match code blocks
|
||||||
|
matches = re.findall(pattern, reply) # find all code blocks in text
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0].strip('python') # code block
|
||||||
|
for match in matches:
|
||||||
|
if 'class TerminalFunction' in match:
|
||||||
|
return match.strip('python') # code block
|
||||||
|
raise RuntimeError("GPT is not generating proper code.")
|
||||||
|
|
||||||
|
def gpt_interact_multi_step(txt, file_type, llm_kwargs, chatbot, history):
|
||||||
|
# 输入
|
||||||
|
prompt_compose = [
|
||||||
|
f'Your job:\n'
|
||||||
|
f'1. write a single Python function, which takes a path of a `{file_type}` file as the only argument and returns a `string` containing the result of analysis or the path of generated files. \n',
|
||||||
|
f"2. You should write this function to perform following task: " + txt + "\n",
|
||||||
|
f"3. Wrap the output python function with markdown codeblock."
|
||||||
|
]
|
||||||
|
i_say = "".join(prompt_compose)
|
||||||
|
demo = []
|
||||||
|
|
||||||
|
# 第一步
|
||||||
|
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=demo,
|
||||||
|
sys_prompt= r"You are a programmer."
|
||||||
|
)
|
||||||
|
history.extend([i_say, gpt_say])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
||||||
|
|
||||||
|
# 第二步
|
||||||
|
prompt_compose = [
|
||||||
|
"If previous stage is successful, rewrite the function you have just written to satisfy following templete: \n",
|
||||||
|
templete
|
||||||
|
]
|
||||||
|
i_say = "".join(prompt_compose); inputs_show_user = "If previous stage is successful, rewrite the function you have just written to satisfy executable templete. "
|
||||||
|
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,
|
||||||
|
sys_prompt= r"You are a programmer."
|
||||||
|
)
|
||||||
|
code_to_return = gpt_say
|
||||||
|
history.extend([i_say, gpt_say])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
||||||
|
|
||||||
|
# # 第三步
|
||||||
|
# i_say = "Please list to packages to install to run the code above. Then show me how to use `try_install_deps` function to install them."
|
||||||
|
# i_say += 'For instance. `try_install_deps(["opencv-python", "scipy", "numpy"])`'
|
||||||
|
# installation_advance = 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,
|
||||||
|
# sys_prompt= r"You are a programmer."
|
||||||
|
# )
|
||||||
|
# # # 第三步
|
||||||
|
# i_say = "Show me how to use `pip` to install packages to run the code above. "
|
||||||
|
# i_say += 'For instance. `pip install -r opencv-python scipy numpy`'
|
||||||
|
# installation_advance = 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=history,
|
||||||
|
# sys_prompt= r"You are a programmer."
|
||||||
|
# )
|
||||||
|
installation_advance = ""
|
||||||
|
|
||||||
|
return code_to_return, installation_advance, txt, file_type, llm_kwargs, chatbot, history
|
||||||
|
|
||||||
|
def make_module(code):
|
||||||
|
module_file = 'gpt_fn_' + gen_time_str().replace('-','_')
|
||||||
|
with open(f'{get_log_folder()}/{module_file}.py', 'w', encoding='utf8') as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
|
def get_class_name(class_string):
|
||||||
|
import re
|
||||||
|
# Use regex to extract the class name
|
||||||
|
class_name = re.search(r'class (\w+)\(', class_string).group(1)
|
||||||
|
return class_name
|
||||||
|
|
||||||
|
class_name = get_class_name(code)
|
||||||
|
return f"{get_log_folder().replace('/', '.')}.{module_file}->{class_name}"
|
||||||
|
|
||||||
|
def init_module_instance(module):
|
||||||
|
import importlib
|
||||||
|
module_, class_ = module.split('->')
|
||||||
|
init_f = getattr(importlib.import_module(module_), class_)
|
||||||
|
return init_f()
|
||||||
|
|
||||||
|
def for_immediate_show_off_when_possible(file_type, fp, chatbot):
|
||||||
|
if file_type in ['png', 'jpg']:
|
||||||
|
image_path = os.path.abspath(fp)
|
||||||
|
chatbot.append(['这是一张图片, 展示如下:',
|
||||||
|
f'本地文件地址: <br/>`{image_path}`<br/>'+
|
||||||
|
f'本地文件预览: <br/><div align="center"><img src="file={image_path}"></div>'
|
||||||
|
])
|
||||||
|
return chatbot
|
||||||
|
|
||||||
|
def subprocess_worker(instance, file_path, return_dict):
|
||||||
|
return_dict['result'] = instance.run(file_path)
|
||||||
|
|
||||||
|
def have_any_recent_upload_files(chatbot):
|
||||||
|
_5min = 5 * 60
|
||||||
|
if not chatbot: return False # chatbot is None
|
||||||
|
most_recent_uploaded = chatbot._cookies.get("most_recent_uploaded", None)
|
||||||
|
if not most_recent_uploaded: return False # most_recent_uploaded is None
|
||||||
|
if time.time() - most_recent_uploaded["time"] < _5min: return True # most_recent_uploaded is new
|
||||||
|
else: return False # most_recent_uploaded is too old
|
||||||
|
|
||||||
|
def get_recent_file_prompt_support(chatbot):
|
||||||
|
most_recent_uploaded = chatbot._cookies.get("most_recent_uploaded", None)
|
||||||
|
path = most_recent_uploaded['path']
|
||||||
|
return path
|
||||||
|
|
||||||
|
@CatchException
|
||||||
|
def 虚空终端CodeInterpreter(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
|
"""
|
||||||
|
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
||||||
|
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
||||||
|
plugin_kwargs 插件模型的参数,暂时没有用武之地
|
||||||
|
chatbot 聊天显示框的句柄,用于显示给用户
|
||||||
|
history 聊天历史,前情提要
|
||||||
|
system_prompt 给gpt的静默提醒
|
||||||
|
web_port 当前软件运行的端口号
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# 清空历史,以免输入溢出
|
||||||
|
history = []; clear_file_downloadzone(chatbot)
|
||||||
|
|
||||||
|
# 基本信息:功能、贡献者
|
||||||
|
chatbot.append([
|
||||||
|
"函数插件功能?",
|
||||||
|
"CodeInterpreter开源版, 此插件处于开发阶段, 建议暂时不要使用, 插件初始化中 ..."
|
||||||
|
])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
|
if have_any_recent_upload_files(chatbot):
|
||||||
|
file_path = get_recent_file_prompt_support(chatbot)
|
||||||
|
else:
|
||||||
|
chatbot.append(["文件检索", "没有发现任何近期上传的文件。"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
|
# 读取文件
|
||||||
|
if ("recently_uploaded_files" in plugin_kwargs) and (plugin_kwargs["recently_uploaded_files"] == ""): plugin_kwargs.pop("recently_uploaded_files")
|
||||||
|
recently_uploaded_files = plugin_kwargs.get("recently_uploaded_files", None)
|
||||||
|
file_path = recently_uploaded_files[-1]
|
||||||
|
file_type = file_path.split('.')[-1]
|
||||||
|
|
||||||
|
# 粗心检查
|
||||||
|
if is_the_upload_folder(txt):
|
||||||
|
chatbot.append([
|
||||||
|
"...",
|
||||||
|
f"请在输入框内填写需求,然后再次点击该插件(文件路径 {file_path} 已经被记忆)"
|
||||||
|
])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
# 开始干正事
|
||||||
|
for j in range(5): # 最多重试5次
|
||||||
|
try:
|
||||||
|
code, installation_advance, txt, file_type, llm_kwargs, chatbot, history = \
|
||||||
|
yield from gpt_interact_multi_step(txt, file_type, llm_kwargs, chatbot, history)
|
||||||
|
code = get_code_block(code)
|
||||||
|
res = make_module(code)
|
||||||
|
instance = init_module_instance(res)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
chatbot.append([f"第{j}次代码生成尝试,失败了", f"错误追踪\n```\n{trimmed_format_exc()}\n```\n"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
|
# 代码生成结束, 开始执行
|
||||||
|
try:
|
||||||
|
import multiprocessing
|
||||||
|
manager = multiprocessing.Manager()
|
||||||
|
return_dict = manager.dict()
|
||||||
|
|
||||||
|
p = multiprocessing.Process(target=subprocess_worker, args=(instance, file_path, return_dict))
|
||||||
|
# only has 10 seconds to run
|
||||||
|
p.start(); p.join(timeout=10)
|
||||||
|
if p.is_alive(): p.terminate(); p.join()
|
||||||
|
p.close()
|
||||||
|
res = return_dict['result']
|
||||||
|
# res = instance.run(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
chatbot.append(["执行失败了", f"错误追踪\n```\n{trimmed_format_exc()}\n```\n"])
|
||||||
|
# chatbot.append(["如果是缺乏依赖,请参考以下建议", installation_advance])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
# 顺利完成,收尾
|
||||||
|
res = str(res)
|
||||||
|
if os.path.exists(res):
|
||||||
|
chatbot.append(["执行成功了,结果是一个有效文件", "结果:" + res])
|
||||||
|
new_file_path = promote_file_to_downloadzone(res, chatbot=chatbot)
|
||||||
|
chatbot = for_immediate_show_off_when_possible(file_type, new_file_path, chatbot)
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
||||||
|
else:
|
||||||
|
chatbot.append(["执行成功了,结果是一个字符串", "结果:" + res])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
||||||
|
|
||||||
|
"""
|
||||||
|
测试:
|
||||||
|
裁剪图像,保留下半部分
|
||||||
|
交换图像的蓝色通道和红色通道
|
||||||
|
将图像转为灰度图像
|
||||||
|
将csv文件转excel表格
|
||||||
|
"""
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
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_legacy(chatbot, history=None, file_name=None):
|
|
||||||
"""
|
|
||||||
将对话记录history以Markdown格式写入文件中。如果没有指定文件名,则使用当前时间生成文件名。
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
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)
|
|
||||||
|
|
||||||
with open(fp, 'w', encoding='utf8') as f:
|
|
||||||
from textwrap import dedent
|
|
||||||
form = dedent("""
|
|
||||||
<!DOCTYPE html><head><meta charset="utf-8"><title>对话存档</title><style>{CSS}</style></head>
|
|
||||||
<body>
|
|
||||||
<div class="test_temp1" style="width:10%; height: 500px; float:left;"></div>
|
|
||||||
<div class="test_temp2" style="width:80%;padding: 40px;float:left;padding-left: 20px;padding-right: 20px;box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 8px 8px;border-radius: 10px;">
|
|
||||||
<div class="chat-body" style="display: flex;justify-content: center;flex-direction: column;align-items: center;flex-wrap: nowrap;">
|
|
||||||
{CHAT_PREVIEW}
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div style="text-align: center;width:80%;padding: 0px;float:left;padding-left:20px;padding-right:20px;box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 1px 2px;border-radius: 1px;">对话(原始数据)</div>
|
|
||||||
{HISTORY_PREVIEW}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="test_temp3" style="width:10%; height: 500px; float:left;"></div>
|
|
||||||
</body>
|
|
||||||
""")
|
|
||||||
|
|
||||||
qa_from = dedent("""
|
|
||||||
<div class="QaBox" style="width:80%;padding: 20px;margin-bottom: 20px;box-shadow: rgb(0 255 159 / 50%) 0px 0px 1px 2px;border-radius: 4px;">
|
|
||||||
<div class="Question" style="border-radius: 2px;">{QUESTION}</div>
|
|
||||||
<hr color="blue" style="border-top: dotted 2px #ccc;">
|
|
||||||
<div class="Answer" style="border-radius: 2px;">{ANSWER}</div>
|
|
||||||
</div>
|
|
||||||
""")
|
|
||||||
|
|
||||||
history_from = dedent("""
|
|
||||||
<div class="historyBox" style="width:80%;padding: 0px;float:left;padding-left:20px;padding-right:20px;box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 1px 2px;border-radius: 1px;">
|
|
||||||
<div class="entry" style="border-radius: 2px;">{ENTRY}</div>
|
|
||||||
</div>
|
|
||||||
""")
|
|
||||||
CHAT_PREVIEW_BUF = ""
|
|
||||||
for i, contents in enumerate(chatbot):
|
|
||||||
question, answer = contents[0], contents[1]
|
|
||||||
if question is None: question = ""
|
|
||||||
try: question = str(question)
|
|
||||||
except: question = ""
|
|
||||||
if answer is None: answer = ""
|
|
||||||
try: answer = str(answer)
|
|
||||||
except: answer = ""
|
|
||||||
CHAT_PREVIEW_BUF += qa_from.format(QUESTION=question, ANSWER=answer)
|
|
||||||
|
|
||||||
HISTORY_PREVIEW_BUF = ""
|
|
||||||
for h in history:
|
|
||||||
HISTORY_PREVIEW_BUF += history_from.format(ENTRY=h)
|
|
||||||
html_content = form.format(CHAT_PREVIEW=CHAT_PREVIEW_BUF, HISTORY_PREVIEW=HISTORY_PREVIEW_BUF, CSS=advanced_css)
|
|
||||||
f.write(html_content)
|
|
||||||
|
|
||||||
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:
|
|
||||||
file_content = f.read()
|
|
||||||
# pattern to match the text between <head> and </head>
|
|
||||||
pattern = re.compile(r'<head>.*?</head>', flags=re.DOTALL)
|
|
||||||
file_content = re.sub(pattern, '', file_content)
|
|
||||||
html, history = file_content.split('<hr color="blue"> \n\n 对话数据 (无渲染):\n')
|
|
||||||
history = history.strip('<code>')
|
|
||||||
history = history.strip('</code>')
|
|
||||||
history = history.split("\n>>>")
|
|
||||||
return list(filter(lambda x:x!="", history))[0][:100]
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def read_file_to_chat(chatbot, history, file_name):
|
|
||||||
with open(file_name, 'r', encoding='utf8') as f:
|
|
||||||
file_content = f.read()
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
soup = BeautifulSoup(file_content, 'lxml')
|
|
||||||
# 提取QaBox信息
|
|
||||||
chatbot.clear()
|
|
||||||
qa_box_list = []
|
|
||||||
qa_boxes = soup.find_all("div", class_="QaBox")
|
|
||||||
for box in qa_boxes:
|
|
||||||
question = box.find("div", class_="Question").get_text(strip=False)
|
|
||||||
answer = box.find("div", class_="Answer").get_text(strip=False)
|
|
||||||
qa_box_list.append({"Question": question, "Answer": answer})
|
|
||||||
chatbot.append([question, answer])
|
|
||||||
# 提取historyBox信息
|
|
||||||
history_box_list = []
|
|
||||||
history_boxes = soup.find_all("div", class_="historyBox")
|
|
||||||
for box in history_boxes:
|
|
||||||
entry = box.find("div", class_="entry").get_text(strip=False)
|
|
||||||
history_box_list.append(entry)
|
|
||||||
history = history_box_list
|
|
||||||
chatbot.append([None, f"[Local Message] 载入对话{len(qa_box_list)}条,上下文{len(history)}条。"])
|
|
||||||
return chatbot, history
|
|
||||||
|
|
||||||
@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地址等)
|
|
||||||
"""
|
|
||||||
file_name = plugin_kwargs.get("file_name", None)
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
|
|
||||||
第一个参数,名称`file_name`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"file_name": ArgProperty(title="保存文件名", description="输入对话存档文件名,留空则使用时间作为文件名", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
yield from 对话历史存档(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def hide_cwd(str):
|
|
||||||
import os
|
|
||||||
current_path = os.getcwd()
|
|
||||||
replace_path = "."
|
|
||||||
return str.replace(current_path, replace_path)
|
|
||||||
|
|
||||||
@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地址等)
|
|
||||||
"""
|
|
||||||
from crazy_functions.crazy_utils import get_files_from_everything
|
|
||||||
success, file_manifest, _ = get_files_from_everything(txt, type='.html')
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
if txt == "": txt = '空空如也的输入栏'
|
|
||||||
import glob
|
|
||||||
local_history = "<br/>".join([
|
|
||||||
"`"+hide_cwd(f)+f" ({gen_file_preview(f)})"+"`"
|
|
||||||
for f in glob.glob(
|
|
||||||
f'{get_log_folder(get_user(chatbot), plugin_name="chat_history")}/**/{f_prefix}*.html',
|
|
||||||
recursive=True
|
|
||||||
)])
|
|
||||||
chatbot.append([f"正在查找对话历史文件(html格式): {txt}", f"找不到任何html文件: {txt}。但本地存储了以下历史文件,您可以将任意一个文件路径粘贴到输入区,然后重试:<br/>{local_history}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
chatbot, history = read_file_to_chat(chatbot, history, file_manifest[0])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
except:
|
|
||||||
chatbot.append([f"载入对话历史文件", f"对话历史文件损坏!"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
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 glob, os
|
|
||||||
local_history = "<br/>".join([
|
|
||||||
"`"+hide_cwd(f)+"`"
|
|
||||||
for f in glob.glob(
|
|
||||||
f'{get_log_folder(get_user(chatbot), plugin_name="chat_history")}/**/{f_prefix}*.html', recursive=True
|
|
||||||
)])
|
|
||||||
for f in glob.glob(f'{get_log_folder(get_user(chatbot), plugin_name="chat_history")}/**/{f_prefix}*.html', recursive=True):
|
|
||||||
os.remove(f)
|
|
||||||
chatbot.append([f"删除所有历史对话文件", f"已删除<br/>{local_history}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
@@ -1,537 +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
|
|
||||||
from shared_utils.fastapi_server import validate_path_safety
|
|
||||||
|
|
||||||
|
|
||||||
@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, query: str, chatbot: List, history: List, system_prompt: str):
|
|
||||||
"""初始化总结器"""
|
|
||||||
self.llm_kwargs = llm_kwargs
|
|
||||||
self.query = query
|
|
||||||
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.query:
|
|
||||||
i_say = (f'请按照用户要求对文件内容进行处理,文件名为{os.path.basename(frag.file_path)},'
|
|
||||||
f'用户要求为:{self.query}:'
|
|
||||||
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
|
|
||||||
|
|
||||||
# 设置超时时间为30秒,给予更多处理时间
|
|
||||||
TIMEOUT_SECONDS = 30
|
|
||||||
timer = threading.Timer(TIMEOUT_SECONDS, 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(f"处理文件 {os.path.basename(fp)} 超时({TIMEOUT_SECONDS}秒)")
|
|
||||||
|
|
||||||
# 更新状态
|
|
||||||
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
|
|
||||||
|
|
||||||
# 更新状态
|
|
||||||
mutable_status[0] = "提取文件内容"
|
|
||||||
mutable_status[1] = time.time()
|
|
||||||
|
|
||||||
# 提取内容 - 使用单独的超时控制
|
|
||||||
content = None
|
|
||||||
extract_start_time = time.time()
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
check_timeout() # 检查全局超时
|
|
||||||
|
|
||||||
# 检查提取过程是否超时(10秒)
|
|
||||||
if time.time() - extract_start_time > 10:
|
|
||||||
raise TimeoutError("文件内容提取超时(10秒)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = extract_text(fp)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if "timeout" in str(e).lower():
|
|
||||||
continue # 如果是临时超时,重试
|
|
||||||
raise # 其他错误直接抛出
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_files.append((fp, f"文件读取失败:{str(e)}"))
|
|
||||||
mutable_status[2] = "读取失败"
|
|
||||||
return fragments
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# 分割文本 - 添加超时检查
|
|
||||||
split_start_time = time.time()
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
check_timeout() # 检查全局超时
|
|
||||||
|
|
||||||
# 检查分割过程是否超时(5秒)
|
|
||||||
if time.time() - split_start_time > 5:
|
|
||||||
raise TimeoutError("文本分割超时(5秒)")
|
|
||||||
|
|
||||||
paper_fragments = breakdown_text_to_satisfy_token_limit(
|
|
||||||
txt=content,
|
|
||||||
limit=self._get_token_limit(),
|
|
||||||
llm_model=self.llm_kwargs['llm_model']
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_files.append((fp, f"文本分割失败:{str(e)}"))
|
|
||||||
mutable_status[2] = "分割失败"
|
|
||||||
return fragments
|
|
||||||
|
|
||||||
# 处理片段
|
|
||||||
rel_path = os.path.relpath(fp, project_folder)
|
|
||||||
for i, frag in enumerate(paper_fragments):
|
|
||||||
check_timeout() # 每处理一个片段检查一次超时
|
|
||||||
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, str(e)))
|
|
||||||
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\n"
|
|
||||||
else:
|
|
||||||
status_str += f"文件 {file_name}: {status} {desc}\n\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
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 保存 HTML
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 保存 Word
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 添加到下载区
|
|
||||||
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
|
|
||||||
query = plugin_kwargs.get("advanced_arg")
|
|
||||||
summarizer = BatchDocumentSummarizer(llm_kwargs, query, 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
|
|
||||||
user_name = chatbot.get_user()
|
|
||||||
validate_path_safety(project_folder, user_name)
|
|
||||||
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,36 +0,0 @@
|
|||||||
import random
|
|
||||||
from toolbox import get_conf
|
|
||||||
from crazy_functions.Document_Conversation import 批量文件询问
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
|
|
||||||
|
|
||||||
class Document_Conversation_Wrap(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
|
|
||||||
第一个参数,名称`main_input`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第二个参数,名称`advanced_arg`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第三个参数,名称`allow_cache`,参数`type`声明这是一个下拉菜单,下拉菜单上方显示`title`+`description`,下拉菜单的选项为`options`,`default_value`为下拉菜单默认值;
|
|
||||||
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="已上传的文件", description="上传文件后自动填充", default_value="", type="string").model_dump_json(),
|
|
||||||
"searxng_url":
|
|
||||||
ArgProperty(title="对材料提问", description="提问", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs:dict, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
yield from 批量文件询问(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
|
|
||||||
@@ -1,673 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
from typing import Dict, List, Generator, Tuple
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
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, supports_format, convert_to_markdown
|
|
||||||
from request_llms.bridge_all import model_info
|
|
||||||
from toolbox import update_ui, CatchException, report_exception, promote_file_to_downloadzone, write_history_to_file
|
|
||||||
from shared_utils.fastapi_server import validate_path_safety
|
|
||||||
|
|
||||||
# 新增:导入结构化论文提取器
|
|
||||||
from crazy_functions.doc_fns.read_fns.unstructured_all.paper_structure_extractor import PaperStructureExtractor, ExtractorConfig, StructuredPaper
|
|
||||||
|
|
||||||
# 导入格式化器
|
|
||||||
from crazy_functions.paper_fns.file2file_doc import (
|
|
||||||
TxtFormatter,
|
|
||||||
MarkdownFormatter,
|
|
||||||
HtmlFormatter,
|
|
||||||
WordFormatter
|
|
||||||
)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TextFragment:
|
|
||||||
"""文本片段数据类,用于组织处理单元"""
|
|
||||||
content: str
|
|
||||||
fragment_index: int
|
|
||||||
total_fragments: int
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentProcessor:
|
|
||||||
"""文档处理器 - 处理单个文档并输出结果"""
|
|
||||||
|
|
||||||
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.processed_results = []
|
|
||||||
self.failed_fragments = []
|
|
||||||
# 新增:初始化论文结构提取器
|
|
||||||
self.paper_extractor = PaperStructureExtractor()
|
|
||||||
|
|
||||||
def _get_token_limit(self) -> int:
|
|
||||||
"""获取模型token限制,返回更小的值以确保更细粒度的分割"""
|
|
||||||
max_token = model_info[self.llm_kwargs['llm_model']]['max_token']
|
|
||||||
# 降低token限制,使每个片段更小
|
|
||||||
return max_token // 4 # 从3/4降低到1/4
|
|
||||||
|
|
||||||
def _create_batch_inputs(self, fragments: List[TextFragment]) -> Tuple[List, List, List]:
|
|
||||||
"""创建批处理输入"""
|
|
||||||
inputs_array = []
|
|
||||||
inputs_show_user_array = []
|
|
||||||
history_array = []
|
|
||||||
|
|
||||||
user_instruction = self.plugin_kwargs.get("advanced_arg", "请润色以下学术文本,提高其语言表达的准确性、专业性和流畅度,保持学术风格,确保逻辑连贯,但不改变原文的科学内容和核心观点")
|
|
||||||
|
|
||||||
for frag in fragments:
|
|
||||||
i_say = (f'请按照以下要求处理文本内容:{user_instruction}\n\n'
|
|
||||||
f'请将对文本的处理结果放在<decision>和</decision>标签之间。\n\n'
|
|
||||||
f'文本内容:\n```\n{frag.content}\n```')
|
|
||||||
|
|
||||||
i_say_show_user = f'正在处理文本片段 {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 _extract_decision(self, text: str) -> str:
|
|
||||||
"""从LLM响应中提取<decision>标签内的内容"""
|
|
||||||
import re
|
|
||||||
pattern = r'<decision>(.*?)</decision>'
|
|
||||||
matches = re.findall(pattern, text, re.DOTALL)
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
return matches[0].strip()
|
|
||||||
else:
|
|
||||||
# 如果没有找到标签,返回原始文本
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
def process_file(self, file_path: str) -> Generator:
|
|
||||||
"""处理单个文件"""
|
|
||||||
self.chatbot.append(["开始处理文件", f"文件路径: {file_path}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 首先尝试转换为Markdown
|
|
||||||
from crazy_functions.rag_fns.rag_file_support import convert_to_markdown
|
|
||||||
file_path = convert_to_markdown(file_path)
|
|
||||||
|
|
||||||
# 1. 检查文件是否为支持的论文格式
|
|
||||||
is_paper_format = any(file_path.lower().endswith(ext) for ext in self.paper_extractor.SUPPORTED_EXTENSIONS)
|
|
||||||
|
|
||||||
if is_paper_format:
|
|
||||||
# 使用结构化提取器处理论文
|
|
||||||
return (yield from self._process_structured_paper(file_path))
|
|
||||||
else:
|
|
||||||
# 使用原有方式处理普通文档
|
|
||||||
return (yield from self._process_regular_file(file_path))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["处理错误", f"文件处理失败: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _process_structured_paper(self, file_path: str) -> Generator:
|
|
||||||
"""处理结构化论文文件"""
|
|
||||||
# 1. 提取论文结构
|
|
||||||
self.chatbot[-1] = ["正在分析论文结构", f"文件路径: {file_path}"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
try:
|
|
||||||
paper = self.paper_extractor.extract_paper_structure(file_path)
|
|
||||||
|
|
||||||
if not paper or not paper.sections:
|
|
||||||
self.chatbot.append(["无法提取论文结构", "将使用全文内容进行处理"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 使用全文内容进行段落切分
|
|
||||||
if paper and paper.full_text:
|
|
||||||
# 使用增强的分割函数进行更细致的分割
|
|
||||||
fragments = self._breakdown_section_content(paper.full_text)
|
|
||||||
|
|
||||||
# 创建文本片段对象
|
|
||||||
text_fragments = []
|
|
||||||
for i, frag in enumerate(fragments):
|
|
||||||
if frag.strip():
|
|
||||||
text_fragments.append(TextFragment(
|
|
||||||
content=frag,
|
|
||||||
fragment_index=i,
|
|
||||||
total_fragments=len(fragments)
|
|
||||||
))
|
|
||||||
|
|
||||||
# 批量处理片段
|
|
||||||
if text_fragments:
|
|
||||||
self.chatbot[-1] = ["开始处理文本", f"共 {len(text_fragments)} 个片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 一次性准备所有输入
|
|
||||||
inputs_array, inputs_show_user_array, history_array = self._create_batch_inputs(text_fragments)
|
|
||||||
|
|
||||||
# 使用系统提示
|
|
||||||
instruction = self.plugin_kwargs.get("advanced_arg", "请润色以下学术文本,提高其语言表达的准确性、专业性和流畅度,保持学术风格,确保逻辑连贯,但不改变原文的科学内容和核心观点")
|
|
||||||
sys_prompt_array = [f"你是一个专业的学术文献编辑助手。请按照用户的要求:'{instruction}'处理文本。保持学术风格,增强表达的准确性和专业性。"] * len(text_fragments)
|
|
||||||
|
|
||||||
# 调用LLM一次性处理所有片段
|
|
||||||
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(text_fragments):
|
|
||||||
try:
|
|
||||||
llm_response = response_collection[j * 2 + 1]
|
|
||||||
processed_text = self._extract_decision(llm_response)
|
|
||||||
|
|
||||||
if processed_text and processed_text.strip():
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': processed_text
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': frag.content
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': frag.content
|
|
||||||
})
|
|
||||||
|
|
||||||
# 按原始顺序合并结果
|
|
||||||
self.processed_results.sort(key=lambda x: x['index'])
|
|
||||||
final_content = "\n".join([item['content'] for item in self.processed_results])
|
|
||||||
|
|
||||||
# 更新UI
|
|
||||||
success_count = len(text_fragments) - len(self.failed_fragments)
|
|
||||||
self.chatbot[-1] = ["处理完成", f"成功处理 {success_count}/{len(text_fragments)} 个片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
return final_content
|
|
||||||
else:
|
|
||||||
self.chatbot.append(["处理失败", "未能提取到有效的文本内容"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
self.chatbot.append(["处理失败", "未能提取到论文内容"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 2. 准备处理章节内容(不处理标题)
|
|
||||||
self.chatbot[-1] = ["已提取论文结构", f"共 {len(paper.sections)} 个主要章节"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 3. 收集所有需要处理的章节内容并分割为合适大小
|
|
||||||
sections_to_process = []
|
|
||||||
section_map = {} # 用于映射处理前后的内容
|
|
||||||
|
|
||||||
def collect_section_contents(sections, parent_path=""):
|
|
||||||
"""递归收集章节内容,跳过参考文献部分"""
|
|
||||||
for i, section in enumerate(sections):
|
|
||||||
current_path = f"{parent_path}/{i}" if parent_path else f"{i}"
|
|
||||||
|
|
||||||
# 检查是否为参考文献部分,如果是则跳过
|
|
||||||
if section.section_type == 'references' or section.title.lower() in ['references', '参考文献', 'bibliography', '文献']:
|
|
||||||
continue # 跳过参考文献部分
|
|
||||||
|
|
||||||
# 只处理内容非空的章节
|
|
||||||
if section.content and section.content.strip():
|
|
||||||
# 使用增强的分割函数进行更细致的分割
|
|
||||||
fragments = self._breakdown_section_content(section.content)
|
|
||||||
|
|
||||||
for fragment_idx, fragment_content in enumerate(fragments):
|
|
||||||
if fragment_content.strip():
|
|
||||||
fragment_index = len(sections_to_process)
|
|
||||||
sections_to_process.append(TextFragment(
|
|
||||||
content=fragment_content,
|
|
||||||
fragment_index=fragment_index,
|
|
||||||
total_fragments=0 # 临时值,稍后更新
|
|
||||||
))
|
|
||||||
|
|
||||||
# 保存映射关系,用于稍后更新章节内容
|
|
||||||
# 为每个片段存储原始章节和片段索引信息
|
|
||||||
section_map[fragment_index] = (current_path, section, fragment_idx, len(fragments))
|
|
||||||
|
|
||||||
# 递归处理子章节
|
|
||||||
if section.subsections:
|
|
||||||
collect_section_contents(section.subsections, current_path)
|
|
||||||
|
|
||||||
# 收集所有章节内容
|
|
||||||
collect_section_contents(paper.sections)
|
|
||||||
|
|
||||||
# 更新总片段数
|
|
||||||
total_fragments = len(sections_to_process)
|
|
||||||
for frag in sections_to_process:
|
|
||||||
frag.total_fragments = total_fragments
|
|
||||||
|
|
||||||
# 4. 如果没有内容需要处理,直接返回
|
|
||||||
if not sections_to_process:
|
|
||||||
self.chatbot.append(["处理完成", "未找到需要处理的内容"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 5. 批量处理章节内容
|
|
||||||
self.chatbot[-1] = ["开始处理论文内容", f"共 {len(sections_to_process)} 个内容片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 一次性准备所有输入
|
|
||||||
inputs_array, inputs_show_user_array, history_array = self._create_batch_inputs(sections_to_process)
|
|
||||||
|
|
||||||
# 使用系统提示
|
|
||||||
instruction = self.plugin_kwargs.get("advanced_arg", "请润色以下学术文本,提高其语言表达的准确性、专业性和流畅度,保持学术风格,确保逻辑连贯,但不改变原文的科学内容和核心观点")
|
|
||||||
sys_prompt_array = [f"你是一个专业的学术文献编辑助手。请按照用户的要求:'{instruction}'处理文本。保持学术风格,增强表达的准确性和专业性。"] * len(sections_to_process)
|
|
||||||
|
|
||||||
# 调用LLM一次性处理所有片段
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 处理响应,重组章节内容
|
|
||||||
section_contents = {} # 用于重组各章节的处理后内容
|
|
||||||
|
|
||||||
for j, frag in enumerate(sections_to_process):
|
|
||||||
try:
|
|
||||||
llm_response = response_collection[j * 2 + 1]
|
|
||||||
processed_text = self._extract_decision(llm_response)
|
|
||||||
|
|
||||||
if processed_text and processed_text.strip():
|
|
||||||
# 保存处理结果
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': processed_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 存储处理后的文本片段,用于后续重组
|
|
||||||
fragment_index = frag.fragment_index
|
|
||||||
if fragment_index in section_map:
|
|
||||||
path, section, fragment_idx, total_fragments = section_map[fragment_index]
|
|
||||||
|
|
||||||
# 初始化此章节的内容容器(如果尚未创建)
|
|
||||||
if path not in section_contents:
|
|
||||||
section_contents[path] = [""] * total_fragments
|
|
||||||
|
|
||||||
# 将处理后的片段放入正确位置
|
|
||||||
section_contents[path][fragment_idx] = processed_text
|
|
||||||
else:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
|
|
||||||
# 重组每个章节的内容
|
|
||||||
for path, fragments in section_contents.items():
|
|
||||||
section = None
|
|
||||||
for idx in section_map:
|
|
||||||
if section_map[idx][0] == path:
|
|
||||||
section = section_map[idx][1]
|
|
||||||
break
|
|
||||||
|
|
||||||
if section:
|
|
||||||
# 合并该章节的所有处理后片段
|
|
||||||
section.content = "\n".join(fragments)
|
|
||||||
|
|
||||||
# 6. 更新UI
|
|
||||||
success_count = total_fragments - len(self.failed_fragments)
|
|
||||||
self.chatbot[-1] = ["处理完成", f"成功处理 {success_count}/{total_fragments} 个内容片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 收集参考文献部分(不进行处理)
|
|
||||||
references_sections = []
|
|
||||||
def collect_references(sections, parent_path=""):
|
|
||||||
"""递归收集参考文献部分"""
|
|
||||||
for i, section in enumerate(sections):
|
|
||||||
current_path = f"{parent_path}/{i}" if parent_path else f"{i}"
|
|
||||||
|
|
||||||
# 检查是否为参考文献部分
|
|
||||||
if section.section_type == 'references' or section.title.lower() in ['references', '参考文献', 'bibliography', '文献']:
|
|
||||||
references_sections.append((current_path, section))
|
|
||||||
|
|
||||||
# 递归检查子章节
|
|
||||||
if section.subsections:
|
|
||||||
collect_references(section.subsections, current_path)
|
|
||||||
|
|
||||||
# 收集参考文献
|
|
||||||
collect_references(paper.sections)
|
|
||||||
|
|
||||||
# 7. 将处理后的结构化论文转换为Markdown
|
|
||||||
markdown_content = self.paper_extractor.generate_markdown(paper)
|
|
||||||
|
|
||||||
# 8. 返回处理后的内容
|
|
||||||
self.chatbot[-1] = ["处理完成", f"成功处理 {success_count}/{total_fragments} 个内容片段,参考文献部分未处理"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
return markdown_content
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["结构化处理失败", f"错误: {str(e)},将尝试作为普通文件处理"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return (yield from self._process_regular_file(file_path))
|
|
||||||
|
|
||||||
def _process_regular_file(self, file_path: str) -> Generator:
|
|
||||||
"""使用原有方式处理普通文件"""
|
|
||||||
# 原有的文件处理逻辑
|
|
||||||
self.chatbot[-1] = ["正在读取文件", f"文件路径: {file_path}"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
content = extract_text(file_path)
|
|
||||||
if not content or not content.strip():
|
|
||||||
self.chatbot.append(["处理失败", "文件内容为空或无法提取内容"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 2. 分割文本
|
|
||||||
self.chatbot[-1] = ["正在分析文件", "将文件内容分割为适当大小的片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 使用增强的分割函数
|
|
||||||
fragments = self._breakdown_section_content(content)
|
|
||||||
|
|
||||||
# 3. 创建文本片段对象
|
|
||||||
text_fragments = []
|
|
||||||
for i, frag in enumerate(fragments):
|
|
||||||
if frag.strip():
|
|
||||||
text_fragments.append(TextFragment(
|
|
||||||
content=frag,
|
|
||||||
fragment_index=i,
|
|
||||||
total_fragments=len(fragments)
|
|
||||||
))
|
|
||||||
|
|
||||||
# 4. 处理所有片段
|
|
||||||
self.chatbot[-1] = ["开始处理文本", f"共 {len(text_fragments)} 个片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 批量处理片段
|
|
||||||
batch_size = 8 # 每批处理的片段数
|
|
||||||
for i in range(0, len(text_fragments), batch_size):
|
|
||||||
batch = text_fragments[i:i + batch_size]
|
|
||||||
|
|
||||||
inputs_array, inputs_show_user_array, history_array = self._create_batch_inputs(batch)
|
|
||||||
|
|
||||||
# 使用系统提示
|
|
||||||
instruction = self.plugin_kwargs.get("advanced_arg", "请润色以下文本")
|
|
||||||
sys_prompt_array = [f"你是一个专业的文本处理助手。请按照用户的要求:'{instruction}'处理文本。"] * len(batch)
|
|
||||||
|
|
||||||
# 调用LLM处理
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
llm_response = response_collection[j * 2 + 1]
|
|
||||||
processed_text = self._extract_decision(llm_response)
|
|
||||||
|
|
||||||
if processed_text and processed_text.strip():
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': processed_text
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': frag.content # 如果处理失败,使用原始内容
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_fragments.append(frag)
|
|
||||||
self.processed_results.append({
|
|
||||||
'index': frag.fragment_index,
|
|
||||||
'content': frag.content # 如果处理失败,使用原始内容
|
|
||||||
})
|
|
||||||
|
|
||||||
# 5. 按原始顺序合并结果
|
|
||||||
self.processed_results.sort(key=lambda x: x['index'])
|
|
||||||
final_content = "\n".join([item['content'] for item in self.processed_results])
|
|
||||||
|
|
||||||
# 6. 更新UI
|
|
||||||
success_count = len(text_fragments) - len(self.failed_fragments)
|
|
||||||
self.chatbot[-1] = ["处理完成", f"成功处理 {success_count}/{len(text_fragments)} 个片段"]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
return final_content
|
|
||||||
|
|
||||||
def save_results(self, content: str, original_file_path: str) -> List[str]:
|
|
||||||
"""保存处理结果为多种格式"""
|
|
||||||
if not content:
|
|
||||||
return []
|
|
||||||
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
original_filename = os.path.basename(original_file_path)
|
|
||||||
filename_without_ext = os.path.splitext(original_filename)[0]
|
|
||||||
base_filename = f"{filename_without_ext}_processed_{timestamp}"
|
|
||||||
|
|
||||||
result_files = []
|
|
||||||
|
|
||||||
# 获取用户指定的处理类型
|
|
||||||
processing_type = self.plugin_kwargs.get("advanced_arg", "文本处理")
|
|
||||||
|
|
||||||
# 1. 保存为TXT
|
|
||||||
try:
|
|
||||||
txt_formatter = TxtFormatter()
|
|
||||||
txt_content = txt_formatter.create_document(content)
|
|
||||||
txt_file = write_history_to_file(
|
|
||||||
history=[txt_content],
|
|
||||||
file_basename=f"{base_filename}.txt"
|
|
||||||
)
|
|
||||||
result_files.append(txt_file)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"TXT格式保存失败: {str(e)}"])
|
|
||||||
|
|
||||||
# 2. 保存为Markdown
|
|
||||||
try:
|
|
||||||
md_formatter = MarkdownFormatter()
|
|
||||||
md_content = md_formatter.create_document(content, processing_type)
|
|
||||||
md_file = write_history_to_file(
|
|
||||||
history=[md_content],
|
|
||||||
file_basename=f"{base_filename}.md"
|
|
||||||
)
|
|
||||||
result_files.append(md_file)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"Markdown格式保存失败: {str(e)}"])
|
|
||||||
|
|
||||||
# 3. 保存为HTML
|
|
||||||
try:
|
|
||||||
html_formatter = HtmlFormatter(processing_type=processing_type)
|
|
||||||
html_content = html_formatter.create_document(content)
|
|
||||||
html_file = write_history_to_file(
|
|
||||||
history=[html_content],
|
|
||||||
file_basename=f"{base_filename}.html"
|
|
||||||
)
|
|
||||||
result_files.append(html_file)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"HTML格式保存失败: {str(e)}"])
|
|
||||||
|
|
||||||
# 4. 保存为Word
|
|
||||||
try:
|
|
||||||
word_formatter = WordFormatter()
|
|
||||||
doc = word_formatter.create_document(content, processing_type)
|
|
||||||
|
|
||||||
# 获取保存路径
|
|
||||||
from toolbox import get_log_folder
|
|
||||||
word_path = os.path.join(get_log_folder(), f"{base_filename}.docx")
|
|
||||||
doc.save(word_path)
|
|
||||||
|
|
||||||
# 5. 保存为PDF(通过Word转换)
|
|
||||||
try:
|
|
||||||
from crazy_functions.paper_fns.file2file_doc.word2pdf import WordToPdfConverter
|
|
||||||
pdf_path = WordToPdfConverter.convert_to_pdf(word_path)
|
|
||||||
result_files.append(pdf_path)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"PDF格式保存失败: {str(e)}"])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"Word格式保存失败: {str(e)}"])
|
|
||||||
|
|
||||||
# 添加到下载区
|
|
||||||
for file in result_files:
|
|
||||||
promote_file_to_downloadzone(file, chatbot=self.chatbot)
|
|
||||||
|
|
||||||
return result_files
|
|
||||||
|
|
||||||
def _breakdown_section_content(self, content: str) -> List[str]:
|
|
||||||
"""对文本内容进行分割与合并
|
|
||||||
|
|
||||||
主要按段落进行组织,只合并较小的段落以减少片段数量
|
|
||||||
保留原始段落结构,不对长段落进行强制分割
|
|
||||||
针对中英文设置不同的阈值,因为字符密度不同
|
|
||||||
"""
|
|
||||||
# 先按段落分割文本
|
|
||||||
paragraphs = content.split('\n\n')
|
|
||||||
|
|
||||||
# 检测语言类型
|
|
||||||
chinese_char_count = sum(1 for char in content if '\u4e00' <= char <= '\u9fff')
|
|
||||||
is_chinese_text = chinese_char_count / max(1, len(content)) > 0.3
|
|
||||||
|
|
||||||
# 根据语言类型设置不同的阈值(只用于合并小段落)
|
|
||||||
if is_chinese_text:
|
|
||||||
# 中文文本:一个汉字就是一个字符,信息密度高
|
|
||||||
min_chunk_size = 300 # 段落合并的最小阈值
|
|
||||||
target_size = 800 # 理想的段落大小
|
|
||||||
else:
|
|
||||||
# 英文文本:一个单词由多个字符组成,信息密度低
|
|
||||||
min_chunk_size = 600 # 段落合并的最小阈值
|
|
||||||
target_size = 1600 # 理想的段落大小
|
|
||||||
|
|
||||||
# 1. 只合并小段落,不对长段落进行分割
|
|
||||||
result_fragments = []
|
|
||||||
current_chunk = []
|
|
||||||
current_length = 0
|
|
||||||
|
|
||||||
for para in paragraphs:
|
|
||||||
# 如果段落太小且不会超过目标大小,则合并
|
|
||||||
if len(para) < min_chunk_size and current_length + len(para) <= target_size:
|
|
||||||
current_chunk.append(para)
|
|
||||||
current_length += len(para)
|
|
||||||
# 否则,创建新段落
|
|
||||||
else:
|
|
||||||
# 如果当前块非空且与当前段落无关,先保存它
|
|
||||||
if current_chunk and current_length > 0:
|
|
||||||
result_fragments.append('\n\n'.join(current_chunk))
|
|
||||||
|
|
||||||
# 当前段落作为新块
|
|
||||||
current_chunk = [para]
|
|
||||||
current_length = len(para)
|
|
||||||
|
|
||||||
# 如果当前块大小已接近目标大小,保存并开始新块
|
|
||||||
if current_length >= target_size:
|
|
||||||
result_fragments.append('\n\n'.join(current_chunk))
|
|
||||||
current_chunk = []
|
|
||||||
current_length = 0
|
|
||||||
|
|
||||||
# 保存最后一个块
|
|
||||||
if current_chunk:
|
|
||||||
result_fragments.append('\n\n'.join(current_chunk))
|
|
||||||
|
|
||||||
# 2. 处理可能过大的片段(确保不超过token限制)
|
|
||||||
final_fragments = []
|
|
||||||
max_token = self._get_token_limit()
|
|
||||||
|
|
||||||
for fragment in result_fragments:
|
|
||||||
# 检查fragment是否可能超出token限制
|
|
||||||
# 根据语言类型调整token估算
|
|
||||||
if is_chinese_text:
|
|
||||||
estimated_tokens = len(fragment) / 1.5 # 中文每个token约1-2个字符
|
|
||||||
else:
|
|
||||||
estimated_tokens = len(fragment) / 4 # 英文每个token约4个字符
|
|
||||||
|
|
||||||
if estimated_tokens > max_token:
|
|
||||||
# 即使可能超出限制,也尽量保持段落的完整性
|
|
||||||
# 使用breakdown_text但设置更大的限制来减少分割
|
|
||||||
larger_limit = max_token * 0.95 # 使用95%的限制
|
|
||||||
sub_fragments = breakdown_text_to_satisfy_token_limit(
|
|
||||||
txt=fragment,
|
|
||||||
limit=larger_limit,
|
|
||||||
llm_model=self.llm_kwargs['llm_model']
|
|
||||||
)
|
|
||||||
final_fragments.extend(sub_fragments)
|
|
||||||
else:
|
|
||||||
final_fragments.append(fragment)
|
|
||||||
|
|
||||||
return final_fragments
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def 自定义智能文档处理(txt: str, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List,
|
|
||||||
history: List, system_prompt: str, user_request: str):
|
|
||||||
"""主函数 - 文件到文件处理"""
|
|
||||||
# 初始化
|
|
||||||
processor = DocumentProcessor(llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
|
||||||
chatbot.append(["函数插件功能", "文件内容处理:将文档内容按照指定要求处理后输出为多种格式"])
|
|
||||||
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
|
|
||||||
|
|
||||||
# 验证路径安全性
|
|
||||||
user_name = chatbot.get_user()
|
|
||||||
validate_path_safety(txt, user_name)
|
|
||||||
|
|
||||||
# 获取文件列表
|
|
||||||
if os.path.isfile(txt):
|
|
||||||
# 单个文件处理
|
|
||||||
file_paths = [txt]
|
|
||||||
else:
|
|
||||||
# 目录处理 - 类似批量文件询问插件
|
|
||||||
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_paths = [f for f in glob.glob(f'{extract_folder}/**', recursive=True)
|
|
||||||
if os.path.isfile(f) and not re.search(exclude_patterns, f)]
|
|
||||||
|
|
||||||
# 过滤支持的文件格式
|
|
||||||
file_paths = [f for f in file_paths if any(f.lower().endswith(ext) for ext in
|
|
||||||
list(processor.paper_extractor.SUPPORTED_EXTENSIONS) + ['.json', '.csv', '.xlsx', '.xls'])]
|
|
||||||
|
|
||||||
if not file_paths:
|
|
||||||
report_exception(chatbot, history, a=f"解析路径: {txt}", b="未找到支持的文件类型")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 处理文件
|
|
||||||
if len(file_paths) > 1:
|
|
||||||
chatbot.append(["发现多个文件", f"共找到 {len(file_paths)} 个文件,将处理第一个文件"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
# 只处理第一个文件
|
|
||||||
file_to_process = file_paths[0]
|
|
||||||
processed_content = yield from processor.process_file(file_to_process)
|
|
||||||
|
|
||||||
if processed_content:
|
|
||||||
# 保存结果
|
|
||||||
result_files = processor.save_results(processed_content, file_to_process)
|
|
||||||
|
|
||||||
if result_files:
|
|
||||||
chatbot.append(["处理完成", f"已生成 {len(result_files)} 个结果文件"])
|
|
||||||
else:
|
|
||||||
chatbot.append(["处理完成", "但未能保存任何结果文件"])
|
|
||||||
else:
|
|
||||||
chatbot.append(["处理失败", "未能生成有效的处理结果"])
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
|
|
||||||
from toolbox import get_conf, update_ui
|
|
||||||
from crazy_functions.Image_Generate import 图片生成_DALLE2, 图片生成_DALLE3, 图片修改_DALLE2
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
|
|
||||||
|
|
||||||
class ImageGen_Wrap(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
|
|
||||||
第一个参数,名称`main_input`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第二个参数,名称`advanced_arg`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="输入图片描述", description="需要生成图像的文本描述,尽量使用英文", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
"model_name":
|
|
||||||
ArgProperty(title="模型", options=["DALLE2", "DALLE3"], default_value="DALLE3", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"resolution":
|
|
||||||
ArgProperty(title="分辨率", options=["256x256(限DALLE2)", "512x512(限DALLE2)", "1024x1024", "1792x1024(限DALLE3)", "1024x1792(限DALLE3)"], default_value="1024x1024", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"quality (仅DALLE3生效)":
|
|
||||||
ArgProperty(title="质量", options=["standard", "hd"], default_value="standard", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"style (仅DALLE3生效)":
|
|
||||||
ArgProperty(title="风格", options=["vivid", "natural"], default_value="vivid", description="无", type="dropdown").model_dump_json(),
|
|
||||||
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
# 分辨率
|
|
||||||
resolution = plugin_kwargs["resolution"].replace("(限DALLE2)", "").replace("(限DALLE3)", "")
|
|
||||||
|
|
||||||
if plugin_kwargs["model_name"] == "DALLE2":
|
|
||||||
plugin_kwargs["advanced_arg"] = resolution
|
|
||||||
yield from 图片生成_DALLE2(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
|
|
||||||
elif plugin_kwargs["model_name"] == "DALLE3":
|
|
||||||
quality = plugin_kwargs["quality (仅DALLE3生效)"]
|
|
||||||
style = plugin_kwargs["style (仅DALLE3生效)"]
|
|
||||||
plugin_kwargs["advanced_arg"] = f"{resolution}-{quality}-{style}"
|
|
||||||
yield from 图片生成_DALLE3(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
|
|
||||||
else:
|
|
||||||
chatbot.append([None, "抱歉,找不到该模型"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
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, 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
|
|
||||||
from crazy_functions.prompts.internet import SearchOptimizerPrompt, SearchAcademicOptimizerPrompt
|
|
||||||
|
|
||||||
def search_optimizer(
|
|
||||||
query,
|
|
||||||
proxies,
|
|
||||||
history,
|
|
||||||
llm_kwargs,
|
|
||||||
optimizer=1,
|
|
||||||
categories="general",
|
|
||||||
searxng_url=None,
|
|
||||||
engines=None,
|
|
||||||
):
|
|
||||||
# ------------- < 第1步:尝试进行搜索优化 > -------------
|
|
||||||
# * 增强优化,会尝试结合历史记录进行搜索优化
|
|
||||||
if optimizer == 2:
|
|
||||||
his = " "
|
|
||||||
if len(history) == 0:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for i, h in enumerate(history):
|
|
||||||
if i % 2 == 0:
|
|
||||||
his += f"Q: {h}\n"
|
|
||||||
else:
|
|
||||||
his += f"A: {h}\n"
|
|
||||||
if categories == "general":
|
|
||||||
sys_prompt = SearchOptimizerPrompt.format(query=query, history=his, num=4)
|
|
||||||
elif categories == "science":
|
|
||||||
sys_prompt = SearchAcademicOptimizerPrompt.format(query=query, history=his, num=4)
|
|
||||||
else:
|
|
||||||
his = " "
|
|
||||||
if categories == "general":
|
|
||||||
sys_prompt = SearchOptimizerPrompt.format(query=query, history=his, num=3)
|
|
||||||
elif categories == "science":
|
|
||||||
sys_prompt = SearchAcademicOptimizerPrompt.format(query=query, history=his, num=3)
|
|
||||||
|
|
||||||
mutable = ["", time.time(), ""]
|
|
||||||
llm_kwargs["temperature"] = 0.8
|
|
||||||
try:
|
|
||||||
query_json = predict_no_ui_long_connection(
|
|
||||||
inputs=query,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
history=[],
|
|
||||||
sys_prompt=sys_prompt,
|
|
||||||
observe_window=mutable,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
query_json = "null"
|
|
||||||
#* 尝试解码优化后的搜索结果
|
|
||||||
query_json = re.sub(r"```json|```", "", query_json)
|
|
||||||
try:
|
|
||||||
queries = json.loads(query_json)
|
|
||||||
except Exception:
|
|
||||||
#* 如果解码失败,降低温度再试一次
|
|
||||||
try:
|
|
||||||
llm_kwargs["temperature"] = 0.4
|
|
||||||
query_json = predict_no_ui_long_connection(
|
|
||||||
inputs=query,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
history=[],
|
|
||||||
sys_prompt=sys_prompt,
|
|
||||||
observe_window=mutable,
|
|
||||||
)
|
|
||||||
query_json = re.sub(r"```json|```", "", query_json)
|
|
||||||
queries = json.loads(query_json)
|
|
||||||
except Exception:
|
|
||||||
#* 如果再次失败,直接返回原始问题
|
|
||||||
queries = [query]
|
|
||||||
links = []
|
|
||||||
success = 0
|
|
||||||
Exceptions = ""
|
|
||||||
for q in queries:
|
|
||||||
try:
|
|
||||||
link = searxng_request(q, proxies, categories, searxng_url, engines=engines)
|
|
||||||
if len(link) > 0:
|
|
||||||
links.append(link[:-5])
|
|
||||||
success += 1
|
|
||||||
except Exception:
|
|
||||||
Exceptions = Exception
|
|
||||||
pass
|
|
||||||
if success == 0:
|
|
||||||
raise ValueError(f"在线搜索失败!\n{Exceptions}")
|
|
||||||
# * 清洗搜索结果,依次放入每组第一,第二个搜索结果,并清洗重复的搜索结果
|
|
||||||
seen_links = set()
|
|
||||||
result = []
|
|
||||||
for tuple in zip_longest(*links, fillvalue=None):
|
|
||||||
for item in tuple:
|
|
||||||
if item is not None:
|
|
||||||
link = item["link"]
|
|
||||||
if link not in seen_links:
|
|
||||||
seen_links.add(link)
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_auth_ip():
|
|
||||||
ip = check_proxy(None, return_ip=True)
|
|
||||||
if ip is None:
|
|
||||||
return '114.114.114.' + str(random.randint(1, 10))
|
|
||||||
return ip
|
|
||||||
|
|
||||||
|
|
||||||
def searxng_request(query, proxies, categories='general', searxng_url=None, engines=None):
|
|
||||||
if searxng_url is None:
|
|
||||||
urls = get_conf("SEARXNG_URLS")
|
|
||||||
url = random.choice(urls)
|
|
||||||
else:
|
|
||||||
url = searxng_url
|
|
||||||
|
|
||||||
if engines == "Mixed":
|
|
||||||
engines = None
|
|
||||||
|
|
||||||
if categories == 'general':
|
|
||||||
params = {
|
|
||||||
'q': query, # 搜索查询
|
|
||||||
'format': 'json', # 输出格式为JSON
|
|
||||||
'language': 'zh', # 搜索语言
|
|
||||||
'engines': engines,
|
|
||||||
}
|
|
||||||
elif categories == 'science':
|
|
||||||
params = {
|
|
||||||
'q': query, # 搜索查询
|
|
||||||
'format': 'json', # 输出格式为JSON
|
|
||||||
'language': 'zh', # 搜索语言
|
|
||||||
'categories': 'science'
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise ValueError('不支持的检索类型')
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
|
||||||
'X-Forwarded-For': get_auth_ip(),
|
|
||||||
'X-Real-IP': get_auth_ip()
|
|
||||||
}
|
|
||||||
results = []
|
|
||||||
response = requests.post(url, params=params, headers=headers, proxies=proxies, timeout=30)
|
|
||||||
if response.status_code == 200:
|
|
||||||
json_result = response.json()
|
|
||||||
for result in json_result['results']:
|
|
||||||
item = {
|
|
||||||
"title": result.get("title", ""),
|
|
||||||
"source": result.get("engines", "unknown"),
|
|
||||||
"content": result.get("content", ""),
|
|
||||||
"link": result["url"],
|
|
||||||
}
|
|
||||||
results.append(item)
|
|
||||||
return results
|
|
||||||
else:
|
|
||||||
if response.status_code == 429:
|
|
||||||
raise ValueError("Searxng(在线搜索服务)当前使用人数太多,请稍后。")
|
|
||||||
else:
|
|
||||||
raise ValueError("在线搜索失败,状态码: " + str(response.status_code) + '\t' + response.content.decode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
def scrape_text(url, proxies) -> str:
|
|
||||||
"""Scrape text from a webpage
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url (str): The URL to scrape text from
|
|
||||||
|
|
||||||
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
|
|
||||||
except:
|
|
||||||
return "无法连接到该网页"
|
|
||||||
soup = BeautifulSoup(response.text, "html.parser")
|
|
||||||
for script in soup(["script", "style"]):
|
|
||||||
script.extract()
|
|
||||||
text = soup.get_text()
|
|
||||||
lines = (line.strip() for line in text.splitlines())
|
|
||||||
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
|
||||||
text = "\n".join(chunk for chunk in chunks if chunk)
|
|
||||||
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]
|
|
||||||
history = [] # 清空历史,以免输入溢出
|
|
||||||
chatbot.append((f"请结合互联网信息回答以下问题:{txt}", "检索中..."))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# ------------- < 第1步:爬取搜索引擎的结果 > -------------
|
|
||||||
from toolbox import get_conf
|
|
||||||
proxies = get_conf('proxies')
|
|
||||||
categories = plugin_kwargs.get('categories', 'general')
|
|
||||||
searxng_url = plugin_kwargs.get('searxng_url', None)
|
|
||||||
engines = plugin_kwargs.get('engine', None)
|
|
||||||
optimizer = plugin_kwargs.get('optimizer', "关闭")
|
|
||||||
if optimizer == "关闭":
|
|
||||||
urls = searxng_request(txt, proxies, categories, searxng_url, engines=engines)
|
|
||||||
else:
|
|
||||||
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获取信息!请尝试更换搜索引擎。"))
|
|
||||||
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
|
|
||||||
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 != "开启(增强)"):
|
|
||||||
i_say = f"从以上搜索结果中抽取信息,然后回答问题:{txt}"
|
|
||||||
i_say, history = input_clipping( # 裁剪输入,从最长的条目开始裁剪,防止爆token
|
|
||||||
inputs=i_say,
|
|
||||||
history=history,
|
|
||||||
max_token_limit=min(model_info[llm_kwargs['llm_model']]['max_token']*3//4, 8192)
|
|
||||||
)
|
|
||||||
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=history,
|
|
||||||
sys_prompt="请从给定的若干条搜索结果中抽取信息,对最相关的两个搜索结果进行总结,然后回答问题。"
|
|
||||||
)
|
|
||||||
chatbot[-1] = (i_say, gpt_say)
|
|
||||||
history.append(i_say);history.append(gpt_say)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
|
||||||
|
|
||||||
#* 或者使用搜索优化器,这样可以保证后续问答能读取到有效的历史记录
|
|
||||||
else:
|
|
||||||
i_say = f"从以上搜索结果中抽取与问题:{txt} 相关的信息:"
|
|
||||||
i_say, history = input_clipping( # 裁剪输入,从最长的条目开始裁剪,防止爆token
|
|
||||||
inputs=i_say,
|
|
||||||
history=history,
|
|
||||||
max_token_limit=min(model_info[llm_kwargs['llm_model']]['max_token']*3//4, 8192)
|
|
||||||
)
|
|
||||||
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=history,
|
|
||||||
sys_prompt="请从给定的若干条搜索结果中抽取信息,对最相关的三个搜索结果进行总结"
|
|
||||||
)
|
|
||||||
chatbot[-1] = (i_say, gpt_say)
|
|
||||||
history = []
|
|
||||||
history.append(i_say);history.append(gpt_say)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
|
||||||
|
|
||||||
# ------------- < 第4步:根据综合回答问题 > -------------
|
|
||||||
i_say = f"请根据以上搜索结果回答问题:{txt}"
|
|
||||||
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=history,
|
|
||||||
sys_prompt="请根据给定的若干条搜索结果回答问题"
|
|
||||||
)
|
|
||||||
chatbot[-1] = (i_say, gpt_say)
|
|
||||||
history.append(i_say);history.append(gpt_say)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import random
|
|
||||||
from toolbox import get_conf
|
|
||||||
from crazy_functions.Internet_GPT import 连接网络回答问题
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkGPT_Wrap(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
|
|
||||||
第一个参数,名称`main_input`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第二个参数,名称`advanced_arg`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第三个参数,名称`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(), # 主输入,自动从输入框同步
|
|
||||||
"categories":
|
|
||||||
ArgProperty(title="搜索分类", options=["网页", "学术论文"], default_value="网页", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"engine":
|
|
||||||
ArgProperty(title="选择搜索引擎", options=["Mixed", "bing", "google", "duckduckgo"], default_value="google", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"optimizer":
|
|
||||||
ArgProperty(title="搜索优化", options=["关闭", "开启", "开启(增强)"], default_value="关闭", description="是否使用搜索增强。注意这可能会消耗较多token", type="dropdown").model_dump_json(),
|
|
||||||
"searxng_url":
|
|
||||||
ArgProperty(title="Searxng服务地址", description="输入Searxng的地址", default_value=url, type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs:dict, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
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,595 +0,0 @@
|
|||||||
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_latest_msg, zip_result, gen_time_str
|
|
||||||
from functools import partial
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
import glob, os, requests, time, json, tarfile, threading
|
|
||||||
|
|
||||||
pj = os.path.join
|
|
||||||
ARXIV_CACHE_DIR = get_conf("ARXIV_CACHE_DIR")
|
|
||||||
|
|
||||||
|
|
||||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 工具函数 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
|
||||||
# 专业词汇声明 = 'If the term "agent" is used in this section, it should be translated to "智能体". '
|
|
||||||
def switch_prompt(pfg, mode, more_requirement):
|
|
||||||
"""
|
|
||||||
Generate prompts and system prompts based on the mode for proofreading or translating.
|
|
||||||
Args:
|
|
||||||
- pfg: Proofreader or Translator instance.
|
|
||||||
- mode: A string specifying the mode, either 'proofread' or 'translate_zh'.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- inputs_array: A list of strings containing prompts for users to respond to.
|
|
||||||
- sys_prompt_array: A list of strings containing prompts for system prompts.
|
|
||||||
"""
|
|
||||||
n_split = len(pfg.sp_file_contents)
|
|
||||||
if mode == 'proofread_en':
|
|
||||||
inputs_array = [r"Below is a section from an academic paper, proofread this section." +
|
|
||||||
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " + more_requirement +
|
|
||||||
r"Answer me only with the revised text:" +
|
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
|
||||||
sys_prompt_array = ["You are a professional academic paper writer." for _ in range(n_split)]
|
|
||||||
elif mode == 'translate_zh':
|
|
||||||
inputs_array = [
|
|
||||||
r"Below is a section from an English academic paper, translate it into Chinese. " + more_requirement +
|
|
||||||
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " +
|
|
||||||
r"Answer me only with the translated text:" +
|
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
|
||||||
sys_prompt_array = ["You are a professional translator." for _ in range(n_split)]
|
|
||||||
else:
|
|
||||||
assert False, "未知指令"
|
|
||||||
return inputs_array, sys_prompt_array
|
|
||||||
|
|
||||||
|
|
||||||
def descend_to_extracted_folder_if_exist(project_folder):
|
|
||||||
"""
|
|
||||||
Descend into the extracted folder if it exists, otherwise return the original folder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- project_folder: A string specifying the folder path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- A string specifying the path to the extracted folder, or the original folder if there is no extracted folder.
|
|
||||||
"""
|
|
||||||
maybe_dir = [f for f in glob.glob(f'{project_folder}/*') if os.path.isdir(f)]
|
|
||||||
if len(maybe_dir) == 0: return project_folder
|
|
||||||
if maybe_dir[0].endswith('.extract'): return maybe_dir[0]
|
|
||||||
return project_folder
|
|
||||||
|
|
||||||
|
|
||||||
def move_project(project_folder, arxiv_id=None):
|
|
||||||
"""
|
|
||||||
Create a new work folder and copy the project folder to it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- project_folder: A string specifying the folder path of the project.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- A string specifying the path to the new work folder.
|
|
||||||
"""
|
|
||||||
import shutil, time
|
|
||||||
time.sleep(2) # avoid time string conflict
|
|
||||||
if arxiv_id is not None:
|
|
||||||
new_workfolder = pj(ARXIV_CACHE_DIR, arxiv_id, 'workfolder')
|
|
||||||
else:
|
|
||||||
new_workfolder = f'{get_log_folder()}/{gen_time_str()}'
|
|
||||||
try:
|
|
||||||
shutil.rmtree(new_workfolder)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# align subfolder if there is a folder wrapper
|
|
||||||
items = glob.glob(pj(project_folder, '*'))
|
|
||||||
items = [item for item in items if os.path.basename(item) != '__MACOSX']
|
|
||||||
if len(glob.glob(pj(project_folder, '*.tex'))) == 0 and len(items) == 1:
|
|
||||||
if os.path.isdir(items[0]): project_folder = items[0]
|
|
||||||
|
|
||||||
shutil.copytree(src=project_folder, dst=new_workfolder)
|
|
||||||
return new_workfolder
|
|
||||||
|
|
||||||
|
|
||||||
def arxiv_download(chatbot, history, txt, allow_cache=True):
|
|
||||||
def check_cached_translation_pdf(arxiv_id):
|
|
||||||
translation_dir = pj(ARXIV_CACHE_DIR, arxiv_id, 'translation')
|
|
||||||
if not os.path.exists(translation_dir):
|
|
||||||
os.makedirs(translation_dir)
|
|
||||||
target_file = pj(translation_dir, 'translate_zh.pdf')
|
|
||||||
if os.path.exists(target_file):
|
|
||||||
promote_file_to_downloadzone(target_file, rename_file=None, chatbot=chatbot)
|
|
||||||
target_file_compare = pj(translation_dir, 'comparison.pdf')
|
|
||||||
if os.path.exists(target_file_compare):
|
|
||||||
promote_file_to_downloadzone(target_file_compare, rename_file=None, chatbot=chatbot)
|
|
||||||
return target_file
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_float(s):
|
|
||||||
try:
|
|
||||||
float(s)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if txt.startswith('https://arxiv.org/pdf/'):
|
|
||||||
arxiv_id = txt.split('/')[-1] # 2402.14207v2.pdf
|
|
||||||
txt = arxiv_id.split('v')[0] # 2402.14207
|
|
||||||
|
|
||||||
if ('.' in txt) and ('/' not in txt) and is_float(txt): # is arxiv ID
|
|
||||||
txt = 'https://arxiv.org/abs/' + txt.strip()
|
|
||||||
if ('.' in txt) and ('/' not in txt) and is_float(txt[:10]): # is arxiv ID
|
|
||||||
txt = 'https://arxiv.org/abs/' + txt[:10]
|
|
||||||
|
|
||||||
if not txt.startswith('https://arxiv.org'):
|
|
||||||
return txt, None # 是本地文件,跳过下载
|
|
||||||
|
|
||||||
# <-------------- inspect format ------------->
|
|
||||||
chatbot.append([f"检测到arxiv文档连接", '尝试下载 ...'])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
|
|
||||||
url_ = txt # https://arxiv.org/abs/1707.06690
|
|
||||||
|
|
||||||
if not txt.startswith('https://arxiv.org/abs/'):
|
|
||||||
msg = f"解析arxiv网址失败, 期望格式例如: https://arxiv.org/abs/1707.06690。实际得到格式: {url_}。"
|
|
||||||
yield from update_ui_latest_msg(msg, chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return msg, None
|
|
||||||
# <-------------- set format ------------->
|
|
||||||
arxiv_id = url_.split('/abs/')[-1]
|
|
||||||
if 'v' in arxiv_id: arxiv_id = arxiv_id[:10]
|
|
||||||
cached_translation_pdf = check_cached_translation_pdf(arxiv_id)
|
|
||||||
if cached_translation_pdf and allow_cache: return cached_translation_pdf, arxiv_id
|
|
||||||
|
|
||||||
extract_dst = pj(ARXIV_CACHE_DIR, arxiv_id, 'extract')
|
|
||||||
translation_dir = pj(ARXIV_CACHE_DIR, arxiv_id, 'e-print')
|
|
||||||
dst = pj(translation_dir, arxiv_id + '.tar')
|
|
||||||
os.makedirs(translation_dir, exist_ok=True)
|
|
||||||
# <-------------- download arxiv source file ------------->
|
|
||||||
|
|
||||||
def fix_url_and_download():
|
|
||||||
# for url_tar in [url_.replace('/abs/', '/e-print/'), url_.replace('/abs/', '/src/')]:
|
|
||||||
for url_tar in [url_.replace('/abs/', '/src/'), url_.replace('/abs/', '/e-print/')]:
|
|
||||||
proxies = get_conf('proxies')
|
|
||||||
r = requests.get(url_tar, proxies=proxies)
|
|
||||||
if r.status_code == 200:
|
|
||||||
with open(dst, 'wb+') as f:
|
|
||||||
f.write(r.content)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if os.path.exists(dst) and allow_cache:
|
|
||||||
yield from update_ui_latest_msg(f"调用缓存 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
success = True
|
|
||||||
else:
|
|
||||||
yield from update_ui_latest_msg(f"开始下载 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
success = fix_url_and_download()
|
|
||||||
yield from update_ui_latest_msg(f"下载完成 {arxiv_id}", chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
yield from update_ui_latest_msg(f"下载失败 {arxiv_id}", chatbot=chatbot, history=history)
|
|
||||||
raise tarfile.ReadError(f"论文下载失败 {arxiv_id}")
|
|
||||||
|
|
||||||
# <-------------- extract file ------------->
|
|
||||||
from toolbox import extract_archive
|
|
||||||
try:
|
|
||||||
extract_archive(file_path=dst, dest_dir=extract_dst)
|
|
||||||
except tarfile.ReadError:
|
|
||||||
os.remove(dst)
|
|
||||||
raise tarfile.ReadError(f"论文下载失败")
|
|
||||||
return extract_dst, arxiv_id
|
|
||||||
|
|
||||||
|
|
||||||
def pdf2tex_project(pdf_file_path, plugin_kwargs):
|
|
||||||
if plugin_kwargs["method"] == "MATHPIX":
|
|
||||||
# Mathpix API credentials
|
|
||||||
app_id, app_key = get_conf('MATHPIX_APPID', 'MATHPIX_APPKEY')
|
|
||||||
headers = {"app_id": app_id, "app_key": app_key}
|
|
||||||
|
|
||||||
# Step 1: Send PDF file for processing
|
|
||||||
options = {
|
|
||||||
"conversion_formats": {"tex.zip": True},
|
|
||||||
"math_inline_delimiters": ["$", "$"],
|
|
||||||
"rm_spaces": True
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url="https://api.mathpix.com/v3/pdf",
|
|
||||||
headers=headers,
|
|
||||||
data={"options_json": json.dumps(options)},
|
|
||||||
files={"file": open(pdf_file_path, "rb")})
|
|
||||||
|
|
||||||
if response.ok:
|
|
||||||
pdf_id = response.json()["pdf_id"]
|
|
||||||
logger.info(f"PDF processing initiated. PDF ID: {pdf_id}")
|
|
||||||
|
|
||||||
# Step 2: Check processing status
|
|
||||||
while True:
|
|
||||||
conversion_response = requests.get(f"https://api.mathpix.com/v3/pdf/{pdf_id}", headers=headers)
|
|
||||||
conversion_data = conversion_response.json()
|
|
||||||
|
|
||||||
if conversion_data["status"] == "completed":
|
|
||||||
logger.info("PDF processing completed.")
|
|
||||||
break
|
|
||||||
elif conversion_data["status"] == "error":
|
|
||||||
logger.info("Error occurred during processing.")
|
|
||||||
else:
|
|
||||||
logger.info(f"Processing status: {conversion_data['status']}")
|
|
||||||
time.sleep(5) # wait for a few seconds before checking again
|
|
||||||
|
|
||||||
# Step 3: Save results to local files
|
|
||||||
output_dir = os.path.join(os.path.dirname(pdf_file_path), 'mathpix_output')
|
|
||||||
if not os.path.exists(output_dir):
|
|
||||||
os.makedirs(output_dir)
|
|
||||||
|
|
||||||
url = f"https://api.mathpix.com/v3/pdf/{pdf_id}.tex"
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
file_name_wo_dot = '_'.join(os.path.basename(pdf_file_path).split('.')[:-1])
|
|
||||||
output_name = f"{file_name_wo_dot}.tex.zip"
|
|
||||||
output_path = os.path.join(output_dir, output_name)
|
|
||||||
with open(output_path, "wb") as output_file:
|
|
||||||
output_file.write(response.content)
|
|
||||||
logger.info(f"tex.zip file saved at: {output_path}")
|
|
||||||
|
|
||||||
import zipfile
|
|
||||||
unzip_dir = os.path.join(output_dir, file_name_wo_dot)
|
|
||||||
with zipfile.ZipFile(output_path, 'r') as zip_ref:
|
|
||||||
zip_ref.extractall(unzip_dir)
|
|
||||||
|
|
||||||
return unzip_dir
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error(f"Error sending PDF for processing. Status code: {response.status_code}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
from crazy_functions.pdf_fns.parse_pdf_via_doc2x import 解析PDF_DOC2X_转Latex
|
|
||||||
unzip_dir = 解析PDF_DOC2X_转Latex(pdf_file_path)
|
|
||||||
return unzip_dir
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 插件主程序1 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def Latex英文纠错加PDF对比(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
# <-------------- information about this plugin ------------->
|
|
||||||
chatbot.append(["函数插件功能?",
|
|
||||||
"对整个Latex项目进行纠错, 用latex编译为PDF对修正处做高亮。函数插件贡献者: Binary-Husky。注意事项: 目前对机器学习类文献转化效果最好,其他类型文献转化效果未知。仅在Windows系统进行了测试,其他操作系统表现未知。"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# <-------------- more requirements ------------->
|
|
||||||
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
|
||||||
more_req = plugin_kwargs.get("advanced_arg", "")
|
|
||||||
_switch_prompt_ = partial(switch_prompt, more_requirement=more_req)
|
|
||||||
|
|
||||||
# <-------------- check deps ------------->
|
|
||||||
try:
|
|
||||||
import glob, os, time, subprocess
|
|
||||||
subprocess.Popen(['pdflatex', '-version'])
|
|
||||||
from .latex_fns.latex_actions import Latex精细分解与转化, 编译Latex
|
|
||||||
except Exception as e:
|
|
||||||
chatbot.append([f"解析项目: {txt}",
|
|
||||||
f"尝试执行Latex指令失败。Latex没有安装, 或者不在环境变量PATH中。安装方法https://tug.org/texlive/。报错信息\n\n```\n\n{trimmed_format_exc()}\n\n```\n\n"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- clear history and read input ------------->
|
|
||||||
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
|
|
||||||
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}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- if is a zip/tar file ------------->
|
|
||||||
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
|
|
||||||
validate_path_safety(project_folder, chatbot.get_user())
|
|
||||||
project_folder = move_project(project_folder, arxiv_id=None)
|
|
||||||
|
|
||||||
# <-------------- if merge_translate_zh is already generated, skip gpt req ------------->
|
|
||||||
if not os.path.exists(project_folder + '/merge_proofread_en.tex'):
|
|
||||||
yield from Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin_kwargs,
|
|
||||||
chatbot, history, system_prompt, mode='proofread_en',
|
|
||||||
switch_prompt=_switch_prompt_)
|
|
||||||
|
|
||||||
# <-------------- compile PDF ------------->
|
|
||||||
success = yield from 编译Latex(chatbot, history, main_file_original='merge',
|
|
||||||
main_file_modified='merge_proofread_en',
|
|
||||||
work_folder_original=project_folder, work_folder_modified=project_folder,
|
|
||||||
work_folder=project_folder)
|
|
||||||
|
|
||||||
# <-------------- zip PDF ------------->
|
|
||||||
zip_res = zip_result(project_folder)
|
|
||||||
if success:
|
|
||||||
chatbot.append((f"成功啦", '请查收结果(压缩包)...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history);
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
else:
|
|
||||||
chatbot.append((f"失败了",
|
|
||||||
'虽然PDF生成失败了, 但请查收结果(压缩包), 内含已经翻译的Tex文档, 也是可读的, 您可以到Github Issue区, 用该压缩包+Conversation_To_File进行反馈 ...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history);
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
|
|
||||||
# <-------------- we are done ------------->
|
|
||||||
return success
|
|
||||||
|
|
||||||
|
|
||||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 插件主程序2 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def Latex翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
# <-------------- information about this plugin ------------->
|
|
||||||
chatbot.append([
|
|
||||||
"函数插件功能?",
|
|
||||||
"对整个Latex项目进行翻译, 生成中文PDF。函数插件贡献者: Binary-Husky。注意事项: 此插件Windows支持最佳,Linux下必须使用Docker安装,详见项目主README.md。目前对机器学习类文献转化效果最好,其他类型文献转化效果未知。"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# <-------------- more requirements ------------->
|
|
||||||
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
|
||||||
more_req = plugin_kwargs.get("advanced_arg", "")
|
|
||||||
|
|
||||||
no_cache = ("--no-cache" in more_req)
|
|
||||||
if no_cache: more_req = more_req.replace("--no-cache", "").strip()
|
|
||||||
|
|
||||||
allow_gptac_cloud_io = ("--allow-cloudio" in more_req) # 从云端下载翻译结果,以及上传翻译结果到云端
|
|
||||||
if allow_gptac_cloud_io: more_req = more_req.replace("--allow-cloudio", "").strip()
|
|
||||||
|
|
||||||
allow_cache = not no_cache
|
|
||||||
_switch_prompt_ = partial(switch_prompt, more_requirement=more_req)
|
|
||||||
|
|
||||||
|
|
||||||
# <-------------- check deps ------------->
|
|
||||||
try:
|
|
||||||
import glob, os, time, subprocess
|
|
||||||
subprocess.Popen(['pdflatex', '-version'])
|
|
||||||
from .latex_fns.latex_actions import Latex精细分解与转化, 编译Latex
|
|
||||||
except Exception as e:
|
|
||||||
chatbot.append([f"解析项目: {txt}",
|
|
||||||
f"尝试执行Latex指令失败。Latex没有安装, 或者不在环境变量PATH中。安装方法https://tug.org/texlive/。报错信息\n\n```\n\n{trimmed_format_exc()}\n\n```\n\n"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- clear history and read input ------------->
|
|
||||||
history = []
|
|
||||||
try:
|
|
||||||
txt, arxiv_id = yield from arxiv_download(chatbot, history, txt, allow_cache)
|
|
||||||
except tarfile.ReadError as e:
|
|
||||||
yield from update_ui_latest_msg(
|
|
||||||
"无法自动下载该论文的Latex源码,请前往arxiv打开此论文下载页面,点other Formats,然后download source手动下载latex源码包。接下来调用本地Latex翻译插件即可。",
|
|
||||||
chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
|
|
||||||
if txt.endswith('.pdf'):
|
|
||||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"发现已经存在翻译好的PDF文档")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# #################################################################
|
|
||||||
if allow_gptac_cloud_io and arxiv_id:
|
|
||||||
# 访问 GPTAC学术云,查询云端是否存在该论文的翻译版本
|
|
||||||
from crazy_functions.latex_fns.latex_actions import check_gptac_cloud
|
|
||||||
success, downloaded = check_gptac_cloud(arxiv_id, chatbot)
|
|
||||||
if success:
|
|
||||||
chatbot.append([
|
|
||||||
f"检测到GPTAC云端存在翻译版本, 如果不满意翻译结果, 请禁用云端分享, 然后重新执行。",
|
|
||||||
None
|
|
||||||
])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- if is a zip/tar file ------------->
|
|
||||||
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
|
|
||||||
validate_path_safety(project_folder, chatbot.get_user())
|
|
||||||
project_folder = move_project(project_folder, arxiv_id)
|
|
||||||
|
|
||||||
# <-------------- if merge_translate_zh is already generated, skip gpt req ------------->
|
|
||||||
if not os.path.exists(project_folder + '/merge_translate_zh.tex'):
|
|
||||||
yield from Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin_kwargs,
|
|
||||||
chatbot, history, system_prompt, mode='translate_zh',
|
|
||||||
switch_prompt=_switch_prompt_)
|
|
||||||
|
|
||||||
# <-------------- compile PDF ------------->
|
|
||||||
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,
|
|
||||||
work_folder=project_folder)
|
|
||||||
|
|
||||||
# <-------------- zip PDF ------------->
|
|
||||||
zip_res = zip_result(project_folder)
|
|
||||||
if success:
|
|
||||||
if allow_gptac_cloud_io and arxiv_id:
|
|
||||||
# 如果用户允许,我们将翻译好的arxiv论文PDF上传到GPTAC学术云
|
|
||||||
from crazy_functions.latex_fns.latex_actions import upload_to_gptac_cloud_if_user_allow
|
|
||||||
threading.Thread(target=upload_to_gptac_cloud_if_user_allow,
|
|
||||||
args=(chatbot, arxiv_id), daemon=True).start()
|
|
||||||
|
|
||||||
chatbot.append((f"成功啦", '请查收结果(压缩包)...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
|
|
||||||
else:
|
|
||||||
chatbot.append((f"失败了",
|
|
||||||
'虽然PDF生成失败了, 但请查收结果(压缩包), 内含已经翻译的Tex文档, 您可以到Github Issue区, 用该压缩包进行反馈。如系统是Linux,请检查系统字体(见Github wiki) ...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
|
|
||||||
# <-------------- we are done ------------->
|
|
||||||
return success
|
|
||||||
|
|
||||||
|
|
||||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 插件主程序3 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
|
||||||
# <-------------- information about this plugin ------------->
|
|
||||||
chatbot.append([
|
|
||||||
"函数插件功能?",
|
|
||||||
"将PDF转换为Latex项目,翻译为中文后重新编译为PDF。函数插件贡献者: Marroh。注意事项: 此插件Windows支持最佳,Linux下必须使用Docker安装,详见项目主README.md。目前对机器学习类文献转化效果最好,其他类型文献转化效果未知。"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# <-------------- more requirements ------------->
|
|
||||||
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
|
||||||
more_req = plugin_kwargs.get("advanced_arg", "")
|
|
||||||
no_cache = more_req.startswith("--no-cache")
|
|
||||||
if no_cache: more_req.lstrip("--no-cache")
|
|
||||||
allow_cache = not no_cache
|
|
||||||
_switch_prompt_ = partial(switch_prompt, more_requirement=more_req)
|
|
||||||
|
|
||||||
# <-------------- check deps ------------->
|
|
||||||
try:
|
|
||||||
import glob, os, time, subprocess
|
|
||||||
subprocess.Popen(['pdflatex', '-version'])
|
|
||||||
from .latex_fns.latex_actions import Latex精细分解与转化, 编译Latex
|
|
||||||
except Exception as e:
|
|
||||||
chatbot.append([f"解析项目: {txt}",
|
|
||||||
f"尝试执行Latex指令失败。Latex没有安装, 或者不在环境变量PATH中。安装方法https://tug.org/texlive/。报错信息\n\n```\n\n{trimmed_format_exc()}\n\n```\n\n"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- clear history and read input ------------->
|
|
||||||
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
|
|
||||||
|
|
||||||
file_manifest = [f for f in glob.glob(f'{project_folder}/**/*.pdf', recursive=True)]
|
|
||||||
if len(file_manifest) == 0:
|
|
||||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"找不到任何.pdf文件: {txt}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
if len(file_manifest) != 1:
|
|
||||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"不支持同时处理多个pdf文件: {txt}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
if plugin_kwargs.get("method", "") == 'MATHPIX':
|
|
||||||
app_id, app_key = get_conf('MATHPIX_APPID', 'MATHPIX_APPKEY')
|
|
||||||
if len(app_id) == 0 or len(app_key) == 0:
|
|
||||||
report_exception(chatbot, history, a="缺失 MATHPIX_APPID 和 MATHPIX_APPKEY。", b=f"请配置 MATHPIX_APPID 和 MATHPIX_APPKEY")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
if plugin_kwargs.get("method", "") == 'DOC2X':
|
|
||||||
app_id, app_key = "", ""
|
|
||||||
DOC2X_API_KEY = get_conf('DOC2X_API_KEY')
|
|
||||||
if len(DOC2X_API_KEY) == 0:
|
|
||||||
report_exception(chatbot, history, a="缺失 DOC2X_API_KEY。", b=f"请配置 DOC2X_API_KEY")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
hash_tag = map_file_to_sha256(file_manifest[0])
|
|
||||||
|
|
||||||
# # <-------------- check repeated pdf ------------->
|
|
||||||
# chatbot.append([f"检查PDF是否被重复上传", "正在检查..."])
|
|
||||||
# yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
# repeat, project_folder = check_repeat_upload(file_manifest[0], hash_tag)
|
|
||||||
|
|
||||||
# if repeat:
|
|
||||||
# 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)
|
|
||||||
# comparison_pdf = [f for f in glob.glob(f'{project_folder}/**/comparison.pdf', recursive=True)][0]
|
|
||||||
# promote_file_to_downloadzone(comparison_pdf, rename_file=None, chatbot=chatbot)
|
|
||||||
# zip_res = zip_result(project_folder)
|
|
||||||
# promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
# return
|
|
||||||
# except:
|
|
||||||
# report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"发现重复上传,但是无法找到相关文件")
|
|
||||||
# yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
# else:
|
|
||||||
# yield from update_ui_latest_msg(f"未发现重复上传", chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
# <-------------- convert pdf into tex ------------->
|
|
||||||
chatbot.append([f"解析项目: {txt}", "正在将PDF转换为tex项目,请耐心等待..."])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
project_folder = pdf2tex_project(file_manifest[0], plugin_kwargs)
|
|
||||||
if project_folder is None:
|
|
||||||
report_exception(chatbot, history, a=f"解析项目: {txt}", b=f"PDF转换为tex项目失败")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# <-------------- translate latex file into Chinese ------------->
|
|
||||||
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}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# <-------------- if is a zip/tar file ------------->
|
|
||||||
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
|
|
||||||
validate_path_safety(project_folder, chatbot.get_user())
|
|
||||||
project_folder = move_project(project_folder)
|
|
||||||
|
|
||||||
# <-------------- set a hash tag for repeat-checking ------------->
|
|
||||||
with open(pj(project_folder, hash_tag + '.tag'), 'w', encoding='utf8') as f:
|
|
||||||
f.write(hash_tag)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
# <-------------- if merge_translate_zh is already generated, skip gpt req ------------->
|
|
||||||
if not os.path.exists(project_folder + '/merge_translate_zh.tex'):
|
|
||||||
yield from Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin_kwargs,
|
|
||||||
chatbot, history, system_prompt, mode='translate_zh',
|
|
||||||
switch_prompt=_switch_prompt_)
|
|
||||||
|
|
||||||
# <-------------- compile PDF ------------->
|
|
||||||
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,
|
|
||||||
work_folder=project_folder)
|
|
||||||
|
|
||||||
# <-------------- zip PDF ------------->
|
|
||||||
zip_res = zip_result(project_folder)
|
|
||||||
if success:
|
|
||||||
chatbot.append((f"成功啦", '请查收结果(压缩包)...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history);
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
else:
|
|
||||||
chatbot.append((f"失败了",
|
|
||||||
'虽然PDF生成失败了, 但请查收结果(压缩包), 内含已经翻译的Tex文档, 您可以到Github Issue区, 用该压缩包进行反馈。如系统是Linux,请检查系统字体(见Github wiki) ...'))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history);
|
|
||||||
time.sleep(1) # 刷新界面
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
|
|
||||||
# <-------------- we are done ------------->
|
|
||||||
return success
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
|
|
||||||
from crazy_functions.Latex_Function import Latex翻译中文并重新编译PDF, PDF翻译中文并重新编译PDF
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
|
|
||||||
|
|
||||||
class Arxiv_Localize(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
|
|
||||||
第一个参数,名称`main_input`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第二个参数,名称`advanced_arg`,参数`type`声明这是一个文本框,文本框上方显示`title`,文本框内部显示`description`,`default_value`为默认值;
|
|
||||||
第三个参数,名称`allow_cache`,参数`type`声明这是一个下拉菜单,下拉菜单上方显示`title`+`description`,下拉菜单的选项为`options`,`default_value`为下拉菜单默认值;
|
|
||||||
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="ArxivID", description="输入Arxiv的ID或者网址", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
"advanced_arg":
|
|
||||||
ArgProperty(title="额外的翻译提示词",
|
|
||||||
description=r"如果有必要, 请在此处给出自定义翻译命令, 解决部分词汇翻译不准确的问题。 "
|
|
||||||
r"例如当单词'agent'翻译不准确时, 请尝试把以下指令复制到高级参数区: "
|
|
||||||
r'If the term "agent" is used in this section, it should be translated to "智能体". ',
|
|
||||||
default_value="", type="string").model_dump_json(), # 高级参数输入区,自动同步
|
|
||||||
"allow_cache":
|
|
||||||
ArgProperty(title="是否允许从缓存中调取结果", options=["允许缓存", "从头执行"], default_value="允许缓存", description="无", type="dropdown").model_dump_json(),
|
|
||||||
"allow_cloudio":
|
|
||||||
ArgProperty(title="是否允许从GPTAC学术云下载(或者上传)翻译结果(仅针对Arxiv论文)", options=["允许", "禁止"], default_value="禁止", description="共享文献,互助互利", type="dropdown").model_dump_json(),
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
allow_cache = plugin_kwargs["allow_cache"]
|
|
||||||
allow_cloudio = plugin_kwargs["allow_cloudio"]
|
|
||||||
advanced_arg = plugin_kwargs["advanced_arg"]
|
|
||||||
|
|
||||||
if allow_cache == "从头执行": plugin_kwargs["advanced_arg"] = "--no-cache " + plugin_kwargs["advanced_arg"]
|
|
||||||
|
|
||||||
# 从云端下载翻译结果,以及上传翻译结果到云端;人人为我,我为人人。
|
|
||||||
if allow_cloudio == "允许": plugin_kwargs["advanced_arg"] = "--allow-cloudio " + plugin_kwargs["advanced_arg"]
|
|
||||||
|
|
||||||
yield from Latex翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PDF_Localize(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="PDF文件路径", description="未指定路径,请上传文件后,再点击该插件", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
"advanced_arg":
|
|
||||||
ArgProperty(title="额外的翻译提示词",
|
|
||||||
description=r"如果有必要, 请在此处给出自定义翻译命令, 解决部分词汇翻译不准确的问题。 "
|
|
||||||
r"例如当单词'agent'翻译不准确时, 请尝试把以下指令复制到高级参数区: "
|
|
||||||
r'If the term "agent" is used in this section, it should be translated to "智能体". ',
|
|
||||||
default_value="", type="string").model_dump_json(), # 高级参数输入区,自动同步
|
|
||||||
"method":
|
|
||||||
ArgProperty(title="采用哪种方法执行转换", options=["MATHPIX", "DOC2X"], default_value="DOC2X", description="无", type="dropdown").model_dump_json(),
|
|
||||||
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
yield from PDF翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from toolbox import update_ui, trimmed_format_exc, promote_file_to_downloadzone, get_log_folder
|
from toolbox import update_ui, trimmed_format_exc, promote_file_to_downloadzone, get_log_folder
|
||||||
from toolbox import CatchException, report_exception, write_history_to_file, zip_folder
|
from toolbox import CatchException, report_exception, write_history_to_file, zip_folder
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
class PaperFileGroup():
|
class PaperFileGroup():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -33,7 +33,7 @@ class PaperFileGroup():
|
|||||||
self.sp_file_index.append(index)
|
self.sp_file_index.append(index)
|
||||||
self.sp_file_tag.append(self.file_paths[index] + f".part-{j}.tex")
|
self.sp_file_tag.append(self.file_paths[index] + f".part-{j}.tex")
|
||||||
|
|
||||||
logger.info('Segmentation: done')
|
print('Segmentation: done')
|
||||||
def merge_result(self):
|
def merge_result(self):
|
||||||
self.file_result = ["" for _ in range(len(self.file_paths))]
|
self.file_result = ["" for _ in range(len(self.file_paths))]
|
||||||
for r, k in zip(self.sp_file_result, self.sp_file_index):
|
for r, k in zip(self.sp_file_result, self.sp_file_index):
|
||||||
@@ -46,7 +46,7 @@ class PaperFileGroup():
|
|||||||
manifest.append(path + '.polish.tex')
|
manifest.append(path + '.polish.tex')
|
||||||
f.write(res)
|
f.write(res)
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
def zip_result(self):
|
def zip_result(self):
|
||||||
import os, time
|
import os, time
|
||||||
folder = os.path.dirname(self.file_paths[0])
|
folder = os.path.dirname(self.file_paths[0])
|
||||||
@@ -56,10 +56,10 @@ class PaperFileGroup():
|
|||||||
|
|
||||||
def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, language='en', mode='polish'):
|
def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, language='en', mode='polish'):
|
||||||
import time, os, re
|
import time, os, re
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
from .crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||||
|
|
||||||
|
|
||||||
# <-------- 读取Latex文件,删除其中的所有注释 ---------->
|
# <-------- 读取Latex文件,删除其中的所有注释 ---------->
|
||||||
pfg = PaperFileGroup()
|
pfg = PaperFileGroup()
|
||||||
|
|
||||||
for index, fp in enumerate(file_manifest):
|
for index, fp in enumerate(file_manifest):
|
||||||
@@ -73,31 +73,31 @@ def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
pfg.file_paths.append(fp)
|
pfg.file_paths.append(fp)
|
||||||
pfg.file_contents.append(clean_tex_content)
|
pfg.file_contents.append(clean_tex_content)
|
||||||
|
|
||||||
# <-------- 拆分过长的latex文件 ---------->
|
# <-------- 拆分过长的latex文件 ---------->
|
||||||
pfg.run_file_split(max_token_limit=1024)
|
pfg.run_file_split(max_token_limit=1024)
|
||||||
n_split = len(pfg.sp_file_contents)
|
n_split = len(pfg.sp_file_contents)
|
||||||
|
|
||||||
|
|
||||||
# <-------- 多线程润色开始 ---------->
|
# <-------- 多线程润色开始 ---------->
|
||||||
if language == 'en':
|
if language == 'en':
|
||||||
if mode == 'polish':
|
if mode == 'polish':
|
||||||
inputs_array = [r"Below is a section from an academic paper, polish this section to meet the academic standard, " +
|
inputs_array = ["Below is a section from an academic paper, polish this section to meet the academic standard, " +
|
||||||
r"improve the grammar, clarity and overall readability, do not modify any latex command such as \section, \cite and equations:" +
|
"improve the grammar, clarity and overall readability, do not modify any latex command such as \section, \cite and equations:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
else:
|
else:
|
||||||
inputs_array = [r"Below is a section from an academic paper, proofread this section." +
|
inputs_array = [r"Below is a section from an academic paper, proofread this section." +
|
||||||
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " +
|
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " +
|
||||||
r"Answer me only with the revised text:" +
|
r"Answer me only with the revised text:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
inputs_show_user_array = [f"Polish {f}" for f in pfg.sp_file_tag]
|
inputs_show_user_array = [f"Polish {f}" for f in pfg.sp_file_tag]
|
||||||
sys_prompt_array = ["You are a professional academic paper writer." for _ in range(n_split)]
|
sys_prompt_array = ["You are a professional academic paper writer." for _ in range(n_split)]
|
||||||
elif language == 'zh':
|
elif language == 'zh':
|
||||||
if mode == 'polish':
|
if mode == 'polish':
|
||||||
inputs_array = [r"以下是一篇学术论文中的一段内容,请将此部分润色以满足学术标准,提高语法、清晰度和整体可读性,不要修改任何LaTeX命令,例如\section,\cite和方程式:" +
|
inputs_array = [f"以下是一篇学术论文中的一段内容,请将此部分润色以满足学术标准,提高语法、清晰度和整体可读性,不要修改任何LaTeX命令,例如\section,\cite和方程式:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
else:
|
else:
|
||||||
inputs_array = [r"以下是一篇学术论文中的一段内容,请对这部分内容进行语法矫正。不要修改任何LaTeX命令,例如\section,\cite和方程式:" +
|
inputs_array = [f"以下是一篇学术论文中的一段内容,请对这部分内容进行语法矫正。不要修改任何LaTeX命令,例如\section,\cite和方程式:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
inputs_show_user_array = [f"润色 {f}" for f in pfg.sp_file_tag]
|
inputs_show_user_array = [f"润色 {f}" for f in pfg.sp_file_tag]
|
||||||
sys_prompt_array=["你是一位专业的中文学术论文作家。" for _ in range(n_split)]
|
sys_prompt_array=["你是一位专业的中文学术论文作家。" for _ in range(n_split)]
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
scroller_max_len = 80
|
scroller_max_len = 80
|
||||||
)
|
)
|
||||||
|
|
||||||
# <-------- 文本碎片重组为完整的tex文件,整理结果为压缩包 ---------->
|
# <-------- 文本碎片重组为完整的tex文件,整理结果为压缩包 ---------->
|
||||||
try:
|
try:
|
||||||
pfg.sp_file_result = []
|
pfg.sp_file_result = []
|
||||||
for i_say, gpt_say in zip(gpt_response_collection[0::2], gpt_response_collection[1::2]):
|
for i_say, gpt_say in zip(gpt_response_collection[0::2], gpt_response_collection[1::2]):
|
||||||
@@ -122,9 +122,9 @@ def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
pfg.write_result()
|
pfg.write_result()
|
||||||
pfg.zip_result()
|
pfg.zip_result()
|
||||||
except:
|
except:
|
||||||
logger.error(trimmed_format_exc())
|
print(trimmed_format_exc())
|
||||||
|
|
||||||
# <-------- 整理结果,退出 ---------->
|
# <-------- 整理结果,退出 ---------->
|
||||||
create_report_file_name = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + f"-chatgpt.polish.md"
|
create_report_file_name = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + f"-chatgpt.polish.md"
|
||||||
res = write_history_to_file(gpt_response_collection, file_basename=create_report_file_name)
|
res = write_history_to_file(gpt_response_collection, file_basename=create_report_file_name)
|
||||||
promote_file_to_downloadzone(res, chatbot=chatbot)
|
promote_file_to_downloadzone(res, chatbot=chatbot)
|
||||||
@@ -135,11 +135,11 @@ def 多文件润色(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
|
|
||||||
|
|
||||||
@CatchException
|
@CatchException
|
||||||
def Latex英文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def Latex英文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# 基本信息:功能、贡献者
|
# 基本信息:功能、贡献者
|
||||||
chatbot.append([
|
chatbot.append([
|
||||||
"函数插件功能?",
|
"函数插件功能?",
|
||||||
"对整个Latex项目进行润色。函数插件贡献者: Binary-Husky。(注意,此插件不调用Latex,如果有Latex环境,请使用「Latex英文纠错+高亮修正位置(需Latex)插件」"])
|
"对整个Latex项目进行润色。函数插件贡献者: Binary-Husky。(注意,此插件不调用Latex,如果有Latex环境,请使用“Latex英文纠错+高亮”插件)"])
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
# 尝试导入依赖,如果缺少依赖,则给出安装建议
|
# 尝试导入依赖,如果缺少依赖,则给出安装建议
|
||||||
@@ -173,7 +173,7 @@ def Latex英文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_p
|
|||||||
|
|
||||||
|
|
||||||
@CatchException
|
@CatchException
|
||||||
def Latex中文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def Latex中文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# 基本信息:功能、贡献者
|
# 基本信息:功能、贡献者
|
||||||
chatbot.append([
|
chatbot.append([
|
||||||
"函数插件功能?",
|
"函数插件功能?",
|
||||||
@@ -209,7 +209,7 @@ def Latex中文润色(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_p
|
|||||||
|
|
||||||
|
|
||||||
@CatchException
|
@CatchException
|
||||||
def Latex英文纠错(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def Latex英文纠错(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# 基本信息:功能、贡献者
|
# 基本信息:功能、贡献者
|
||||||
chatbot.append([
|
chatbot.append([
|
||||||
"函数插件功能?",
|
"函数插件功能?",
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from toolbox import update_ui, promote_file_to_downloadzone
|
from toolbox import update_ui, promote_file_to_downloadzone
|
||||||
from toolbox import CatchException, report_exception, write_history_to_file
|
from toolbox import CatchException, report_exception, write_history_to_file
|
||||||
from loguru import logger
|
fast_debug = False
|
||||||
|
|
||||||
class PaperFileGroup():
|
class PaperFileGroup():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -33,13 +33,13 @@ class PaperFileGroup():
|
|||||||
self.sp_file_index.append(index)
|
self.sp_file_index.append(index)
|
||||||
self.sp_file_tag.append(self.file_paths[index] + f".part-{j}.tex")
|
self.sp_file_tag.append(self.file_paths[index] + f".part-{j}.tex")
|
||||||
|
|
||||||
logger.info('Segmentation: done')
|
print('Segmentation: done')
|
||||||
|
|
||||||
def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, language='en'):
|
def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, language='en'):
|
||||||
import time, os, re
|
import time, os, re
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
from .crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||||
|
|
||||||
# <-------- 读取Latex文件,删除其中的所有注释 ---------->
|
# <-------- 读取Latex文件,删除其中的所有注释 ---------->
|
||||||
pfg = PaperFileGroup()
|
pfg = PaperFileGroup()
|
||||||
|
|
||||||
for index, fp in enumerate(file_manifest):
|
for index, fp in enumerate(file_manifest):
|
||||||
@@ -53,11 +53,11 @@ def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
pfg.file_paths.append(fp)
|
pfg.file_paths.append(fp)
|
||||||
pfg.file_contents.append(clean_tex_content)
|
pfg.file_contents.append(clean_tex_content)
|
||||||
|
|
||||||
# <-------- 拆分过长的latex文件 ---------->
|
# <-------- 拆分过长的latex文件 ---------->
|
||||||
pfg.run_file_split(max_token_limit=1024)
|
pfg.run_file_split(max_token_limit=1024)
|
||||||
n_split = len(pfg.sp_file_contents)
|
n_split = len(pfg.sp_file_contents)
|
||||||
|
|
||||||
# <-------- 抽取摘要 ---------->
|
# <-------- 抽取摘要 ---------->
|
||||||
# if language == 'en':
|
# if language == 'en':
|
||||||
# abs_extract_inputs = f"Please write an abstract for this paper"
|
# abs_extract_inputs = f"Please write an abstract for this paper"
|
||||||
|
|
||||||
@@ -70,14 +70,14 @@ def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
# sys_prompt="Your job is to collect information from materials。",
|
# sys_prompt="Your job is to collect information from materials。",
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# <-------- 多线程润色开始 ---------->
|
# <-------- 多线程润色开始 ---------->
|
||||||
if language == 'en->zh':
|
if language == 'en->zh':
|
||||||
inputs_array = ["Below is a section from an English academic paper, translate it into Chinese, do not modify any latex command such as \section, \cite and equations:" +
|
inputs_array = ["Below is a section from an English academic paper, translate it into Chinese, do not modify any latex command such as \section, \cite and equations:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
inputs_show_user_array = [f"翻译 {f}" for f in pfg.sp_file_tag]
|
inputs_show_user_array = [f"翻译 {f}" for f in pfg.sp_file_tag]
|
||||||
sys_prompt_array = ["You are a professional academic paper translator." for _ in range(n_split)]
|
sys_prompt_array = ["You are a professional academic paper translator." for _ in range(n_split)]
|
||||||
elif language == 'zh->en':
|
elif language == 'zh->en':
|
||||||
inputs_array = [f"Below is a section from a Chinese academic paper, translate it into English, do not modify any latex command such as \section, \cite and equations:" +
|
inputs_array = [f"Below is a section from a Chinese academic paper, translate it into English, do not modify any latex command such as \section, \cite and equations:" +
|
||||||
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
inputs_show_user_array = [f"翻译 {f}" for f in pfg.sp_file_tag]
|
inputs_show_user_array = [f"翻译 {f}" for f in pfg.sp_file_tag]
|
||||||
sys_prompt_array = ["You are a professional academic paper translator." for _ in range(n_split)]
|
sys_prompt_array = ["You are a professional academic paper translator." for _ in range(n_split)]
|
||||||
@@ -93,7 +93,7 @@ def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
scroller_max_len = 80
|
scroller_max_len = 80
|
||||||
)
|
)
|
||||||
|
|
||||||
# <-------- 整理结果,退出 ---------->
|
# <-------- 整理结果,退出 ---------->
|
||||||
create_report_file_name = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + f"-chatgpt.polish.md"
|
create_report_file_name = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + f"-chatgpt.polish.md"
|
||||||
res = write_history_to_file(gpt_response_collection, create_report_file_name)
|
res = write_history_to_file(gpt_response_collection, create_report_file_name)
|
||||||
promote_file_to_downloadzone(res, chatbot=chatbot)
|
promote_file_to_downloadzone(res, chatbot=chatbot)
|
||||||
@@ -106,7 +106,7 @@ def 多文件翻译(file_manifest, project_folder, llm_kwargs, plugin_kwargs, ch
|
|||||||
|
|
||||||
|
|
||||||
@CatchException
|
@CatchException
|
||||||
def Latex英译中(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def Latex英译中(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# 基本信息:功能、贡献者
|
# 基本信息:功能、贡献者
|
||||||
chatbot.append([
|
chatbot.append([
|
||||||
"函数插件功能?",
|
"函数插件功能?",
|
||||||
@@ -143,7 +143,7 @@ def Latex英译中(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prom
|
|||||||
|
|
||||||
|
|
||||||
@CatchException
|
@CatchException
|
||||||
def Latex中译英(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def Latex中译英(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# 基本信息:功能、贡献者
|
# 基本信息:功能、贡献者
|
||||||
chatbot.append([
|
chatbot.append([
|
||||||
"函数插件功能?",
|
"函数插件功能?",
|
||||||
306
crazy_functions/Latex输出PDF结果.py
Normal file
306
crazy_functions/Latex输出PDF结果.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
from toolbox import update_ui, trimmed_format_exc, get_conf, get_log_folder, promote_file_to_downloadzone
|
||||||
|
from toolbox import CatchException, report_exception, update_ui_lastest_msg, zip_result, gen_time_str
|
||||||
|
from functools import partial
|
||||||
|
import glob, os, requests, time
|
||||||
|
pj = os.path.join
|
||||||
|
ARXIV_CACHE_DIR = os.path.expanduser(f"~/arxiv_cache/")
|
||||||
|
|
||||||
|
# =================================== 工具函数 ===============================================
|
||||||
|
# 专业词汇声明 = 'If the term "agent" is used in this section, it should be translated to "智能体". '
|
||||||
|
def switch_prompt(pfg, mode, more_requirement):
|
||||||
|
"""
|
||||||
|
Generate prompts and system prompts based on the mode for proofreading or translating.
|
||||||
|
Args:
|
||||||
|
- pfg: Proofreader or Translator instance.
|
||||||
|
- mode: A string specifying the mode, either 'proofread' or 'translate_zh'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- inputs_array: A list of strings containing prompts for users to respond to.
|
||||||
|
- sys_prompt_array: A list of strings containing prompts for system prompts.
|
||||||
|
"""
|
||||||
|
n_split = len(pfg.sp_file_contents)
|
||||||
|
if mode == 'proofread_en':
|
||||||
|
inputs_array = [r"Below is a section from an academic paper, proofread this section." +
|
||||||
|
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " + more_requirement +
|
||||||
|
r"Answer me only with the revised text:" +
|
||||||
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
|
sys_prompt_array = ["You are a professional academic paper writer." for _ in range(n_split)]
|
||||||
|
elif mode == 'translate_zh':
|
||||||
|
inputs_array = [r"Below is a section from an English academic paper, translate it into Chinese. " + more_requirement +
|
||||||
|
r"Do not modify any latex command such as \section, \cite, \begin, \item and equations. " +
|
||||||
|
r"Answer me only with the translated text:" +
|
||||||
|
f"\n\n{frag}" for frag in pfg.sp_file_contents]
|
||||||
|
sys_prompt_array = ["You are a professional translator." for _ in range(n_split)]
|
||||||
|
else:
|
||||||
|
assert False, "未知指令"
|
||||||
|
return inputs_array, sys_prompt_array
|
||||||
|
|
||||||
|
def desend_to_extracted_folder_if_exist(project_folder):
|
||||||
|
"""
|
||||||
|
Descend into the extracted folder if it exists, otherwise return the original folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- project_folder: A string specifying the folder path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A string specifying the path to the extracted folder, or the original folder if there is no extracted folder.
|
||||||
|
"""
|
||||||
|
maybe_dir = [f for f in glob.glob(f'{project_folder}/*') if os.path.isdir(f)]
|
||||||
|
if len(maybe_dir) == 0: return project_folder
|
||||||
|
if maybe_dir[0].endswith('.extract'): return maybe_dir[0]
|
||||||
|
return project_folder
|
||||||
|
|
||||||
|
def move_project(project_folder, arxiv_id=None):
|
||||||
|
"""
|
||||||
|
Create a new work folder and copy the project folder to it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- project_folder: A string specifying the folder path of the project.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A string specifying the path to the new work folder.
|
||||||
|
"""
|
||||||
|
import shutil, time
|
||||||
|
time.sleep(2) # avoid time string conflict
|
||||||
|
if arxiv_id is not None:
|
||||||
|
new_workfolder = pj(ARXIV_CACHE_DIR, arxiv_id, 'workfolder')
|
||||||
|
else:
|
||||||
|
new_workfolder = f'{get_log_folder()}/{gen_time_str()}'
|
||||||
|
try:
|
||||||
|
shutil.rmtree(new_workfolder)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# align subfolder if there is a folder wrapper
|
||||||
|
items = glob.glob(pj(project_folder,'*'))
|
||||||
|
items = [item for item in items if os.path.basename(item)!='__MACOSX']
|
||||||
|
if len(glob.glob(pj(project_folder,'*.tex'))) == 0 and len(items) == 1:
|
||||||
|
if os.path.isdir(items[0]): project_folder = items[0]
|
||||||
|
|
||||||
|
shutil.copytree(src=project_folder, dst=new_workfolder)
|
||||||
|
return new_workfolder
|
||||||
|
|
||||||
|
def arxiv_download(chatbot, history, txt, allow_cache=True):
|
||||||
|
def check_cached_translation_pdf(arxiv_id):
|
||||||
|
translation_dir = pj(ARXIV_CACHE_DIR, arxiv_id, 'translation')
|
||||||
|
if not os.path.exists(translation_dir):
|
||||||
|
os.makedirs(translation_dir)
|
||||||
|
target_file = pj(translation_dir, 'translate_zh.pdf')
|
||||||
|
if os.path.exists(target_file):
|
||||||
|
promote_file_to_downloadzone(target_file, rename_file=None, chatbot=chatbot)
|
||||||
|
target_file_compare = pj(translation_dir, 'comparison.pdf')
|
||||||
|
if os.path.exists(target_file_compare):
|
||||||
|
promote_file_to_downloadzone(target_file_compare, rename_file=None, chatbot=chatbot)
|
||||||
|
return target_file
|
||||||
|
return False
|
||||||
|
def is_float(s):
|
||||||
|
try:
|
||||||
|
float(s)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if ('.' in txt) and ('/' not in txt) and is_float(txt): # is arxiv ID
|
||||||
|
txt = 'https://arxiv.org/abs/' + txt.strip()
|
||||||
|
if ('.' in txt) and ('/' not in txt) and is_float(txt[:10]): # is arxiv ID
|
||||||
|
txt = 'https://arxiv.org/abs/' + txt[:10]
|
||||||
|
if not txt.startswith('https://arxiv.org'):
|
||||||
|
return txt, None
|
||||||
|
|
||||||
|
# <-------------- inspect format ------------->
|
||||||
|
chatbot.append([f"检测到arxiv文档连接", '尝试下载 ...'])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history)
|
||||||
|
time.sleep(1) # 刷新界面
|
||||||
|
|
||||||
|
url_ = txt # https://arxiv.org/abs/1707.06690
|
||||||
|
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) # 刷新界面
|
||||||
|
return msg, None
|
||||||
|
# <-------------- set format ------------->
|
||||||
|
arxiv_id = url_.split('/abs/')[-1]
|
||||||
|
if 'v' in arxiv_id: arxiv_id = arxiv_id[:10]
|
||||||
|
cached_translation_pdf = check_cached_translation_pdf(arxiv_id)
|
||||||
|
if cached_translation_pdf and allow_cache: return cached_translation_pdf, arxiv_id
|
||||||
|
|
||||||
|
url_tar = url_.replace('/abs/', '/e-print/')
|
||||||
|
translation_dir = pj(ARXIV_CACHE_DIR, arxiv_id, 'e-print')
|
||||||
|
extract_dst = pj(ARXIV_CACHE_DIR, arxiv_id, 'extract')
|
||||||
|
os.makedirs(translation_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# <-------------- download arxiv source file ------------->
|
||||||
|
dst = pj(translation_dir, arxiv_id+'.tar')
|
||||||
|
if os.path.exists(dst):
|
||||||
|
yield from update_ui_lastest_msg("调用缓存", chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
else:
|
||||||
|
yield from update_ui_lastest_msg("开始下载", chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
proxies = get_conf('proxies')
|
||||||
|
r = requests.get(url_tar, proxies=proxies)
|
||||||
|
with open(dst, 'wb+') as f:
|
||||||
|
f.write(r.content)
|
||||||
|
# <-------------- extract file ------------->
|
||||||
|
yield from update_ui_lastest_msg("下载完成", chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
from toolbox import extract_archive
|
||||||
|
extract_archive(file_path=dst, dest_dir=extract_dst)
|
||||||
|
return extract_dst, arxiv_id
|
||||||
|
# ========================================= 插件主程序1 =====================================================
|
||||||
|
|
||||||
|
|
||||||
|
@CatchException
|
||||||
|
def Latex英文纠错加PDF对比(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
|
# <-------------- information about this plugin ------------->
|
||||||
|
chatbot.append([ "函数插件功能?",
|
||||||
|
"对整个Latex项目进行纠错, 用latex编译为PDF对修正处做高亮。函数插件贡献者: Binary-Husky。注意事项: 目前仅支持GPT3.5/GPT4,其他模型转化效果未知。目前对机器学习类文献转化效果最好,其他类型文献转化效果未知。仅在Windows系统进行了测试,其他操作系统表现未知。"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
|
# <-------------- more requirements ------------->
|
||||||
|
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
||||||
|
more_req = plugin_kwargs.get("advanced_arg", "")
|
||||||
|
_switch_prompt_ = partial(switch_prompt, more_requirement=more_req)
|
||||||
|
|
||||||
|
# <-------------- check deps ------------->
|
||||||
|
try:
|
||||||
|
import glob, os, time, subprocess
|
||||||
|
subprocess.Popen(['pdflatex', '-version'])
|
||||||
|
from .latex_fns.latex_actions import Latex精细分解与转化, 编译Latex
|
||||||
|
except Exception as e:
|
||||||
|
chatbot.append([ f"解析项目: {txt}",
|
||||||
|
f"尝试执行Latex指令失败。Latex没有安装, 或者不在环境变量PATH中。安装方法https://tug.org/texlive/。报错信息\n\n```\n\n{trimmed_format_exc()}\n\n```\n\n"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- clear history and read input ------------->
|
||||||
|
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
|
||||||
|
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}")
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- if is a zip/tar file ------------->
|
||||||
|
project_folder = desend_to_extracted_folder_if_exist(project_folder)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- move latex project away from temp folder ------------->
|
||||||
|
project_folder = move_project(project_folder, arxiv_id=None)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- if merge_translate_zh is already generated, skip gpt req ------------->
|
||||||
|
if not os.path.exists(project_folder + '/merge_proofread_en.tex'):
|
||||||
|
yield from Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin_kwargs,
|
||||||
|
chatbot, history, system_prompt, mode='proofread_en', switch_prompt=_switch_prompt_)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- compile PDF ------------->
|
||||||
|
success = yield from 编译Latex(chatbot, history, main_file_original='merge', main_file_modified='merge_proofread_en',
|
||||||
|
work_folder_original=project_folder, work_folder_modified=project_folder, work_folder=project_folder)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- zip PDF ------------->
|
||||||
|
zip_res = zip_result(project_folder)
|
||||||
|
if success:
|
||||||
|
chatbot.append((f"成功啦", '请查收结果(压缩包)...'))
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history); time.sleep(1) # 刷新界面
|
||||||
|
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
||||||
|
else:
|
||||||
|
chatbot.append((f"失败了", '虽然PDF生成失败了, 但请查收结果(压缩包), 内含已经翻译的Tex文档, 也是可读的, 您可以到Github Issue区, 用该压缩包+对话历史存档进行反馈 ...'))
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history); time.sleep(1) # 刷新界面
|
||||||
|
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
||||||
|
|
||||||
|
# <-------------- we are done ------------->
|
||||||
|
return success
|
||||||
|
|
||||||
|
# ========================================= 插件主程序2 =====================================================
|
||||||
|
|
||||||
|
@CatchException
|
||||||
|
def Latex翻译中文并重新编译PDF(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
|
# <-------------- information about this plugin ------------->
|
||||||
|
chatbot.append([
|
||||||
|
"函数插件功能?",
|
||||||
|
"对整个Latex项目进行翻译, 生成中文PDF。函数插件贡献者: Binary-Husky。注意事项: 此插件Windows支持最佳,Linux下必须使用Docker安装,详见项目主README.md。目前仅支持GPT3.5/GPT4,其他模型转化效果未知。目前对机器学习类文献转化效果最好,其他类型文献转化效果未知。"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
|
# <-------------- more requirements ------------->
|
||||||
|
if ("advanced_arg" in plugin_kwargs) and (plugin_kwargs["advanced_arg"] == ""): plugin_kwargs.pop("advanced_arg")
|
||||||
|
more_req = plugin_kwargs.get("advanced_arg", "")
|
||||||
|
no_cache = more_req.startswith("--no-cache")
|
||||||
|
if no_cache: more_req.lstrip("--no-cache")
|
||||||
|
allow_cache = not no_cache
|
||||||
|
_switch_prompt_ = partial(switch_prompt, more_requirement=more_req)
|
||||||
|
|
||||||
|
# <-------------- check deps ------------->
|
||||||
|
try:
|
||||||
|
import glob, os, time, subprocess
|
||||||
|
subprocess.Popen(['pdflatex', '-version'])
|
||||||
|
from .latex_fns.latex_actions import Latex精细分解与转化, 编译Latex
|
||||||
|
except Exception as e:
|
||||||
|
chatbot.append([ f"解析项目: {txt}",
|
||||||
|
f"尝试执行Latex指令失败。Latex没有安装, 或者不在环境变量PATH中。安装方法https://tug.org/texlive/。报错信息\n\n```\n\n{trimmed_format_exc()}\n\n```\n\n"])
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- clear history and read input ------------->
|
||||||
|
history = []
|
||||||
|
txt, arxiv_id = yield from arxiv_download(chatbot, history, txt, allow_cache)
|
||||||
|
if txt.endswith('.pdf'):
|
||||||
|
report_exception(chatbot, history, a = f"解析项目: {txt}", b = f"发现已经存在翻译好的PDF文档")
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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}")
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- if is a zip/tar file ------------->
|
||||||
|
project_folder = desend_to_extracted_folder_if_exist(project_folder)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- move latex project away from temp folder ------------->
|
||||||
|
project_folder = move_project(project_folder, arxiv_id)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- if merge_translate_zh is already generated, skip gpt req ------------->
|
||||||
|
if not os.path.exists(project_folder + '/merge_translate_zh.tex'):
|
||||||
|
yield from Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin_kwargs,
|
||||||
|
chatbot, history, system_prompt, mode='translate_zh', switch_prompt=_switch_prompt_)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- compile PDF ------------->
|
||||||
|
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, work_folder=project_folder)
|
||||||
|
|
||||||
|
# <-------------- zip PDF ------------->
|
||||||
|
zip_res = zip_result(project_folder)
|
||||||
|
if success:
|
||||||
|
chatbot.append((f"成功啦", '请查收结果(压缩包)...'))
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history); time.sleep(1) # 刷新界面
|
||||||
|
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
||||||
|
else:
|
||||||
|
chatbot.append((f"失败了", '虽然PDF生成失败了, 但请查收结果(压缩包), 内含已经翻译的Tex文档, 您可以到Github Issue区, 用该压缩包进行反馈。如系统是Linux,请检查系统字体(见Github wiki) ...'))
|
||||||
|
yield from update_ui(chatbot=chatbot, history=history); time.sleep(1) # 刷新界面
|
||||||
|
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------------- we are done ------------->
|
||||||
|
return success
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
from toolbox import CatchException, update_ui, report_exception
|
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import (
|
|
||||||
GptAcademicPluginTemplate,
|
|
||||||
)
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import ArgProperty
|
|
||||||
|
|
||||||
# 以下是每类图表的PROMPT
|
|
||||||
SELECT_PROMPT = """
|
|
||||||
“{subject}”
|
|
||||||
=============
|
|
||||||
以上是从文章中提取的摘要,将会使用这些摘要绘制图表。请你选择一个合适的图表类型:
|
|
||||||
1 流程图
|
|
||||||
2 序列图
|
|
||||||
3 类图
|
|
||||||
4 饼图
|
|
||||||
5 甘特图
|
|
||||||
6 状态图
|
|
||||||
7 实体关系图
|
|
||||||
8 象限提示图
|
|
||||||
不需要解释原因,仅需要输出单个不带任何标点符号的数字。
|
|
||||||
"""
|
|
||||||
# 没有思维导图!!!测试发现模型始终会优先选择思维导图
|
|
||||||
# 流程图
|
|
||||||
PROMPT_1 = """
|
|
||||||
请你给出围绕“{subject}”的逻辑关系图,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
P("编程") --> L1("Python")
|
|
||||||
P("编程") --> L2("C")
|
|
||||||
P("编程") --> L3("C++")
|
|
||||||
P("编程") --> L4("Javascipt")
|
|
||||||
P("编程") --> L5("PHP")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 序列图
|
|
||||||
PROMPT_2 = """
|
|
||||||
请你给出围绕“{subject}”的序列图,使用mermaid语法。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant A as 用户
|
|
||||||
participant B as 系统
|
|
||||||
A->>B: 登录请求
|
|
||||||
B->>A: 登录成功
|
|
||||||
A->>B: 获取数据
|
|
||||||
B->>A: 返回数据
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 类图
|
|
||||||
PROMPT_3 = """
|
|
||||||
请你给出围绕“{subject}”的类图,使用mermaid语法。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
Class01 <|-- AveryLongClass : Cool
|
|
||||||
Class03 *-- Class04
|
|
||||||
Class05 o-- Class06
|
|
||||||
Class07 .. Class08
|
|
||||||
Class09 --> C2 : Where am i?
|
|
||||||
Class09 --* C3
|
|
||||||
Class09 --|> Class07
|
|
||||||
Class07 : equals()
|
|
||||||
Class07 : Object[] elementData
|
|
||||||
Class01 : size()
|
|
||||||
Class01 : int chimp
|
|
||||||
Class01 : int gorilla
|
|
||||||
Class08 <--> C2: Cool label
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 饼图
|
|
||||||
PROMPT_4 = """
|
|
||||||
请你给出围绕“{subject}”的饼图,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
pie title Pets adopted by volunteers
|
|
||||||
"狗" : 386
|
|
||||||
"猫" : 85
|
|
||||||
"兔子" : 15
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 甘特图
|
|
||||||
PROMPT_5 = """
|
|
||||||
请你给出围绕“{subject}”的甘特图,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
gantt
|
|
||||||
title "项目开发流程"
|
|
||||||
dateFormat YYYY-MM-DD
|
|
||||||
section "设计"
|
|
||||||
"需求分析" :done, des1, 2024-01-06,2024-01-08
|
|
||||||
"原型设计" :active, des2, 2024-01-09, 3d
|
|
||||||
"UI设计" : des3, after des2, 5d
|
|
||||||
section "开发"
|
|
||||||
"前端开发" :2024-01-20, 10d
|
|
||||||
"后端开发" :2024-01-20, 10d
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 状态图
|
|
||||||
PROMPT_6 = """
|
|
||||||
请你给出围绕“{subject}”的状态图,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
stateDiagram-v2
|
|
||||||
[*] --> "Still"
|
|
||||||
"Still" --> [*]
|
|
||||||
"Still" --> "Moving"
|
|
||||||
"Moving" --> "Still"
|
|
||||||
"Moving" --> "Crash"
|
|
||||||
"Crash" --> [*]
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 实体关系图
|
|
||||||
PROMPT_7 = """
|
|
||||||
请你给出围绕“{subject}”的实体关系图,使用mermaid语法。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
CUSTOMER ||--o{ ORDER : places
|
|
||||||
ORDER ||--|{ LINE-ITEM : contains
|
|
||||||
CUSTOMER {
|
|
||||||
string name
|
|
||||||
string id
|
|
||||||
}
|
|
||||||
ORDER {
|
|
||||||
string orderNumber
|
|
||||||
date orderDate
|
|
||||||
string customerID
|
|
||||||
}
|
|
||||||
LINE-ITEM {
|
|
||||||
number quantity
|
|
||||||
string productID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 象限提示图
|
|
||||||
PROMPT_8 = """
|
|
||||||
请你给出围绕“{subject}”的象限图,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A["Hard skill"] --> B("Programming")
|
|
||||||
A["Hard skill"] --> C("Design")
|
|
||||||
D["Soft skill"] --> E("Coordination")
|
|
||||||
D["Soft skill"] --> F("Communication")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
# 思维导图
|
|
||||||
PROMPT_9 = """
|
|
||||||
{subject}
|
|
||||||
==========
|
|
||||||
请给出上方内容的思维导图,充分考虑其之间的逻辑,使用mermaid语法,注意需要使用双引号将内容括起来。
|
|
||||||
mermaid语法举例:
|
|
||||||
```mermaid
|
|
||||||
mindmap
|
|
||||||
root((mindmap))
|
|
||||||
("Origins")
|
|
||||||
("Long history")
|
|
||||||
::icon(fa fa-book)
|
|
||||||
("Popularisation")
|
|
||||||
("British popular psychology author Tony Buzan")
|
|
||||||
::icon(fa fa-user)
|
|
||||||
("Research")
|
|
||||||
("On effectiveness<br/>and features")
|
|
||||||
::icon(fa fa-search)
|
|
||||||
("On Automatic creation")
|
|
||||||
::icon(fa fa-robot)
|
|
||||||
("Uses")
|
|
||||||
("Creative techniques")
|
|
||||||
::icon(fa fa-lightbulb-o)
|
|
||||||
("Strategic planning")
|
|
||||||
::icon(fa fa-flag)
|
|
||||||
("Argument mapping")
|
|
||||||
::icon(fa fa-comments)
|
|
||||||
("Tools")
|
|
||||||
("Pen and paper")
|
|
||||||
::icon(fa fa-pencil)
|
|
||||||
("Mermaid")
|
|
||||||
::icon(fa fa-code)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def 解析历史输入(history, llm_kwargs, file_manifest, chatbot, plugin_kwargs):
|
|
||||||
############################## <第 0 步,切割输入> ##################################
|
|
||||||
# 借用PDF切割中的函数对文本进行切割
|
|
||||||
TOKEN_LIMIT_PER_FRAGMENT = 2500
|
|
||||||
txt = (
|
|
||||||
str(history).encode("utf-8", "ignore").decode()
|
|
||||||
) # avoid reading non-utf8 chars
|
|
||||||
from crazy_functions.pdf_fns.breakdown_txt import (
|
|
||||||
breakdown_text_to_satisfy_token_limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
txt = breakdown_text_to_satisfy_token_limit(
|
|
||||||
txt=txt, limit=TOKEN_LIMIT_PER_FRAGMENT, llm_model=llm_kwargs["llm_model"]
|
|
||||||
)
|
|
||||||
############################## <第 1 步,迭代地历遍整个文章,提取精炼信息> ##################################
|
|
||||||
results = []
|
|
||||||
MAX_WORD_TOTAL = 4096
|
|
||||||
n_txt = len(txt)
|
|
||||||
last_iteration_result = "从以下文本中提取摘要。"
|
|
||||||
|
|
||||||
for i in range(n_txt):
|
|
||||||
NUM_OF_WORD = MAX_WORD_TOTAL // n_txt
|
|
||||||
i_say = f"Read this section, recapitulate the content of this section with less than {NUM_OF_WORD} words in Chinese: {txt[i]}"
|
|
||||||
i_say_show_user = f"[{i+1}/{n_txt}] Read this section, recapitulate the content of this section with less than {NUM_OF_WORD} words: {txt[i][:200]} ...."
|
|
||||||
gpt_say = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
|
||||||
i_say,
|
|
||||||
i_say_show_user, # i_say=真正给chatgpt的提问, i_say_show_user=给用户看的提问
|
|
||||||
llm_kwargs,
|
|
||||||
chatbot,
|
|
||||||
history=[
|
|
||||||
"The main content of the previous section is?",
|
|
||||||
last_iteration_result,
|
|
||||||
], # 迭代上一次的结果
|
|
||||||
sys_prompt="Extracts the main content from the text section where it is located for graphing purposes, answer me with Chinese.", # 提示
|
|
||||||
)
|
|
||||||
results.append(gpt_say)
|
|
||||||
last_iteration_result = gpt_say
|
|
||||||
############################## <第 2 步,根据整理的摘要选择图表类型> ##################################
|
|
||||||
gpt_say = str(plugin_kwargs) # 将图表类型参数赋值为插件参数
|
|
||||||
results_txt = "\n".join(results) # 合并摘要
|
|
||||||
if gpt_say not in [
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5",
|
|
||||||
"6",
|
|
||||||
"7",
|
|
||||||
"8",
|
|
||||||
"9",
|
|
||||||
]: # 如插件参数不正确则使用对话模型判断
|
|
||||||
i_say_show_user = (
|
|
||||||
f"接下来将判断适合的图表类型,如连续3次判断失败将会使用流程图进行绘制"
|
|
||||||
)
|
|
||||||
gpt_say = "[Local Message] 收到。" # 用户提示
|
|
||||||
chatbot.append([i_say_show_user, gpt_say])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=[]) # 更新UI
|
|
||||||
i_say = SELECT_PROMPT.format(subject=results_txt)
|
|
||||||
i_say_show_user = f'请判断适合使用的流程图类型,其中数字对应关系为:1-流程图,2-序列图,3-类图,4-饼图,5-甘特图,6-状态图,7-实体关系图,8-象限提示图。由于不管提供文本是什么,模型大概率认为"思维导图"最合适,因此思维导图仅能通过参数调用。'
|
|
||||||
for i in range(3):
|
|
||||||
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="",
|
|
||||||
)
|
|
||||||
if gpt_say in [
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5",
|
|
||||||
"6",
|
|
||||||
"7",
|
|
||||||
"8",
|
|
||||||
"9",
|
|
||||||
]: # 判断返回是否正确
|
|
||||||
break
|
|
||||||
if gpt_say not in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]:
|
|
||||||
gpt_say = "1"
|
|
||||||
############################## <第 3 步,根据选择的图表类型绘制图表> ##################################
|
|
||||||
if gpt_say == "1":
|
|
||||||
i_say = PROMPT_1.format(subject=results_txt)
|
|
||||||
elif gpt_say == "2":
|
|
||||||
i_say = PROMPT_2.format(subject=results_txt)
|
|
||||||
elif gpt_say == "3":
|
|
||||||
i_say = PROMPT_3.format(subject=results_txt)
|
|
||||||
elif gpt_say == "4":
|
|
||||||
i_say = PROMPT_4.format(subject=results_txt)
|
|
||||||
elif gpt_say == "5":
|
|
||||||
i_say = PROMPT_5.format(subject=results_txt)
|
|
||||||
elif gpt_say == "6":
|
|
||||||
i_say = PROMPT_6.format(subject=results_txt)
|
|
||||||
elif gpt_say == "7":
|
|
||||||
i_say = PROMPT_7.replace("{subject}", results_txt) # 由于实体关系图用到了{}符号
|
|
||||||
elif gpt_say == "8":
|
|
||||||
i_say = PROMPT_8.format(subject=results_txt)
|
|
||||||
elif gpt_say == "9":
|
|
||||||
i_say = PROMPT_9.format(subject=results_txt)
|
|
||||||
i_say_show_user = f"请根据判断结果绘制相应的图表。如需绘制思维导图请使用参数调用,同时过大的图表可能需要复制到在线编辑器中进行渲染。"
|
|
||||||
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="",
|
|
||||||
)
|
|
||||||
history.append(gpt_say)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # 界面更新
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def Mermaid_Figure_Gen(
|
|
||||||
txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
|
||||||
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
|
||||||
plugin_kwargs 插件模型的参数,用于灵活调整复杂功能的各种参数
|
|
||||||
chatbot 聊天显示框的句柄,用于显示给用户
|
|
||||||
history 聊天历史,前情提要
|
|
||||||
system_prompt 给gpt的静默提醒
|
|
||||||
web_port 当前软件运行的端口号
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
|
|
||||||
# 基本信息:功能、贡献者
|
|
||||||
chatbot.append(
|
|
||||||
[
|
|
||||||
"函数插件功能?",
|
|
||||||
"根据当前聊天历史或指定的路径文件(文件内容优先)绘制多种mermaid图表,将会由对话模型首先判断适合的图表类型,随后绘制图表。\
|
|
||||||
\n您也可以使用插件参数指定绘制的图表类型,函数插件贡献者: Menghuan1918",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
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, exception = (
|
|
||||||
extract_text_from_files(txt, chatbot, history)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
file_exist = False
|
|
||||||
exception = ""
|
|
||||||
file_manifest = []
|
|
||||||
|
|
||||||
if exception != "":
|
|
||||||
if exception == "word":
|
|
||||||
report_exception(
|
|
||||||
chatbot,
|
|
||||||
history,
|
|
||||||
a=f"解析项目: {txt}",
|
|
||||||
b=f"找到了.doc文件,但是该文件格式不被支持,请先转化为.docx格式。",
|
|
||||||
)
|
|
||||||
|
|
||||||
elif exception == "pdf":
|
|
||||||
report_exception(
|
|
||||||
chatbot,
|
|
||||||
history,
|
|
||||||
a=f"解析项目: {txt}",
|
|
||||||
b=f"导入软件依赖失败。使用该模块需要额外依赖,安装方法```pip install --upgrade pymupdf```。",
|
|
||||||
)
|
|
||||||
|
|
||||||
elif exception == "word_pip":
|
|
||||||
report_exception(
|
|
||||||
chatbot,
|
|
||||||
history,
|
|
||||||
a=f"解析项目: {txt}",
|
|
||||||
b=f"导入软件依赖失败。使用该模块需要额外依赖,安装方法```pip install --upgrade python-docx pywin32```。",
|
|
||||||
)
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
else:
|
|
||||||
if not file_exist:
|
|
||||||
history.append(txt) # 如输入区不是文件则将输入区内容加入历史记录
|
|
||||||
i_say_show_user = f"首先你从历史记录中提取摘要。"
|
|
||||||
gpt_say = "[Local Message] 收到。" # 用户提示
|
|
||||||
chatbot.append([i_say_show_user, gpt_say])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 更新UI
|
|
||||||
yield from 解析历史输入(
|
|
||||||
history, llm_kwargs, file_manifest, chatbot, plugin_kwargs
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
file_num = len(file_manifest)
|
|
||||||
for i in range(file_num): # 依次处理文件
|
|
||||||
i_say_show_user = f"[{i+1}/{file_num}]处理文件{file_manifest[i]}"
|
|
||||||
gpt_say = "[Local Message] 收到。" # 用户提示
|
|
||||||
chatbot.append([i_say_show_user, gpt_say])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 更新UI
|
|
||||||
history = [] # 如输入区内容为文件则清空历史记录
|
|
||||||
history.append(final_result[i])
|
|
||||||
yield from 解析历史输入(
|
|
||||||
history, llm_kwargs, file_manifest, chatbot, plugin_kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Mermaid_Gen(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
gui_definition = {
|
|
||||||
"Type_of_Mermaid": ArgProperty(
|
|
||||||
title="绘制的Mermaid图表类型",
|
|
||||||
options=[
|
|
||||||
"由LLM决定",
|
|
||||||
"流程图",
|
|
||||||
"序列图",
|
|
||||||
"类图",
|
|
||||||
"饼图",
|
|
||||||
"甘特图",
|
|
||||||
"状态图",
|
|
||||||
"实体关系图",
|
|
||||||
"象限提示图",
|
|
||||||
"思维导图",
|
|
||||||
],
|
|
||||||
default_value="由LLM决定",
|
|
||||||
description="选择'由LLM决定'时将由对话模型判断适合的图表类型(不包括思维导图),选择其他类型时将直接绘制指定的图表类型。",
|
|
||||||
type="dropdown",
|
|
||||||
).model_dump_json(),
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(
|
|
||||||
txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request
|
|
||||||
):
|
|
||||||
options = [
|
|
||||||
"由LLM决定",
|
|
||||||
"流程图",
|
|
||||||
"序列图",
|
|
||||||
"类图",
|
|
||||||
"饼图",
|
|
||||||
"甘特图",
|
|
||||||
"状态图",
|
|
||||||
"实体关系图",
|
|
||||||
"象限提示图",
|
|
||||||
"思维导图",
|
|
||||||
]
|
|
||||||
plugin_kwargs = options.index(plugin_kwargs['Type_of_Mermaid'])
|
|
||||||
yield from Mermaid_Figure_Gen(
|
|
||||||
txt,
|
|
||||||
llm_kwargs,
|
|
||||||
plugin_kwargs,
|
|
||||||
chatbot,
|
|
||||||
history,
|
|
||||||
system_prompt,
|
|
||||||
user_request,
|
|
||||||
)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from toolbox import CatchException, check_packages, get_conf
|
|
||||||
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
|
|
||||||
from crazy_functions.pdf_fns.parse_pdf_via_doc2x import 解析PDF_基于DOC2X
|
|
||||||
from crazy_functions.pdf_fns.parse_pdf_legacy import 解析PDF_简单拆解
|
|
||||||
from crazy_functions.pdf_fns.parse_pdf_grobid import 解析PDF_基于GROBID
|
|
||||||
from shared_utils.colorful import *
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def 批量翻译PDF文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
|
|
||||||
disable_auto_promotion(chatbot)
|
|
||||||
# 基本信息:功能、贡献者
|
|
||||||
chatbot.append([None, "插件功能:批量翻译PDF文档。函数插件贡献者: Binary-Husky"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# 尝试导入依赖,如果缺少依赖,则给出安装建议
|
|
||||||
try:
|
|
||||||
check_packages(["fitz", "tiktoken", "scipdf"])
|
|
||||||
except:
|
|
||||||
chatbot.append([None, f"导入软件依赖失败。使用该模块需要额外依赖,安装方法```pip install --upgrade pymupdf tiktoken scipdf_parser```。"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# 清空历史,以免输入溢出
|
|
||||||
history = []
|
|
||||||
success, file_manifest, project_folder = get_files_from_everything(txt, type='.pdf')
|
|
||||||
|
|
||||||
# 检测输入参数,如没有给定输入参数,直接退出
|
|
||||||
if (not success) and txt == "": txt = '空空如也的输入栏。提示:请先上传文件(把PDF文件拖入对话)。'
|
|
||||||
|
|
||||||
# 如果没找到任何文件
|
|
||||||
if len(file_manifest) == 0:
|
|
||||||
chatbot.append([None, f"找不到任何.pdf拓展名的文件: {txt}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# 开始正式执行任务
|
|
||||||
method = plugin_kwargs.get("pdf_parse_method", None)
|
|
||||||
if method == "DOC2X":
|
|
||||||
# ------- 第一种方法,效果最好,但是需要DOC2X服务 -------
|
|
||||||
DOC2X_API_KEY = get_conf("DOC2X_API_KEY")
|
|
||||||
if len(DOC2X_API_KEY) != 0:
|
|
||||||
try:
|
|
||||||
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()}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
if method == "GROBID":
|
|
||||||
# ------- 第二种方法,效果次优 -------
|
|
||||||
grobid_url = get_avail_grobid_url()
|
|
||||||
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
|
|
||||||
|
|
||||||
if method == "Classic":
|
|
||||||
# ------- 第三种方法,早期代码,效果不理想 -------
|
|
||||||
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
|
|
||||||
|
|
||||||
if method is None:
|
|
||||||
# ------- 以上三种方法都试一遍 -------
|
|
||||||
DOC2X_API_KEY = get_conf("DOC2X_API_KEY")
|
|
||||||
if len(DOC2X_API_KEY) != 0:
|
|
||||||
try:
|
|
||||||
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服务不可用,正在尝试GROBID。{trimmed_format_exc_markdown()}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
grobid_url = get_avail_grobid_url()
|
|
||||||
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_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
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
from .PDF_Translate import 批量翻译PDF文档
|
|
||||||
|
|
||||||
|
|
||||||
class PDF_Tran(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="PDF文件路径", description="未指定路径,请上传文件后,再点击该插件", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
"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(),
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
main_input = plugin_kwargs["main_input"]
|
|
||||||
additional_prompt = plugin_kwargs["additional_prompt"]
|
|
||||||
pdf_parse_method = plugin_kwargs["pdf_parse_method"]
|
|
||||||
yield from 批量翻译PDF文档(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import glob
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Dict, List, Generator, Tuple
|
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
|
||||||
from toolbox import update_ui, promote_file_to_downloadzone, write_history_to_file, CatchException, report_exception
|
|
||||||
from shared_utils.fastapi_server import validate_path_safety
|
|
||||||
from crazy_functions.paper_fns.paper_download import extract_paper_id, extract_paper_ids, get_arxiv_paper, format_arxiv_id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PaperQuestion:
|
|
||||||
"""论文分析问题类"""
|
|
||||||
id: str # 问题ID
|
|
||||||
question: str # 问题内容
|
|
||||||
importance: int # 重要性 (1-5,5最高)
|
|
||||||
description: str # 问题描述
|
|
||||||
|
|
||||||
|
|
||||||
class PaperAnalyzer:
|
|
||||||
"""论文快速分析器"""
|
|
||||||
|
|
||||||
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.paper_content = ""
|
|
||||||
self.results = {}
|
|
||||||
|
|
||||||
# 定义论文分析问题库(已合并为4个核心问题)
|
|
||||||
self.questions = [
|
|
||||||
PaperQuestion(
|
|
||||||
id="research_and_methods",
|
|
||||||
question="这篇论文的主要研究问题、目标和方法是什么?请分析:1)论文的核心研究问题和研究动机;2)论文提出的关键方法、模型或理论框架;3)这些方法如何解决研究问题。",
|
|
||||||
importance=5,
|
|
||||||
description="研究问题与方法"
|
|
||||||
),
|
|
||||||
PaperQuestion(
|
|
||||||
id="findings_and_innovation",
|
|
||||||
question="论文的主要发现、结论及创新点是什么?请分析:1)论文的核心结果与主要发现;2)作者得出的关键结论;3)研究的创新点与对领域的贡献;4)与已有工作的区别。",
|
|
||||||
importance=4,
|
|
||||||
description="研究发现与创新"
|
|
||||||
),
|
|
||||||
PaperQuestion(
|
|
||||||
id="methodology_and_data",
|
|
||||||
question="论文使用了什么研究方法和数据?请详细分析:1)研究设计与实验设置;2)数据收集方法与数据集特点;3)分析技术与评估方法;4)方法学上的合理性。",
|
|
||||||
importance=3,
|
|
||||||
description="研究方法与数据"
|
|
||||||
),
|
|
||||||
PaperQuestion(
|
|
||||||
id="limitations_and_impact",
|
|
||||||
question="论文的局限性、未来方向及潜在影响是什么?请分析:1)研究的不足与限制因素;2)作者提出的未来研究方向;3)该研究对学术界和行业可能产生的影响;4)研究结果的适用范围与推广价值。",
|
|
||||||
importance=2,
|
|
||||||
description="局限性与影响"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# 按重要性排序
|
|
||||||
self.questions.sort(key=lambda q: q.importance, reverse=True)
|
|
||||||
|
|
||||||
def _load_paper(self, paper_path: str) -> Generator:
|
|
||||||
from crazy_functions.doc_fns.text_content_loader import TextContentLoader
|
|
||||||
"""加载论文内容"""
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 使用TextContentLoader读取文件
|
|
||||||
loader = TextContentLoader(self.chatbot, self.history)
|
|
||||||
|
|
||||||
yield from loader.execute_single_file(paper_path)
|
|
||||||
|
|
||||||
# 获取加载的内容
|
|
||||||
if len(self.history) >= 2 and self.history[-2]:
|
|
||||||
self.paper_content = self.history[-2]
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.chatbot.append(["错误", "无法读取论文内容,请检查文件是否有效"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _analyze_question(self, question: PaperQuestion) -> Generator:
|
|
||||||
"""分析单个问题 - 直接显示问题和答案"""
|
|
||||||
try:
|
|
||||||
# 创建分析提示
|
|
||||||
prompt = f"请基于以下论文内容回答问题:\n\n{self.paper_content}\n\n问题:{question.question}"
|
|
||||||
|
|
||||||
# 使用单线程版本的请求函数
|
|
||||||
response = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
|
||||||
inputs=prompt,
|
|
||||||
inputs_show_user=question.question, # 显示问题本身
|
|
||||||
llm_kwargs=self.llm_kwargs,
|
|
||||||
chatbot=self.chatbot,
|
|
||||||
history=[], # 空历史,确保每个问题独立分析
|
|
||||||
sys_prompt="你是一个专业的科研论文分析助手,需要仔细阅读论文内容并回答问题。请保持客观、准确,并基于论文内容提供深入分析。"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
self.results[question.id] = response
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["错误", f"分析问题时出错: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _generate_summary(self) -> Generator:
|
|
||||||
"""生成最终总结报告"""
|
|
||||||
self.chatbot.append(["生成报告", "正在整合分析结果,生成最终报告..."])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
summary_prompt = "请基于以下对论文的各个方面的分析,生成一份全面的论文解读报告。报告应该简明扼要地呈现论文的关键内容,并保持逻辑连贯性。"
|
|
||||||
|
|
||||||
for q in self.questions:
|
|
||||||
if q.id in self.results:
|
|
||||||
summary_prompt += f"\n\n关于{q.description}的分析:\n{self.results[q.id]}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 使用单线程版本的请求函数,可以在前端实时显示生成结果
|
|
||||||
response = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
|
||||||
inputs=summary_prompt,
|
|
||||||
inputs_show_user="生成论文解读报告",
|
|
||||||
llm_kwargs=self.llm_kwargs,
|
|
||||||
chatbot=self.chatbot,
|
|
||||||
history=[],
|
|
||||||
sys_prompt="你是一个科研论文解读专家,请将多个方面的分析整合为一份完整、连贯、有条理的报告。报告应当重点突出,层次分明,并且保持学术性和客观性。"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
return "报告生成失败"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["错误", f"生成报告时出错: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return "报告生成失败: " + str(e)
|
|
||||||
|
|
||||||
def save_report(self, report: str) -> Generator:
|
|
||||||
"""保存分析报告"""
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
|
|
||||||
# 保存为Markdown文件
|
|
||||||
try:
|
|
||||||
md_content = f"# 论文快速解读报告\n\n{report}"
|
|
||||||
for q in self.questions:
|
|
||||||
if q.id in self.results:
|
|
||||||
md_content += f"\n\n## {q.description}\n\n{self.results[q.id]}"
|
|
||||||
|
|
||||||
result_file = write_history_to_file(
|
|
||||||
history=[md_content],
|
|
||||||
file_basename=f"论文解读_{timestamp}.md"
|
|
||||||
)
|
|
||||||
|
|
||||||
if result_file and os.path.exists(result_file):
|
|
||||||
promote_file_to_downloadzone(result_file, chatbot=self.chatbot)
|
|
||||||
self.chatbot.append(["保存成功", f"解读报告已保存至: {os.path.basename(result_file)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
else:
|
|
||||||
self.chatbot.append(["警告", "保存报告成功但找不到文件"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.append(["警告", f"保存报告失败: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
def analyze_paper(self, paper_path: str) -> Generator:
|
|
||||||
"""分析论文主流程"""
|
|
||||||
# 加载论文
|
|
||||||
success = yield from self._load_paper(paper_path)
|
|
||||||
if not success:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 分析关键问题 - 直接询问每个问题,不显示进度信息
|
|
||||||
for question in self.questions:
|
|
||||||
yield from self._analyze_question(question)
|
|
||||||
|
|
||||||
# 生成总结报告
|
|
||||||
final_report = yield from self._generate_summary()
|
|
||||||
|
|
||||||
# 显示最终报告
|
|
||||||
# self.chatbot.append(["论文解读报告", final_report])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
# 保存报告
|
|
||||||
yield from self.save_report(final_report)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_paper_file(path: str) -> str:
|
|
||||||
"""查找路径中的论文文件(简化版)"""
|
|
||||||
if os.path.isfile(path):
|
|
||||||
return path
|
|
||||||
|
|
||||||
# 支持的文件扩展名(按优先级排序)
|
|
||||||
extensions = ["pdf", "docx", "doc", "txt", "md", "tex"]
|
|
||||||
|
|
||||||
# 简单地遍历目录
|
|
||||||
if os.path.isdir(path):
|
|
||||||
try:
|
|
||||||
for ext in extensions:
|
|
||||||
# 手动检查每个可能的文件,而不使用glob
|
|
||||||
potential_file = os.path.join(path, f"paper.{ext}")
|
|
||||||
if os.path.exists(potential_file) and os.path.isfile(potential_file):
|
|
||||||
return potential_file
|
|
||||||
|
|
||||||
# 如果没找到特定命名的文件,检查目录中的所有文件
|
|
||||||
for file in os.listdir(path):
|
|
||||||
file_path = os.path.join(path, file)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
file_ext = file.split('.')[-1].lower() if '.' in file else ""
|
|
||||||
if file_ext in extensions:
|
|
||||||
return file_path
|
|
||||||
except Exception:
|
|
||||||
pass # 忽略任何错误
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def download_paper_by_id(paper_info, chatbot, history) -> str:
|
|
||||||
"""下载论文并返回保存路径
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paper_info: 元组,包含论文ID类型(arxiv或doi)和ID值
|
|
||||||
chatbot: 聊天机器人对象
|
|
||||||
history: 历史记录
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 下载的论文路径或None
|
|
||||||
"""
|
|
||||||
from crazy_functions.review_fns.data_sources.scihub_source import SciHub
|
|
||||||
id_type, paper_id = paper_info
|
|
||||||
|
|
||||||
# 创建保存目录 - 使用时间戳创建唯一文件夹
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
user_name = chatbot.get_user() if hasattr(chatbot, 'get_user') else "default"
|
|
||||||
from toolbox import get_log_folder, get_user
|
|
||||||
base_save_dir = get_log_folder(get_user(chatbot), plugin_name='paper_download')
|
|
||||||
save_dir = os.path.join(base_save_dir, f"papers_{timestamp}")
|
|
||||||
if not os.path.exists(save_dir):
|
|
||||||
os.makedirs(save_dir)
|
|
||||||
save_path = Path(save_dir)
|
|
||||||
|
|
||||||
chatbot.append([f"下载论文", f"正在下载{'arXiv' if id_type == 'arxiv' else 'DOI'} {paper_id} 的论文..."])
|
|
||||||
update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
pdf_path = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if id_type == 'arxiv':
|
|
||||||
# 使用改进的arxiv查询方法
|
|
||||||
formatted_id = format_arxiv_id(paper_id)
|
|
||||||
paper_result = get_arxiv_paper(formatted_id)
|
|
||||||
|
|
||||||
if not paper_result:
|
|
||||||
chatbot.append([f"下载失败", f"未找到arXiv论文: {paper_id}"])
|
|
||||||
update_ui(chatbot=chatbot, history=history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 下载PDF
|
|
||||||
filename = f"arxiv_{paper_id.replace('/', '_')}.pdf"
|
|
||||||
pdf_path = str(save_path / filename)
|
|
||||||
paper_result.download_pdf(filename=pdf_path)
|
|
||||||
|
|
||||||
else: # doi
|
|
||||||
# 下载DOI
|
|
||||||
sci_hub = SciHub(
|
|
||||||
doi=paper_id,
|
|
||||||
path=save_path
|
|
||||||
)
|
|
||||||
pdf_path = sci_hub.fetch()
|
|
||||||
|
|
||||||
# 检查下载结果
|
|
||||||
if pdf_path and os.path.exists(pdf_path):
|
|
||||||
promote_file_to_downloadzone(pdf_path, chatbot=chatbot)
|
|
||||||
chatbot.append([f"下载成功", f"已成功下载论文: {os.path.basename(pdf_path)}"])
|
|
||||||
update_ui(chatbot=chatbot, history=history)
|
|
||||||
return pdf_path
|
|
||||||
else:
|
|
||||||
chatbot.append([f"下载失败", f"论文下载失败: {paper_id}"])
|
|
||||||
update_ui(chatbot=chatbot, history=history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
chatbot.append([f"下载错误", f"下载论文时出错: {str(e)}"])
|
|
||||||
update_ui(chatbot=chatbot, history=history)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def 快速论文解读(txt: str, llm_kwargs: Dict, plugin_kwargs: Dict, chatbot: List,
|
|
||||||
history: List, system_prompt: str, user_request: str):
|
|
||||||
"""主函数 - 论文快速解读"""
|
|
||||||
# 初始化分析器
|
|
||||||
chatbot.append(["函数插件功能及使用方式", "论文快速解读:通过分析论文的关键要素,帮助您迅速理解论文内容,适用于各学科领域的科研论文。 <br><br>📋 使用方式:<br>1、直接上传PDF文件或者输入DOI号(仅针对SCI hub存在的论文)或arXiv ID(如2501.03916)<br>2、点击插件开始分析"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
paper_file = None
|
|
||||||
|
|
||||||
# 检查输入是否为论文ID(arxiv或DOI)
|
|
||||||
paper_info = extract_paper_id(txt)
|
|
||||||
|
|
||||||
if paper_info:
|
|
||||||
# 如果是论文ID,下载论文
|
|
||||||
chatbot.append(["检测到论文ID", f"检测到{'arXiv' if paper_info[0] == 'arxiv' else 'DOI'} ID: {paper_info[1]},准备下载论文..."])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
# 下载论文 - 完全重新实现
|
|
||||||
paper_file = download_paper_by_id(paper_info, chatbot, history)
|
|
||||||
|
|
||||||
if not paper_file:
|
|
||||||
report_exception(chatbot, history, a=f"下载论文失败", b=f"无法下载{'arXiv' if paper_info[0] == 'arxiv' else 'DOI'}论文: {paper_info[1]}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# 检查输入路径
|
|
||||||
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
|
|
||||||
|
|
||||||
# 验证路径安全性
|
|
||||||
user_name = chatbot.get_user()
|
|
||||||
validate_path_safety(txt, user_name)
|
|
||||||
|
|
||||||
# 查找论文文件
|
|
||||||
paper_file = _find_paper_file(txt)
|
|
||||||
|
|
||||||
if not paper_file:
|
|
||||||
report_exception(chatbot, history, a=f"解析论文", b=f"在路径 {txt} 中未找到支持的论文文件")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
# 增加调试信息,检查paper_file的类型和值
|
|
||||||
chatbot.append(["文件类型检查", f"paper_file类型: {type(paper_file)}, 值: {paper_file}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
chatbot.pop() # 移除调试信息
|
|
||||||
|
|
||||||
# 确保paper_file是字符串
|
|
||||||
if paper_file is not None and not isinstance(paper_file, str):
|
|
||||||
# 尝试转换为字符串
|
|
||||||
try:
|
|
||||||
paper_file = str(paper_file)
|
|
||||||
except:
|
|
||||||
report_exception(chatbot, history, a=f"类型错误", b=f"论文路径不是有效的字符串: {type(paper_file)}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 分析论文
|
|
||||||
chatbot.append(["开始分析", f"正在分析论文: {os.path.basename(paper_file)}"])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
|
||||||
|
|
||||||
analyzer = PaperAnalyzer(llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
|
||||||
yield from analyzer.analyze_paper(paper_file)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import os,glob
|
|
||||||
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_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
|
|
||||||
|
|
||||||
RAG_WORKER_REGISTER = {}
|
|
||||||
MAX_HISTORY_ROUND = 5
|
|
||||||
MAX_CONTEXT_TOKEN_LIMIT = 4096
|
|
||||||
REMEMBER_PREVIEW = 1000
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def handle_document_upload(files: List[str], llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request, rag_worker):
|
|
||||||
"""
|
|
||||||
Handles document uploads by extracting text and adding it to the vector store.
|
|
||||||
"""
|
|
||||||
from llama_index.core import Document
|
|
||||||
from crazy_functions.rag_fns.rag_file_support import extract_text, supports_format
|
|
||||||
user_name = chatbot.get_user()
|
|
||||||
checkpoint_dir = get_log_folder(user_name, plugin_name='experimental_rag')
|
|
||||||
|
|
||||||
for file_path in files:
|
|
||||||
try:
|
|
||||||
validate_path_safety(file_path, user_name)
|
|
||||||
text = extract_text(file_path)
|
|
||||||
if text is None:
|
|
||||||
chatbot.append(
|
|
||||||
[f"上传文件: {os.path.basename(file_path)}", f"文件解析失败,无法提取文本内容,请更换文件。失败原因可能为:1.文档格式过于复杂;2. 不支持的文件格式,支持的文件格式后缀有:" + ", ".join(supports_format)])
|
|
||||||
else:
|
|
||||||
chatbot.append(
|
|
||||||
[f"上传文件: {os.path.basename(file_path)}", f"上传文件前50个字符为:{text[:50]}。"])
|
|
||||||
document = Document(text=text, metadata={"source": file_path})
|
|
||||||
rag_worker.add_documents_to_vector_store([document])
|
|
||||||
chatbot.append([f"上传文件: {os.path.basename(file_path)}", "文件已成功添加到知识库。"])
|
|
||||||
except Exception as e:
|
|
||||||
report_exception(chatbot, history, a=f"处理文件: {file_path}", b=str(e))
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Main Q&A function with document upload support
|
|
||||||
@CatchException
|
|
||||||
def Rag问答(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
|
|
||||||
# import vector store lib
|
|
||||||
VECTOR_STORE_TYPE = "Milvus"
|
|
||||||
if VECTOR_STORE_TYPE == "Milvus":
|
|
||||||
try:
|
|
||||||
from crazy_functions.rag_fns.milvus_worker import MilvusRagWorker as LlamaIndexRagWorker
|
|
||||||
except:
|
|
||||||
VECTOR_STORE_TYPE = "Simple"
|
|
||||||
if VECTOR_STORE_TYPE == "Simple":
|
|
||||||
from crazy_functions.rag_fns.llama_index_worker import LlamaIndexRagWorker
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
rag_worker = RAG_WORKER_REGISTER[user_name] = LlamaIndexRagWorker(
|
|
||||||
user_name,
|
|
||||||
llm_kwargs,
|
|
||||||
checkpoint_dir=checkpoint_dir,
|
|
||||||
auto_load_checkpoint=True
|
|
||||||
)
|
|
||||||
|
|
||||||
current_context = f"{VECTOR_STORE_TYPE} @ {checkpoint_dir}"
|
|
||||||
tip = "提示:输入“清空向量数据库”可以清空RAG向量数据库"
|
|
||||||
|
|
||||||
# 2. Handle special commands
|
|
||||||
if os.path.exists(txt) and os.path.isdir(txt):
|
|
||||||
project_folder = txt
|
|
||||||
validate_path_safety(project_folder, chatbot.get_user())
|
|
||||||
# Extract file paths from the user input
|
|
||||||
# Assuming the user inputs file paths separated by commas after the command
|
|
||||||
file_paths = [f for f in glob.glob(f'{project_folder}/**/*', recursive=True)]
|
|
||||||
chatbot.append([txt, f'正在处理上传的文档 ({current_context}) ...'])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
yield from handle_document_upload(file_paths, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request, rag_worker)
|
|
||||||
return
|
|
||||||
|
|
||||||
elif txt == "清空向量数据库":
|
|
||||||
chatbot.append([txt, f'正在清空 ({current_context}) ...'])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
rag_worker.purge_vector_store()
|
|
||||||
yield from update_ui_latest_msg('已清空', chatbot, history, delay=0) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
# 3. Normal Q&A processing
|
|
||||||
chatbot.append([txt, f'正在召回知识 ({current_context}) ...'])
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
# 4. Clip history to reduce token consumption
|
|
||||||
txt_origin = txt
|
|
||||||
|
|
||||||
if len(history) > MAX_HISTORY_ROUND * 2:
|
|
||||||
history = history[-(MAX_HISTORY_ROUND * 2):]
|
|
||||||
txt_clip, history, flags = input_clipping(txt, history, max_token_limit=MAX_CONTEXT_TOKEN_LIMIT, return_clip_flags=True)
|
|
||||||
input_is_clipped_flag = (flags["original_input_len"] != flags["clipped_input_len"])
|
|
||||||
|
|
||||||
# 5. If input is clipped, add input to vector store before retrieve
|
|
||||||
if input_is_clipped_flag:
|
|
||||||
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_latest_msg('向量化完成 ...', chatbot, history, delay=0) # 刷新界面
|
|
||||||
|
|
||||||
if len(txt_origin) > REMEMBER_PREVIEW:
|
|
||||||
HALF = REMEMBER_PREVIEW // 2
|
|
||||||
i_say_to_remember = txt[:HALF] + f" ...\n...(省略{len(txt_origin)-REMEMBER_PREVIEW}字)...\n... " + txt[-HALF:]
|
|
||||||
if (flags["original_input_len"] - flags["clipped_input_len"]) > HALF:
|
|
||||||
txt_clip = txt_clip + f" ...\n...(省略{len(txt_origin)-len(txt_clip)-HALF}字)...\n... " + txt[-HALF:]
|
|
||||||
else:
|
|
||||||
i_say_to_remember = i_say = txt_clip
|
|
||||||
else:
|
|
||||||
i_say_to_remember = i_say = txt_clip
|
|
||||||
|
|
||||||
# 6. Search vector store and build prompts
|
|
||||||
nodes = rag_worker.retrieve_from_store_with_query(i_say)
|
|
||||||
prompt = rag_worker.build_prompt(query=i_say, nodes=nodes)
|
|
||||||
# 7. Query language model
|
|
||||||
if len(chatbot) != 0:
|
|
||||||
chatbot.pop(-1) # Pop temp chat, because we are going to add them again inside `request_gpt_model_in_new_thread_with_ui_alive`
|
|
||||||
|
|
||||||
model_say = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
|
||||||
inputs=prompt,
|
|
||||||
inputs_show_user=i_say,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
chatbot=chatbot,
|
|
||||||
history=history,
|
|
||||||
sys_prompt=system_prompt,
|
|
||||||
retry_times_at_unknown_error=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# 8. Remember Q&A
|
|
||||||
yield from update_ui_latest_msg(
|
|
||||||
model_say + '</br></br>' + f'对话记忆中, 请稍等 ({current_context}) ...',
|
|
||||||
chatbot, history, delay=0.5
|
|
||||||
)
|
|
||||||
rag_worker.remember_qa(i_say_to_remember, model_say)
|
|
||||||
history.extend([i_say, model_say])
|
|
||||||
|
|
||||||
# 9. Final UI Update
|
|
||||||
yield from update_ui_latest_msg(model_say, chatbot, history, delay=0, msg=tip)
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import pickle, os, random
|
|
||||||
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
|
|
||||||
from crazy_functions.json_fns.select_tool import structure_output, select_tool
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from loguru import logger
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
SOCIAL_NETWORK_WORKER_REGISTER = {}
|
|
||||||
|
|
||||||
class SocialNetwork():
|
|
||||||
def __init__(self):
|
|
||||||
self.people = []
|
|
||||||
|
|
||||||
class SaveAndLoad():
|
|
||||||
def __init__(self, user_name, llm_kwargs, auto_load_checkpoint=True, checkpoint_dir=None) -> None:
|
|
||||||
self.user_name = user_name
|
|
||||||
self.checkpoint_dir = checkpoint_dir
|
|
||||||
if auto_load_checkpoint:
|
|
||||||
self.social_network = self.load_from_checkpoint(checkpoint_dir)
|
|
||||||
else:
|
|
||||||
self.social_network = SocialNetwork()
|
|
||||||
|
|
||||||
def does_checkpoint_exist(self, checkpoint_dir=None):
|
|
||||||
import os, glob
|
|
||||||
if checkpoint_dir is None: checkpoint_dir = self.checkpoint_dir
|
|
||||||
if not os.path.exists(checkpoint_dir): return False
|
|
||||||
if len(glob.glob(os.path.join(checkpoint_dir, "social_network.pkl"))) == 0: return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def save_to_checkpoint(self, checkpoint_dir=None):
|
|
||||||
if checkpoint_dir is None: checkpoint_dir = self.checkpoint_dir
|
|
||||||
with open(os.path.join(checkpoint_dir, 'social_network.pkl'), "wb+") as f:
|
|
||||||
pickle.dump(self.social_network, f)
|
|
||||||
return
|
|
||||||
|
|
||||||
def load_from_checkpoint(self, checkpoint_dir=None):
|
|
||||||
if checkpoint_dir is None: checkpoint_dir = self.checkpoint_dir
|
|
||||||
if self.does_checkpoint_exist(checkpoint_dir=checkpoint_dir):
|
|
||||||
with open(os.path.join(checkpoint_dir, 'social_network.pkl'), "rb") as f:
|
|
||||||
social_network = pickle.load(f)
|
|
||||||
return social_network
|
|
||||||
else:
|
|
||||||
return SocialNetwork()
|
|
||||||
|
|
||||||
|
|
||||||
class Friend(BaseModel):
|
|
||||||
friend_name: str = Field(description="name of a friend")
|
|
||||||
friend_description: str = Field(description="description of a friend (everything about this friend)")
|
|
||||||
friend_relationship: str = Field(description="The relationship with a friend (e.g. friend, family, colleague)")
|
|
||||||
|
|
||||||
class FriendList(BaseModel):
|
|
||||||
friends_list: List[Friend] = Field(description="The list of friends")
|
|
||||||
|
|
||||||
|
|
||||||
class SocialNetworkWorker(SaveAndLoad):
|
|
||||||
def ai_socail_advice(self, prompt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, run_gpt_fn, intention_type):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ai_remove_friend(self, prompt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, run_gpt_fn, intention_type):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ai_list_friends(self, prompt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, run_gpt_fn, intention_type):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ai_add_multi_friends(self, prompt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, run_gpt_fn, intention_type):
|
|
||||||
friend, err_msg = structure_output(
|
|
||||||
txt=prompt,
|
|
||||||
prompt="根据提示, 解析多个联系人的身份信息\n\n",
|
|
||||||
err_msg=f"不能理解该联系人",
|
|
||||||
run_gpt_fn=run_gpt_fn,
|
|
||||||
pydantic_cls=FriendList
|
|
||||||
)
|
|
||||||
if friend.friends_list:
|
|
||||||
for f in friend.friends_list:
|
|
||||||
self.add_friend(f)
|
|
||||||
msg = f"成功添加{len(friend.friends_list)}个联系人: {str(friend.friends_list)}"
|
|
||||||
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):
|
|
||||||
prompt = txt
|
|
||||||
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=[])
|
|
||||||
self.tools_to_select = {
|
|
||||||
"SocialAdvice":{
|
|
||||||
"explain_to_llm": "如果用户希望获取社交指导,调用SocialAdvice生成一些社交建议",
|
|
||||||
"callback": self.ai_socail_advice,
|
|
||||||
},
|
|
||||||
"AddFriends":{
|
|
||||||
"explain_to_llm": "如果用户给出了联系人,调用AddMultiFriends把联系人添加到数据库",
|
|
||||||
"callback": self.ai_add_multi_friends,
|
|
||||||
},
|
|
||||||
"RemoveFriend":{
|
|
||||||
"explain_to_llm": "如果用户希望移除某个联系人,调用RemoveFriend",
|
|
||||||
"callback": self.ai_remove_friend,
|
|
||||||
},
|
|
||||||
"ListFriends":{
|
|
||||||
"explain_to_llm": "如果用户列举联系人,调用ListFriends",
|
|
||||||
"callback": self.ai_list_friends,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
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"Explanation:\n{Explanation}",
|
|
||||||
default="SocialAdvice"
|
|
||||||
)
|
|
||||||
pydantic_cls_instance, err_msg = select_tool(
|
|
||||||
prompt=txt,
|
|
||||||
run_gpt_fn=run_gpt_fn,
|
|
||||||
pydantic_cls=UserSociaIntention
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
yield from update_ui_latest_msg(
|
|
||||||
lastmsg=f"无法理解用户意图 {err_msg}",
|
|
||||||
chatbot=chatbot,
|
|
||||||
history=history,
|
|
||||||
delay=0
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
intention_type = pydantic_cls_instance.intention_type
|
|
||||||
intention_callback = self.tools_to_select[pydantic_cls_instance.intention_type]['callback']
|
|
||||||
yield from intention_callback(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, run_gpt_fn, intention_type)
|
|
||||||
|
|
||||||
|
|
||||||
def add_friend(self, friend):
|
|
||||||
# check whether the friend is already in the social network
|
|
||||||
for f in self.social_network.people:
|
|
||||||
if f.friend_name == friend.friend_name:
|
|
||||||
f.friend_description = friend.friend_description
|
|
||||||
f.friend_relationship = friend.friend_relationship
|
|
||||||
logger.info(f"Repeated friend, update info: {friend}")
|
|
||||||
return
|
|
||||||
logger.info(f"Add a new friend: {friend}")
|
|
||||||
self.social_network.people.append(friend)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def I人助手(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
|
|
||||||
# 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_NETWORK_WORKER_REGISTER:
|
|
||||||
social_network_worker = SOCIAL_NETWORK_WORKER_REGISTER[user_name]
|
|
||||||
else:
|
|
||||||
social_network_worker = SOCIAL_NETWORK_WORKER_REGISTER[user_name] = SocialNetworkWorker(
|
|
||||||
user_name,
|
|
||||||
llm_kwargs,
|
|
||||||
checkpoint_dir=checkpoint_dir,
|
|
||||||
auto_load_checkpoint=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. save
|
|
||||||
yield from social_network_worker.run(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
social_network_worker.save_to_checkpoint(checkpoint_dir)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import os, copy, time
|
|
||||||
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
|
|
||||||
from crazy_functions.crazy_utils import request_gpt_model_in_new_thread_with_ui_alive
|
|
||||||
from crazy_functions.agent_fns.python_comment_agent import PythonCodeComment
|
|
||||||
from crazy_functions.diagram_fns.file_tree import FileNode
|
|
||||||
from crazy_functions.agent_fns.watchdog import WatchDog
|
|
||||||
from shared_utils.advanced_markdown_format import markdown_convertion_for_file
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
def 注释源代码(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt):
|
|
||||||
|
|
||||||
summary_batch_isolation = True
|
|
||||||
inputs_array = []
|
|
||||||
inputs_show_user_array = []
|
|
||||||
history_array = []
|
|
||||||
sys_prompt_array = []
|
|
||||||
|
|
||||||
assert len(file_manifest) <= 512, "源文件太多(超过512个), 请缩减输入文件的数量。或者,您也可以选择删除此行警告,并修改代码拆分file_manifest列表,从而实现分批次处理。"
|
|
||||||
|
|
||||||
# 建立文件树
|
|
||||||
file_tree_struct = FileNode("root", build_manifest=True)
|
|
||||||
for file_path in file_manifest:
|
|
||||||
file_tree_struct.add_file(file_path, file_path)
|
|
||||||
|
|
||||||
# <第一步,逐个文件分析,多线程>
|
|
||||||
lang = "" if not plugin_kwargs["use_chinese"] else " (you must use Chinese)"
|
|
||||||
for index, fp in enumerate(file_manifest):
|
|
||||||
# 读取文件
|
|
||||||
with open(fp, 'r', encoding='utf-8', errors='replace') as f:
|
|
||||||
file_content = f.read()
|
|
||||||
prefix = ""
|
|
||||||
i_say = prefix + f'Please conclude the following source code at {os.path.relpath(fp, project_folder)} with only one sentence{lang}, the code is:\n```{file_content}```'
|
|
||||||
i_say_show_user = prefix + f'[{index+1}/{len(file_manifest)}] 请用一句话对下面的程序文件做一个整体概述: {fp}'
|
|
||||||
# 装载请求内容
|
|
||||||
MAX_TOKEN_SINGLE_FILE = 2560
|
|
||||||
i_say, _ = input_clipping(inputs=i_say, history=[], max_token_limit=MAX_TOKEN_SINGLE_FILE)
|
|
||||||
inputs_array.append(i_say)
|
|
||||||
inputs_show_user_array.append(i_say_show_user)
|
|
||||||
history_array.append([])
|
|
||||||
sys_prompt_array.append(f"You are a software architecture analyst analyzing a source code project. Do not dig into details, tell me what the code is doing in general. Your answer must be short, simple and clear{lang}.")
|
|
||||||
# 文件读取完成,对每一个源代码文件,生成一个请求线程,发送到大模型进行分析
|
|
||||||
gpt_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,
|
|
||||||
history_array = history_array,
|
|
||||||
sys_prompt_array = sys_prompt_array,
|
|
||||||
llm_kwargs = llm_kwargs,
|
|
||||||
chatbot = chatbot,
|
|
||||||
show_user_at_complete = True
|
|
||||||
)
|
|
||||||
|
|
||||||
# <第二步,逐个文件分析,生成带注释文件>
|
|
||||||
tasks = ["" for _ in range(len(file_manifest))]
|
|
||||||
def bark_fn(tasks):
|
|
||||||
for i in range(len(tasks)): tasks[i] = "watchdog is dead"
|
|
||||||
wd = WatchDog(timeout=10, bark_fn=lambda: bark_fn(tasks), interval=3, msg="ThreadWatcher timeout")
|
|
||||||
wd.begin_watch()
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
executor = ThreadPoolExecutor(max_workers=get_conf('DEFAULT_WORKER_NUM'))
|
|
||||||
def _task_multi_threading(i_say, gpt_say, fp, file_tree_struct, index):
|
|
||||||
language = 'Chinese' if plugin_kwargs["use_chinese"] else 'English'
|
|
||||||
def observe_window_update(x):
|
|
||||||
if tasks[index] == "watchdog is dead":
|
|
||||||
raise TimeoutError("ThreadWatcher: watchdog is dead")
|
|
||||||
tasks[index] = x
|
|
||||||
pcc = PythonCodeComment(llm_kwargs, plugin_kwargs, language=language, observe_window_update=observe_window_update)
|
|
||||||
pcc.read_file(path=fp, brief=gpt_say)
|
|
||||||
revised_path, revised_content = pcc.begin_comment_source_code(None, None)
|
|
||||||
file_tree_struct.manifest[fp].revised_path = revised_path
|
|
||||||
file_tree_struct.manifest[fp].revised_content = revised_content
|
|
||||||
# <将结果写回源文件>
|
|
||||||
with open(fp, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(file_tree_struct.manifest[fp].revised_content)
|
|
||||||
# <生成对比html>
|
|
||||||
with open("crazy_functions/agent_fns/python_comment_compare.html", 'r', encoding='utf-8') as f:
|
|
||||||
html_template = f.read()
|
|
||||||
warp = lambda x: "```python\n\n" + x + "\n\n```"
|
|
||||||
from themes.theme import load_dynamic_theme
|
|
||||||
_, advanced_css, _, _ = load_dynamic_theme("Default")
|
|
||||||
html_template = html_template.replace("ADVANCED_CSS", advanced_css)
|
|
||||||
html_template = html_template.replace("REPLACE_CODE_FILE_LEFT", pcc.get_markdown_block_in_html(markdown_convertion_for_file(warp(pcc.original_content))))
|
|
||||||
html_template = html_template.replace("REPLACE_CODE_FILE_RIGHT", pcc.get_markdown_block_in_html(markdown_convertion_for_file(warp(revised_content))))
|
|
||||||
compare_html_path = fp + '.compare.html'
|
|
||||||
file_tree_struct.manifest[fp].compare_html = compare_html_path
|
|
||||||
with open(compare_html_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(html_template)
|
|
||||||
tasks[index] = ""
|
|
||||||
|
|
||||||
chatbot.append([None, f"正在处理:"])
|
|
||||||
futures = []
|
|
||||||
index = 0
|
|
||||||
for i_say, gpt_say, fp in zip(gpt_response_collection[0::2], gpt_response_collection[1::2], file_manifest):
|
|
||||||
future = executor.submit(_task_multi_threading, i_say, gpt_say, fp, file_tree_struct, index)
|
|
||||||
index += 1
|
|
||||||
futures.append(future)
|
|
||||||
|
|
||||||
# <第三步,等待任务完成>
|
|
||||||
cnt = 0
|
|
||||||
while True:
|
|
||||||
cnt += 1
|
|
||||||
wd.feed()
|
|
||||||
time.sleep(3)
|
|
||||||
worker_done = [h.done() for h in futures]
|
|
||||||
remain = len(worker_done) - sum(worker_done)
|
|
||||||
|
|
||||||
# <展示已经完成的部分>
|
|
||||||
preview_html_list = []
|
|
||||||
for done, fp in zip(worker_done, file_manifest):
|
|
||||||
if not done: continue
|
|
||||||
if hasattr(file_tree_struct.manifest[fp], 'compare_html'):
|
|
||||||
preview_html_list.append(file_tree_struct.manifest[fp].compare_html)
|
|
||||||
else:
|
|
||||||
logger.error(f"文件: {fp} 的注释结果未能成功")
|
|
||||||
file_links = generate_file_link(preview_html_list)
|
|
||||||
|
|
||||||
yield from update_ui_latest_msg(
|
|
||||||
f"当前任务: <br/>{'<br/>'.join(tasks)}.<br/>" +
|
|
||||||
f"剩余源文件数量: {remain}.<br/>" +
|
|
||||||
f"已完成的文件: {sum(worker_done)}.<br/>" +
|
|
||||||
file_links +
|
|
||||||
"<br/>" +
|
|
||||||
''.join(['.']*(cnt % 10 + 1)
|
|
||||||
), chatbot=chatbot, history=history, delay=0)
|
|
||||||
yield from update_ui(chatbot=chatbot, history=[]) # 刷新界面
|
|
||||||
if all(worker_done):
|
|
||||||
executor.shutdown()
|
|
||||||
break
|
|
||||||
|
|
||||||
# <第四步,压缩结果>
|
|
||||||
zip_res = zip_result(project_folder)
|
|
||||||
promote_file_to_downloadzone(file=zip_res, chatbot=chatbot)
|
|
||||||
|
|
||||||
# <END>
|
|
||||||
chatbot.append((None, "所有源文件均已处理完毕。"))
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@CatchException
|
|
||||||
def 注释Python项目(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
history = [] # 清空历史,以免输入溢出
|
|
||||||
plugin_kwargs["use_chinese"] = plugin_kwargs.get("use_chinese", False)
|
|
||||||
import glob, os
|
|
||||||
if os.path.exists(txt):
|
|
||||||
project_folder = txt
|
|
||||||
validate_path_safety(project_folder, chatbot.get_user())
|
|
||||||
else:
|
|
||||||
if txt == "": txt = '空空如也的输入栏'
|
|
||||||
report_exception(chatbot, history, a = f"解析项目: {txt}", b = f"找不到本地项目或无权访问: {txt}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
file_manifest = [f for f in glob.glob(f'{project_folder}/**/*.py', recursive=True)]
|
|
||||||
if len(file_manifest) == 0:
|
|
||||||
report_exception(chatbot, history, a = f"解析项目: {txt}", b = f"找不到任何python文件: {txt}")
|
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
|
||||||
return
|
|
||||||
|
|
||||||
yield from 注释源代码(file_manifest, project_folder, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
from toolbox import get_conf, update_ui
|
|
||||||
from crazy_functions.plugin_template.plugin_class_template import GptAcademicPluginTemplate, ArgProperty
|
|
||||||
from crazy_functions.SourceCode_Comment import 注释Python项目
|
|
||||||
|
|
||||||
class SourceCodeComment_Wrap(GptAcademicPluginTemplate):
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
请注意`execute`会执行在不同的线程中,因此您在定义和使用类变量时,应当慎之又慎!
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def define_arg_selection_menu(self):
|
|
||||||
"""
|
|
||||||
定义插件的二级选项菜单
|
|
||||||
"""
|
|
||||||
gui_definition = {
|
|
||||||
"main_input":
|
|
||||||
ArgProperty(title="路径", description="程序路径(上传文件后自动填写)", default_value="", type="string").model_dump_json(), # 主输入,自动从输入框同步
|
|
||||||
"use_chinese":
|
|
||||||
ArgProperty(title="注释语言", options=["英文", "中文"], default_value="英文", description="无", type="dropdown").model_dump_json(),
|
|
||||||
# "use_emoji":
|
|
||||||
# ArgProperty(title="在注释中使用emoji", options=["禁止", "允许"], default_value="禁止", description="无", type="dropdown").model_dump_json(),
|
|
||||||
}
|
|
||||||
return gui_definition
|
|
||||||
|
|
||||||
def execute(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
|
||||||
"""
|
|
||||||
执行插件
|
|
||||||
"""
|
|
||||||
if plugin_kwargs["use_chinese"] == "中文":
|
|
||||||
plugin_kwargs["use_chinese"] = True
|
|
||||||
else:
|
|
||||||
plugin_kwargs["use_chinese"] = False
|
|
||||||
|
|
||||||
yield from 注释Python项目(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request)
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
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 CatchException, update_ui, gen_time_str, trimmed_format_exc, ProxyNetworkActivate
|
||||||
from toolbox import report_exception, get_log_folder, update_ui_latest_msg, Singleton
|
from toolbox import report_exception, get_log_folder, update_ui_lastest_msg, Singleton
|
||||||
from crazy_functions.agent_fns.pipe import PluginMultiprocessManager, PipeCom
|
from crazy_functions.agent_fns.pipe import PluginMultiprocessManager, PipeCom
|
||||||
from crazy_functions.agent_fns.general import AutoGenGeneral
|
from crazy_functions.agent_fns.general import AutoGenGeneral
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from crazy_functions.agent_fns.pipe import PluginMultiprocessManager, PipeCom
|
from crazy_functions.agent_fns.pipe import PluginMultiprocessManager, PipeCom
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
class EchoDemo(PluginMultiprocessManager):
|
class EchoDemo(PluginMultiprocessManager):
|
||||||
def subprocess_worker(self, child_conn):
|
def subprocess_worker(self, child_conn):
|
||||||
@@ -8,7 +7,7 @@ class EchoDemo(PluginMultiprocessManager):
|
|||||||
while True:
|
while True:
|
||||||
msg = self.child_conn.recv() # PipeCom
|
msg = self.child_conn.recv() # PipeCom
|
||||||
if msg.cmd == "user_input":
|
if msg.cmd == "user_input":
|
||||||
# wait father user input
|
# wait futher user input
|
||||||
self.child_conn.send(PipeCom("show", msg.content))
|
self.child_conn.send(PipeCom("show", msg.content))
|
||||||
wait_success = self.subprocess_worker_wait_user_feedback(wait_msg="我准备好处理下一个问题了.")
|
wait_success = self.subprocess_worker_wait_user_feedback(wait_msg="我准备好处理下一个问题了.")
|
||||||
if not wait_success:
|
if not wait_success:
|
||||||
@@ -17,4 +16,4 @@ class EchoDemo(PluginMultiprocessManager):
|
|||||||
elif msg.cmd == "terminate":
|
elif msg.cmd == "terminate":
|
||||||
self.child_conn.send(PipeCom("done", ""))
|
self.child_conn.send(PipeCom("done", ""))
|
||||||
break
|
break
|
||||||
logger.info('[debug] subprocess_worker terminated')
|
print('[debug] subprocess_worker terminated')
|
||||||
@@ -27,7 +27,7 @@ def gpt_academic_generate_oai_reply(
|
|||||||
llm_kwargs=llm_config,
|
llm_kwargs=llm_config,
|
||||||
history=history,
|
history=history,
|
||||||
sys_prompt=self._oai_system_message[0]['content'],
|
sys_prompt=self._oai_system_message[0]['content'],
|
||||||
console_silence=True
|
console_slience=True
|
||||||
)
|
)
|
||||||
assumed_done = reply.endswith('\nTERMINATE')
|
assumed_done = reply.endswith('\nTERMINATE')
|
||||||
return True, reply
|
return True, reply
|
||||||
@@ -35,11 +35,7 @@ def gpt_academic_generate_oai_reply(
|
|||||||
class AutoGenGeneral(PluginMultiprocessManager):
|
class AutoGenGeneral(PluginMultiprocessManager):
|
||||||
def gpt_academic_print_override(self, user_proxy, message, sender):
|
def gpt_academic_print_override(self, user_proxy, message, sender):
|
||||||
# ⭐⭐ run in subprocess
|
# ⭐⭐ run in subprocess
|
||||||
try:
|
self.child_conn.send(PipeCom("show", sender.name + "\n\n---\n\n" + message["content"]))
|
||||||
print_msg = sender.name + "\n\n---\n\n" + message["content"]
|
|
||||||
except:
|
|
||||||
print_msg = sender.name + "\n\n---\n\n" + message
|
|
||||||
self.child_conn.send(PipeCom("show", print_msg))
|
|
||||||
|
|
||||||
def gpt_academic_get_human_input(self, user_proxy, message):
|
def gpt_academic_get_human_input(self, user_proxy, message):
|
||||||
# ⭐⭐ run in subprocess
|
# ⭐⭐ run in subprocess
|
||||||
@@ -66,33 +62,33 @@ class AutoGenGeneral(PluginMultiprocessManager):
|
|||||||
def exe_autogen(self, input):
|
def exe_autogen(self, input):
|
||||||
# ⭐⭐ run in subprocess
|
# ⭐⭐ run in subprocess
|
||||||
input = input.content
|
input = input.content
|
||||||
code_execution_config = {"work_dir": self.autogen_work_dir, "use_docker": self.use_docker}
|
with ProxyNetworkActivate("AutoGen"):
|
||||||
agents = self.define_agents()
|
code_execution_config = {"work_dir": self.autogen_work_dir, "use_docker": self.use_docker}
|
||||||
user_proxy = None
|
agents = self.define_agents()
|
||||||
assistant = None
|
user_proxy = None
|
||||||
for agent_kwargs in agents:
|
assistant = None
|
||||||
agent_cls = agent_kwargs.pop('cls')
|
for agent_kwargs in agents:
|
||||||
kwargs = {
|
agent_cls = agent_kwargs.pop('cls')
|
||||||
'llm_config':self.llm_kwargs,
|
kwargs = {
|
||||||
'code_execution_config':code_execution_config
|
'llm_config':self.llm_kwargs,
|
||||||
}
|
'code_execution_config':code_execution_config
|
||||||
kwargs.update(agent_kwargs)
|
}
|
||||||
agent_handle = agent_cls(**kwargs)
|
kwargs.update(agent_kwargs)
|
||||||
agent_handle._print_received_message = lambda a,b: self.gpt_academic_print_override(agent_kwargs, a, b)
|
agent_handle = agent_cls(**kwargs)
|
||||||
for d in agent_handle._reply_func_list:
|
agent_handle._print_received_message = lambda a,b: self.gpt_academic_print_override(agent_kwargs, a, b)
|
||||||
if hasattr(d['reply_func'],'__name__') and d['reply_func'].__name__ == 'generate_oai_reply':
|
for d in agent_handle._reply_func_list:
|
||||||
d['reply_func'] = gpt_academic_generate_oai_reply
|
if hasattr(d['reply_func'],'__name__') and d['reply_func'].__name__ == 'generate_oai_reply':
|
||||||
if agent_kwargs['name'] == 'user_proxy':
|
d['reply_func'] = gpt_academic_generate_oai_reply
|
||||||
agent_handle.get_human_input = lambda a: self.gpt_academic_get_human_input(user_proxy, a)
|
if agent_kwargs['name'] == 'user_proxy':
|
||||||
user_proxy = agent_handle
|
agent_handle.get_human_input = lambda a: self.gpt_academic_get_human_input(user_proxy, a)
|
||||||
if agent_kwargs['name'] == 'assistant': assistant = agent_handle
|
user_proxy = agent_handle
|
||||||
try:
|
if agent_kwargs['name'] == 'assistant': assistant = agent_handle
|
||||||
if user_proxy is None or assistant is None: raise Exception("用户代理或助理代理未定义")
|
try:
|
||||||
with ProxyNetworkActivate("AutoGen"):
|
if user_proxy is None or assistant is None: raise Exception("用户代理或助理代理未定义")
|
||||||
user_proxy.initiate_chat(assistant, message=input)
|
user_proxy.initiate_chat(assistant, message=input)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tb_str = '```\n' + trimmed_format_exc() + '```'
|
tb_str = '```\n' + trimmed_format_exc() + '```'
|
||||||
self.child_conn.send(PipeCom("done", "AutoGen 执行失败: \n\n" + tb_str))
|
self.child_conn.send(PipeCom("done", "AutoGen 执行失败: \n\n" + tb_str))
|
||||||
|
|
||||||
def subprocess_worker(self, child_conn):
|
def subprocess_worker(self, child_conn):
|
||||||
# ⭐⭐ run in subprocess
|
# ⭐⭐ run in subprocess
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from toolbox import get_log_folder, update_ui, gen_time_str, get_conf, promote_file_to_downloadzone
|
from toolbox import get_log_folder, update_ui, gen_time_str, get_conf, promote_file_to_downloadzone
|
||||||
from crazy_functions.agent_fns.watchdog import WatchDog
|
from crazy_functions.agent_fns.watchdog import WatchDog
|
||||||
from loguru import logger
|
|
||||||
import time, os
|
import time, os
|
||||||
|
|
||||||
class PipeCom:
|
class PipeCom:
|
||||||
@@ -10,7 +9,7 @@ class PipeCom:
|
|||||||
|
|
||||||
|
|
||||||
class PluginMultiprocessManager:
|
class PluginMultiprocessManager:
|
||||||
def __init__(self, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request):
|
def __init__(self, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, web_port):
|
||||||
# ⭐ run in main process
|
# ⭐ run in main process
|
||||||
self.autogen_work_dir = os.path.join(get_log_folder("autogen"), gen_time_str())
|
self.autogen_work_dir = os.path.join(get_log_folder("autogen"), gen_time_str())
|
||||||
self.previous_work_dir_files = {}
|
self.previous_work_dir_files = {}
|
||||||
@@ -19,7 +18,7 @@ class PluginMultiprocessManager:
|
|||||||
self.chatbot = chatbot
|
self.chatbot = chatbot
|
||||||
self.history = history
|
self.history = history
|
||||||
self.system_prompt = system_prompt
|
self.system_prompt = system_prompt
|
||||||
# self.user_request = user_request
|
# self.web_port = web_port
|
||||||
self.alive = True
|
self.alive = True
|
||||||
self.use_docker = get_conf("AUTOGEN_USE_DOCKER")
|
self.use_docker = get_conf("AUTOGEN_USE_DOCKER")
|
||||||
self.last_user_input = ""
|
self.last_user_input = ""
|
||||||
@@ -48,7 +47,7 @@ class PluginMultiprocessManager:
|
|||||||
def terminate(self):
|
def terminate(self):
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
self.alive = False
|
self.alive = False
|
||||||
logger.info("[debug] instance terminated")
|
print("[debug] instance terminated")
|
||||||
|
|
||||||
def subprocess_worker(self, child_conn):
|
def subprocess_worker(self, child_conn):
|
||||||
# ⭐⭐ run in subprocess
|
# ⭐⭐ run in subprocess
|
||||||
@@ -73,7 +72,7 @@ class PluginMultiprocessManager:
|
|||||||
if file_type.lower() in ['png', 'jpg']:
|
if file_type.lower() in ['png', 'jpg']:
|
||||||
image_path = os.path.abspath(fp)
|
image_path = os.path.abspath(fp)
|
||||||
self.chatbot.append([
|
self.chatbot.append([
|
||||||
'检测到新生图像:',
|
'检测到新生图像:',
|
||||||
f'本地文件预览: <br/><div align="center"><img src="file={image_path}"></div>'
|
f'本地文件预览: <br/><div align="center"><img src="file={image_path}"></div>'
|
||||||
])
|
])
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||||
@@ -115,21 +114,21 @@ class PluginMultiprocessManager:
|
|||||||
self.cnt = 1
|
self.cnt = 1
|
||||||
self.parent_conn = self.launch_subprocess_with_pipe() # ⭐⭐⭐
|
self.parent_conn = self.launch_subprocess_with_pipe() # ⭐⭐⭐
|
||||||
repeated, cmd_to_autogen = self.send_command(txt)
|
repeated, cmd_to_autogen = self.send_command(txt)
|
||||||
if txt == 'exit':
|
if txt == 'exit':
|
||||||
self.chatbot.append([f"结束", "结束信号已明确,终止AutoGen程序。"])
|
self.chatbot.append([f"结束", "结束信号已明确,终止AutoGen程序。"])
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||||
self.terminate()
|
self.terminate()
|
||||||
return "terminate"
|
return "terminate"
|
||||||
|
|
||||||
# patience = 10
|
# patience = 10
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
if not self.alive:
|
if not self.alive:
|
||||||
# the heartbeat watchdog might have it killed
|
# the heartbeat watchdog might have it killed
|
||||||
self.terminate()
|
self.terminate()
|
||||||
return "terminate"
|
return "terminate"
|
||||||
if self.parent_conn.poll():
|
if self.parent_conn.poll():
|
||||||
self.feed_heartbeat_watchdog()
|
self.feed_heartbeat_watchdog()
|
||||||
if "[GPT-Academic] 等待中" in self.chatbot[-1][-1]:
|
if "[GPT-Academic] 等待中" in self.chatbot[-1][-1]:
|
||||||
self.chatbot.pop(-1) # remove the last line
|
self.chatbot.pop(-1) # remove the last line
|
||||||
@@ -153,8 +152,8 @@ class PluginMultiprocessManager:
|
|||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
||||||
if msg.cmd == "interact":
|
if msg.cmd == "interact":
|
||||||
yield from self.overwatch_workdir_file_change()
|
yield from self.overwatch_workdir_file_change()
|
||||||
self.chatbot.append([f"程序抵达用户反馈节点.", msg.content +
|
self.chatbot.append([f"程序抵达用户反馈节点.", msg.content +
|
||||||
"\n\n等待您的进一步指令." +
|
"\n\n等待您的进一步指令." +
|
||||||
"\n\n(1) 一般情况下您不需要说什么, 清空输入区, 然后直接点击“提交”以继续. " +
|
"\n\n(1) 一般情况下您不需要说什么, 清空输入区, 然后直接点击“提交”以继续. " +
|
||||||
"\n\n(2) 如果您需要补充些什么, 输入要反馈的内容, 直接点击“提交”以继续. " +
|
"\n\n(2) 如果您需要补充些什么, 输入要反馈的内容, 直接点击“提交”以继续. " +
|
||||||
"\n\n(3) 如果您想终止程序, 输入exit, 直接点击“提交”以终止AutoGen并解锁. "
|
"\n\n(3) 如果您想终止程序, 输入exit, 直接点击“提交”以终止AutoGen并解锁. "
|
||||||
|
|||||||
@@ -1,457 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from loguru import logger
|
|
||||||
from textwrap import dedent
|
|
||||||
from toolbox import CatchException, update_ui
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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 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.
|
|
||||||
- Only focus on functions declared by `def` keyword. Ignore inline functions. Ignore function calls.
|
|
||||||
|
|
||||||
------------------ Example ------------------
|
|
||||||
INPUT:
|
|
||||||
|
|
||||||
```
|
|
||||||
L0000 |import sys
|
|
||||||
L0001 |import re
|
|
||||||
L0002 |
|
|
||||||
L0003 |def trimmed_format_exc():
|
|
||||||
L0004 | import os
|
|
||||||
L0005 | import traceback
|
|
||||||
L0006 | str = traceback.format_exc()
|
|
||||||
L0007 | current_path = os.getcwd()
|
|
||||||
L0008 | replace_path = "."
|
|
||||||
L0009 | return str.replace(current_path, replace_path)
|
|
||||||
L0010 |
|
|
||||||
L0011 |
|
|
||||||
L0012 |def trimmed_format_exc_markdown():
|
|
||||||
L0013 | ...
|
|
||||||
L0014 | ...
|
|
||||||
```
|
|
||||||
|
|
||||||
OUTPUT:
|
|
||||||
|
|
||||||
```
|
|
||||||
<first_function_end_at>L0009</first_function_end_at>
|
|
||||||
<next_function_begin_from>L0012</next_function_begin_from>
|
|
||||||
```
|
|
||||||
|
|
||||||
------------------ End of Example ------------------
|
|
||||||
|
|
||||||
|
|
||||||
------------------ the real INPUT you need to process NOW ------------------
|
|
||||||
```
|
|
||||||
{THE_TAGGED_CODE}
|
|
||||||
```
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
Be aware:
|
|
||||||
1. You must NOT modify the indent of code.
|
|
||||||
2. You are NOT authorized to change or translate non-comment code, and you are NOT authorized to add empty lines either, toggle qu.
|
|
||||||
3. Use {LANG} to add comments and docstrings. Do NOT translate Chinese that is already in the code.
|
|
||||||
4. Besides adding a docstring, use the ⭐ symbol to annotate the most core and important line of code within the function, explaining its role.
|
|
||||||
|
|
||||||
------------------ Example ------------------
|
|
||||||
INPUT:
|
|
||||||
```
|
|
||||||
L0000 |
|
|
||||||
L0001 |def zip_result(folder):
|
|
||||||
L0002 | t = gen_time_str()
|
|
||||||
L0003 | zip_folder(folder, get_log_folder(), f"result.zip")
|
|
||||||
L0004 | return os.path.join(get_log_folder(), f"result.zip")
|
|
||||||
L0005 |
|
|
||||||
L0006 |
|
|
||||||
```
|
|
||||||
|
|
||||||
OUTPUT:
|
|
||||||
|
|
||||||
<instruction_1_purpose>
|
|
||||||
This function compresses a given folder, and return the path of the resulting `zip` file.
|
|
||||||
</instruction_1_purpose>
|
|
||||||
<instruction_2_revised_code>
|
|
||||||
```
|
|
||||||
def zip_result(folder):
|
|
||||||
"""
|
|
||||||
Compresses the specified folder into a zip file and stores it in the log folder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
folder (str): The path to the folder that needs to be compressed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The path to the created zip file in the log folder.
|
|
||||||
"""
|
|
||||||
t = gen_time_str()
|
|
||||||
zip_folder(folder, get_log_folder(), f"result.zip") # ⭐ Execute the zipping of folder
|
|
||||||
return os.path.join(get_log_folder(), f"result.zip")
|
|
||||||
```
|
|
||||||
</instruction_2_revised_code>
|
|
||||||
------------------ End of Example ------------------
|
|
||||||
|
|
||||||
|
|
||||||
------------------ the real INPUT you need to process NOW ({FILE_BASENAME}) ------------------
|
|
||||||
```
|
|
||||||
{THE_CODE}
|
|
||||||
```
|
|
||||||
{INDENT_REMINDER}
|
|
||||||
{BRIEF_REMINDER}
|
|
||||||
{HINT_REMINDER}
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
revise_function_prompt_chinese = '''
|
|
||||||
您需要阅读以下代码,并根据以下说明修订源代码({FILE_BASENAME}):
|
|
||||||
1. 如果源代码中包含函数的话, 你应该分析给定函数实现了什么功能
|
|
||||||
2. 如果源代码中包含函数的话, 你需要为函数添加docstring, docstring必须使用中文
|
|
||||||
|
|
||||||
请注意:
|
|
||||||
1. 你不得修改代码的缩进
|
|
||||||
2. 你无权更改或翻译代码中的非注释部分,也不允许添加空行
|
|
||||||
3. 使用 {LANG} 添加注释和文档字符串。不要翻译代码中已有的中文
|
|
||||||
4. 除了添加docstring之外, 使用⭐符号给该函数中最核心、最重要的一行代码添加注释,并说明其作用
|
|
||||||
|
|
||||||
------------------ 示例 ------------------
|
|
||||||
INPUT:
|
|
||||||
```
|
|
||||||
L0000 |
|
|
||||||
L0001 |def zip_result(folder):
|
|
||||||
L0002 | t = gen_time_str()
|
|
||||||
L0003 | zip_folder(folder, get_log_folder(), f"result.zip")
|
|
||||||
L0004 | return os.path.join(get_log_folder(), f"result.zip")
|
|
||||||
L0005 |
|
|
||||||
L0006 |
|
|
||||||
```
|
|
||||||
|
|
||||||
OUTPUT:
|
|
||||||
|
|
||||||
<instruction_1_purpose>
|
|
||||||
该函数用于压缩指定文件夹,并返回生成的`zip`文件的路径。
|
|
||||||
</instruction_1_purpose>
|
|
||||||
<instruction_2_revised_code>
|
|
||||||
```
|
|
||||||
def zip_result(folder):
|
|
||||||
"""
|
|
||||||
该函数将指定的文件夹压缩成ZIP文件, 并将其存储在日志文件夹中。
|
|
||||||
|
|
||||||
输入参数:
|
|
||||||
folder (str): 需要压缩的文件夹的路径。
|
|
||||||
返回值:
|
|
||||||
str: 日志文件夹中创建的ZIP文件的路径。
|
|
||||||
"""
|
|
||||||
t = gen_time_str()
|
|
||||||
zip_folder(folder, get_log_folder(), f"result.zip") # ⭐ 执行文件夹的压缩
|
|
||||||
return os.path.join(get_log_folder(), f"result.zip")
|
|
||||||
```
|
|
||||||
</instruction_2_revised_code>
|
|
||||||
------------------ End of Example ------------------
|
|
||||||
|
|
||||||
|
|
||||||
------------------ the real INPUT you need to process NOW ({FILE_BASENAME}) ------------------
|
|
||||||
```
|
|
||||||
{THE_CODE}
|
|
||||||
```
|
|
||||||
{INDENT_REMINDER}
|
|
||||||
{BRIEF_REMINDER}
|
|
||||||
{HINT_REMINDER}
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
class PythonCodeComment():
|
|
||||||
|
|
||||||
def __init__(self, llm_kwargs, plugin_kwargs, language, observe_window_update) -> None:
|
|
||||||
self.original_content = ""
|
|
||||||
self.full_context = []
|
|
||||||
self.full_context_with_line_no = []
|
|
||||||
self.current_page_start = 0
|
|
||||||
self.page_limit = 100 # 100 lines of code each page
|
|
||||||
self.ignore_limit = 20
|
|
||||||
self.llm_kwargs = llm_kwargs
|
|
||||||
self.plugin_kwargs = plugin_kwargs
|
|
||||||
self.language = language
|
|
||||||
self.observe_window_update = observe_window_update
|
|
||||||
if self.language == "chinese":
|
|
||||||
self.core_prompt = revise_function_prompt_chinese
|
|
||||||
else:
|
|
||||||
self.core_prompt = revise_function_prompt
|
|
||||||
self.path = None
|
|
||||||
self.file_basename = None
|
|
||||||
self.file_brief = ""
|
|
||||||
|
|
||||||
def generate_tagged_code_from_full_context(self):
|
|
||||||
for i, code in enumerate(self.full_context):
|
|
||||||
number = i
|
|
||||||
padded_number = f"{number:04}"
|
|
||||||
result = f"L{padded_number}"
|
|
||||||
self.full_context_with_line_no.append(f"{result} | {code}")
|
|
||||||
return self.full_context_with_line_no
|
|
||||||
|
|
||||||
def read_file(self, path, brief):
|
|
||||||
with open(path, 'r', encoding='utf8') as f:
|
|
||||||
self.full_context = f.readlines()
|
|
||||||
self.original_content = ''.join(self.full_context)
|
|
||||||
self.file_basename = os.path.basename(path)
|
|
||||||
self.file_brief = brief
|
|
||||||
self.full_context_with_line_no = self.generate_tagged_code_from_full_context()
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
def find_next_function_begin(self, tagged_code:list, begin_and_end):
|
|
||||||
begin, end = begin_and_end
|
|
||||||
THE_TAGGED_CODE = ''.join(tagged_code)
|
|
||||||
self.llm_kwargs['temperature'] = 0
|
|
||||||
result = predict_no_ui_long_connection(
|
|
||||||
inputs=find_function_end_prompt.format(THE_TAGGED_CODE=THE_TAGGED_CODE),
|
|
||||||
llm_kwargs=self.llm_kwargs,
|
|
||||||
history=[],
|
|
||||||
sys_prompt="",
|
|
||||||
observe_window=[],
|
|
||||||
console_silence=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract_number(text):
|
|
||||||
# 使用正则表达式匹配模式
|
|
||||||
match = re.search(r'<next_function_begin_from>L(\d+)</next_function_begin_from>', text)
|
|
||||||
if match:
|
|
||||||
# 提取匹配的数字部分并转换为整数
|
|
||||||
return int(match.group(1))
|
|
||||||
return None
|
|
||||||
|
|
||||||
line_no = extract_number(result)
|
|
||||||
if line_no is not None:
|
|
||||||
return line_no
|
|
||||||
else:
|
|
||||||
return end
|
|
||||||
|
|
||||||
def _get_next_window(self):
|
|
||||||
#
|
|
||||||
current_page_start = self.current_page_start
|
|
||||||
|
|
||||||
if self.current_page_start == len(self.full_context) + 1:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
# 如果剩余的行数非常少,一鼓作气处理掉
|
|
||||||
if len(self.full_context) - self.current_page_start < self.ignore_limit:
|
|
||||||
future_page_start = len(self.full_context) + 1
|
|
||||||
self.current_page_start = future_page_start
|
|
||||||
return current_page_start, future_page_start
|
|
||||||
|
|
||||||
|
|
||||||
tagged_code = self.full_context_with_line_no[ self.current_page_start: self.current_page_start + self.page_limit]
|
|
||||||
line_no = self.find_next_function_begin(tagged_code, [self.current_page_start, self.current_page_start + self.page_limit])
|
|
||||||
|
|
||||||
if line_no > len(self.full_context) - 5:
|
|
||||||
line_no = len(self.full_context) + 1
|
|
||||||
|
|
||||||
future_page_start = line_no
|
|
||||||
self.current_page_start = future_page_start
|
|
||||||
|
|
||||||
# ! consider eof
|
|
||||||
return current_page_start, future_page_start
|
|
||||||
|
|
||||||
def dedent(self, text):
|
|
||||||
"""Remove any common leading whitespace from every line in `text`.
|
|
||||||
"""
|
|
||||||
# Look for the longest leading string of spaces and tabs common to
|
|
||||||
# all lines.
|
|
||||||
margin = None
|
|
||||||
_whitespace_only_re = re.compile('^[ \t]+$', re.MULTILINE)
|
|
||||||
_leading_whitespace_re = re.compile('(^[ \t]*)(?:[^ \t\n])', re.MULTILINE)
|
|
||||||
text = _whitespace_only_re.sub('', text)
|
|
||||||
indents = _leading_whitespace_re.findall(text)
|
|
||||||
for indent in indents:
|
|
||||||
if margin is None:
|
|
||||||
margin = indent
|
|
||||||
|
|
||||||
# Current line more deeply indented than previous winner:
|
|
||||||
# no change (previous winner is still on top).
|
|
||||||
elif indent.startswith(margin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Current line consistent with and no deeper than previous winner:
|
|
||||||
# it's the new winner.
|
|
||||||
elif margin.startswith(indent):
|
|
||||||
margin = indent
|
|
||||||
|
|
||||||
# Find the largest common whitespace between current line and previous
|
|
||||||
# winner.
|
|
||||||
else:
|
|
||||||
for i, (x, y) in enumerate(zip(margin, indent)):
|
|
||||||
if x != y:
|
|
||||||
margin = margin[:i]
|
|
||||||
break
|
|
||||||
|
|
||||||
# sanity check (testing/debugging only)
|
|
||||||
if 0 and margin:
|
|
||||||
for line in text.split("\n"):
|
|
||||||
assert not line or line.startswith(margin), \
|
|
||||||
"line = %r, margin = %r" % (line, margin)
|
|
||||||
|
|
||||||
if margin:
|
|
||||||
text = re.sub(r'(?m)^' + margin, '', text)
|
|
||||||
return text, len(margin)
|
|
||||||
else:
|
|
||||||
return text, 0
|
|
||||||
|
|
||||||
def get_next_batch(self):
|
|
||||||
current_page_start, future_page_start = self._get_next_window()
|
|
||||||
return ''.join(self.full_context[current_page_start: future_page_start]), current_page_start, future_page_start
|
|
||||||
|
|
||||||
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 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
|
|
||||||
result = predict_no_ui_long_connection(
|
|
||||||
inputs=self.core_prompt.format(
|
|
||||||
LANG=self.language,
|
|
||||||
FILE_BASENAME=self.file_basename,
|
|
||||||
THE_CODE=code,
|
|
||||||
INDENT_REMINDER=indent_reminder,
|
|
||||||
BRIEF_REMINDER=brief_reminder,
|
|
||||||
HINT_REMINDER=hint_reminder
|
|
||||||
),
|
|
||||||
llm_kwargs=self.llm_kwargs,
|
|
||||||
history=[],
|
|
||||||
sys_prompt="",
|
|
||||||
observe_window=[],
|
|
||||||
console_silence=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_code_block(reply):
|
|
||||||
import re
|
|
||||||
pattern = r"```([\s\S]*?)```" # regex pattern to match code blocks
|
|
||||||
matches = re.findall(pattern, reply) # find all code blocks in text
|
|
||||||
if len(matches) == 1:
|
|
||||||
return matches[0].strip('python') # code block
|
|
||||||
return None
|
|
||||||
|
|
||||||
code_block = get_code_block(result)
|
|
||||||
if code_block is not None:
|
|
||||||
code_block = self.sync_and_patch(original=code, revised=code_block)
|
|
||||||
return code_block
|
|
||||||
else:
|
|
||||||
return code
|
|
||||||
|
|
||||||
def get_markdown_block_in_html(self, html):
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
soup = BeautifulSoup(html, 'lxml')
|
|
||||||
found_list = soup.find_all("div", class_="markdown-body")
|
|
||||||
if found_list:
|
|
||||||
res = found_list[0]
|
|
||||||
return res.prettify()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def sync_and_patch(self, original, revised):
|
|
||||||
"""Ensure the number of pre-string empty lines in revised matches those in original."""
|
|
||||||
|
|
||||||
def count_leading_empty_lines(s, reverse=False):
|
|
||||||
"""Count the number of leading empty lines in a string."""
|
|
||||||
lines = s.split('\n')
|
|
||||||
if reverse: lines = list(reversed(lines))
|
|
||||||
count = 0
|
|
||||||
for line in lines:
|
|
||||||
if line.strip() == '':
|
|
||||||
count += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return count
|
|
||||||
|
|
||||||
original_empty_lines = count_leading_empty_lines(original)
|
|
||||||
revised_empty_lines = count_leading_empty_lines(revised)
|
|
||||||
|
|
||||||
if original_empty_lines > revised_empty_lines:
|
|
||||||
additional_lines = '\n' * (original_empty_lines - revised_empty_lines)
|
|
||||||
revised = additional_lines + revised
|
|
||||||
elif original_empty_lines < revised_empty_lines:
|
|
||||||
lines = revised.split('\n')
|
|
||||||
revised = '\n'.join(lines[revised_empty_lines - original_empty_lines:])
|
|
||||||
|
|
||||||
original_empty_lines = count_leading_empty_lines(original, reverse=True)
|
|
||||||
revised_empty_lines = count_leading_empty_lines(revised, reverse=True)
|
|
||||||
|
|
||||||
if original_empty_lines > revised_empty_lines:
|
|
||||||
additional_lines = '\n' * (original_empty_lines - revised_empty_lines)
|
|
||||||
revised = revised + additional_lines
|
|
||||||
elif original_empty_lines < revised_empty_lines:
|
|
||||||
lines = revised.split('\n')
|
|
||||||
revised = '\n'.join(lines[:-(revised_empty_lines - original_empty_lines)])
|
|
||||||
|
|
||||||
return revised
|
|
||||||
|
|
||||||
def begin_comment_source_code(self, chatbot=None, history=None):
|
|
||||||
# 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'
|
|
||||||
|
|
||||||
write_content = ""
|
|
||||||
# with open(self.path + '.revised.py', 'w+', encoding='utf8') as f:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
# 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_latest_msg(f"({self.file_basename}) 处理代码片段:\n\n{next_batch}", chatbot=chatbot, history=history, delay=0)
|
|
||||||
|
|
||||||
hint = None
|
|
||||||
MAX_ATTEMPT = 2
|
|
||||||
for attempt in range(MAX_ATTEMPT):
|
|
||||||
result = self.tag_code(next_batch, hint)
|
|
||||||
try:
|
|
||||||
successful, hint = self.verify_successful(next_batch, result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error('ignored exception:\n' + str(e))
|
|
||||||
break
|
|
||||||
if successful:
|
|
||||||
break
|
|
||||||
if attempt == MAX_ATTEMPT - 1:
|
|
||||||
# cannot deal with this, give up
|
|
||||||
result = next_batch
|
|
||||||
break
|
|
||||||
|
|
||||||
# f.write(result)
|
|
||||||
write_content += result
|
|
||||||
except StopIteration:
|
|
||||||
next_batch, line_no_start, line_no_end = [], -1, -1
|
|
||||||
return None, write_content
|
|
||||||
|
|
||||||
def verify_successful(self, original, revised):
|
|
||||||
""" Determine whether the revised code contains every line that already exists
|
|
||||||
"""
|
|
||||||
from crazy_functions.ast_fns.comment_remove import remove_python_comments
|
|
||||||
original = remove_python_comments(original)
|
|
||||||
original_lines = original.split('\n')
|
|
||||||
revised_lines = revised.split('\n')
|
|
||||||
|
|
||||||
for l in original_lines:
|
|
||||||
l = l.strip()
|
|
||||||
if '\'' in l or '\"' in l: continue # ast sometimes toggle " to '
|
|
||||||
found = False
|
|
||||||
for lt in revised_lines:
|
|
||||||
if l in lt:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
return False, l
|
|
||||||
return True, None
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<style>ADVANCED_CSS</style>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>源文件对比</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
width: 95%;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
.code-container {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="code-container">
|
|
||||||
REPLACE_CODE_FILE_LEFT
|
|
||||||
</div>
|
|
||||||
<div class="code-container">
|
|
||||||
REPLACE_CODE_FILE_RIGHT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import threading, time
|
import threading, time
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
class WatchDog():
|
class WatchDog():
|
||||||
def __init__(self, timeout, bark_fn, interval=3, msg="") -> None:
|
def __init__(self, timeout, bark_fn, interval=3, msg="") -> None:
|
||||||
@@ -9,12 +8,12 @@ class WatchDog():
|
|||||||
self.interval = interval
|
self.interval = interval
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.kill_dog = False
|
self.kill_dog = False
|
||||||
|
|
||||||
def watch(self):
|
def watch(self):
|
||||||
while True:
|
while True:
|
||||||
if self.kill_dog: break
|
if self.kill_dog: break
|
||||||
if time.time() - self.last_feed > self.timeout:
|
if time.time() - self.last_feed > self.timeout:
|
||||||
if len(self.msg) > 0: logger.info(self.msg)
|
if len(self.msg) > 0: print(self.msg)
|
||||||
self.bark_fn()
|
self.bark_fn()
|
||||||
break
|
break
|
||||||
time.sleep(self.interval)
|
time.sleep(self.interval)
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import token
|
|
||||||
import tokenize
|
|
||||||
import copy
|
|
||||||
import io
|
|
||||||
|
|
||||||
|
|
||||||
def remove_python_comments(input_source: str) -> str:
|
|
||||||
source_flag = copy.copy(input_source)
|
|
||||||
source = io.StringIO(input_source)
|
|
||||||
ls = input_source.split('\n')
|
|
||||||
prev_toktype = token.INDENT
|
|
||||||
readline = source.readline
|
|
||||||
|
|
||||||
def get_char_index(lineno, col):
|
|
||||||
# find the index of the char in the source code
|
|
||||||
if lineno == 1:
|
|
||||||
return len('\n'.join(ls[:(lineno-1)])) + col
|
|
||||||
else:
|
|
||||||
return len('\n'.join(ls[:(lineno-1)])) + col + 1
|
|
||||||
|
|
||||||
def replace_char_between(start_lineno, start_col, end_lineno, end_col, source, replace_char, ls):
|
|
||||||
# replace char between start_lineno, start_col and end_lineno, end_col with replace_char, but keep '\n' and ' '
|
|
||||||
b = get_char_index(start_lineno, start_col)
|
|
||||||
e = get_char_index(end_lineno, end_col)
|
|
||||||
for i in range(b, e):
|
|
||||||
if source[i] == '\n':
|
|
||||||
source = source[:i] + '\n' + source[i+1:]
|
|
||||||
elif source[i] == ' ':
|
|
||||||
source = source[:i] + ' ' + source[i+1:]
|
|
||||||
else:
|
|
||||||
source = source[:i] + replace_char + source[i+1:]
|
|
||||||
return source
|
|
||||||
|
|
||||||
tokgen = tokenize.generate_tokens(readline)
|
|
||||||
for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen:
|
|
||||||
if toktype == token.STRING and (prev_toktype == token.INDENT):
|
|
||||||
source_flag = replace_char_between(slineno, scol, elineno, ecol, source_flag, ' ', ls)
|
|
||||||
elif toktype == token.STRING and (prev_toktype == token.NEWLINE):
|
|
||||||
source_flag = replace_char_between(slineno, scol, elineno, ecol, source_flag, ' ', ls)
|
|
||||||
elif toktype == tokenize.COMMENT:
|
|
||||||
source_flag = replace_char_between(slineno, scol, elineno, ecol, source_flag, ' ', ls)
|
|
||||||
prev_toktype = toktype
|
|
||||||
return source_flag
|
|
||||||
|
|
||||||
|
|
||||||
# 示例使用
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open("source.py", "r", encoding="utf-8") as f:
|
|
||||||
source_code = f.read()
|
|
||||||
|
|
||||||
cleaned_code = remove_python_comments(source_code)
|
|
||||||
|
|
||||||
with open("cleaned_source.py", "w", encoding="utf-8") as f:
|
|
||||||
f.write(cleaned_code)
|
|
||||||
141
crazy_functions/chatglm微调工具.py
Normal file
141
crazy_functions/chatglm微调工具.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
from toolbox import CatchException, update_ui, promote_file_to_downloadzone
|
||||||
|
from .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, web_port):
|
||||||
|
"""
|
||||||
|
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
||||||
|
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
||||||
|
plugin_kwargs 插件模型的参数
|
||||||
|
chatbot 聊天显示框的句柄,用于显示给用户
|
||||||
|
history 聊天历史,前情提要
|
||||||
|
system_prompt 给gpt的静默提醒
|
||||||
|
web_port 当前软件运行的端口号
|
||||||
|
"""
|
||||||
|
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, web_port):
|
||||||
|
"""
|
||||||
|
txt 输入栏用户输入的文本,例如需要翻译的一段话,再例如一个包含了待处理文件的路径
|
||||||
|
llm_kwargs gpt模型参数,如温度和top_p等,一般原样传递下去就行
|
||||||
|
plugin_kwargs 插件模型的参数
|
||||||
|
chatbot 聊天显示框的句柄,用于显示给用户
|
||||||
|
history 聊天历史,前情提要
|
||||||
|
system_prompt 给gpt的静默提醒
|
||||||
|
web_port 当前软件运行的端口号
|
||||||
|
"""
|
||||||
|
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,41 +1,27 @@
|
|||||||
import os
|
|
||||||
import threading
|
|
||||||
from loguru import logger
|
|
||||||
from shared_utils.char_visual_effect import scrolling_visual_effect
|
|
||||||
from toolbox import update_ui, get_conf, trimmed_format_exc, get_max_token, Singleton
|
from toolbox import update_ui, get_conf, trimmed_format_exc, get_max_token, Singleton
|
||||||
|
import threading
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
def input_clipping(inputs, history, max_token_limit, return_clip_flags=False):
|
def input_clipping(inputs, history, max_token_limit):
|
||||||
"""
|
|
||||||
当输入文本 + 历史文本超出最大限制时,采取措施丢弃一部分文本。
|
|
||||||
输入:
|
|
||||||
- inputs 本次请求
|
|
||||||
- history 历史上下文
|
|
||||||
- max_token_limit 最大token限制
|
|
||||||
输出:
|
|
||||||
- inputs 本次请求(经过clip)
|
|
||||||
- history 历史上下文(经过clip)
|
|
||||||
"""
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from request_llms.bridge_all import model_info
|
from request_llms.bridge_all import model_info
|
||||||
enc = model_info["gpt-3.5-turbo"]['tokenizer']
|
enc = model_info["gpt-3.5-turbo"]['tokenizer']
|
||||||
def get_token_num(txt): return len(enc.encode(txt, disallowed_special=()))
|
def get_token_num(txt): return len(enc.encode(txt, disallowed_special=()))
|
||||||
|
|
||||||
|
|
||||||
mode = 'input-and-history'
|
mode = 'input-and-history'
|
||||||
# 当 输入部分的token占比 小于 全文的一半时,只裁剪历史
|
# 当 输入部分的token占比 小于 全文的一半时,只裁剪历史
|
||||||
input_token_num = get_token_num(inputs)
|
input_token_num = get_token_num(inputs)
|
||||||
original_input_len = len(inputs)
|
if input_token_num < max_token_limit//2:
|
||||||
if input_token_num < max_token_limit//2:
|
|
||||||
mode = 'only-history'
|
mode = 'only-history'
|
||||||
max_token_limit = max_token_limit - input_token_num
|
max_token_limit = max_token_limit - input_token_num
|
||||||
|
|
||||||
everything = [inputs] if mode == 'input-and-history' else ['']
|
everything = [inputs] if mode == 'input-and-history' else ['']
|
||||||
everything.extend(history)
|
everything.extend(history)
|
||||||
full_token_num = n_token = get_token_num('\n'.join(everything))
|
n_token = get_token_num('\n'.join(everything))
|
||||||
everything_token = [get_token_num(e) for e in everything]
|
everything_token = [get_token_num(e) for e in everything]
|
||||||
everything_token_num = sum(everything_token)
|
|
||||||
delta = max(everything_token) // 16 # 截断时的颗粒度
|
delta = max(everything_token) // 16 # 截断时的颗粒度
|
||||||
|
|
||||||
while n_token > max_token_limit:
|
while n_token > max_token_limit:
|
||||||
where = np.argmax(everything_token)
|
where = np.argmax(everything_token)
|
||||||
encoded = enc.encode(everything[where], disallowed_special=())
|
encoded = enc.encode(everything[where], disallowed_special=())
|
||||||
@@ -46,29 +32,15 @@ def input_clipping(inputs, history, max_token_limit, return_clip_flags=False):
|
|||||||
|
|
||||||
if mode == 'input-and-history':
|
if mode == 'input-and-history':
|
||||||
inputs = everything[0]
|
inputs = everything[0]
|
||||||
full_token_num = everything_token_num
|
|
||||||
else:
|
else:
|
||||||
full_token_num = everything_token_num + input_token_num
|
pass
|
||||||
|
|
||||||
history = everything[1:]
|
history = everything[1:]
|
||||||
|
return inputs, history
|
||||||
flags = {
|
|
||||||
"mode": mode,
|
|
||||||
"original_input_token_num": input_token_num,
|
|
||||||
"original_full_token_num": full_token_num,
|
|
||||||
"original_input_len": original_input_len,
|
|
||||||
"clipped_input_len": len(inputs),
|
|
||||||
}
|
|
||||||
|
|
||||||
if not return_clip_flags:
|
|
||||||
return inputs, history
|
|
||||||
else:
|
|
||||||
return inputs, history, flags
|
|
||||||
|
|
||||||
def request_gpt_model_in_new_thread_with_ui_alive(
|
def request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs, inputs_show_user, llm_kwargs,
|
inputs, inputs_show_user, llm_kwargs,
|
||||||
chatbot, history, sys_prompt, refresh_interval=0.2,
|
chatbot, history, sys_prompt, refresh_interval=0.2,
|
||||||
handle_token_exceed=True,
|
handle_token_exceed=True,
|
||||||
retry_times_at_unknown_error=2,
|
retry_times_at_unknown_error=2,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -105,7 +77,7 @@ def request_gpt_model_in_new_thread_with_ui_alive(
|
|||||||
exceeded_cnt = 0
|
exceeded_cnt = 0
|
||||||
while True:
|
while True:
|
||||||
# watchdog error
|
# watchdog error
|
||||||
if len(mutable) >= 2 and (time.time()-mutable[1]) > watch_dog_patience:
|
if len(mutable) >= 2 and (time.time()-mutable[1]) > watch_dog_patience:
|
||||||
raise RuntimeError("检测到程序终止。")
|
raise RuntimeError("检测到程序终止。")
|
||||||
try:
|
try:
|
||||||
# 【第一种情况】:顺利完成
|
# 【第一种情况】:顺利完成
|
||||||
@@ -133,7 +105,7 @@ def request_gpt_model_in_new_thread_with_ui_alive(
|
|||||||
except:
|
except:
|
||||||
# 【第三种情况】:其他错误:重试几次
|
# 【第三种情况】:其他错误:重试几次
|
||||||
tb_str = '```\n' + trimmed_format_exc() + '```'
|
tb_str = '```\n' + trimmed_format_exc() + '```'
|
||||||
logger.error(tb_str)
|
print(tb_str)
|
||||||
mutable[0] += f"[Local Message] 警告,在执行过程中遭遇问题, Traceback:\n\n{tb_str}\n\n"
|
mutable[0] += f"[Local Message] 警告,在执行过程中遭遇问题, Traceback:\n\n{tb_str}\n\n"
|
||||||
if retry_op > 0:
|
if retry_op > 0:
|
||||||
retry_op -= 1
|
retry_op -= 1
|
||||||
@@ -163,31 +135,18 @@ def request_gpt_model_in_new_thread_with_ui_alive(
|
|||||||
yield from update_ui(chatbot=chatbot, history=[]) # 如果最后成功了,则删除报错信息
|
yield from update_ui(chatbot=chatbot, history=[]) # 如果最后成功了,则删除报错信息
|
||||||
return final_result
|
return final_result
|
||||||
|
|
||||||
def can_multi_process(llm) -> bool:
|
def can_multi_process(llm):
|
||||||
from request_llms.bridge_all import model_info
|
if llm.startswith('gpt-'): return True
|
||||||
|
if llm.startswith('api2d-'): return True
|
||||||
def default_condition(llm) -> bool:
|
if llm.startswith('azure-'): return True
|
||||||
# legacy condition
|
if llm.startswith('spark'): return True
|
||||||
if llm.startswith('gpt-'): return True
|
if llm.startswith('zhipuai'): return True
|
||||||
if llm.startswith('chatgpt-'): return True
|
return False
|
||||||
if llm.startswith('api2d-'): return True
|
|
||||||
if llm.startswith('azure-'): return True
|
|
||||||
if llm.startswith('spark'): return True
|
|
||||||
if llm.startswith('zhipuai') or llm.startswith('glm-'): return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if llm in model_info:
|
|
||||||
if 'can_multi_thread' in model_info[llm]:
|
|
||||||
return model_info[llm]['can_multi_thread']
|
|
||||||
else:
|
|
||||||
return default_condition(llm)
|
|
||||||
else:
|
|
||||||
return default_condition(llm)
|
|
||||||
|
|
||||||
def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
||||||
inputs_array, inputs_show_user_array, llm_kwargs,
|
inputs_array, inputs_show_user_array, llm_kwargs,
|
||||||
chatbot, history_array, sys_prompt_array,
|
chatbot, history_array, sys_prompt_array,
|
||||||
refresh_interval=0.2, max_workers=-1, scroller_max_len=75,
|
refresh_interval=0.2, max_workers=-1, scroller_max_len=30,
|
||||||
handle_token_exceed=True, show_user_at_complete=False,
|
handle_token_exceed=True, show_user_at_complete=False,
|
||||||
retry_times_at_unknown_error=2,
|
retry_times_at_unknown_error=2,
|
||||||
):
|
):
|
||||||
@@ -230,7 +189,7 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
# 屏蔽掉 chatglm的多线程,可能会导致严重卡顿
|
# 屏蔽掉 chatglm的多线程,可能会导致严重卡顿
|
||||||
if not can_multi_process(llm_kwargs['llm_model']):
|
if not can_multi_process(llm_kwargs['llm_model']):
|
||||||
max_workers = 1
|
max_workers = 1
|
||||||
|
|
||||||
executor = ThreadPoolExecutor(max_workers=max_workers)
|
executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||||
n_frag = len(inputs_array)
|
n_frag = len(inputs_array)
|
||||||
# 用户反馈
|
# 用户反馈
|
||||||
@@ -255,8 +214,8 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
try:
|
try:
|
||||||
# 【第一种情况】:顺利完成
|
# 【第一种情况】:顺利完成
|
||||||
gpt_say = predict_no_ui_long_connection(
|
gpt_say = predict_no_ui_long_connection(
|
||||||
inputs=inputs, llm_kwargs=llm_kwargs, history=history,
|
inputs=inputs, llm_kwargs=llm_kwargs, history=history,
|
||||||
sys_prompt=sys_prompt, observe_window=mutable[index], console_silence=True
|
sys_prompt=sys_prompt, observe_window=mutable[index], console_slience=True
|
||||||
)
|
)
|
||||||
mutable[index][2] = "已成功"
|
mutable[index][2] = "已成功"
|
||||||
return gpt_say
|
return gpt_say
|
||||||
@@ -284,10 +243,10 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
# 【第三种情况】:其他错误
|
# 【第三种情况】:其他错误
|
||||||
if detect_timeout(): raise RuntimeError("检测到程序终止。")
|
if detect_timeout(): raise RuntimeError("检测到程序终止。")
|
||||||
tb_str = '```\n' + trimmed_format_exc() + '```'
|
tb_str = '```\n' + trimmed_format_exc() + '```'
|
||||||
logger.error(tb_str)
|
print(tb_str)
|
||||||
gpt_say += f"[Local Message] 警告,线程{index}在执行过程中遭遇问题, Traceback:\n\n{tb_str}\n\n"
|
gpt_say += f"[Local Message] 警告,线程{index}在执行过程中遭遇问题, Traceback:\n\n{tb_str}\n\n"
|
||||||
if len(mutable[index][0]) > 0: gpt_say += "此线程失败前收到的回答:\n\n" + mutable[index][0]
|
if len(mutable[index][0]) > 0: gpt_say += "此线程失败前收到的回答:\n\n" + mutable[index][0]
|
||||||
if retry_op > 0:
|
if retry_op > 0:
|
||||||
retry_op -= 1
|
retry_op -= 1
|
||||||
wait = random.randint(5, 20)
|
wait = random.randint(5, 20)
|
||||||
if ("Rate limit reached" in tb_str) or ("Too Many Requests" in tb_str):
|
if ("Rate limit reached" in tb_str) or ("Too Many Requests" in tb_str):
|
||||||
@@ -312,8 +271,6 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
futures = [executor.submit(_req_gpt, index, inputs, history, sys_prompt) for index, inputs, history, sys_prompt in zip(
|
futures = [executor.submit(_req_gpt, index, inputs, history, sys_prompt) for index, inputs, history, sys_prompt in zip(
|
||||||
range(len(inputs_array)), inputs_array, history_array, sys_prompt_array)]
|
range(len(inputs_array)), inputs_array, history_array, sys_prompt_array)]
|
||||||
cnt = 0
|
cnt = 0
|
||||||
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# yield一次以刷新前端页面
|
# yield一次以刷新前端页面
|
||||||
time.sleep(refresh_interval)
|
time.sleep(refresh_interval)
|
||||||
@@ -326,11 +283,13 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
mutable[thread_index][1] = time.time()
|
mutable[thread_index][1] = time.time()
|
||||||
# 在前端打印些好玩的东西
|
# 在前端打印些好玩的东西
|
||||||
for thread_index, _ in enumerate(worker_done):
|
for thread_index, _ in enumerate(worker_done):
|
||||||
print_something_really_funny = f"[ ...`{scrolling_visual_effect(mutable[thread_index][0], scroller_max_len)}`... ]"
|
print_something_really_funny = "[ ...`"+mutable[thread_index][0][-scroller_max_len:].\
|
||||||
|
replace('\n', '').replace('`', '.').replace(
|
||||||
|
' ', '.').replace('<br/>', '.....').replace('$', '.')+"`... ]"
|
||||||
observe_win.append(print_something_really_funny)
|
observe_win.append(print_something_really_funny)
|
||||||
# 在前端打印些好玩的东西
|
# 在前端打印些好玩的东西
|
||||||
stat_str = ''.join([f'`{mutable[thread_index][2]}`: {obs}\n\n'
|
stat_str = ''.join([f'`{mutable[thread_index][2]}`: {obs}\n\n'
|
||||||
if not done else f'`{mutable[thread_index][2]}`\n\n'
|
if not done else f'`{mutable[thread_index][2]}`\n\n'
|
||||||
for thread_index, done, obs in zip(range(len(worker_done)), worker_done, observe_win)])
|
for thread_index, done, obs in zip(range(len(worker_done)), worker_done, observe_win)])
|
||||||
# 在前端打印些好玩的东西
|
# 在前端打印些好玩的东西
|
||||||
chatbot[-1] = [chatbot[-1][0], f'多线程操作已经开始,完成情况: \n\n{stat_str}' + ''.join(['.']*(cnt % 10+1))]
|
chatbot[-1] = [chatbot[-1][0], f'多线程操作已经开始,完成情况: \n\n{stat_str}' + ''.join(['.']*(cnt % 10+1))]
|
||||||
@@ -344,7 +303,7 @@ def request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency(
|
|||||||
for inputs_show_user, f in zip(inputs_show_user_array, futures):
|
for inputs_show_user, f in zip(inputs_show_user_array, futures):
|
||||||
gpt_res = f.result()
|
gpt_res = f.result()
|
||||||
gpt_response_collection.extend([inputs_show_user, gpt_res])
|
gpt_response_collection.extend([inputs_show_user, gpt_res])
|
||||||
|
|
||||||
# 是否在结束时,在界面上显示结果
|
# 是否在结束时,在界面上显示结果
|
||||||
if show_user_at_complete:
|
if show_user_at_complete:
|
||||||
for inputs_show_user, f in zip(inputs_show_user_array, futures):
|
for inputs_show_user, f in zip(inputs_show_user_array, futures):
|
||||||
@@ -379,7 +338,7 @@ def read_and_clean_pdf_text(fp):
|
|||||||
import fitz, copy
|
import fitz, copy
|
||||||
import re
|
import re
|
||||||
import numpy as np
|
import numpy as np
|
||||||
# from shared_utils.colorful import print亮黄, print亮绿
|
from colorful import print亮黄, print亮绿
|
||||||
fc = 0 # Index 0 文本
|
fc = 0 # Index 0 文本
|
||||||
fs = 1 # Index 1 字体
|
fs = 1 # Index 1 字体
|
||||||
fb = 2 # Index 2 框框
|
fb = 2 # Index 2 框框
|
||||||
@@ -389,12 +348,12 @@ def read_and_clean_pdf_text(fp):
|
|||||||
"""
|
"""
|
||||||
提取文本块主字体
|
提取文本块主字体
|
||||||
"""
|
"""
|
||||||
fsize_statistics = {}
|
fsize_statiscs = {}
|
||||||
for wtf in l['spans']:
|
for wtf in l['spans']:
|
||||||
if wtf['size'] not in fsize_statistics: fsize_statistics[wtf['size']] = 0
|
if wtf['size'] not in fsize_statiscs: fsize_statiscs[wtf['size']] = 0
|
||||||
fsize_statistics[wtf['size']] += len(wtf['text'])
|
fsize_statiscs[wtf['size']] += len(wtf['text'])
|
||||||
return max(fsize_statistics, key=fsize_statistics.get)
|
return max(fsize_statiscs, key=fsize_statiscs.get)
|
||||||
|
|
||||||
def ffsize_same(a,b):
|
def ffsize_same(a,b):
|
||||||
"""
|
"""
|
||||||
提取字体大小是否近似相等
|
提取字体大小是否近似相等
|
||||||
@@ -430,14 +389,14 @@ def read_and_clean_pdf_text(fp):
|
|||||||
if index == 0:
|
if index == 0:
|
||||||
page_one_meta = [" ".join(["".join([wtf['text'] for wtf in l['spans']]) for l in t['lines']]).replace(
|
page_one_meta = [" ".join(["".join([wtf['text'] for wtf in l['spans']]) for l in t['lines']]).replace(
|
||||||
'- ', '') for t in text_areas['blocks'] if 'lines' in t]
|
'- ', '') for t in text_areas['blocks'] if 'lines' in t]
|
||||||
|
|
||||||
############################## <第 2 步,获取正文主字体> ##################################
|
############################## <第 2 步,获取正文主字体> ##################################
|
||||||
try:
|
try:
|
||||||
fsize_statistics = {}
|
fsize_statiscs = {}
|
||||||
for span in meta_span:
|
for span in meta_span:
|
||||||
if span[1] not in fsize_statistics: fsize_statistics[span[1]] = 0
|
if span[1] not in fsize_statiscs: fsize_statiscs[span[1]] = 0
|
||||||
fsize_statistics[span[1]] += span[2]
|
fsize_statiscs[span[1]] += span[2]
|
||||||
main_fsize = max(fsize_statistics, key=fsize_statistics.get)
|
main_fsize = max(fsize_statiscs, key=fsize_statiscs.get)
|
||||||
if REMOVE_FOOT_NOTE:
|
if REMOVE_FOOT_NOTE:
|
||||||
give_up_fize_threshold = main_fsize * REMOVE_FOOT_FFSIZE_PERCENT
|
give_up_fize_threshold = main_fsize * REMOVE_FOOT_FFSIZE_PERCENT
|
||||||
except:
|
except:
|
||||||
@@ -446,7 +405,7 @@ def read_and_clean_pdf_text(fp):
|
|||||||
mega_sec = []
|
mega_sec = []
|
||||||
sec = []
|
sec = []
|
||||||
for index, line in enumerate(meta_line):
|
for index, line in enumerate(meta_line):
|
||||||
if index == 0:
|
if index == 0:
|
||||||
sec.append(line[fc])
|
sec.append(line[fc])
|
||||||
continue
|
continue
|
||||||
if REMOVE_FOOT_NOTE:
|
if REMOVE_FOOT_NOTE:
|
||||||
@@ -543,12 +502,12 @@ def get_files_from_everything(txt, type): # type='.md'
|
|||||||
"""
|
"""
|
||||||
这个函数是用来获取指定目录下所有指定类型(如.md)的文件,并且对于网络上的文件,也可以获取它。
|
这个函数是用来获取指定目录下所有指定类型(如.md)的文件,并且对于网络上的文件,也可以获取它。
|
||||||
下面是对每个参数和返回值的说明:
|
下面是对每个参数和返回值的说明:
|
||||||
参数
|
参数
|
||||||
- txt: 路径或网址,表示要搜索的文件或者文件夹路径或网络上的文件。
|
- txt: 路径或网址,表示要搜索的文件或者文件夹路径或网络上的文件。
|
||||||
- type: 字符串,表示要搜索的文件类型。默认是.md。
|
- type: 字符串,表示要搜索的文件类型。默认是.md。
|
||||||
返回值
|
返回值
|
||||||
- success: 布尔值,表示函数是否成功执行。
|
- success: 布尔值,表示函数是否成功执行。
|
||||||
- file_manifest: 文件路径列表,里面包含以指定类型为后缀名的所有文件的绝对路径。
|
- file_manifest: 文件路径列表,里面包含以指定类型为后缀名的所有文件的绝对路径。
|
||||||
- project_folder: 字符串,表示文件所在的文件夹路径。如果是网络上的文件,就是临时文件夹的路径。
|
- project_folder: 字符串,表示文件所在的文件夹路径。如果是网络上的文件,就是临时文件夹的路径。
|
||||||
该函数详细注释已添加,请确认是否满足您的需要。
|
该函数详细注释已添加,请确认是否满足您的需要。
|
||||||
"""
|
"""
|
||||||
@@ -596,23 +555,23 @@ class nougat_interface():
|
|||||||
def nougat_with_timeout(self, command, cwd, timeout=3600):
|
def nougat_with_timeout(self, command, cwd, timeout=3600):
|
||||||
import subprocess
|
import subprocess
|
||||||
from toolbox import ProxyNetworkActivate
|
from toolbox import ProxyNetworkActivate
|
||||||
logger.info(f'正在执行命令 {command}')
|
logging.info(f'正在执行命令 {command}')
|
||||||
with ProxyNetworkActivate("Nougat_Download"):
|
with ProxyNetworkActivate("Nougat_Download"):
|
||||||
process = subprocess.Popen(command, shell=False, cwd=cwd, env=os.environ)
|
process = subprocess.Popen(command, shell=True, cwd=cwd, env=os.environ)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = process.communicate(timeout=timeout)
|
stdout, stderr = process.communicate(timeout=timeout)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
process.kill()
|
process.kill()
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
logger.error("Process timed out!")
|
print("Process timed out!")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def NOUGAT_parse_pdf(self, fp, chatbot, history):
|
def NOUGAT_parse_pdf(self, fp, chatbot, history):
|
||||||
from toolbox import update_ui_latest_msg
|
from toolbox import update_ui_lastest_msg
|
||||||
|
|
||||||
yield from update_ui_latest_msg("正在解析论文, 请稍候。进度:正在排队, 等待线程锁...",
|
yield from update_ui_lastest_msg("正在解析论文, 请稍候。进度:正在排队, 等待线程锁...",
|
||||||
chatbot=chatbot, history=history, delay=0)
|
chatbot=chatbot, history=history, delay=0)
|
||||||
self.threadLock.acquire()
|
self.threadLock.acquire()
|
||||||
import glob, threading, os
|
import glob, threading, os
|
||||||
@@ -620,10 +579,9 @@ class nougat_interface():
|
|||||||
dst = os.path.join(get_log_folder(plugin_name='nougat'), gen_time_str())
|
dst = os.path.join(get_log_folder(plugin_name='nougat'), gen_time_str())
|
||||||
os.makedirs(dst)
|
os.makedirs(dst)
|
||||||
|
|
||||||
yield from update_ui_latest_msg("正在解析论文, 请稍候。进度:正在加载NOUGAT... (提示:首次运行需要花费较长时间下载NOUGAT参数)",
|
yield from update_ui_lastest_msg("正在解析论文, 请稍候。进度:正在加载NOUGAT... (提示:首次运行需要花费较长时间下载NOUGAT参数)",
|
||||||
chatbot=chatbot, history=history, delay=0)
|
chatbot=chatbot, history=history, delay=0)
|
||||||
command = ['nougat', '--out', os.path.abspath(dst), os.path.abspath(fp)]
|
self.nougat_with_timeout(f'nougat --out "{os.path.abspath(dst)}" "{os.path.abspath(fp)}"', os.getcwd(), timeout=3600)
|
||||||
self.nougat_with_timeout(command, cwd=os.getcwd(), timeout=3600)
|
|
||||||
res = glob.glob(os.path.join(dst,'*.mmd'))
|
res = glob.glob(os.path.join(dst,'*.mmd'))
|
||||||
if len(res) == 0:
|
if len(res) == 0:
|
||||||
self.threadLock.release()
|
self.threadLock.release()
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import os
|
|
||||||
from textwrap import indent
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
class FileNode:
|
|
||||||
def __init__(self, name, build_manifest=False):
|
|
||||||
self.name = name
|
|
||||||
self.children = []
|
|
||||||
self.is_leaf = False
|
|
||||||
self.level = 0
|
|
||||||
self.parenting_ship = []
|
|
||||||
self.comment = ""
|
|
||||||
self.comment_maxlen_show = 50
|
|
||||||
self.build_manifest = build_manifest
|
|
||||||
self.manifest = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_linebreaks_at_spaces(string, interval=10):
|
|
||||||
return '\n'.join(string[i:i+interval] for i in range(0, len(string), interval))
|
|
||||||
|
|
||||||
def sanitize_comment(self, comment):
|
|
||||||
if len(comment) > self.comment_maxlen_show: suf = '...'
|
|
||||||
else: suf = ''
|
|
||||||
comment = comment[:self.comment_maxlen_show]
|
|
||||||
comment = comment.replace('\"', '').replace('`', '').replace('\n', '').replace('`', '').replace('$', '')
|
|
||||||
comment = self.add_linebreaks_at_spaces(comment, 10)
|
|
||||||
return '`' + comment + suf + '`'
|
|
||||||
|
|
||||||
def add_file(self, file_path, file_comment):
|
|
||||||
directory_names, file_name = os.path.split(file_path)
|
|
||||||
current_node = self
|
|
||||||
level = 1
|
|
||||||
if directory_names == "":
|
|
||||||
new_node = FileNode(file_name)
|
|
||||||
self.manifest[file_path] = new_node
|
|
||||||
current_node.children.append(new_node)
|
|
||||||
new_node.is_leaf = True
|
|
||||||
new_node.comment = self.sanitize_comment(file_comment)
|
|
||||||
new_node.level = level
|
|
||||||
current_node = new_node
|
|
||||||
else:
|
|
||||||
dnamesplit = directory_names.split(os.sep)
|
|
||||||
for i, directory_name in enumerate(dnamesplit):
|
|
||||||
found_child = False
|
|
||||||
level += 1
|
|
||||||
for child in current_node.children:
|
|
||||||
if child.name == directory_name:
|
|
||||||
current_node = child
|
|
||||||
found_child = True
|
|
||||||
break
|
|
||||||
if not found_child:
|
|
||||||
new_node = FileNode(directory_name)
|
|
||||||
current_node.children.append(new_node)
|
|
||||||
new_node.level = level - 1
|
|
||||||
current_node = new_node
|
|
||||||
term = FileNode(file_name)
|
|
||||||
self.manifest[file_path] = term
|
|
||||||
term.level = level
|
|
||||||
term.comment = self.sanitize_comment(file_comment)
|
|
||||||
term.is_leaf = True
|
|
||||||
current_node.children.append(term)
|
|
||||||
|
|
||||||
def print_files_recursively(self, level=0, code="R0"):
|
|
||||||
logger.info(' '*level + self.name + ' ' + str(self.is_leaf) + ' ' + str(self.level))
|
|
||||||
for j, child in enumerate(self.children):
|
|
||||||
child.print_files_recursively(level=level+1, code=code+str(j))
|
|
||||||
self.parenting_ship.extend(child.parenting_ship)
|
|
||||||
p1 = f"""{code}[\"🗎{self.name}\"]""" if self.is_leaf else f"""{code}[[\"📁{self.name}\"]]"""
|
|
||||||
p2 = """ --> """
|
|
||||||
p3 = f"""{code+str(j)}[\"🗎{child.name}\"]""" if child.is_leaf else f"""{code+str(j)}[[\"📁{child.name}\"]]"""
|
|
||||||
edge_code = p1 + p2 + p3
|
|
||||||
if edge_code in self.parenting_ship:
|
|
||||||
continue
|
|
||||||
self.parenting_ship.append(edge_code)
|
|
||||||
if self.comment != "":
|
|
||||||
pc1 = f"""{code}[\"🗎{self.name}\"]""" if self.is_leaf else f"""{code}[[\"📁{self.name}\"]]"""
|
|
||||||
pc2 = f""" -.-x """
|
|
||||||
pc3 = f"""C{code}[\"{self.comment}\"]:::Comment"""
|
|
||||||
edge_code = pc1 + pc2 + pc3
|
|
||||||
self.parenting_ship.append(edge_code)
|
|
||||||
|
|
||||||
|
|
||||||
MERMAID_TEMPLATE = r"""
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
%% <gpt_academic_hide_mermaid_code> 一个特殊标记,用于在生成mermaid图表时隐藏代码块
|
|
||||||
classDef Comment stroke-dasharray: 5 5
|
|
||||||
subgraph {graph_name}
|
|
||||||
{relationship}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_file_tree_mermaid_diagram(file_manifest, file_comments, graph_name):
|
|
||||||
# Create the root node
|
|
||||||
file_tree_struct = FileNode("root")
|
|
||||||
# Build the tree structure
|
|
||||||
for file_path, file_comment in zip(file_manifest, file_comments):
|
|
||||||
file_tree_struct.add_file(file_path, file_comment)
|
|
||||||
file_tree_struct.print_files_recursively()
|
|
||||||
cc = "\n".join(file_tree_struct.parenting_ship)
|
|
||||||
ccc = indent(cc, prefix=" "*8)
|
|
||||||
return MERMAID_TEMPLATE.format(graph_name=graph_name, relationship=ccc)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# File manifest
|
|
||||||
file_manifest = [
|
|
||||||
"cradle_void_terminal.ipynb",
|
|
||||||
"tests/test_utils.py",
|
|
||||||
"tests/test_plugins.py",
|
|
||||||
"tests/test_llms.py",
|
|
||||||
"config.py",
|
|
||||||
"build/ChatGLM-6b-onnx-u8s8/chatglm-6b-int8-onnx-merged/model_weights_0.bin",
|
|
||||||
"crazy_functions/latex_fns/latex_actions.py",
|
|
||||||
"crazy_functions/latex_fns/latex_toolbox.py"
|
|
||||||
]
|
|
||||||
file_comments = [
|
|
||||||
"根据位置和名称,可能是一个模块的初始化文件根据位置和名称,可能是一个模块的初始化文件根据位置和名称,可能是一个模块的初始化文件",
|
|
||||||
"包含一些用于文本处理和模型微调的函数和装饰器包含一些用于文本处理和模型微调的函数和装饰器包含一些用于文本处理和模型微调的函数和装饰器",
|
|
||||||
"用于构建HTML报告的类和方法用于构建HTML报告的类和方法用于构建HTML报告的类和方法",
|
|
||||||
"包含了用于文本切分的函数,以及处理PDF文件的示例代码包含了用于文本切分的函数,以及处理PDF文件的示例代码包含了用于文本切分的函数,以及处理PDF文件的示例代码",
|
|
||||||
"用于解析和翻译PDF文件的功能和相关辅助函数用于解析和翻译PDF文件的功能和相关辅助函数用于解析和翻译PDF文件的功能和相关辅助函数",
|
|
||||||
"是一个包的初始化文件,用于初始化包的属性和导入模块是一个包的初始化文件,用于初始化包的属性和导入模块是一个包的初始化文件,用于初始化包的属性和导入模块",
|
|
||||||
"用于加载和分割文件中的文本的通用文件加载器用于加载和分割文件中的文本的通用文件加载器用于加载和分割文件中的文本的通用文件加载器",
|
|
||||||
"包含了用于构建和管理向量数据库的函数和类包含了用于构建和管理向量数据库的函数和类包含了用于构建和管理向量数据库的函数和类",
|
|
||||||
]
|
|
||||||
logger.info(build_file_tree_mermaid_diagram(file_manifest, file_comments, "项目文件树"))
|
|
||||||
@@ -1,812 +0,0 @@
|
|||||||
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>
|
|
||||||
"""
|
|
||||||
@@ -1,812 +0,0 @@
|
|||||||
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>
|
|
||||||
"""
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
# 设置日志
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# 自定义异常类定义
|
|
||||||
class FoldingError(Exception):
|
|
||||||
"""折叠相关的自定义异常基类"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FormattingError(FoldingError):
|
|
||||||
"""格式化过程中的错误"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataError(FoldingError):
|
|
||||||
"""元数据相关的错误"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(FoldingError):
|
|
||||||
"""验证错误"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FoldingStyle(Enum):
|
|
||||||
"""折叠样式枚举"""
|
|
||||||
SIMPLE = auto() # 简单折叠
|
|
||||||
DETAILED = auto() # 详细折叠(带有额外信息)
|
|
||||||
NESTED = auto() # 嵌套折叠
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FoldingOptions:
|
|
||||||
"""折叠选项配置"""
|
|
||||||
style: FoldingStyle = FoldingStyle.DETAILED
|
|
||||||
code_language: Optional[str] = None # 代码块的语言
|
|
||||||
show_timestamp: bool = False # 是否显示时间戳
|
|
||||||
indent_level: int = 0 # 缩进级别
|
|
||||||
custom_css: Optional[str] = None # 自定义CSS类
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T') # 用于泛型类型
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMetadata(ABC):
|
|
||||||
"""元数据基类"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def validate(self) -> bool:
|
|
||||||
"""验证元数据的有效性"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _validate_non_empty_str(self, value: Optional[str]) -> bool:
|
|
||||||
"""验证字符串非空"""
|
|
||||||
return bool(value and value.strip())
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FileMetadata(BaseMetadata):
|
|
||||||
"""文件元数据"""
|
|
||||||
rel_path: str
|
|
||||||
size: float
|
|
||||||
last_modified: Optional[datetime] = None
|
|
||||||
mime_type: Optional[str] = None
|
|
||||||
encoding: str = 'utf-8'
|
|
||||||
|
|
||||||
def validate(self) -> bool:
|
|
||||||
"""验证文件元数据的有效性"""
|
|
||||||
try:
|
|
||||||
if not self._validate_non_empty_str(self.rel_path):
|
|
||||||
return False
|
|
||||||
if self.size < 0:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"File metadata validation error: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ContentFormatter(ABC, Generic[T]):
|
|
||||||
"""内容格式化抽象基类
|
|
||||||
|
|
||||||
支持泛型类型参数,可以指定具体的元数据类型。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def format(self,
|
|
||||||
content: str,
|
|
||||||
metadata: T,
|
|
||||||
options: Optional[FoldingOptions] = None) -> str:
|
|
||||||
"""格式化内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: 需要格式化的内容
|
|
||||||
metadata: 类型化的元数据
|
|
||||||
options: 折叠选项
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 格式化后的内容
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FormattingError: 格式化过程中的错误
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _create_summary(self, metadata: T) -> str:
|
|
||||||
"""创建折叠摘要,可被子类重写"""
|
|
||||||
return str(metadata)
|
|
||||||
|
|
||||||
def _format_content_block(self,
|
|
||||||
content: str,
|
|
||||||
options: Optional[FoldingOptions]) -> str:
|
|
||||||
"""格式化内容块,处理代码块等特殊格式"""
|
|
||||||
if not options:
|
|
||||||
return content
|
|
||||||
|
|
||||||
if options.code_language:
|
|
||||||
return f"```{options.code_language}\n{content}\n```"
|
|
||||||
return content
|
|
||||||
|
|
||||||
def _add_indent(self, text: str, level: int) -> str:
|
|
||||||
"""添加缩进"""
|
|
||||||
if level <= 0:
|
|
||||||
return text
|
|
||||||
indent = " " * level
|
|
||||||
return "\n".join(indent + line for line in text.splitlines())
|
|
||||||
|
|
||||||
|
|
||||||
class FileContentFormatter(ContentFormatter[FileMetadata]):
|
|
||||||
"""文件内容格式化器"""
|
|
||||||
|
|
||||||
def format(self,
|
|
||||||
content: str,
|
|
||||||
metadata: FileMetadata,
|
|
||||||
options: Optional[FoldingOptions] = None) -> str:
|
|
||||||
"""格式化文件内容"""
|
|
||||||
if not metadata.validate():
|
|
||||||
raise MetadataError("Invalid file metadata")
|
|
||||||
|
|
||||||
try:
|
|
||||||
options = options or FoldingOptions()
|
|
||||||
|
|
||||||
# 构建摘要信息
|
|
||||||
summary_parts = [
|
|
||||||
f"{metadata.rel_path} ({metadata.size:.2f}MB)",
|
|
||||||
f"Type: {metadata.mime_type}" if metadata.mime_type else None,
|
|
||||||
(f"Modified: {metadata.last_modified.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
||||||
if metadata.last_modified and options.show_timestamp else None)
|
|
||||||
]
|
|
||||||
summary = " | ".join(filter(None, summary_parts))
|
|
||||||
|
|
||||||
# 构建HTML类
|
|
||||||
css_class = f' class="{options.custom_css}"' if options.custom_css else ''
|
|
||||||
|
|
||||||
# 格式化内容
|
|
||||||
formatted_content = self._format_content_block(content, options)
|
|
||||||
|
|
||||||
# 组装最终结果
|
|
||||||
result = (
|
|
||||||
f'<details{css_class}><summary>{summary}</summary>\n\n'
|
|
||||||
f'{formatted_content}\n\n'
|
|
||||||
f'</details>\n\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._add_indent(result, options.indent_level)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error formatting file content: {str(e)}")
|
|
||||||
raise FormattingError(f"Failed to format file content: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
class ContentFoldingManager:
|
|
||||||
"""内容折叠管理器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""初始化折叠管理器"""
|
|
||||||
self._formatters: Dict[str, ContentFormatter] = {}
|
|
||||||
self._register_default_formatters()
|
|
||||||
|
|
||||||
def _register_default_formatters(self) -> None:
|
|
||||||
"""注册默认的格式化器"""
|
|
||||||
self.register_formatter('file', FileContentFormatter())
|
|
||||||
|
|
||||||
def register_formatter(self, name: str, formatter: ContentFormatter) -> None:
|
|
||||||
"""注册新的格式化器"""
|
|
||||||
if not isinstance(formatter, ContentFormatter):
|
|
||||||
raise TypeError("Formatter must implement ContentFormatter interface")
|
|
||||||
self._formatters[name] = formatter
|
|
||||||
|
|
||||||
def _guess_language(self, extension: str) -> Optional[str]:
|
|
||||||
"""根据文件扩展名猜测编程语言"""
|
|
||||||
extension = extension.lower().lstrip('.')
|
|
||||||
language_map = {
|
|
||||||
'py': 'python',
|
|
||||||
'js': 'javascript',
|
|
||||||
'java': 'java',
|
|
||||||
'cpp': 'cpp',
|
|
||||||
'cs': 'csharp',
|
|
||||||
'html': 'html',
|
|
||||||
'css': 'css',
|
|
||||||
'md': 'markdown',
|
|
||||||
'json': 'json',
|
|
||||||
'xml': 'xml',
|
|
||||||
'sql': 'sql',
|
|
||||||
'sh': 'bash',
|
|
||||||
'yaml': 'yaml',
|
|
||||||
'yml': 'yaml',
|
|
||||||
'txt': None # 纯文本不需要语言标识
|
|
||||||
}
|
|
||||||
return language_map.get(extension)
|
|
||||||
|
|
||||||
def format_content(self,
|
|
||||||
content: str,
|
|
||||||
formatter_type: str,
|
|
||||||
metadata: Union[FileMetadata],
|
|
||||||
options: Optional[FoldingOptions] = None) -> str:
|
|
||||||
"""格式化内容"""
|
|
||||||
formatter = self._formatters.get(formatter_type)
|
|
||||||
if not formatter:
|
|
||||||
raise KeyError(f"No formatter registered for type: {formatter_type}")
|
|
||||||
|
|
||||||
if not isinstance(metadata, FileMetadata):
|
|
||||||
raise TypeError("Invalid metadata type")
|
|
||||||
|
|
||||||
return formatter.format(content, metadata, options)
|
|
||||||
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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>
|
|
||||||
"""
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
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)}")
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import nltk
|
|
||||||
nltk.data.path.append('~/nltk_data')
|
|
||||||
nltk.download('averaged_perceptron_tagger', download_dir='~/nltk_data')
|
|
||||||
nltk.download('punkt', download_dir='~/nltk_data')
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,493 +0,0 @@
|
|||||||
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
@@ -1,86 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
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,451 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
import glob
|
|
||||||
import time
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from typing import List, Generator, Tuple, Set, Optional, Dict
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from loguru import logger
|
|
||||||
from toolbox import update_ui
|
|
||||||
from crazy_functions.rag_fns.rag_file_support import extract_text
|
|
||||||
from crazy_functions.doc_fns.content_folder import ContentFoldingManager, FileMetadata, FoldingOptions, FoldingStyle, FoldingError
|
|
||||||
from shared_utils.fastapi_server import validate_path_safety
|
|
||||||
from datetime import datetime
|
|
||||||
import mimetypes
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FileInfo:
|
|
||||||
"""文件信息数据类"""
|
|
||||||
path: str # 完整路径
|
|
||||||
rel_path: str # 相对路径
|
|
||||||
size: float # 文件大小(MB)
|
|
||||||
extension: str # 文件扩展名
|
|
||||||
last_modified: str # 最后修改时间
|
|
||||||
|
|
||||||
|
|
||||||
class TextContentLoader:
|
|
||||||
"""优化版本的文本内容加载器 - 保持原有接口"""
|
|
||||||
|
|
||||||
# 压缩文件扩展名
|
|
||||||
COMPRESSED_EXTENSIONS: Set[str] = {'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz'}
|
|
||||||
|
|
||||||
# 系统配置
|
|
||||||
MAX_FILE_SIZE: int = 100 * 1024 * 1024 # 最大文件大小(100MB)
|
|
||||||
MAX_TOTAL_SIZE: int = 100 * 1024 * 1024 # 最大总大小(100MB)
|
|
||||||
MAX_FILES: int = 100 # 最大文件数量
|
|
||||||
CHUNK_SIZE: int = 1024 * 1024 # 文件读取块大小(1MB)
|
|
||||||
MAX_WORKERS: int = min(32, (os.cpu_count() or 1) * 4) # 最大工作线程数
|
|
||||||
BATCH_SIZE: int = 5 # 批处理大小
|
|
||||||
|
|
||||||
def __init__(self, chatbot: List, history: List):
|
|
||||||
"""初始化加载器"""
|
|
||||||
self.chatbot = chatbot
|
|
||||||
self.history = history
|
|
||||||
self.failed_files: List[Tuple[str, str]] = []
|
|
||||||
self.processed_size: int = 0
|
|
||||||
self.start_time: float = 0
|
|
||||||
self.file_cache: Dict[str, str] = {}
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self.executor = ThreadPoolExecutor(max_workers=self.MAX_WORKERS)
|
|
||||||
self.results_queue = queue.Queue()
|
|
||||||
self.folding_manager = ContentFoldingManager()
|
|
||||||
|
|
||||||
def _create_file_info(self, entry: os.DirEntry, root_path: str) -> FileInfo:
|
|
||||||
"""优化的文件信息创建
|
|
||||||
|
|
||||||
Args:
|
|
||||||
entry: 目录入口对象
|
|
||||||
root_path: 根路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
FileInfo: 文件信息对象
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
stats = entry.stat() # 使用缓存的文件状态
|
|
||||||
return FileInfo(
|
|
||||||
path=entry.path,
|
|
||||||
rel_path=os.path.relpath(entry.path, root_path),
|
|
||||||
size=stats.st_size / (1024 * 1024),
|
|
||||||
extension=os.path.splitext(entry.path)[1].lower(),
|
|
||||||
last_modified=time.strftime('%Y-%m-%d %H:%M:%S',
|
|
||||||
time.localtime(stats.st_mtime))
|
|
||||||
)
|
|
||||||
except (OSError, ValueError) as e:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _process_file_batch(self, file_batch: List[FileInfo]) -> List[Tuple[FileInfo, Optional[str]]]:
|
|
||||||
"""批量处理文件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_batch: 要处理的文件信息列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Tuple[FileInfo, Optional[str]]]: 处理结果列表
|
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
futures = {}
|
|
||||||
|
|
||||||
for file_info in file_batch:
|
|
||||||
if file_info.path in self.file_cache:
|
|
||||||
results.append((file_info, self.file_cache[file_info.path]))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if file_info.size * 1024 * 1024 > self.MAX_FILE_SIZE:
|
|
||||||
with self._lock:
|
|
||||||
self.failed_files.append(
|
|
||||||
(file_info.rel_path,
|
|
||||||
f"文件过大({file_info.size:.2f}MB > {self.MAX_FILE_SIZE / (1024 * 1024)}MB)")
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
future = self.executor.submit(self._read_file_content, file_info)
|
|
||||||
futures[future] = file_info
|
|
||||||
|
|
||||||
for future in as_completed(futures):
|
|
||||||
file_info = futures[future]
|
|
||||||
try:
|
|
||||||
content = future.result()
|
|
||||||
if content:
|
|
||||||
with self._lock:
|
|
||||||
self.file_cache[file_info.path] = content
|
|
||||||
self.processed_size += file_info.size * 1024 * 1024
|
|
||||||
results.append((file_info, content))
|
|
||||||
except Exception as e:
|
|
||||||
with self._lock:
|
|
||||||
self.failed_files.append((file_info.rel_path, f"读取失败: {str(e)}"))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def _read_file_content(self, file_info: FileInfo) -> Optional[str]:
|
|
||||||
"""读取单个文件内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_info: 文件信息对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[str]: 文件内容
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
content = extract_text(file_info.path)
|
|
||||||
if not content or not content.strip():
|
|
||||||
return None
|
|
||||||
return content
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"读取文件失败: {str(e)}")
|
|
||||||
raise Exception(f"读取文件失败: {str(e)}")
|
|
||||||
|
|
||||||
def _is_valid_file(self, file_path: str) -> bool:
|
|
||||||
"""检查文件是否有效
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: 文件路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否为有效文件
|
|
||||||
"""
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
extension = os.path.splitext(file_path)[1].lower()
|
|
||||||
if (extension in self.COMPRESSED_EXTENSIONS or
|
|
||||||
os.path.basename(file_path).startswith('.') or
|
|
||||||
not os.access(file_path, os.R_OK)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 只要文件可以访问且不在排除列表中就认为是有效的
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _collect_files(self, path: str) -> List[FileInfo]:
|
|
||||||
"""收集文件信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: 目标路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[FileInfo]: 有效文件信息列表
|
|
||||||
"""
|
|
||||||
files = []
|
|
||||||
total_size = 0
|
|
||||||
|
|
||||||
# 处理单个文件的情况
|
|
||||||
if os.path.isfile(path):
|
|
||||||
if self._is_valid_file(path):
|
|
||||||
file_info = self._create_file_info(os.DirEntry(os.path.dirname(path)), os.path.dirname(path))
|
|
||||||
if file_info:
|
|
||||||
return [file_info]
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 处理目录的情况
|
|
||||||
try:
|
|
||||||
# 使用os.walk来递归遍历目录
|
|
||||||
for root, _, filenames in os.walk(path):
|
|
||||||
for filename in filenames:
|
|
||||||
if len(files) >= self.MAX_FILES:
|
|
||||||
self.failed_files.append((filename, f"超出最大文件数限制({self.MAX_FILES})"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
file_path = os.path.join(root, filename)
|
|
||||||
|
|
||||||
if not self._is_valid_file(file_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
stats = os.stat(file_path)
|
|
||||||
file_size = stats.st_size / (1024 * 1024) # 转换为MB
|
|
||||||
|
|
||||||
if file_size * 1024 * 1024 > self.MAX_FILE_SIZE:
|
|
||||||
self.failed_files.append((file_path,
|
|
||||||
f"文件过大({file_size:.2f}MB > {self.MAX_FILE_SIZE / (1024 * 1024)}MB)"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if total_size + file_size * 1024 * 1024 > self.MAX_TOTAL_SIZE:
|
|
||||||
self.failed_files.append((file_path, "超出总大小限制"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
file_info = FileInfo(
|
|
||||||
path=file_path,
|
|
||||||
rel_path=os.path.relpath(file_path, path),
|
|
||||||
size=file_size,
|
|
||||||
extension=os.path.splitext(file_path)[1].lower(),
|
|
||||||
last_modified=time.strftime('%Y-%m-%d %H:%M:%S',
|
|
||||||
time.localtime(stats.st_mtime))
|
|
||||||
)
|
|
||||||
|
|
||||||
total_size += file_size * 1024 * 1024
|
|
||||||
files.append(file_info)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_files.append((file_path, f"处理文件失败: {str(e)}"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.failed_files.append(("目录扫描", f"扫描失败: {str(e)}"))
|
|
||||||
return []
|
|
||||||
|
|
||||||
return sorted(files, key=lambda x: x.rel_path)
|
|
||||||
|
|
||||||
def _format_content_with_fold(self, file_info, content: str) -> str:
|
|
||||||
"""使用折叠管理器格式化文件内容"""
|
|
||||||
try:
|
|
||||||
metadata = FileMetadata(
|
|
||||||
rel_path=file_info.rel_path,
|
|
||||||
size=file_info.size,
|
|
||||||
last_modified=datetime.fromtimestamp(
|
|
||||||
os.path.getmtime(file_info.path)
|
|
||||||
),
|
|
||||||
mime_type=mimetypes.guess_type(file_info.path)[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
options = FoldingOptions(
|
|
||||||
style=FoldingStyle.DETAILED,
|
|
||||||
code_language=self.folding_manager._guess_language(
|
|
||||||
os.path.splitext(file_info.path)[1]
|
|
||||||
),
|
|
||||||
show_timestamp=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.folding_manager.format_content(
|
|
||||||
content=content,
|
|
||||||
formatter_type='file',
|
|
||||||
metadata=metadata,
|
|
||||||
options=options
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error formatting content: {str(e)}"
|
|
||||||
|
|
||||||
def _format_content_for_llm(self, file_infos: List[FileInfo], contents: List[str]) -> str:
|
|
||||||
"""格式化用于LLM的内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_infos: 文件信息列表
|
|
||||||
contents: 内容列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 格式化后的内容
|
|
||||||
"""
|
|
||||||
if len(file_infos) != len(contents):
|
|
||||||
raise ValueError("文件信息和内容数量不匹配")
|
|
||||||
|
|
||||||
result = [
|
|
||||||
"以下是多个文件的内容集合。每个文件的内容都以 '===== 文件 {序号}: {文件名} =====' 开始,",
|
|
||||||
"以 '===== 文件 {序号} 结束 =====' 结束。你可以根据这些分隔符来识别不同文件的内容。\n\n"
|
|
||||||
]
|
|
||||||
|
|
||||||
for idx, (file_info, content) in enumerate(zip(file_infos, contents), 1):
|
|
||||||
result.extend([
|
|
||||||
f"===== 文件 {idx}: {file_info.rel_path} =====",
|
|
||||||
"文件内容:",
|
|
||||||
content.strip(),
|
|
||||||
f"===== 文件 {idx} 结束 =====\n"
|
|
||||||
])
|
|
||||||
|
|
||||||
return "\n".join(result)
|
|
||||||
|
|
||||||
def execute(self, txt: str) -> Generator:
|
|
||||||
"""执行文本加载和显示 - 保持原有接口
|
|
||||||
|
|
||||||
Args:
|
|
||||||
txt: 目标路径
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Generator: UI更新生成器
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 首先显示正在处理的提示信息
|
|
||||||
self.chatbot.append(["提示", "正在提取文本内容,请稍作等待..."])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
user_name = self.chatbot.get_user()
|
|
||||||
validate_path_safety(txt, user_name)
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.processed_size = 0
|
|
||||||
self.failed_files.clear()
|
|
||||||
successful_files = []
|
|
||||||
successful_contents = []
|
|
||||||
|
|
||||||
# 收集文件
|
|
||||||
files = self._collect_files(txt)
|
|
||||||
if not files:
|
|
||||||
# 移除之前的提示信息
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["提示", "未找到任何有效文件"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 批量处理文件
|
|
||||||
content_blocks = []
|
|
||||||
for i in range(0, len(files), self.BATCH_SIZE):
|
|
||||||
batch = files[i:i + self.BATCH_SIZE]
|
|
||||||
results = self._process_file_batch(batch)
|
|
||||||
|
|
||||||
for file_info, content in results:
|
|
||||||
if content:
|
|
||||||
content_blocks.append(self._format_content_with_fold(file_info, content))
|
|
||||||
successful_files.append(file_info)
|
|
||||||
successful_contents.append(content)
|
|
||||||
|
|
||||||
# 显示文件内容,替换之前的提示信息
|
|
||||||
if content_blocks:
|
|
||||||
# 移除之前的提示信息
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["文件内容", "\n".join(content_blocks)])
|
|
||||||
self.history.extend([
|
|
||||||
self._format_content_for_llm(successful_files, successful_contents),
|
|
||||||
"我已经接收到你上传的文件的内容,请提问"
|
|
||||||
])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 发生错误时,移除之前的提示信息
|
|
||||||
if len(self.chatbot) > 0 and self.chatbot[-1][0] == "提示":
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"处理过程中出现错误: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.executor.shutdown(wait=False)
|
|
||||||
self.file_cache.clear()
|
|
||||||
|
|
||||||
def execute_single_file(self, file_path: str) -> Generator:
|
|
||||||
"""执行单个文件的加载和显示
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: 文件路径
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Generator: UI更新生成器
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 首先显示正在处理的提示信息
|
|
||||||
self.chatbot.append(["提示", "正在提取文本内容,请稍作等待..."])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
user_name = self.chatbot.get_user()
|
|
||||||
validate_path_safety(file_path, user_name)
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.processed_size = 0
|
|
||||||
self.failed_files.clear()
|
|
||||||
|
|
||||||
# 验证文件是否存在且可读
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"指定路径不是文件: {file_path}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._is_valid_file(file_path):
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"无效的文件类型或无法读取: {file_path}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 创建文件信息
|
|
||||||
try:
|
|
||||||
stats = os.stat(file_path)
|
|
||||||
file_size = stats.st_size / (1024 * 1024) # 转换为MB
|
|
||||||
|
|
||||||
if file_size * 1024 * 1024 > self.MAX_FILE_SIZE:
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"文件过大({file_size:.2f}MB > {self.MAX_FILE_SIZE / (1024 * 1024)}MB)"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
file_info = FileInfo(
|
|
||||||
path=file_path,
|
|
||||||
rel_path=os.path.basename(file_path),
|
|
||||||
size=file_size,
|
|
||||||
extension=os.path.splitext(file_path)[1].lower(),
|
|
||||||
last_modified=time.strftime('%Y-%m-%d %H:%M:%S',
|
|
||||||
time.localtime(stats.st_mtime))
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"处理文件失败: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 读取文件内容
|
|
||||||
try:
|
|
||||||
content = self._read_file_content(file_info)
|
|
||||||
if not content:
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["提示", f"文件内容为空或无法提取: {file_path}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"读取文件失败: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 格式化内容并更新UI
|
|
||||||
formatted_content = self._format_content_with_fold(file_info, content)
|
|
||||||
|
|
||||||
# 移除之前的提示信息
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["文件内容", formatted_content])
|
|
||||||
|
|
||||||
# 更新历史记录,便于LLM处理
|
|
||||||
llm_content = self._format_content_for_llm([file_info], [content])
|
|
||||||
self.history.extend([llm_content, "我已经接收到你上传的文件的内容,请提问"])
|
|
||||||
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 发生错误时,移除之前的提示信息
|
|
||||||
if len(self.chatbot) > 0 and self.chatbot[-1][0] == "提示":
|
|
||||||
self.chatbot.pop()
|
|
||||||
self.chatbot.append(["错误", f"处理过程中出现错误: {str(e)}"])
|
|
||||||
yield from update_ui(chatbot=self.chatbot, history=self.history)
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
"""析构函数 - 确保资源被正确释放"""
|
|
||||||
if hasattr(self, 'executor'):
|
|
||||||
self.executor.shutdown(wait=False)
|
|
||||||
if hasattr(self, 'file_cache'):
|
|
||||||
self.file_cache.clear()
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from toolbox import CatchException, update_ui, update_ui_latest_msg
|
from toolbox import CatchException, update_ui, update_ui_lastest_msg
|
||||||
from crazy_functions.multi_stage.multi_stage_utils import GptAcademicGameBaseState
|
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 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
|
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||||
@@ -8,12 +8,12 @@ import random
|
|||||||
|
|
||||||
class MiniGame_ASCII_Art(GptAcademicGameBaseState):
|
class MiniGame_ASCII_Art(GptAcademicGameBaseState):
|
||||||
def step(self, prompt, chatbot, history):
|
def step(self, prompt, chatbot, history):
|
||||||
if self.step_cnt == 0:
|
if self.step_cnt == 0:
|
||||||
chatbot.append(["我画你猜(动物)", "请稍等..."])
|
chatbot.append(["我画你猜(动物)", "请稍等..."])
|
||||||
else:
|
else:
|
||||||
if prompt.strip() == 'exit':
|
if prompt.strip() == 'exit':
|
||||||
self.delete_game = True
|
self.delete_game = True
|
||||||
yield from update_ui_latest_msg(lastmsg=f"谜底是{self.obj},游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_msg(lastmsg=f"谜底是{self.obj},游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||||
return
|
return
|
||||||
chatbot.append([prompt, ""])
|
chatbot.append([prompt, ""])
|
||||||
yield from update_ui(chatbot=chatbot, history=history)
|
yield from update_ui(chatbot=chatbot, history=history)
|
||||||
@@ -31,12 +31,12 @@ class MiniGame_ASCII_Art(GptAcademicGameBaseState):
|
|||||||
self.cur_task = 'identify user guess'
|
self.cur_task = 'identify user guess'
|
||||||
res = get_code_block(raw_res)
|
res = get_code_block(raw_res)
|
||||||
history += ['', f'the answer is {self.obj}', inputs, res]
|
history += ['', f'the answer is {self.obj}', inputs, res]
|
||||||
yield from update_ui_latest_msg(lastmsg=res, chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_msg(lastmsg=res, chatbot=chatbot, history=history, delay=0.)
|
||||||
|
|
||||||
elif self.cur_task == 'identify user guess':
|
elif self.cur_task == 'identify user guess':
|
||||||
if is_same_thing(self.obj, prompt, self.llm_kwargs):
|
if is_same_thing(self.obj, prompt, self.llm_kwargs):
|
||||||
self.delete_game = True
|
self.delete_game = True
|
||||||
yield from update_ui_latest_msg(lastmsg="你猜对了!", chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_msg(lastmsg="你猜对了!", chatbot=chatbot, history=history, delay=0.)
|
||||||
else:
|
else:
|
||||||
self.cur_task = 'identify user guess'
|
self.cur_task = 'identify user guess'
|
||||||
yield from update_ui_latest_msg(lastmsg="猜错了,再试试,输入“exit”获取答案。", chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_msg(lastmsg="猜错了,再试试,输入“exit”获取答案。", chatbot=chatbot, history=history, delay=0.)
|
||||||
@@ -63,7 +63,7 @@ prompts_terminate = """小说的前文回顾:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from toolbox import CatchException, update_ui, update_ui_latest_msg
|
from toolbox import CatchException, update_ui, update_ui_lastest_msg
|
||||||
from crazy_functions.multi_stage.multi_stage_utils import GptAcademicGameBaseState
|
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 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
|
from request_llms.bridge_all import predict_no_ui_long_connection
|
||||||
@@ -88,23 +88,23 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
self.story = []
|
self.story = []
|
||||||
chatbot.append(["互动写故事", f"这次的故事开头是:{self.headstart}"])
|
chatbot.append(["互动写故事", f"这次的故事开头是:{self.headstart}"])
|
||||||
self.sys_prompt_ = '你是一个想象力丰富的杰出作家。正在与你的朋友互动,一起写故事,因此你每次写的故事段落应少于300字(结局除外)。'
|
self.sys_prompt_ = '你是一个想象力丰富的杰出作家。正在与你的朋友互动,一起写故事,因此你每次写的故事段落应少于300字(结局除外)。'
|
||||||
|
|
||||||
|
|
||||||
def generate_story_image(self, story_paragraph):
|
def generate_story_image(self, story_paragraph):
|
||||||
try:
|
try:
|
||||||
from crazy_functions.Image_Generate import gen_image
|
from crazy_functions.图片生成 import gen_image
|
||||||
prompt_ = predict_no_ui_long_connection(inputs=story_paragraph, llm_kwargs=self.llm_kwargs, history=[], sys_prompt='你需要根据用户给出的小说段落,进行简短的环境描写。要求:80字以内。')
|
prompt_ = predict_no_ui_long_connection(inputs=story_paragraph, llm_kwargs=self.llm_kwargs, history=[], sys_prompt='你需要根据用户给出的小说段落,进行简短的环境描写。要求:80字以内。')
|
||||||
image_url, image_path = gen_image(self.llm_kwargs, prompt_, '512x512', model="dall-e-2", quality='standard', style='natural')
|
image_url, image_path = gen_image(self.llm_kwargs, prompt_, '512x512', model="dall-e-2", quality='standard', style='natural')
|
||||||
return f'<br/><div align="center"><img src="file={image_path}"></div>'
|
return f'<br/><div align="center"><img src="file={image_path}"></div>'
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def step(self, prompt, chatbot, history):
|
def step(self, prompt, chatbot, history):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
首先,处理游戏初始化等特殊情况
|
首先,处理游戏初始化等特殊情况
|
||||||
"""
|
"""
|
||||||
if self.step_cnt == 0:
|
if self.step_cnt == 0:
|
||||||
self.begin_game_step_0(prompt, chatbot, history)
|
self.begin_game_step_0(prompt, chatbot, history)
|
||||||
self.lock_plugin(chatbot)
|
self.lock_plugin(chatbot)
|
||||||
self.cur_task = 'head_start'
|
self.cur_task = 'head_start'
|
||||||
@@ -112,7 +112,7 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
if prompt.strip() == 'exit' or prompt.strip() == '结束剧情':
|
if prompt.strip() == 'exit' or prompt.strip() == '结束剧情':
|
||||||
# should we terminate game here?
|
# should we terminate game here?
|
||||||
self.delete_game = True
|
self.delete_game = True
|
||||||
yield from update_ui_latest_msg(lastmsg=f"游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_msg(lastmsg=f"游戏结束。", chatbot=chatbot, history=history, delay=0.)
|
||||||
return
|
return
|
||||||
if '剧情收尾' in prompt:
|
if '剧情收尾' in prompt:
|
||||||
self.cur_task = 'story_terminate'
|
self.cur_task = 'story_terminate'
|
||||||
@@ -132,13 +132,13 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
inputs_ = prompts_hs.format(headstart=self.headstart)
|
inputs_ = prompts_hs.format(headstart=self.headstart)
|
||||||
history_ = []
|
history_ = []
|
||||||
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs_, '故事开头', self.llm_kwargs,
|
inputs_, '故事开头', self.llm_kwargs,
|
||||||
chatbot, history_, self.sys_prompt_
|
chatbot, history_, self.sys_prompt_
|
||||||
)
|
)
|
||||||
self.story.append(story_paragraph)
|
self.story.append(story_paragraph)
|
||||||
# # 配图
|
# # 配图
|
||||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_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.)
|
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||||
|
|
||||||
# # 构建后续剧情引导
|
# # 构建后续剧情引导
|
||||||
previously_on_story = ""
|
previously_on_story = ""
|
||||||
@@ -147,7 +147,7 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
inputs_ = prompts_interact.format(previously_on_story=previously_on_story)
|
inputs_ = prompts_interact.format(previously_on_story=previously_on_story)
|
||||||
history_ = []
|
history_ = []
|
||||||
self.next_choices = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
self.next_choices = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs_, '请在以下几种故事走向中,选择一种(当然,您也可以选择给出其他故事走向):', self.llm_kwargs,
|
inputs_, '请在以下几种故事走向中,选择一种(当然,您也可以选择给出其他故事走向):', self.llm_kwargs,
|
||||||
chatbot,
|
chatbot,
|
||||||
history_,
|
history_,
|
||||||
self.sys_prompt_
|
self.sys_prompt_
|
||||||
@@ -166,13 +166,13 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
inputs_ = prompts_resume.format(previously_on_story=previously_on_story, choice=self.next_choices, user_choice=prompt)
|
inputs_ = prompts_resume.format(previously_on_story=previously_on_story, choice=self.next_choices, user_choice=prompt)
|
||||||
history_ = []
|
history_ = []
|
||||||
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs_, f'下一段故事(您的选择是:{prompt})。', self.llm_kwargs,
|
inputs_, f'下一段故事(您的选择是:{prompt})。', self.llm_kwargs,
|
||||||
chatbot, history_, self.sys_prompt_
|
chatbot, history_, self.sys_prompt_
|
||||||
)
|
)
|
||||||
self.story.append(story_paragraph)
|
self.story.append(story_paragraph)
|
||||||
# # 配图
|
# # 配图
|
||||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_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.)
|
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||||
|
|
||||||
# # 构建后续剧情引导
|
# # 构建后续剧情引导
|
||||||
previously_on_story = ""
|
previously_on_story = ""
|
||||||
@@ -181,10 +181,10 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
inputs_ = prompts_interact.format(previously_on_story=previously_on_story)
|
inputs_ = prompts_interact.format(previously_on_story=previously_on_story)
|
||||||
history_ = []
|
history_ = []
|
||||||
self.next_choices = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
self.next_choices = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs_,
|
inputs_,
|
||||||
'请在以下几种故事走向中,选择一种。当然,您也可以给出您心中的其他故事走向。另外,如果您希望剧情立即收尾,请输入剧情走向,并以“剧情收尾”四个字提示程序。', self.llm_kwargs,
|
'请在以下几种故事走向中,选择一种。当然,您也可以给出您心中的其他故事走向。另外,如果您希望剧情立即收尾,请输入剧情走向,并以“剧情收尾”四个字提示程序。', self.llm_kwargs,
|
||||||
chatbot,
|
chatbot,
|
||||||
history_,
|
history_,
|
||||||
self.sys_prompt_
|
self.sys_prompt_
|
||||||
)
|
)
|
||||||
self.cur_task = 'user_choice'
|
self.cur_task = 'user_choice'
|
||||||
@@ -200,12 +200,12 @@ class MiniGame_ResumeStory(GptAcademicGameBaseState):
|
|||||||
inputs_ = prompts_terminate.format(previously_on_story=previously_on_story, user_choice=prompt)
|
inputs_ = prompts_terminate.format(previously_on_story=previously_on_story, user_choice=prompt)
|
||||||
history_ = []
|
history_ = []
|
||||||
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
story_paragraph = yield from request_gpt_model_in_new_thread_with_ui_alive(
|
||||||
inputs_, f'故事收尾(您的选择是:{prompt})。', self.llm_kwargs,
|
inputs_, f'故事收尾(您的选择是:{prompt})。', self.llm_kwargs,
|
||||||
chatbot, history_, self.sys_prompt_
|
chatbot, history_, self.sys_prompt_
|
||||||
)
|
)
|
||||||
# # 配图
|
# # 配图
|
||||||
yield from update_ui_latest_msg(lastmsg=story_paragraph + '<br/>正在生成插图中 ...', chatbot=chatbot, history=history, delay=0.)
|
yield from update_ui_lastest_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.)
|
yield from update_ui_lastest_msg(lastmsg=story_paragraph + '<br/>'+ self.generate_story_image(story_paragraph), chatbot=chatbot, history=history, delay=0.)
|
||||||
|
|
||||||
# terminate game
|
# terminate game
|
||||||
self.delete_game = True
|
self.delete_game = True
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def get_code_block(reply):
|
|||||||
import re
|
import re
|
||||||
pattern = r"```([\s\S]*?)```" # regex pattern to match code blocks
|
pattern = r"```([\s\S]*?)```" # regex pattern to match code blocks
|
||||||
matches = re.findall(pattern, reply) # find all code blocks in text
|
matches = re.findall(pattern, reply) # find all code blocks in text
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
return "```" + matches[0] + "```" # code block
|
return "```" + matches[0] + "```" # code block
|
||||||
raise RuntimeError("GPT is not generating proper code.")
|
raise RuntimeError("GPT is not generating proper code.")
|
||||||
|
|
||||||
@@ -13,10 +13,10 @@ def is_same_thing(a, b, llm_kwargs):
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
class IsSameThing(BaseModel):
|
class IsSameThing(BaseModel):
|
||||||
is_same_thing: bool = Field(description="determine whether two objects are same thing.", default=False)
|
is_same_thing: bool = Field(description="determine whether two objects are same thing.", default=False)
|
||||||
|
|
||||||
def run_gpt_fn(inputs, sys_prompt, history=[]):
|
def run_gpt_fn(inputs, sys_prompt, history=[]):
|
||||||
return predict_no_ui_long_connection(
|
return predict_no_ui_long_connection(
|
||||||
inputs=inputs, llm_kwargs=llm_kwargs,
|
inputs=inputs, llm_kwargs=llm_kwargs,
|
||||||
history=history, sys_prompt=sys_prompt, observe_window=[]
|
history=history, sys_prompt=sys_prompt, observe_window=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def is_same_thing(a, b, llm_kwargs):
|
|||||||
inputs_01 = "Identity whether the user input and the target is the same thing: \n target object: {a} \n user input object: {b} \n\n\n".format(a=a, b=b)
|
inputs_01 = "Identity whether the user input and the target is the same thing: \n target object: {a} \n user input object: {b} \n\n\n".format(a=a, b=b)
|
||||||
inputs_01 += "\n\n\n Note that the user may describe the target object with a different language, e.g. cat and 猫 are the same thing."
|
inputs_01 += "\n\n\n Note that the user may describe the target object with a different language, e.g. cat and 猫 are the same thing."
|
||||||
analyze_res_cot_01 = run_gpt_fn(inputs_01, "", [])
|
analyze_res_cot_01 = run_gpt_fn(inputs_01, "", [])
|
||||||
|
|
||||||
inputs_02 = inputs_01 + gpt_json_io.format_instructions
|
inputs_02 = inputs_01 + gpt_json_io.format_instructions
|
||||||
analyze_res = run_gpt_fn(inputs_02, "", [inputs_01, analyze_res_cot_01])
|
analyze_res = run_gpt_fn(inputs_02, "", [inputs_01, analyze_res_cot_01])
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import time
|
|||||||
import importlib
|
import importlib
|
||||||
from toolbox import trimmed_format_exc, gen_time_str, get_log_folder
|
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 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_latest_msg
|
from toolbox import promote_file_to_downloadzone, get_log_folder, update_ui_lastest_msg
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
def get_class_name(class_string):
|
def get_class_name(class_string):
|
||||||
@@ -41,11 +41,11 @@ def is_function_successfully_generated(fn_path, class_name, return_dict):
|
|||||||
# Now you can create an instance of the class
|
# Now you can create an instance of the class
|
||||||
instance = some_class()
|
instance = some_class()
|
||||||
return_dict['success'] = True
|
return_dict['success'] = True
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
return_dict['traceback'] = trimmed_format_exc()
|
return_dict['traceback'] = trimmed_format_exc()
|
||||||
return
|
return
|
||||||
|
|
||||||
def subprocess_worker(code, file_path, return_dict):
|
def subprocess_worker(code, file_path, return_dict):
|
||||||
return_dict['result'] = None
|
return_dict['result'] = None
|
||||||
return_dict['success'] = False
|
return_dict['success'] = False
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import platform
|
import platform
|
||||||
import pickle
|
import pickle
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ class Actor(BaseModel):
|
|||||||
film_names: List[str] = Field(description="list of names of films they starred in")
|
film_names: List[str] = Field(description="list of names of films they starred in")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json, re
|
import json, re, logging
|
||||||
from loguru import logger as logging
|
|
||||||
|
|
||||||
PYDANTIC_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
|
PYDANTIC_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ class GptJsonIO():
|
|||||||
if "type" in reduced_schema:
|
if "type" in reduced_schema:
|
||||||
del reduced_schema["type"]
|
del reduced_schema["type"]
|
||||||
# Ensure json in context is well-formed with double quotes.
|
# Ensure json in context is well-formed with double quotes.
|
||||||
schema_str = json.dumps(reduced_schema)
|
|
||||||
if self.example_instruction:
|
if self.example_instruction:
|
||||||
|
schema_str = json.dumps(reduced_schema)
|
||||||
return PYDANTIC_FORMAT_INSTRUCTIONS.format(schema=schema_str)
|
return PYDANTIC_FORMAT_INSTRUCTIONS.format(schema=schema_str)
|
||||||
else:
|
else:
|
||||||
return PYDANTIC_FORMAT_INSTRUCTIONS_SIMPLE.format(schema=schema_str)
|
return PYDANTIC_FORMAT_INSTRUCTIONS_SIMPLE.format(schema=schema_str)
|
||||||
@@ -89,7 +89,7 @@ class GptJsonIO():
|
|||||||
error + "\n\n" + \
|
error + "\n\n" + \
|
||||||
"Now, fix this json string. \n\n"
|
"Now, fix this json string. \n\n"
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
def generate_output_auto_repair(self, response, gpt_gen_fn):
|
def generate_output_auto_repair(self, response, gpt_gen_fn):
|
||||||
"""
|
"""
|
||||||
response: string containing canidate json
|
response: string containing canidate json
|
||||||
@@ -102,10 +102,10 @@ class GptJsonIO():
|
|||||||
logging.info(f'Repairing json:{response}')
|
logging.info(f'Repairing json:{response}')
|
||||||
repair_prompt = self.generate_repair_prompt(broken_json = response, error=repr(e))
|
repair_prompt = self.generate_repair_prompt(broken_json = response, error=repr(e))
|
||||||
result = self.generate_output(gpt_gen_fn(repair_prompt, self.format_instructions))
|
result = self.generate_output(gpt_gen_fn(repair_prompt, self.format_instructions))
|
||||||
logging.info('Repair json success.')
|
logging.info('Repaire json success.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 没辙了,放弃治疗
|
# 没辙了,放弃治疗
|
||||||
logging.info('Repair json fail.')
|
logging.info('Repaire json fail.')
|
||||||
raise JsonStringError('Cannot repair json.', str(e))
|
raise JsonStringError('Cannot repair json.', str(e))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
|
||||||
|
|
||||||
def structure_output(txt, prompt, err_msg, run_gpt_fn, pydantic_cls):
|
|
||||||
gpt_json_io = GptJsonIO(pydantic_cls)
|
|
||||||
analyze_res = run_gpt_fn(
|
|
||||||
txt,
|
|
||||||
sys_prompt=prompt + gpt_json_io.format_instructions
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
friend = gpt_json_io.generate_output_auto_repair(analyze_res, run_gpt_fn)
|
|
||||||
except JsonStringError as e:
|
|
||||||
return None, err_msg
|
|
||||||
|
|
||||||
err_msg = ""
|
|
||||||
return friend, err_msg
|
|
||||||
|
|
||||||
|
|
||||||
def select_tool(prompt, run_gpt_fn, pydantic_cls):
|
|
||||||
pydantic_cls_instance, err_msg = structure_output(
|
|
||||||
txt=prompt,
|
|
||||||
prompt="根据提示, 分析应该调用哪个工具函数\n\n",
|
|
||||||
err_msg=f"不能理解该联系人",
|
|
||||||
run_gpt_fn=run_gpt_fn,
|
|
||||||
pydantic_cls=pydantic_cls
|
|
||||||
)
|
|
||||||
return pydantic_cls_instance, err_msg
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import os
|
from toolbox import update_ui, update_ui_lastest_msg, get_log_folder
|
||||||
import re
|
from toolbox import get_conf, objdump, objload, promote_file_to_downloadzone
|
||||||
import shutil
|
from .latex_toolbox import PRESERVE, TRANSFORM
|
||||||
import numpy as np
|
from .latex_toolbox import set_forbidden_text, set_forbidden_text_begin_end, set_forbidden_text_careful_brace
|
||||||
from loguru import logger
|
from .latex_toolbox import reverse_forbidden_text_careful_brace, reverse_forbidden_text, convert_to_linklist, post_process
|
||||||
from toolbox import update_ui, update_ui_latest_msg, get_log_folder, gen_time_str
|
from .latex_toolbox import fix_content, find_main_tex_file, merge_tex_files, compile_latex_with_timeout
|
||||||
from toolbox import get_conf, promote_file_to_downloadzone
|
from .latex_toolbox import find_title_and_abs
|
||||||
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
|
|
||||||
from crazy_functions.latex_fns.latex_toolbox import reverse_forbidden_text_careful_brace, reverse_forbidden_text, convert_to_linklist, post_process
|
|
||||||
from crazy_functions.latex_fns.latex_toolbox import fix_content, find_main_tex_file, merge_tex_files, compile_latex_with_timeout
|
|
||||||
from crazy_functions.latex_fns.latex_toolbox import find_title_and_abs
|
|
||||||
from crazy_functions.latex_fns.latex_pickle_io import objdump, objload
|
|
||||||
|
|
||||||
|
import os, shutil
|
||||||
|
import re
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
pj = os.path.join
|
pj = os.path.join
|
||||||
|
|
||||||
@@ -20,7 +17,7 @@ def split_subprocess(txt, project_folder, return_dict, opts):
|
|||||||
"""
|
"""
|
||||||
break down latex file to a linked list,
|
break down latex file to a linked list,
|
||||||
each node use a preserve flag to indicate whether it should
|
each node use a preserve flag to indicate whether it should
|
||||||
be processed by GPT.
|
be proccessed by GPT.
|
||||||
"""
|
"""
|
||||||
text = txt
|
text = txt
|
||||||
mask = np.zeros(len(txt), dtype=np.uint8) + TRANSFORM
|
mask = np.zeros(len(txt), dtype=np.uint8) + TRANSFORM
|
||||||
@@ -85,24 +82,24 @@ class LatexPaperSplit():
|
|||||||
"""
|
"""
|
||||||
break down latex file to a linked list,
|
break down latex file to a linked list,
|
||||||
each node use a preserve flag to indicate whether it should
|
each node use a preserve flag to indicate whether it should
|
||||||
be processed by GPT.
|
be proccessed by GPT.
|
||||||
"""
|
"""
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.nodes = None
|
self.nodes = None
|
||||||
self.msg = "*{\\scriptsize\\textbf{警告:该PDF由GPT-Academic开源项目调用大语言模型+Latex翻译插件一键生成," + \
|
self.msg = "*{\\scriptsize\\textbf{警告:该PDF由GPT-Academic开源项目调用大语言模型+Latex翻译插件一键生成," + \
|
||||||
"版权归原文作者所有。翻译内容可靠性无保障,请仔细鉴别并以原文为准。" + \
|
"版权归原文作者所有。翻译内容可靠性无保障,请仔细鉴别并以原文为准。" + \
|
||||||
"项目Github地址 \\url{https://github.com/binary-husky/gpt_academic/}。"
|
"项目Github地址 \\url{https://github.com/binary-husky/gpt_academic/}。"
|
||||||
# 请您不要删除或修改这行警告,除非您是论文的原作者(如果您是论文原作者,欢迎加README中的QQ联系开发者)
|
# 请您不要删除或修改这行警告,除非您是论文的原作者(如果您是论文原作者,欢迎加REAME中的QQ联系开发者)
|
||||||
self.msg_declare = "为了防止大语言模型的意外谬误产生扩散影响,禁止移除或修改此警告。}}\\\\"
|
self.msg_declare = "为了防止大语言模型的意外谬误产生扩散影响,禁止移除或修改此警告。}}\\\\"
|
||||||
self.title = "unknown"
|
self.title = "unknown"
|
||||||
self.abstract = "unknown"
|
self.abstract = "unknown"
|
||||||
|
|
||||||
def read_title_and_abstract(self, txt):
|
def read_title_and_abstract(self, txt):
|
||||||
try:
|
try:
|
||||||
title, abstract = find_title_and_abs(txt)
|
title, abstract = find_title_and_abs(txt)
|
||||||
if title is not None:
|
if title is not None:
|
||||||
self.title = title.replace('\n', ' ').replace('\\\\', ' ').replace(' ', '').replace(' ', '')
|
self.title = title.replace('\n', ' ').replace('\\\\', ' ').replace(' ', '').replace(' ', '')
|
||||||
if abstract is not None:
|
if abstract is not None:
|
||||||
self.abstract = abstract.replace('\n', ' ').replace('\\\\', ' ').replace(' ', '').replace(' ', '')
|
self.abstract = abstract.replace('\n', ' ').replace('\\\\', ' ').replace(' ', '').replace(' ', '')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -114,7 +111,7 @@ class LatexPaperSplit():
|
|||||||
result_string = ""
|
result_string = ""
|
||||||
node_cnt = 0
|
node_cnt = 0
|
||||||
line_cnt = 0
|
line_cnt = 0
|
||||||
|
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
if node.preserve:
|
if node.preserve:
|
||||||
line_cnt += node.string.count('\n')
|
line_cnt += node.string.count('\n')
|
||||||
@@ -147,18 +144,18 @@ class LatexPaperSplit():
|
|||||||
return result_string
|
return result_string
|
||||||
|
|
||||||
|
|
||||||
def split(self, txt, project_folder, opts):
|
def split(self, txt, project_folder, opts):
|
||||||
"""
|
"""
|
||||||
break down latex file to a linked list,
|
break down latex file to a linked list,
|
||||||
each node use a preserve flag to indicate whether it should
|
each node use a preserve flag to indicate whether it should
|
||||||
be processed by GPT.
|
be proccessed by GPT.
|
||||||
P.S. use multiprocessing to avoid timeout error
|
P.S. use multiprocessing to avoid timeout error
|
||||||
"""
|
"""
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
manager = multiprocessing.Manager()
|
manager = multiprocessing.Manager()
|
||||||
return_dict = manager.dict()
|
return_dict = manager.dict()
|
||||||
p = multiprocessing.Process(
|
p = multiprocessing.Process(
|
||||||
target=split_subprocess,
|
target=split_subprocess,
|
||||||
args=(txt, project_folder, return_dict, opts))
|
args=(txt, project_folder, return_dict, opts))
|
||||||
p.start()
|
p.start()
|
||||||
p.join()
|
p.join()
|
||||||
@@ -220,13 +217,13 @@ def Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin
|
|||||||
from ..crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
from ..crazy_utils import request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency
|
||||||
from .latex_actions import LatexPaperFileGroup, LatexPaperSplit
|
from .latex_actions import LatexPaperFileGroup, LatexPaperSplit
|
||||||
|
|
||||||
# <-------- 寻找主tex文件 ---------->
|
# <-------- 寻找主tex文件 ---------->
|
||||||
maintex = find_main_tex_file(file_manifest, mode)
|
maintex = find_main_tex_file(file_manifest, mode)
|
||||||
chatbot.append((f"定位主Latex文件", f'[Local Message] 分析结果:该项目的Latex主文件是{maintex}, 如果分析错误, 请立即终止程序, 删除或修改歧义文件, 然后重试。主程序即将开始, 请稍候。'))
|
chatbot.append((f"定位主Latex文件", f'[Local Message] 分析结果:该项目的Latex主文件是{maintex}, 如果分析错误, 请立即终止程序, 删除或修改歧义文件, 然后重试。主程序即将开始, 请稍候。'))
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
# <-------- 读取Latex文件, 将多文件tex工程融合为一个巨型tex ---------->
|
# <-------- 读取Latex文件, 将多文件tex工程融合为一个巨型tex ---------->
|
||||||
main_tex_basename = os.path.basename(maintex)
|
main_tex_basename = os.path.basename(maintex)
|
||||||
assert main_tex_basename.endswith('.tex')
|
assert main_tex_basename.endswith('.tex')
|
||||||
main_tex_basename_bare = main_tex_basename[:-4]
|
main_tex_basename_bare = main_tex_basename[:-4]
|
||||||
@@ -243,13 +240,13 @@ def Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin
|
|||||||
with open(project_folder + '/merge.tex', 'w', encoding='utf-8', errors='replace') as f:
|
with open(project_folder + '/merge.tex', 'w', encoding='utf-8', errors='replace') as f:
|
||||||
f.write(merged_content)
|
f.write(merged_content)
|
||||||
|
|
||||||
# <-------- 精细切分latex文件 ---------->
|
# <-------- 精细切分latex文件 ---------->
|
||||||
chatbot.append((f"Latex文件融合完成", f'[Local Message] 正在精细切分latex文件,这需要一段时间计算,文档越长耗时越长,请耐心等待。'))
|
chatbot.append((f"Latex文件融合完成", f'[Local Message] 正在精细切分latex文件,这需要一段时间计算,文档越长耗时越长,请耐心等待。'))
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
lps = LatexPaperSplit()
|
lps = LatexPaperSplit()
|
||||||
lps.read_title_and_abstract(merged_content)
|
lps.read_title_and_abstract(merged_content)
|
||||||
res = lps.split(merged_content, project_folder, opts) # 消耗时间的函数
|
res = lps.split(merged_content, project_folder, opts) # 消耗时间的函数
|
||||||
# <-------- 拆分过长的latex片段 ---------->
|
# <-------- 拆分过长的latex片段 ---------->
|
||||||
pfg = LatexPaperFileGroup()
|
pfg = LatexPaperFileGroup()
|
||||||
for index, r in enumerate(res):
|
for index, r in enumerate(res):
|
||||||
pfg.file_paths.append('segment-' + str(index))
|
pfg.file_paths.append('segment-' + str(index))
|
||||||
@@ -258,17 +255,17 @@ def Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin
|
|||||||
pfg.run_file_split(max_token_limit=1024)
|
pfg.run_file_split(max_token_limit=1024)
|
||||||
n_split = len(pfg.sp_file_contents)
|
n_split = len(pfg.sp_file_contents)
|
||||||
|
|
||||||
# <-------- 根据需要切换prompt ---------->
|
# <-------- 根据需要切换prompt ---------->
|
||||||
inputs_array, sys_prompt_array = switch_prompt(pfg, mode)
|
inputs_array, sys_prompt_array = switch_prompt(pfg, mode)
|
||||||
inputs_show_user_array = [f"{mode} {f}" for f in pfg.sp_file_tag]
|
inputs_show_user_array = [f"{mode} {f}" for f in pfg.sp_file_tag]
|
||||||
|
|
||||||
if os.path.exists(pj(project_folder,'temp.pkl')):
|
if os.path.exists(pj(project_folder,'temp.pkl')):
|
||||||
|
|
||||||
# <-------- 【仅调试】如果存在调试缓存文件,则跳过GPT请求环节 ---------->
|
# <-------- 【仅调试】如果存在调试缓存文件,则跳过GPT请求环节 ---------->
|
||||||
pfg = objload(file=pj(project_folder,'temp.pkl'))
|
pfg = objload(file=pj(project_folder,'temp.pkl'))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# <-------- gpt 多线程请求 ---------->
|
# <-------- gpt 多线程请求 ---------->
|
||||||
history_array = [[""] for _ in range(n_split)]
|
history_array = [[""] for _ in range(n_split)]
|
||||||
# LATEX_EXPERIMENTAL, = get_conf('LATEX_EXPERIMENTAL')
|
# LATEX_EXPERIMENTAL, = get_conf('LATEX_EXPERIMENTAL')
|
||||||
# if LATEX_EXPERIMENTAL:
|
# if LATEX_EXPERIMENTAL:
|
||||||
@@ -287,33 +284,32 @@ def Latex精细分解与转化(file_manifest, project_folder, llm_kwargs, plugin
|
|||||||
scroller_max_len = 40
|
scroller_max_len = 40
|
||||||
)
|
)
|
||||||
|
|
||||||
# <-------- 文本碎片重组为完整的tex片段 ---------->
|
# <-------- 文本碎片重组为完整的tex片段 ---------->
|
||||||
pfg.sp_file_result = []
|
pfg.sp_file_result = []
|
||||||
for i_say, gpt_say, orig_content in zip(gpt_response_collection[0::2], gpt_response_collection[1::2], pfg.sp_file_contents):
|
for i_say, gpt_say, orig_content in zip(gpt_response_collection[0::2], gpt_response_collection[1::2], pfg.sp_file_contents):
|
||||||
pfg.sp_file_result.append(gpt_say)
|
pfg.sp_file_result.append(gpt_say)
|
||||||
pfg.merge_result()
|
pfg.merge_result()
|
||||||
|
|
||||||
# <-------- 临时存储用于调试 ---------->
|
# <-------- 临时存储用于调试 ---------->
|
||||||
pfg.get_token_num = None
|
pfg.get_token_num = None
|
||||||
objdump(pfg, file=pj(project_folder,'temp.pkl'))
|
objdump(pfg, file=pj(project_folder,'temp.pkl'))
|
||||||
|
|
||||||
write_html(pfg.sp_file_contents, pfg.sp_file_result, chatbot=chatbot, project_folder=project_folder)
|
write_html(pfg.sp_file_contents, pfg.sp_file_result, chatbot=chatbot, project_folder=project_folder)
|
||||||
|
|
||||||
# <-------- 写出文件 ---------->
|
# <-------- 写出文件 ---------->
|
||||||
model_name = llm_kwargs['llm_model'].replace('_', '\\_') # 替换LLM模型名称中的下划线为转义字符
|
msg = f"当前大语言模型: {llm_kwargs['llm_model']},当前语言模型温度设定: {llm_kwargs['temperature']}。"
|
||||||
msg = f"当前大语言模型: {model_name},当前语言模型温度设定: {llm_kwargs['temperature']}。"
|
|
||||||
final_tex = lps.merge_result(pfg.file_result, mode, msg)
|
final_tex = lps.merge_result(pfg.file_result, mode, msg)
|
||||||
objdump((lps, pfg.file_result, mode, msg), file=pj(project_folder,'merge_result.pkl'))
|
objdump((lps, pfg.file_result, mode, msg), file=pj(project_folder,'merge_result.pkl'))
|
||||||
|
|
||||||
with open(project_folder + f'/merge_{mode}.tex', 'w', encoding='utf-8', errors='replace') as f:
|
with open(project_folder + f'/merge_{mode}.tex', 'w', encoding='utf-8', errors='replace') as f:
|
||||||
if mode != 'translate_zh' or "binary" in final_tex: f.write(final_tex)
|
if mode != 'translate_zh' or "binary" in final_tex: f.write(final_tex)
|
||||||
|
|
||||||
|
|
||||||
|
# <-------- 整理结果, 退出 ---------->
|
||||||
# <-------- 整理结果, 退出 ---------->
|
|
||||||
chatbot.append((f"完成了吗?", 'GPT结果已输出, 即将编译PDF'))
|
chatbot.append((f"完成了吗?", 'GPT结果已输出, 即将编译PDF'))
|
||||||
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
yield from update_ui(chatbot=chatbot, history=history) # 刷新界面
|
||||||
|
|
||||||
# <-------- 返回 ---------->
|
# <-------- 返回 ---------->
|
||||||
return project_folder + f'/merge_{mode}.tex'
|
return project_folder + f'/merge_{mode}.tex'
|
||||||
|
|
||||||
|
|
||||||
@@ -326,7 +322,7 @@ def remove_buggy_lines(file_path, log_path, tex_name, tex_name_pure, n_fix, work
|
|||||||
buggy_lines = [int(l) for l in buggy_lines]
|
buggy_lines = [int(l) for l in buggy_lines]
|
||||||
buggy_lines = sorted(buggy_lines)
|
buggy_lines = sorted(buggy_lines)
|
||||||
buggy_line = buggy_lines[0]-1
|
buggy_line = buggy_lines[0]-1
|
||||||
logger.warning("reversing tex line that has errors", buggy_line)
|
print("reversing tex line that has errors", buggy_line)
|
||||||
|
|
||||||
# 重组,逆转出错的段落
|
# 重组,逆转出错的段落
|
||||||
if buggy_line not in fixed_line:
|
if buggy_line not in fixed_line:
|
||||||
@@ -340,7 +336,7 @@ def remove_buggy_lines(file_path, log_path, tex_name, tex_name_pure, n_fix, work
|
|||||||
|
|
||||||
return True, f"{tex_name_pure}_fix_{n_fix}", buggy_lines
|
return True, f"{tex_name_pure}_fix_{n_fix}", buggy_lines
|
||||||
except:
|
except:
|
||||||
logger.error("Fatal error occurred, but we cannot identify error, please download zip, read latex log, and compile manually.")
|
print("Fatal error occurred, but we cannot identify error, please download zip, read latex log, and compile manually.")
|
||||||
return False, -1, [-1]
|
return False, -1, [-1]
|
||||||
|
|
||||||
|
|
||||||
@@ -351,42 +347,7 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
|||||||
max_try = 32
|
max_try = 32
|
||||||
chatbot.append([f"正在编译PDF文档", f'编译已经开始。当前工作路径为{work_folder},如果程序停顿5分钟以上,请直接去该路径下取回翻译结果,或者重启之后再度尝试 ...']); yield from update_ui(chatbot=chatbot, history=history)
|
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]) # 刷新界面
|
chatbot.append([f"正在编译PDF文档", '...']); yield from update_ui(chatbot=chatbot, history=history); time.sleep(1); chatbot[-1] = list(chatbot[-1]) # 刷新界面
|
||||||
yield from update_ui_latest_msg('编译已经开始...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_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:
|
while True:
|
||||||
import os
|
import os
|
||||||
@@ -396,59 +357,59 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
|||||||
shutil.copyfile(may_exist_bbl, target_bbl)
|
shutil.copyfile(may_exist_bbl, target_bbl)
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/738755/dont-make-me-manually-abort-a-latex-compile-when-theres-an-error
|
# https://stackoverflow.com/questions/738755/dont-make-me-manually-abort-a-latex-compile-when-theres-an-error
|
||||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译原始PDF ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_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)
|
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_modified), work_folder_modified)
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
if ok and os.path.exists(pj(work_folder_modified, f'{main_file_modified}.pdf')):
|
if ok and os.path.exists(pj(work_folder_modified, f'{main_file_modified}.pdf')):
|
||||||
# 只有第二步成功,才能继续下面的步骤
|
# 只有第二步成功,才能继续下面的步骤
|
||||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译BibTex ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译BibTex ...', chatbot, history) # 刷新Gradio前端界面
|
||||||
if not os.path.exists(pj(work_folder_original, f'{main_file_original}.bbl')):
|
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)
|
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')):
|
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)
|
ok = compile_latex_with_timeout(f'bibtex {main_file_modified}.aux', work_folder_modified)
|
||||||
|
|
||||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 编译文献交叉引用 ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_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(f'pdflatex -interaction=batchmode -file-line-error {main_file_original}.tex', work_folder_original)
|
||||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_modified), work_folder_modified)
|
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_modified}.tex', work_folder_modified)
|
||||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_original), work_folder_original)
|
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_original}.tex', work_folder_original)
|
||||||
ok = compile_latex_with_timeout(get_compile_command(compiler, main_file_modified), work_folder_modified)
|
ok = compile_latex_with_timeout(f'pdflatex -interaction=batchmode -file-line-error {main_file_modified}.tex', work_folder_modified)
|
||||||
|
|
||||||
if mode!='translate_zh':
|
if mode!='translate_zh':
|
||||||
yield from update_ui_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 使用latexdiff生成论文转化前后对比 ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_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')
|
print( 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())
|
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_latest_msg(f'尝试第 {n_fix}/{max_try} 次编译, 正在编译对比PDF ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_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'pdflatex -interaction=batchmode -file-line-error merge_diff.tex', work_folder)
|
||||||
ok = compile_latex_with_timeout(f'bibtex merge_diff.aux', work_folder)
|
ok = compile_latex_with_timeout(f'bibtex merge_diff.aux', work_folder)
|
||||||
ok = compile_latex_with_timeout(get_compile_command(compiler, 'merge_diff'), 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(f'pdflatex -interaction=batchmode -file-line-error merge_diff.tex', work_folder)
|
||||||
|
|
||||||
# <---------- 检查结果 ----------->
|
# <---------- 检查结果 ----------->
|
||||||
results_ = ""
|
results_ = ""
|
||||||
original_pdf_success = os.path.exists(pj(work_folder_original, f'{main_file_original}.pdf'))
|
original_pdf_success = os.path.exists(pj(work_folder_original, f'{main_file_original}.pdf'))
|
||||||
modified_pdf_success = os.path.exists(pj(work_folder_modified, f'{main_file_modified}.pdf'))
|
modified_pdf_success = os.path.exists(pj(work_folder_modified, f'{main_file_modified}.pdf'))
|
||||||
diff_pdf_success = os.path.exists(pj(work_folder, f'merge_diff.pdf'))
|
diff_pdf_success = os.path.exists(pj(work_folder, f'merge_diff.pdf'))
|
||||||
results_ += f"原始PDF编译是否成功: {original_pdf_success};"
|
results_ += f"原始PDF编译是否成功: {original_pdf_success};"
|
||||||
results_ += f"转化PDF编译是否成功: {modified_pdf_success};"
|
results_ += f"转化PDF编译是否成功: {modified_pdf_success};"
|
||||||
results_ += f"对比PDF编译是否成功: {diff_pdf_success};"
|
results_ += f"对比PDF编译是否成功: {diff_pdf_success};"
|
||||||
yield from update_ui_latest_msg(f'第{n_fix}编译结束:<br/>{results_}...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_msg(f'第{n_fix}编译结束:<br/>{results_}...', chatbot, history) # 刷新Gradio前端界面
|
||||||
|
|
||||||
if diff_pdf_success:
|
if diff_pdf_success:
|
||||||
result_pdf = pj(work_folder_modified, f'merge_diff.pdf') # get pdf path
|
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
|
promote_file_to_downloadzone(result_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
||||||
if modified_pdf_success:
|
if modified_pdf_success:
|
||||||
yield from update_ui_latest_msg(f'转化PDF编译已经成功, 正在尝试生成对比PDF, 请稍候 ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_msg(f'转化PDF编译已经成功, 正在尝试生成对比PDF, 请稍候 ...', chatbot, history) # 刷新Gradio前端界面
|
||||||
result_pdf = pj(work_folder_modified, f'{main_file_modified}.pdf') # get pdf path
|
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
|
origin_pdf = pj(work_folder_original, f'{main_file_original}.pdf') # get pdf path
|
||||||
if os.path.exists(pj(work_folder, '..', 'translation')):
|
if os.path.exists(pj(work_folder, '..', 'translation')):
|
||||||
shutil.copyfile(result_pdf, pj(work_folder, '..', 'translation', 'translate_zh.pdf'))
|
shutil.copyfile(result_pdf, pj(work_folder, '..', 'translation', 'translate_zh.pdf'))
|
||||||
promote_file_to_downloadzone(result_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
promote_file_to_downloadzone(result_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
||||||
# 将两个PDF拼接
|
# 将两个PDF拼接
|
||||||
if original_pdf_success:
|
if original_pdf_success:
|
||||||
try:
|
try:
|
||||||
from .latex_toolbox import merge_pdfs
|
from .latex_toolbox import merge_pdfs
|
||||||
concat_pdf = pj(work_folder_modified, f'comparison.pdf')
|
concat_pdf = pj(work_folder_modified, f'comparison.pdf')
|
||||||
@@ -457,14 +418,14 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
|||||||
shutil.copyfile(concat_pdf, pj(work_folder, '..', 'translation', 'comparison.pdf'))
|
shutil.copyfile(concat_pdf, pj(work_folder, '..', 'translation', 'comparison.pdf'))
|
||||||
promote_file_to_downloadzone(concat_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
promote_file_to_downloadzone(concat_pdf, rename_file=None, chatbot=chatbot) # promote file to web UI
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
print(e)
|
||||||
pass
|
pass
|
||||||
return True # 成功啦
|
return True # 成功啦
|
||||||
else:
|
else:
|
||||||
if n_fix>=max_try: break
|
if n_fix>=max_try: break
|
||||||
n_fix += 1
|
n_fix += 1
|
||||||
can_retry, main_file_modified, buggy_lines = remove_buggy_lines(
|
can_retry, main_file_modified, buggy_lines = remove_buggy_lines(
|
||||||
file_path=pj(work_folder_modified, f'{main_file_modified}.tex'),
|
file_path=pj(work_folder_modified, f'{main_file_modified}.tex'),
|
||||||
log_path=pj(work_folder_modified, f'{main_file_modified}.log'),
|
log_path=pj(work_folder_modified, f'{main_file_modified}.log'),
|
||||||
tex_name=f'{main_file_modified}.tex',
|
tex_name=f'{main_file_modified}.tex',
|
||||||
tex_name_pure=f'{main_file_modified}',
|
tex_name_pure=f'{main_file_modified}',
|
||||||
@@ -472,7 +433,7 @@ def 编译Latex(chatbot, history, main_file_original, main_file_modified, work_f
|
|||||||
work_folder_modified=work_folder_modified,
|
work_folder_modified=work_folder_modified,
|
||||||
fixed_line=fixed_line
|
fixed_line=fixed_line
|
||||||
)
|
)
|
||||||
yield from update_ui_latest_msg(f'由于最为关键的转化PDF编译失败, 将根据报错信息修正tex源文件并重试, 当前报错的latex代码处于第{buggy_lines}行 ...', chatbot, history) # 刷新Gradio前端界面
|
yield from update_ui_lastest_msg(f'由于最为关键的转化PDF编译失败, 将根据报错信息修正tex源文件并重试, 当前报错的latex代码处于第{buggy_lines}行 ...', chatbot, history) # 刷新Gradio前端界面
|
||||||
if not can_retry: break
|
if not can_retry: break
|
||||||
|
|
||||||
return False # 失败啦
|
return False # 失败啦
|
||||||
@@ -484,14 +445,14 @@ def write_html(sp_file_contents, sp_file_result, chatbot, project_folder):
|
|||||||
import shutil
|
import shutil
|
||||||
from crazy_functions.pdf_fns.report_gen_html import construct_html
|
from crazy_functions.pdf_fns.report_gen_html import construct_html
|
||||||
from toolbox import gen_time_str
|
from toolbox import gen_time_str
|
||||||
ch = construct_html()
|
ch = construct_html()
|
||||||
orig = ""
|
orig = ""
|
||||||
trans = ""
|
trans = ""
|
||||||
final = []
|
final = []
|
||||||
for c,r in zip(sp_file_contents, sp_file_result):
|
for c,r in zip(sp_file_contents, sp_file_result):
|
||||||
final.append(c)
|
final.append(c)
|
||||||
final.append(r)
|
final.append(r)
|
||||||
for i, k in enumerate(final):
|
for i, k in enumerate(final):
|
||||||
if i%2==0:
|
if i%2==0:
|
||||||
orig = k
|
orig = k
|
||||||
if i%2==1:
|
if i%2==1:
|
||||||
@@ -503,71 +464,4 @@ def write_html(sp_file_contents, sp_file_result, chatbot, project_folder):
|
|||||||
promote_file_to_downloadzone(file=res, chatbot=chatbot)
|
promote_file_to_downloadzone(file=res, chatbot=chatbot)
|
||||||
except:
|
except:
|
||||||
from toolbox import trimmed_format_exc
|
from toolbox import trimmed_format_exc
|
||||||
logger.error('writing html result failed:', trimmed_format_exc())
|
print('writing html result failed:', trimmed_format_exc())
|
||||||
|
|
||||||
|
|
||||||
def upload_to_gptac_cloud_if_user_allow(chatbot, arxiv_id):
|
|
||||||
try:
|
|
||||||
# 如果用户允许,我们将arxiv论文PDF上传到GPTAC学术云
|
|
||||||
from toolbox import map_file_to_sha256
|
|
||||||
# 检查是否顺利,如果没有生成预期的文件,则跳过
|
|
||||||
is_result_good = False
|
|
||||||
for file_path in chatbot._cookies.get("files_to_promote", []):
|
|
||||||
if file_path.endswith('translate_zh.pdf'):
|
|
||||||
is_result_good = True
|
|
||||||
if not is_result_good:
|
|
||||||
return
|
|
||||||
# 上传文件
|
|
||||||
for file_path in chatbot._cookies.get("files_to_promote", []):
|
|
||||||
align_name = None
|
|
||||||
# normalized name
|
|
||||||
for name in ['translate_zh.pdf', 'comparison.pdf']:
|
|
||||||
if file_path.endswith(name): align_name = name
|
|
||||||
# if match any align name
|
|
||||||
if align_name:
|
|
||||||
logger.info(f'Uploading to GPTAC cloud as the user has set `allow_cloud_io`: {file_path}')
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
import requests
|
|
||||||
url = 'https://cloud-2.agent-matrix.com/arxiv_tf_paper_normal_upload'
|
|
||||||
files = {'file': (align_name, f, 'application/octet-stream')}
|
|
||||||
data = {
|
|
||||||
'arxiv_id': arxiv_id,
|
|
||||||
'file_hash': map_file_to_sha256(file_path),
|
|
||||||
'language': 'zh',
|
|
||||||
'trans_prompt': 'to_be_implemented',
|
|
||||||
'llm_model': 'to_be_implemented',
|
|
||||||
'llm_model_param': 'to_be_implemented',
|
|
||||||
}
|
|
||||||
resp = requests.post(url=url, files=files, data=data, timeout=30)
|
|
||||||
logger.info(f'Uploading terminate ({resp.status_code})`: {file_path}')
|
|
||||||
except:
|
|
||||||
# 如果上传失败,不会中断程序,因为这是次要功能
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check_gptac_cloud(arxiv_id, chatbot):
|
|
||||||
import requests
|
|
||||||
success = False
|
|
||||||
downloaded = []
|
|
||||||
try:
|
|
||||||
for pdf_target in ['translate_zh.pdf', 'comparison.pdf']:
|
|
||||||
url = 'https://cloud-2.agent-matrix.com/arxiv_tf_paper_normal_exist'
|
|
||||||
data = {
|
|
||||||
'arxiv_id': arxiv_id,
|
|
||||||
'name': pdf_target,
|
|
||||||
}
|
|
||||||
resp = requests.post(url=url, data=data)
|
|
||||||
cache_hit_result = resp.text.strip('"')
|
|
||||||
if cache_hit_result.startswith("http"):
|
|
||||||
url = cache_hit_result
|
|
||||||
logger.info(f'Downloading from GPTAC cloud: {url}')
|
|
||||||
resp = requests.get(url=url, timeout=30)
|
|
||||||
target = os.path.join(get_log_folder(plugin_name='gptac_cloud'), gen_time_str(), pdf_target)
|
|
||||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
||||||
with open(target, 'wb') as f:
|
|
||||||
f.write(resp.content)
|
|
||||||
new_path = promote_file_to_downloadzone(target, chatbot=chatbot)
|
|
||||||
success = True
|
|
||||||
downloaded.append(new_path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return success, downloaded
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import pickle
|
|
||||||
|
|
||||||
|
|
||||||
class SafeUnpickler(pickle.Unpickler):
|
|
||||||
|
|
||||||
def get_safe_classes(self):
|
|
||||||
from crazy_functions.latex_fns.latex_actions import LatexPaperFileGroup, LatexPaperSplit
|
|
||||||
from crazy_functions.latex_fns.latex_toolbox import LinkedListNode
|
|
||||||
from numpy.core.multiarray import scalar
|
|
||||||
from numpy import dtype
|
|
||||||
# 定义允许的安全类
|
|
||||||
safe_classes = {
|
|
||||||
# 在这里添加其他安全的类
|
|
||||||
'LatexPaperFileGroup': LatexPaperFileGroup,
|
|
||||||
'LatexPaperSplit': LatexPaperSplit,
|
|
||||||
'LinkedListNode': LinkedListNode,
|
|
||||||
'scalar': scalar,
|
|
||||||
'dtype': dtype,
|
|
||||||
}
|
|
||||||
return safe_classes
|
|
||||||
|
|
||||||
def find_class(self, module, name):
|
|
||||||
# 只允许特定的类进行反序列化
|
|
||||||
self.safe_classes = self.get_safe_classes()
|
|
||||||
match_class_name = None
|
|
||||||
for class_name in self.safe_classes.keys():
|
|
||||||
if (class_name in f'{module}.{name}'):
|
|
||||||
match_class_name = class_name
|
|
||||||
if match_class_name is not None:
|
|
||||||
return self.safe_classes[match_class_name]
|
|
||||||
# 如果尝试加载未授权的类,则抛出异常
|
|
||||||
raise pickle.UnpicklingError(f"Attempted to deserialize unauthorized class '{name}' from module '{module}'")
|
|
||||||
|
|
||||||
def objdump(obj, file="objdump.tmp"):
|
|
||||||
|
|
||||||
with open(file, "wb+") as f:
|
|
||||||
pickle.dump(obj, f)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def objload(file="objdump.tmp"):
|
|
||||||
import os
|
|
||||||
|
|
||||||
if not os.path.exists(file):
|
|
||||||
return
|
|
||||||
with open(file, "rb") as f:
|
|
||||||
unpickler = SafeUnpickler(f)
|
|
||||||
return unpickler.load()
|
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import os
|
import os, shutil
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
PRESERVE = 0
|
PRESERVE = 0
|
||||||
TRANSFORM = 1
|
TRANSFORM = 1
|
||||||
|
|
||||||
pj = os.path.join
|
pj = os.path.join
|
||||||
|
|
||||||
|
class LinkedListNode():
|
||||||
class LinkedListNode:
|
|
||||||
"""
|
"""
|
||||||
Linked List Node
|
Linked List Node
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, string, preserve=True) -> None:
|
def __init__(self, string, preserve=True) -> None:
|
||||||
self.string = string
|
self.string = string
|
||||||
self.preserve = preserve
|
self.preserve = preserve
|
||||||
@@ -23,47 +18,41 @@ class LinkedListNode:
|
|||||||
# self.begin_line = 0
|
# self.begin_line = 0
|
||||||
# self.begin_char = 0
|
# self.begin_char = 0
|
||||||
|
|
||||||
|
|
||||||
def convert_to_linklist(text, mask):
|
def convert_to_linklist(text, mask):
|
||||||
root = LinkedListNode("", preserve=True)
|
root = LinkedListNode("", preserve=True)
|
||||||
current_node = root
|
current_node = root
|
||||||
for c, m, i in zip(text, mask, range(len(text))):
|
for c, m, i in zip(text, mask, range(len(text))):
|
||||||
if (m == PRESERVE and current_node.preserve) or (
|
if (m==PRESERVE and current_node.preserve) \
|
||||||
m == TRANSFORM and not current_node.preserve
|
or (m==TRANSFORM and not current_node.preserve):
|
||||||
):
|
|
||||||
# add
|
# add
|
||||||
current_node.string += c
|
current_node.string += c
|
||||||
else:
|
else:
|
||||||
current_node.next = LinkedListNode(c, preserve=(m == PRESERVE))
|
current_node.next = LinkedListNode(c, preserve=(m==PRESERVE))
|
||||||
current_node = current_node.next
|
current_node = current_node.next
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
def post_process(root):
|
def post_process(root):
|
||||||
# 修复括号
|
# 修复括号
|
||||||
node = root
|
node = root
|
||||||
while True:
|
while True:
|
||||||
string = node.string
|
string = node.string
|
||||||
if node.preserve:
|
if node.preserve:
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def break_check(string):
|
def break_check(string):
|
||||||
str_stack = [""] # (lv, index)
|
str_stack = [""] # (lv, index)
|
||||||
for i, c in enumerate(string):
|
for i, c in enumerate(string):
|
||||||
if c == "{":
|
if c == '{':
|
||||||
str_stack.append("{")
|
str_stack.append('{')
|
||||||
elif c == "}":
|
elif c == '}':
|
||||||
if len(str_stack) == 1:
|
if len(str_stack) == 1:
|
||||||
logger.warning("fixing brace error")
|
print('stack fix')
|
||||||
return i
|
return i
|
||||||
str_stack.pop(-1)
|
str_stack.pop(-1)
|
||||||
else:
|
else:
|
||||||
str_stack[-1] += c
|
str_stack[-1] += c
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
bp = break_check(string)
|
bp = break_check(string)
|
||||||
|
|
||||||
if bp == -1:
|
if bp == -1:
|
||||||
@@ -80,66 +69,51 @@ def post_process(root):
|
|||||||
node.next = q
|
node.next = q
|
||||||
|
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
|
|
||||||
# 屏蔽空行和太短的句子
|
# 屏蔽空行和太短的句子
|
||||||
node = root
|
node = root
|
||||||
while True:
|
while True:
|
||||||
if len(node.string.strip("\n").strip("")) == 0:
|
if len(node.string.strip('\n').strip(''))==0: node.preserve = True
|
||||||
node.preserve = True
|
if len(node.string.strip('\n').strip(''))<42: node.preserve = True
|
||||||
if len(node.string.strip("\n").strip("")) < 42:
|
|
||||||
node.preserve = True
|
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
node = root
|
node = root
|
||||||
while True:
|
while True:
|
||||||
if node.next and node.preserve and node.next.preserve:
|
if node.next and node.preserve and node.next.preserve:
|
||||||
node.string += node.next.string
|
node.string += node.next.string
|
||||||
node.next = node.next.next
|
node.next = node.next.next
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
|
|
||||||
# 将前后断行符脱离
|
# 将前后断行符脱离
|
||||||
node = root
|
node = root
|
||||||
prev_node = None
|
prev_node = None
|
||||||
while True:
|
while True:
|
||||||
if not node.preserve:
|
if not node.preserve:
|
||||||
lstriped_ = node.string.lstrip().lstrip("\n")
|
lstriped_ = node.string.lstrip().lstrip('\n')
|
||||||
if (
|
if (prev_node is not None) and (prev_node.preserve) and (len(lstriped_)!=len(node.string)):
|
||||||
(prev_node is not None)
|
prev_node.string += node.string[:-len(lstriped_)]
|
||||||
and (prev_node.preserve)
|
|
||||||
and (len(lstriped_) != len(node.string))
|
|
||||||
):
|
|
||||||
prev_node.string += node.string[: -len(lstriped_)]
|
|
||||||
node.string = lstriped_
|
node.string = lstriped_
|
||||||
rstriped_ = node.string.rstrip().rstrip("\n")
|
rstriped_ = node.string.rstrip().rstrip('\n')
|
||||||
if (
|
if (node.next is not None) and (node.next.preserve) and (len(rstriped_)!=len(node.string)):
|
||||||
(node.next is not None)
|
node.next.string = node.string[len(rstriped_):] + node.next.string
|
||||||
and (node.next.preserve)
|
|
||||||
and (len(rstriped_) != len(node.string))
|
|
||||||
):
|
|
||||||
node.next.string = node.string[len(rstriped_) :] + node.next.string
|
|
||||||
node.string = rstriped_
|
node.string = rstriped_
|
||||||
# =-=-=
|
# =====
|
||||||
prev_node = node
|
prev_node = node
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
|
|
||||||
# 标注节点的行数范围
|
# 标注节点的行数范围
|
||||||
node = root
|
node = root
|
||||||
n_line = 0
|
n_line = 0
|
||||||
expansion = 2
|
expansion = 2
|
||||||
while True:
|
while True:
|
||||||
n_l = node.string.count("\n")
|
n_l = node.string.count('\n')
|
||||||
node.range = [n_line - expansion, n_line + n_l + expansion] # 失败时,扭转的范围
|
node.range = [n_line-expansion, n_line+n_l+expansion] # 失败时,扭转的范围
|
||||||
n_line = n_line + n_l
|
n_line = n_line+n_l
|
||||||
node = node.next
|
node = node.next
|
||||||
if node is None:
|
if node is None: break
|
||||||
break
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
@@ -154,125 +128,97 @@ def set_forbidden_text(text, mask, pattern, flags=0):
|
|||||||
"""
|
"""
|
||||||
Add a preserve text area in this paper
|
Add a preserve text area in this paper
|
||||||
e.g. with pattern = r"\\begin\{algorithm\}(.*?)\\end\{algorithm\}"
|
e.g. with pattern = r"\\begin\{algorithm\}(.*?)\\end\{algorithm\}"
|
||||||
you can mask out (mask = PRESERVE so that text become untouchable for GPT)
|
you can mask out (mask = PRESERVE so that text become untouchable for GPT)
|
||||||
everything between "\begin{equation}" and "\end{equation}"
|
everything between "\begin{equation}" and "\end{equation}"
|
||||||
"""
|
"""
|
||||||
if isinstance(pattern, list):
|
if isinstance(pattern, list): pattern = '|'.join(pattern)
|
||||||
pattern = "|".join(pattern)
|
|
||||||
pattern_compile = re.compile(pattern, flags)
|
pattern_compile = re.compile(pattern, flags)
|
||||||
for res in pattern_compile.finditer(text):
|
for res in pattern_compile.finditer(text):
|
||||||
mask[res.span()[0] : res.span()[1]] = PRESERVE
|
mask[res.span()[0]:res.span()[1]] = PRESERVE
|
||||||
return text, mask
|
return text, mask
|
||||||
|
|
||||||
|
|
||||||
def reverse_forbidden_text(text, mask, pattern, flags=0, forbid_wrapper=True):
|
def reverse_forbidden_text(text, mask, pattern, flags=0, forbid_wrapper=True):
|
||||||
"""
|
"""
|
||||||
Move area out of preserve area (make text editable for GPT)
|
Move area out of preserve area (make text editable for GPT)
|
||||||
count the number of the braces so as to catch complete text area.
|
count the number of the braces so as to catch compelete text area.
|
||||||
e.g.
|
e.g.
|
||||||
\begin{abstract} blablablablablabla. \end{abstract}
|
\begin{abstract} blablablablablabla. \end{abstract}
|
||||||
"""
|
"""
|
||||||
if isinstance(pattern, list):
|
if isinstance(pattern, list): pattern = '|'.join(pattern)
|
||||||
pattern = "|".join(pattern)
|
|
||||||
pattern_compile = re.compile(pattern, flags)
|
pattern_compile = re.compile(pattern, flags)
|
||||||
for res in pattern_compile.finditer(text):
|
for res in pattern_compile.finditer(text):
|
||||||
if not forbid_wrapper:
|
if not forbid_wrapper:
|
||||||
mask[res.span()[0] : res.span()[1]] = TRANSFORM
|
mask[res.span()[0]:res.span()[1]] = TRANSFORM
|
||||||
else:
|
else:
|
||||||
mask[res.regs[0][0] : res.regs[1][0]] = PRESERVE # '\\begin{abstract}'
|
mask[res.regs[0][0]: res.regs[1][0]] = PRESERVE # '\\begin{abstract}'
|
||||||
mask[res.regs[1][0] : res.regs[1][1]] = TRANSFORM # abstract
|
mask[res.regs[1][0]: res.regs[1][1]] = TRANSFORM # abstract
|
||||||
mask[res.regs[1][1] : res.regs[0][1]] = PRESERVE # abstract
|
mask[res.regs[1][1]: res.regs[0][1]] = PRESERVE # abstract
|
||||||
return text, mask
|
return text, mask
|
||||||
|
|
||||||
|
|
||||||
def set_forbidden_text_careful_brace(text, mask, pattern, flags=0):
|
def set_forbidden_text_careful_brace(text, mask, pattern, flags=0):
|
||||||
"""
|
"""
|
||||||
Add a preserve text area in this paper (text become untouchable for GPT).
|
Add a preserve text area in this paper (text become untouchable for GPT).
|
||||||
count the number of the braces so as to catch complete text area.
|
count the number of the braces so as to catch compelete text area.
|
||||||
e.g.
|
e.g.
|
||||||
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
||||||
"""
|
"""
|
||||||
pattern_compile = re.compile(pattern, flags)
|
pattern_compile = re.compile(pattern, flags)
|
||||||
for res in pattern_compile.finditer(text):
|
for res in pattern_compile.finditer(text):
|
||||||
brace_level = -1
|
brace_level = -1
|
||||||
p = begin = end = res.regs[0][0]
|
p = begin = end = res.regs[0][0]
|
||||||
for _ in range(1024 * 16):
|
for _ in range(1024*16):
|
||||||
if text[p] == "}" and brace_level == 0:
|
if text[p] == '}' and brace_level == 0: break
|
||||||
break
|
elif text[p] == '}': brace_level -= 1
|
||||||
elif text[p] == "}":
|
elif text[p] == '{': brace_level += 1
|
||||||
brace_level -= 1
|
|
||||||
elif text[p] == "{":
|
|
||||||
brace_level += 1
|
|
||||||
p += 1
|
p += 1
|
||||||
end = p + 1
|
end = p+1
|
||||||
mask[begin:end] = PRESERVE
|
mask[begin:end] = PRESERVE
|
||||||
return text, mask
|
return text, mask
|
||||||
|
|
||||||
|
def reverse_forbidden_text_careful_brace(text, mask, pattern, flags=0, forbid_wrapper=True):
|
||||||
def reverse_forbidden_text_careful_brace(
|
|
||||||
text, mask, pattern, flags=0, forbid_wrapper=True
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Move area out of preserve area (make text editable for GPT)
|
Move area out of preserve area (make text editable for GPT)
|
||||||
count the number of the braces so as to catch complete text area.
|
count the number of the braces so as to catch compelete text area.
|
||||||
e.g.
|
e.g.
|
||||||
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
\caption{blablablablabla\texbf{blablabla}blablabla.}
|
||||||
"""
|
"""
|
||||||
pattern_compile = re.compile(pattern, flags)
|
pattern_compile = re.compile(pattern, flags)
|
||||||
for res in pattern_compile.finditer(text):
|
for res in pattern_compile.finditer(text):
|
||||||
brace_level = 0
|
brace_level = 0
|
||||||
p = begin = end = res.regs[1][0]
|
p = begin = end = res.regs[1][0]
|
||||||
for _ in range(1024 * 16):
|
for _ in range(1024*16):
|
||||||
if text[p] == "}" and brace_level == 0:
|
if text[p] == '}' and brace_level == 0: break
|
||||||
break
|
elif text[p] == '}': brace_level -= 1
|
||||||
elif text[p] == "}":
|
elif text[p] == '{': brace_level += 1
|
||||||
brace_level -= 1
|
|
||||||
elif text[p] == "{":
|
|
||||||
brace_level += 1
|
|
||||||
p += 1
|
p += 1
|
||||||
end = p
|
end = p
|
||||||
mask[begin:end] = TRANSFORM
|
mask[begin:end] = TRANSFORM
|
||||||
if forbid_wrapper:
|
if forbid_wrapper:
|
||||||
mask[res.regs[0][0] : begin] = PRESERVE
|
mask[res.regs[0][0]:begin] = PRESERVE
|
||||||
mask[end : res.regs[0][1]] = PRESERVE
|
mask[end:res.regs[0][1]] = PRESERVE
|
||||||
return text, mask
|
return text, mask
|
||||||
|
|
||||||
|
|
||||||
def set_forbidden_text_begin_end(text, mask, pattern, flags=0, limit_n_lines=42):
|
def set_forbidden_text_begin_end(text, mask, pattern, flags=0, limit_n_lines=42):
|
||||||
"""
|
"""
|
||||||
Find all \begin{} ... \end{} text block that with less than limit_n_lines lines.
|
Find all \begin{} ... \end{} text block that with less than limit_n_lines lines.
|
||||||
Add it to preserve area
|
Add it to preserve area
|
||||||
"""
|
"""
|
||||||
pattern_compile = re.compile(pattern, flags)
|
pattern_compile = re.compile(pattern, flags)
|
||||||
|
|
||||||
def search_with_line_limit(text, mask):
|
def search_with_line_limit(text, mask):
|
||||||
for res in pattern_compile.finditer(text):
|
for res in pattern_compile.finditer(text):
|
||||||
cmd = res.group(1) # begin{what}
|
cmd = res.group(1) # begin{what}
|
||||||
this = res.group(2) # content between begin and end
|
this = res.group(2) # content between begin and end
|
||||||
this_mask = mask[res.regs[2][0] : res.regs[2][1]]
|
this_mask = mask[res.regs[2][0]:res.regs[2][1]]
|
||||||
white_list = [
|
white_list = ['document', 'abstract', 'lemma', 'definition', 'sproof',
|
||||||
"document",
|
'em', 'emph', 'textit', 'textbf', 'itemize', 'enumerate']
|
||||||
"abstract",
|
if (cmd in white_list) or this.count('\n') >= limit_n_lines: # use a magical number 42
|
||||||
"lemma",
|
|
||||||
"definition",
|
|
||||||
"sproof",
|
|
||||||
"em",
|
|
||||||
"emph",
|
|
||||||
"textit",
|
|
||||||
"textbf",
|
|
||||||
"itemize",
|
|
||||||
"enumerate",
|
|
||||||
]
|
|
||||||
if (cmd in white_list) or this.count(
|
|
||||||
"\n"
|
|
||||||
) >= limit_n_lines: # use a magical number 42
|
|
||||||
this, this_mask = search_with_line_limit(this, this_mask)
|
this, this_mask = search_with_line_limit(this, this_mask)
|
||||||
mask[res.regs[2][0] : res.regs[2][1]] = this_mask
|
mask[res.regs[2][0]:res.regs[2][1]] = this_mask
|
||||||
else:
|
else:
|
||||||
mask[res.regs[0][0] : res.regs[0][1]] = PRESERVE
|
mask[res.regs[0][0]:res.regs[0][1]] = PRESERVE
|
||||||
return text, mask
|
return text, mask
|
||||||
|
return search_with_line_limit(text, mask)
|
||||||
|
|
||||||
return search_with_line_limit(text, mask)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -281,56 +227,45 @@ Latex Merge File
|
|||||||
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def find_main_tex_file(file_manifest, mode):
|
def find_main_tex_file(file_manifest, mode):
|
||||||
"""
|
"""
|
||||||
在多Tex文档中,寻找主文件,必须包含documentclass,返回找到的第一个。
|
在多Tex文档中,寻找主文件,必须包含documentclass,返回找到的第一个。
|
||||||
P.S. 但愿没人把latex模板放在里面传进来 (6.25 加入判定latex模板的代码)
|
P.S. 但愿没人把latex模板放在里面传进来 (6.25 加入判定latex模板的代码)
|
||||||
"""
|
"""
|
||||||
candidates = []
|
canidates = []
|
||||||
for texf in file_manifest:
|
for texf in file_manifest:
|
||||||
if os.path.basename(texf).startswith("merge"):
|
if os.path.basename(texf).startswith('merge'):
|
||||||
continue
|
continue
|
||||||
with open(texf, "r", encoding="utf8", errors="ignore") as f:
|
with open(texf, 'r', encoding='utf8', errors='ignore') as f:
|
||||||
file_content = f.read()
|
file_content = f.read()
|
||||||
if r"\documentclass" in file_content:
|
if r'\documentclass' in file_content:
|
||||||
candidates.append(texf)
|
canidates.append(texf)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(candidates) == 0:
|
if len(canidates) == 0:
|
||||||
raise RuntimeError("无法找到一个主Tex文件(包含documentclass关键字)")
|
raise RuntimeError('无法找到一个主Tex文件(包含documentclass关键字)')
|
||||||
elif len(candidates) == 1:
|
elif len(canidates) == 1:
|
||||||
return candidates[0]
|
return canidates[0]
|
||||||
else: # if len(candidates) >= 2 通过一些Latex模板中常见(但通常不会出现在正文)的单词,对不同latex源文件扣分,取评分最高者返回
|
else: # if len(canidates) >= 2 通过一些Latex模板中常见(但通常不会出现在正文)的单词,对不同latex源文件扣分,取评分最高者返回
|
||||||
candidates_score = []
|
canidates_score = []
|
||||||
# 给出一些判定模板文档的词作为扣分项
|
# 给出一些判定模板文档的词作为扣分项
|
||||||
unexpected_words = [
|
unexpected_words = ['\\LaTeX', 'manuscript', 'Guidelines', 'font', 'citations', 'rejected', 'blind review', 'reviewers']
|
||||||
"\\LaTeX",
|
expected_words = ['\\input', '\\ref', '\\cite']
|
||||||
"manuscript",
|
for texf in canidates:
|
||||||
"Guidelines",
|
canidates_score.append(0)
|
||||||
"font",
|
with open(texf, 'r', encoding='utf8', errors='ignore') as f:
|
||||||
"citations",
|
|
||||||
"rejected",
|
|
||||||
"blind review",
|
|
||||||
"reviewers",
|
|
||||||
]
|
|
||||||
expected_words = ["\\input", "\\ref", "\\cite"]
|
|
||||||
for texf in candidates:
|
|
||||||
candidates_score.append(0)
|
|
||||||
with open(texf, "r", encoding="utf8", errors="ignore") as f:
|
|
||||||
file_content = f.read()
|
file_content = f.read()
|
||||||
file_content = rm_comments(file_content)
|
file_content = rm_comments(file_content)
|
||||||
for uw in unexpected_words:
|
for uw in unexpected_words:
|
||||||
if uw in file_content:
|
if uw in file_content:
|
||||||
candidates_score[-1] -= 1
|
canidates_score[-1] -= 1
|
||||||
for uw in expected_words:
|
for uw in expected_words:
|
||||||
if uw in file_content:
|
if uw in file_content:
|
||||||
candidates_score[-1] += 1
|
canidates_score[-1] += 1
|
||||||
select = np.argmax(candidates_score) # 取评分最高者返回
|
select = np.argmax(canidates_score) # 取评分最高者返回
|
||||||
return candidates[select]
|
return canidates[select]
|
||||||
|
|
||||||
|
|
||||||
def rm_comments(main_file):
|
def rm_comments(main_file):
|
||||||
new_file_remove_comment_lines = []
|
new_file_remove_comment_lines = []
|
||||||
for l in main_file.splitlines():
|
for l in main_file.splitlines():
|
||||||
@@ -339,42 +274,33 @@ def rm_comments(main_file):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
new_file_remove_comment_lines.append(l)
|
new_file_remove_comment_lines.append(l)
|
||||||
main_file = "\n".join(new_file_remove_comment_lines)
|
main_file = '\n'.join(new_file_remove_comment_lines)
|
||||||
# main_file = re.sub(r"\\include{(.*?)}", r"\\input{\1}", main_file) # 将 \include 命令转换为 \input 命令
|
# main_file = re.sub(r"\\include{(.*?)}", r"\\input{\1}", main_file) # 将 \include 命令转换为 \input 命令
|
||||||
main_file = re.sub(r"(?<!\\)%.*", "", main_file) # 使用正则表达式查找半行注释, 并替换为空字符串
|
main_file = re.sub(r'(?<!\\)%.*', '', main_file) # 使用正则表达式查找半行注释, 并替换为空字符串
|
||||||
return main_file
|
return main_file
|
||||||
|
|
||||||
|
|
||||||
def find_tex_file_ignore_case(fp):
|
def find_tex_file_ignore_case(fp):
|
||||||
dir_name = os.path.dirname(fp)
|
dir_name = os.path.dirname(fp)
|
||||||
base_name = os.path.basename(fp)
|
base_name = os.path.basename(fp)
|
||||||
# 如果输入的文件路径是正确的
|
# 如果输入的文件路径是正确的
|
||||||
if os.path.isfile(pj(dir_name, base_name)):
|
if os.path.isfile(pj(dir_name, base_name)): return pj(dir_name, base_name)
|
||||||
return pj(dir_name, base_name)
|
|
||||||
# 如果不正确,试着加上.tex后缀试试
|
# 如果不正确,试着加上.tex后缀试试
|
||||||
if not base_name.endswith(".tex"):
|
if not base_name.endswith('.tex'): base_name+='.tex'
|
||||||
base_name += ".tex"
|
if os.path.isfile(pj(dir_name, base_name)): return pj(dir_name, base_name)
|
||||||
if os.path.isfile(pj(dir_name, base_name)):
|
|
||||||
return pj(dir_name, base_name)
|
|
||||||
# 如果还找不到,解除大小写限制,再试一次
|
# 如果还找不到,解除大小写限制,再试一次
|
||||||
import glob
|
import glob
|
||||||
|
for f in glob.glob(dir_name+'/*.tex'):
|
||||||
for f in glob.glob(dir_name + "/*.tex"):
|
|
||||||
base_name_s = os.path.basename(fp)
|
base_name_s = os.path.basename(fp)
|
||||||
base_name_f = os.path.basename(f)
|
base_name_f = os.path.basename(f)
|
||||||
if base_name_s.lower() == base_name_f.lower():
|
if base_name_s.lower() == base_name_f.lower(): return f
|
||||||
return f
|
|
||||||
# 试着加上.tex后缀试试
|
# 试着加上.tex后缀试试
|
||||||
if not base_name_s.endswith(".tex"):
|
if not base_name_s.endswith('.tex'): base_name_s+='.tex'
|
||||||
base_name_s += ".tex"
|
if base_name_s.lower() == base_name_f.lower(): return f
|
||||||
if base_name_s.lower() == base_name_f.lower():
|
|
||||||
return f
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def merge_tex_files_(project_foler, main_file, mode):
|
def merge_tex_files_(project_foler, main_file, mode):
|
||||||
"""
|
"""
|
||||||
Merge Tex project recursively
|
Merge Tex project recrusively
|
||||||
"""
|
"""
|
||||||
main_file = rm_comments(main_file)
|
main_file = rm_comments(main_file)
|
||||||
for s in reversed([q for q in re.finditer(r"\\input\{(.*?)\}", main_file, re.M)]):
|
for s in reversed([q for q in re.finditer(r"\\input\{(.*?)\}", main_file, re.M)]):
|
||||||
@@ -383,18 +309,18 @@ def merge_tex_files_(project_foler, main_file, mode):
|
|||||||
fp_ = find_tex_file_ignore_case(fp)
|
fp_ = find_tex_file_ignore_case(fp)
|
||||||
if fp_:
|
if fp_:
|
||||||
try:
|
try:
|
||||||
with open(fp_, "r", encoding="utf-8", errors="replace") as fx:
|
with open(fp_, 'r', encoding='utf-8', errors='replace') as fx: c = fx.read()
|
||||||
c = fx.read()
|
|
||||||
except:
|
except:
|
||||||
c = f"\n\nWarning from GPT-Academic: LaTex source file is missing!\n\n"
|
c = f"\n\nWarning from GPT-Academic: LaTex source file is missing!\n\n"
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"找不到{fp},Tex源文件缺失!")
|
raise RuntimeError(f'找不到{fp},Tex源文件缺失!')
|
||||||
c = merge_tex_files_(project_foler, c, mode)
|
c = merge_tex_files_(project_foler, c, mode)
|
||||||
main_file = main_file[: s.span()[0]] + c + main_file[s.span()[1] :]
|
main_file = main_file[:s.span()[0]] + c + main_file[s.span()[1]:]
|
||||||
return main_file
|
return main_file
|
||||||
|
|
||||||
|
|
||||||
def find_title_and_abs(main_file):
|
def find_title_and_abs(main_file):
|
||||||
|
|
||||||
def extract_abstract_1(text):
|
def extract_abstract_1(text):
|
||||||
pattern = r"\\abstract\{(.*?)\}"
|
pattern = r"\\abstract\{(.*?)\}"
|
||||||
match = re.search(pattern, text, re.DOTALL)
|
match = re.search(pattern, text, re.DOTALL)
|
||||||
@@ -429,37 +355,28 @@ def find_title_and_abs(main_file):
|
|||||||
|
|
||||||
def merge_tex_files(project_foler, main_file, mode):
|
def merge_tex_files(project_foler, main_file, mode):
|
||||||
"""
|
"""
|
||||||
Merge Tex project recursively
|
Merge Tex project recrusively
|
||||||
P.S. 顺便把CTEX塞进去以支持中文
|
P.S. 顺便把CTEX塞进去以支持中文
|
||||||
P.S. 顺便把Latex的注释去除
|
P.S. 顺便把Latex的注释去除
|
||||||
"""
|
"""
|
||||||
main_file = merge_tex_files_(project_foler, main_file, mode)
|
main_file = merge_tex_files_(project_foler, main_file, mode)
|
||||||
main_file = rm_comments(main_file)
|
main_file = rm_comments(main_file)
|
||||||
|
|
||||||
if mode == "translate_zh":
|
if mode == 'translate_zh':
|
||||||
# find paper documentclass
|
# find paper documentclass
|
||||||
pattern = re.compile(r"\\documentclass.*\n")
|
pattern = re.compile(r'\\documentclass.*\n')
|
||||||
match = pattern.search(main_file)
|
match = pattern.search(main_file)
|
||||||
assert match is not None, "Cannot find documentclass statement!"
|
assert match is not None, "Cannot find documentclass statement!"
|
||||||
position = match.end()
|
position = match.end()
|
||||||
add_ctex = "\\usepackage{ctex}\n"
|
add_ctex = '\\usepackage{ctex}\n'
|
||||||
add_url = "\\usepackage{url}\n" if "{url}" not in main_file else ""
|
add_url = '\\usepackage{url}\n' if '{url}' not in main_file else ''
|
||||||
main_file = main_file[:position] + add_ctex + add_url + main_file[position:]
|
main_file = main_file[:position] + add_ctex + add_url + main_file[position:]
|
||||||
# fontset=windows
|
# fontset=windows
|
||||||
import platform
|
import platform
|
||||||
|
main_file = re.sub(r"\\documentclass\[(.*?)\]{(.*?)}", r"\\documentclass[\1,fontset=windows,UTF8]{\2}",main_file)
|
||||||
main_file = re.sub(
|
main_file = re.sub(r"\\documentclass{(.*?)}", r"\\documentclass[fontset=windows,UTF8]{\1}",main_file)
|
||||||
r"\\documentclass\[(.*?)\]{(.*?)}",
|
|
||||||
r"\\documentclass[\1,fontset=windows,UTF8]{\2}",
|
|
||||||
main_file,
|
|
||||||
)
|
|
||||||
main_file = re.sub(
|
|
||||||
r"\\documentclass{(.*?)}",
|
|
||||||
r"\\documentclass[fontset=windows,UTF8]{\1}",
|
|
||||||
main_file,
|
|
||||||
)
|
|
||||||
# find paper abstract
|
# find paper abstract
|
||||||
pattern_opt1 = re.compile(r"\\begin\{abstract\}.*\n")
|
pattern_opt1 = re.compile(r'\\begin\{abstract\}.*\n')
|
||||||
pattern_opt2 = re.compile(r"\\abstract\{(.*?)\}", flags=re.DOTALL)
|
pattern_opt2 = re.compile(r"\\abstract\{(.*?)\}", flags=re.DOTALL)
|
||||||
match_opt1 = pattern_opt1.search(main_file)
|
match_opt1 = pattern_opt1.search(main_file)
|
||||||
match_opt2 = pattern_opt2.search(main_file)
|
match_opt2 = pattern_opt2.search(main_file)
|
||||||
@@ -468,9 +385,7 @@ def merge_tex_files(project_foler, main_file, mode):
|
|||||||
main_file = insert_abstract(main_file)
|
main_file = insert_abstract(main_file)
|
||||||
match_opt1 = pattern_opt1.search(main_file)
|
match_opt1 = pattern_opt1.search(main_file)
|
||||||
match_opt2 = pattern_opt2.search(main_file)
|
match_opt2 = pattern_opt2.search(main_file)
|
||||||
assert (match_opt1 is not None) or (
|
assert (match_opt1 is not None) or (match_opt2 is not None), "Cannot find paper abstract section!"
|
||||||
match_opt2 is not None
|
|
||||||
), "Cannot find paper abstract section!"
|
|
||||||
return main_file
|
return main_file
|
||||||
|
|
||||||
|
|
||||||
@@ -480,7 +395,6 @@ The GPT-Academic program cannot find abstract section in this paper.
|
|||||||
\end{abstract}
|
\end{abstract}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def insert_abstract(tex_content):
|
def insert_abstract(tex_content):
|
||||||
if "\\maketitle" in tex_content:
|
if "\\maketitle" in tex_content:
|
||||||
# find the position of "\maketitle"
|
# find the position of "\maketitle"
|
||||||
@@ -488,13 +402,7 @@ def insert_abstract(tex_content):
|
|||||||
# find the nearest ending line
|
# find the nearest ending line
|
||||||
end_line_index = tex_content.find("\n", find_index)
|
end_line_index = tex_content.find("\n", find_index)
|
||||||
# insert "abs_str" on the next line
|
# insert "abs_str" on the next line
|
||||||
modified_tex = (
|
modified_tex = tex_content[:end_line_index+1] + '\n\n' + insert_missing_abs_str + '\n\n' + tex_content[end_line_index+1:]
|
||||||
tex_content[: end_line_index + 1]
|
|
||||||
+ "\n\n"
|
|
||||||
+ insert_missing_abs_str
|
|
||||||
+ "\n\n"
|
|
||||||
+ tex_content[end_line_index + 1 :]
|
|
||||||
)
|
|
||||||
return modified_tex
|
return modified_tex
|
||||||
elif r"\begin{document}" in tex_content:
|
elif r"\begin{document}" in tex_content:
|
||||||
# find the position of "\maketitle"
|
# find the position of "\maketitle"
|
||||||
@@ -502,39 +410,29 @@ def insert_abstract(tex_content):
|
|||||||
# find the nearest ending line
|
# find the nearest ending line
|
||||||
end_line_index = tex_content.find("\n", find_index)
|
end_line_index = tex_content.find("\n", find_index)
|
||||||
# insert "abs_str" on the next line
|
# insert "abs_str" on the next line
|
||||||
modified_tex = (
|
modified_tex = tex_content[:end_line_index+1] + '\n\n' + insert_missing_abs_str + '\n\n' + tex_content[end_line_index+1:]
|
||||||
tex_content[: end_line_index + 1]
|
|
||||||
+ "\n\n"
|
|
||||||
+ insert_missing_abs_str
|
|
||||||
+ "\n\n"
|
|
||||||
+ tex_content[end_line_index + 1 :]
|
|
||||||
)
|
|
||||||
return modified_tex
|
return modified_tex
|
||||||
else:
|
else:
|
||||||
return tex_content
|
return tex_content
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||||
Post process
|
Post process
|
||||||
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def mod_inbraket(match):
|
def mod_inbraket(match):
|
||||||
"""
|
"""
|
||||||
为啥chatgpt会把cite里面的逗号换成中文逗号呀
|
为啥chatgpt会把cite里面的逗号换成中文逗号呀
|
||||||
"""
|
"""
|
||||||
# get the matched string
|
# get the matched string
|
||||||
cmd = match.group(1)
|
cmd = match.group(1)
|
||||||
str_to_modify = match.group(2)
|
str_to_modify = match.group(2)
|
||||||
# modify the matched string
|
# modify the matched string
|
||||||
str_to_modify = str_to_modify.replace(":", ":") # 前面是中文冒号,后面是英文冒号
|
str_to_modify = str_to_modify.replace(':', ':') # 前面是中文冒号,后面是英文冒号
|
||||||
str_to_modify = str_to_modify.replace(",", ",") # 前面是中文逗号,后面是英文逗号
|
str_to_modify = str_to_modify.replace(',', ',') # 前面是中文逗号,后面是英文逗号
|
||||||
# str_to_modify = 'BOOM'
|
# str_to_modify = 'BOOM'
|
||||||
return "\\" + cmd + "{" + str_to_modify + "}"
|
return "\\" + cmd + "{" + str_to_modify + "}"
|
||||||
|
|
||||||
|
|
||||||
def fix_content(final_tex, node_string):
|
def fix_content(final_tex, node_string):
|
||||||
"""
|
"""
|
||||||
Fix common GPT errors to increase success rate
|
Fix common GPT errors to increase success rate
|
||||||
@@ -545,10 +443,10 @@ def fix_content(final_tex, node_string):
|
|||||||
final_tex = re.sub(r"\\([a-z]{2,10})\{([^\}]*?)\}", mod_inbraket, string=final_tex)
|
final_tex = re.sub(r"\\([a-z]{2,10})\{([^\}]*?)\}", mod_inbraket, string=final_tex)
|
||||||
|
|
||||||
if "Traceback" in final_tex and "[Local Message]" in final_tex:
|
if "Traceback" in final_tex and "[Local Message]" in final_tex:
|
||||||
final_tex = node_string # 出问题了,还原原文
|
final_tex = node_string # 出问题了,还原原文
|
||||||
if node_string.count("\\begin") != final_tex.count("\\begin"):
|
if node_string.count('\\begin') != final_tex.count('\\begin'):
|
||||||
final_tex = node_string # 出问题了,还原原文
|
final_tex = node_string # 出问题了,还原原文
|
||||||
if node_string.count("\_") > 0 and node_string.count("\_") > final_tex.count("\_"):
|
if node_string.count('\_') > 0 and node_string.count('\_') > final_tex.count('\_'):
|
||||||
# walk and replace any _ without \
|
# walk and replace any _ without \
|
||||||
final_tex = re.sub(r"(?<!\\)_", "\\_", final_tex)
|
final_tex = re.sub(r"(?<!\\)_", "\\_", final_tex)
|
||||||
|
|
||||||
@@ -556,32 +454,24 @@ def fix_content(final_tex, node_string):
|
|||||||
# this function count the number of { and }
|
# this function count the number of { and }
|
||||||
brace_level = 0
|
brace_level = 0
|
||||||
for c in string:
|
for c in string:
|
||||||
if c == "{":
|
if c == "{": brace_level += 1
|
||||||
brace_level += 1
|
elif c == "}": brace_level -= 1
|
||||||
elif c == "}":
|
|
||||||
brace_level -= 1
|
|
||||||
return brace_level
|
return brace_level
|
||||||
|
|
||||||
def join_most(tex_t, tex_o):
|
def join_most(tex_t, tex_o):
|
||||||
# this function join translated string and original string when something goes wrong
|
# this function join translated string and original string when something goes wrong
|
||||||
p_t = 0
|
p_t = 0
|
||||||
p_o = 0
|
p_o = 0
|
||||||
|
|
||||||
def find_next(string, chars, begin):
|
def find_next(string, chars, begin):
|
||||||
p = begin
|
p = begin
|
||||||
while p < len(string):
|
while p < len(string):
|
||||||
if string[p] in chars:
|
if string[p] in chars: return p, string[p]
|
||||||
return p, string[p]
|
|
||||||
p += 1
|
p += 1
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
res1, char = find_next(tex_o, ["{", "}"], p_o)
|
res1, char = find_next(tex_o, ['{','}'], p_o)
|
||||||
if res1 is None:
|
if res1 is None: break
|
||||||
break
|
|
||||||
res2, char = find_next(tex_t, [char], p_t)
|
res2, char = find_next(tex_t, [char], p_t)
|
||||||
if res2 is None:
|
if res2 is None: break
|
||||||
break
|
|
||||||
p_o = res1 + 1
|
p_o = res1 + 1
|
||||||
p_t = res2 + 1
|
p_t = res2 + 1
|
||||||
return tex_t[:p_t] + tex_o[p_o:]
|
return tex_t[:p_t] + tex_o[p_o:]
|
||||||
@@ -590,279 +480,56 @@ def fix_content(final_tex, node_string):
|
|||||||
# 出问题了,还原部分原文,保证括号正确
|
# 出问题了,还原部分原文,保证括号正确
|
||||||
final_tex = join_most(final_tex, node_string)
|
final_tex = join_most(final_tex, node_string)
|
||||||
return final_tex
|
return final_tex
|
||||||
|
|
||||||
|
|
||||||
def compile_latex_with_timeout(command, cwd, timeout=60):
|
def compile_latex_with_timeout(command, cwd, timeout=60):
|
||||||
import subprocess
|
import subprocess
|
||||||
|
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
|
||||||
process = subprocess.Popen(
|
|
||||||
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
stdout, stderr = process.communicate(timeout=timeout)
|
stdout, stderr = process.communicate(timeout=timeout)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
process.kill()
|
process.kill()
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
logger.error("Process timed out (compile_latex_with_timeout)!")
|
print("Process timed out!")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_in_subprocess_wrapper_func(func, args, kwargs, return_dict, exception_dict):
|
def run_in_subprocess_wrapper_func(func, args, kwargs, return_dict, exception_dict):
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
return_dict["result"] = result
|
return_dict['result'] = result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
exception_dict["exception"] = exc_info
|
exception_dict['exception'] = exc_info
|
||||||
|
|
||||||
|
|
||||||
def run_in_subprocess(func):
|
def run_in_subprocess(func):
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
return_dict = multiprocessing.Manager().dict()
|
return_dict = multiprocessing.Manager().dict()
|
||||||
exception_dict = multiprocessing.Manager().dict()
|
exception_dict = multiprocessing.Manager().dict()
|
||||||
process = multiprocessing.Process(
|
process = multiprocessing.Process(target=run_in_subprocess_wrapper_func,
|
||||||
target=run_in_subprocess_wrapper_func,
|
args=(func, args, kwargs, return_dict, exception_dict))
|
||||||
args=(func, args, kwargs, return_dict, exception_dict),
|
|
||||||
)
|
|
||||||
process.start()
|
process.start()
|
||||||
process.join()
|
process.join()
|
||||||
process.close()
|
process.close()
|
||||||
if "exception" in exception_dict:
|
if 'exception' in exception_dict:
|
||||||
# ooops, the subprocess ran into an exception
|
# ooops, the subprocess ran into an exception
|
||||||
exc_info = exception_dict["exception"]
|
exc_info = exception_dict['exception']
|
||||||
raise exc_info[1].with_traceback(exc_info[2])
|
raise exc_info[1].with_traceback(exc_info[2])
|
||||||
if "result" in return_dict.keys():
|
if 'result' in return_dict.keys():
|
||||||
# If the subprocess ran successfully, return the result
|
# If the subprocess ran successfully, return the result
|
||||||
return return_dict["result"]
|
return return_dict['result']
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def _merge_pdfs(pdf1_path, pdf2_path, output_path):
|
def _merge_pdfs(pdf1_path, pdf2_path, output_path):
|
||||||
try:
|
import PyPDF2 # PyPDF2这个库有严重的内存泄露问题,把它放到子进程中运行,从而方便内存的释放
|
||||||
logger.info("Merging PDFs using _merge_pdfs_ng")
|
|
||||||
_merge_pdfs_ng(pdf1_path, pdf2_path, output_path)
|
|
||||||
except:
|
|
||||||
logger.info("Merging PDFs using _merge_pdfs_legacy")
|
|
||||||
_merge_pdfs_legacy(pdf1_path, pdf2_path, output_path)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_pdfs_ng(pdf1_path, pdf2_path, output_path):
|
|
||||||
import PyPDF2 # PyPDF2这个库有严重的内存泄露问题,把它放到子进程中运行,从而方便内存的释放
|
|
||||||
from PyPDF2.generic import NameObject, TextStringObject, ArrayObject, FloatObject, NumberObject
|
|
||||||
|
|
||||||
Percent = 1
|
|
||||||
# raise RuntimeError('PyPDF2 has a serious memory leak problem, please use other tools to merge PDF files.')
|
|
||||||
# Open the first PDF file
|
|
||||||
with open(pdf1_path, "rb") as pdf1_file:
|
|
||||||
pdf1_reader = PyPDF2.PdfFileReader(pdf1_file)
|
|
||||||
# Open the second PDF file
|
|
||||||
with open(pdf2_path, "rb") as pdf2_file:
|
|
||||||
pdf2_reader = PyPDF2.PdfFileReader(pdf2_file)
|
|
||||||
# Create a new PDF file to store the merged pages
|
|
||||||
output_writer = PyPDF2.PdfFileWriter()
|
|
||||||
# Determine the number of pages in each PDF file
|
|
||||||
num_pages = max(pdf1_reader.numPages, pdf2_reader.numPages)
|
|
||||||
# Merge the pages from the two PDF files
|
|
||||||
for page_num in range(num_pages):
|
|
||||||
# Add the page from the first PDF file
|
|
||||||
if page_num < pdf1_reader.numPages:
|
|
||||||
page1 = pdf1_reader.getPage(page_num)
|
|
||||||
else:
|
|
||||||
page1 = PyPDF2.PageObject.createBlankPage(pdf1_reader)
|
|
||||||
# Add the page from the second PDF file
|
|
||||||
if page_num < pdf2_reader.numPages:
|
|
||||||
page2 = pdf2_reader.getPage(page_num)
|
|
||||||
else:
|
|
||||||
page2 = PyPDF2.PageObject.createBlankPage(pdf1_reader)
|
|
||||||
# Create a new empty page with double width
|
|
||||||
new_page = PyPDF2.PageObject.createBlankPage(
|
|
||||||
width=int(
|
|
||||||
int(page1.mediaBox.getWidth())
|
|
||||||
+ int(page2.mediaBox.getWidth()) * Percent
|
|
||||||
),
|
|
||||||
height=max(page1.mediaBox.getHeight(), page2.mediaBox.getHeight()),
|
|
||||||
)
|
|
||||||
new_page.mergeTranslatedPage(page1, 0, 0)
|
|
||||||
new_page.mergeTranslatedPage(
|
|
||||||
page2,
|
|
||||||
int(
|
|
||||||
int(page1.mediaBox.getWidth())
|
|
||||||
- int(page2.mediaBox.getWidth()) * (1 - Percent)
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if "/Annots" in new_page:
|
|
||||||
annotations = new_page["/Annots"]
|
|
||||||
for i, annot in enumerate(annotations):
|
|
||||||
annot_obj = annot.get_object()
|
|
||||||
|
|
||||||
# 检查注释类型是否是链接(/Link)
|
|
||||||
if annot_obj.get("/Subtype") == "/Link":
|
|
||||||
# 检查是否为内部链接跳转(/GoTo)或外部URI链接(/URI)
|
|
||||||
action = annot_obj.get("/A")
|
|
||||||
if action:
|
|
||||||
|
|
||||||
if "/S" in action and action["/S"] == "/GoTo":
|
|
||||||
# 内部链接:跳转到文档中的某个页面
|
|
||||||
dest = action.get("/D") # 目标页或目标位置
|
|
||||||
# if dest and annot.idnum in page2_annot_id:
|
|
||||||
# if dest in pdf2_reader.named_destinations:
|
|
||||||
if dest and page2.annotations:
|
|
||||||
if annot in page2.annotations:
|
|
||||||
# 获取原始文件中跳转信息,包括跳转页面
|
|
||||||
destination = pdf2_reader.named_destinations[
|
|
||||||
dest
|
|
||||||
]
|
|
||||||
page_number = (
|
|
||||||
pdf2_reader.get_destination_page_number(
|
|
||||||
destination
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# 更新跳转信息,跳转到对应的页面和,指定坐标 (100, 150),缩放比例为 100%
|
|
||||||
# “/D”:[10,'/XYZ',100,100,0]
|
|
||||||
if destination.dest_array[1] == "/XYZ":
|
|
||||||
annot_obj["/A"].update(
|
|
||||||
{
|
|
||||||
NameObject("/D"): ArrayObject(
|
|
||||||
[
|
|
||||||
NumberObject(page_number),
|
|
||||||
destination.dest_array[1],
|
|
||||||
FloatObject(
|
|
||||||
destination.dest_array[
|
|
||||||
2
|
|
||||||
]
|
|
||||||
+ int(
|
|
||||||
page1.mediaBox.getWidth()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
destination.dest_array[3],
|
|
||||||
destination.dest_array[4],
|
|
||||||
]
|
|
||||||
) # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
annot_obj["/A"].update(
|
|
||||||
{
|
|
||||||
NameObject("/D"): ArrayObject(
|
|
||||||
[
|
|
||||||
NumberObject(page_number),
|
|
||||||
destination.dest_array[1],
|
|
||||||
]
|
|
||||||
) # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
rect = annot_obj.get("/Rect")
|
|
||||||
# 更新点击坐标
|
|
||||||
rect = ArrayObject(
|
|
||||||
[
|
|
||||||
FloatObject(
|
|
||||||
rect[0]
|
|
||||||
+ int(page1.mediaBox.getWidth())
|
|
||||||
),
|
|
||||||
rect[1],
|
|
||||||
FloatObject(
|
|
||||||
rect[2]
|
|
||||||
+ int(page1.mediaBox.getWidth())
|
|
||||||
),
|
|
||||||
rect[3],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
annot_obj.update(
|
|
||||||
{
|
|
||||||
NameObject(
|
|
||||||
"/Rect"
|
|
||||||
): rect # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# if dest and annot.idnum in page1_annot_id:
|
|
||||||
# if dest in pdf1_reader.named_destinations:
|
|
||||||
if dest and page1.annotations:
|
|
||||||
if annot in page1.annotations:
|
|
||||||
# 获取原始文件中跳转信息,包括跳转页面
|
|
||||||
destination = pdf1_reader.named_destinations[
|
|
||||||
dest
|
|
||||||
]
|
|
||||||
page_number = (
|
|
||||||
pdf1_reader.get_destination_page_number(
|
|
||||||
destination
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# 更新跳转信息,跳转到对应的页面和,指定坐标 (100, 150),缩放比例为 100%
|
|
||||||
# “/D”:[10,'/XYZ',100,100,0]
|
|
||||||
if destination.dest_array[1] == "/XYZ":
|
|
||||||
annot_obj["/A"].update(
|
|
||||||
{
|
|
||||||
NameObject("/D"): ArrayObject(
|
|
||||||
[
|
|
||||||
NumberObject(page_number),
|
|
||||||
destination.dest_array[1],
|
|
||||||
FloatObject(
|
|
||||||
destination.dest_array[
|
|
||||||
2
|
|
||||||
]
|
|
||||||
),
|
|
||||||
destination.dest_array[3],
|
|
||||||
destination.dest_array[4],
|
|
||||||
]
|
|
||||||
) # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
annot_obj["/A"].update(
|
|
||||||
{
|
|
||||||
NameObject("/D"): ArrayObject(
|
|
||||||
[
|
|
||||||
NumberObject(page_number),
|
|
||||||
destination.dest_array[1],
|
|
||||||
]
|
|
||||||
) # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
rect = annot_obj.get("/Rect")
|
|
||||||
rect = ArrayObject(
|
|
||||||
[
|
|
||||||
FloatObject(rect[0]),
|
|
||||||
rect[1],
|
|
||||||
FloatObject(rect[2]),
|
|
||||||
rect[3],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
annot_obj.update(
|
|
||||||
{
|
|
||||||
NameObject(
|
|
||||||
"/Rect"
|
|
||||||
): rect # 确保键和值是 PdfObject
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
elif "/S" in action and action["/S"] == "/URI":
|
|
||||||
# 外部链接:跳转到某个URI
|
|
||||||
uri = action.get("/URI")
|
|
||||||
output_writer.addPage(new_page)
|
|
||||||
# Save the merged PDF file
|
|
||||||
with open(output_path, "wb") as output_file:
|
|
||||||
output_writer.write(output_file)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_pdfs_legacy(pdf1_path, pdf2_path, output_path):
|
|
||||||
import PyPDF2 # PyPDF2这个库有严重的内存泄露问题,把它放到子进程中运行,从而方便内存的释放
|
|
||||||
|
|
||||||
Percent = 0.95
|
Percent = 0.95
|
||||||
# raise RuntimeError('PyPDF2 has a serious memory leak problem, please use other tools to merge PDF files.')
|
# raise RuntimeError('PyPDF2 has a serious memory leak problem, please use other tools to merge PDF files.')
|
||||||
# Open the first PDF file
|
# Open the first PDF file
|
||||||
with open(pdf1_path, "rb") as pdf1_file:
|
with open(pdf1_path, 'rb') as pdf1_file:
|
||||||
pdf1_reader = PyPDF2.PdfFileReader(pdf1_file)
|
pdf1_reader = PyPDF2.PdfFileReader(pdf1_file)
|
||||||
# Open the second PDF file
|
# Open the second PDF file
|
||||||
with open(pdf2_path, "rb") as pdf2_file:
|
with open(pdf2_path, 'rb') as pdf2_file:
|
||||||
pdf2_reader = PyPDF2.PdfFileReader(pdf2_file)
|
pdf2_reader = PyPDF2.PdfFileReader(pdf2_file)
|
||||||
# Create a new PDF file to store the merged pages
|
# Create a new PDF file to store the merged pages
|
||||||
output_writer = PyPDF2.PdfFileWriter()
|
output_writer = PyPDF2.PdfFileWriter()
|
||||||
@@ -882,25 +549,14 @@ def _merge_pdfs_legacy(pdf1_path, pdf2_path, output_path):
|
|||||||
page2 = PyPDF2.PageObject.createBlankPage(pdf1_reader)
|
page2 = PyPDF2.PageObject.createBlankPage(pdf1_reader)
|
||||||
# Create a new empty page with double width
|
# Create a new empty page with double width
|
||||||
new_page = PyPDF2.PageObject.createBlankPage(
|
new_page = PyPDF2.PageObject.createBlankPage(
|
||||||
width=int(
|
width = int(int(page1.mediaBox.getWidth()) + int(page2.mediaBox.getWidth()) * Percent),
|
||||||
int(page1.mediaBox.getWidth())
|
height = max(page1.mediaBox.getHeight(), page2.mediaBox.getHeight())
|
||||||
+ int(page2.mediaBox.getWidth()) * Percent
|
|
||||||
),
|
|
||||||
height=max(page1.mediaBox.getHeight(), page2.mediaBox.getHeight()),
|
|
||||||
)
|
)
|
||||||
new_page.mergeTranslatedPage(page1, 0, 0)
|
new_page.mergeTranslatedPage(page1, 0, 0)
|
||||||
new_page.mergeTranslatedPage(
|
new_page.mergeTranslatedPage(page2, int(int(page1.mediaBox.getWidth())-int(page2.mediaBox.getWidth())* (1-Percent)), 0)
|
||||||
page2,
|
|
||||||
int(
|
|
||||||
int(page1.mediaBox.getWidth())
|
|
||||||
- int(page2.mediaBox.getWidth()) * (1 - Percent)
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
output_writer.addPage(new_page)
|
output_writer.addPage(new_page)
|
||||||
# Save the merged PDF file
|
# Save the merged PDF file
|
||||||
with open(output_path, "wb") as output_file:
|
with open(output_path, 'wb') as output_file:
|
||||||
output_writer.write(output_file)
|
output_writer.write(output_file)
|
||||||
|
|
||||||
|
merge_pdfs = run_in_subprocess(_merge_pdfs) # PyPDF2这个库有严重的内存泄露问题,把它放到子进程中运行,从而方便内存的释放
|
||||||
merge_pdfs = run_in_subprocess(_merge_pdfs) # PyPDF2这个库有严重的内存泄露问题,把它放到子进程中运行,从而方便内存的释放
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import time, json, sys, struct
|
import time, logging, json, sys, struct
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from loguru import logger as logging
|
|
||||||
from scipy.io.wavfile import WAVE_FORMAT
|
from scipy.io.wavfile import WAVE_FORMAT
|
||||||
|
|
||||||
def write_numpy_to_wave(filename, rate, data, add_header=False):
|
def write_numpy_to_wave(filename, rate, data, add_header=False):
|
||||||
@@ -86,8 +85,8 @@ def write_numpy_to_wave(filename, rate, data, add_header=False):
|
|||||||
|
|
||||||
def is_speaker_speaking(vad, data, sample_rate):
|
def is_speaker_speaking(vad, data, sample_rate):
|
||||||
# Function to detect if the speaker is speaking
|
# Function to detect if the speaker is speaking
|
||||||
# The WebRTC VAD only accepts 16-bit mono PCM audio,
|
# The WebRTC VAD only accepts 16-bit mono PCM audio,
|
||||||
# sampled at 8000, 16000, 32000 or 48000 Hz.
|
# sampled at 8000, 16000, 32000 or 48000 Hz.
|
||||||
# A frame must be either 10, 20, or 30 ms in duration:
|
# A frame must be either 10, 20, or 30 ms in duration:
|
||||||
frame_duration = 30
|
frame_duration = 30
|
||||||
n_bit_each = int(sample_rate * frame_duration / 1000)*2 # x2 because audio is 16 bit (2 bytes)
|
n_bit_each = int(sample_rate * frame_duration / 1000)*2 # x2 because audio is 16 bit (2 bytes)
|
||||||
@@ -95,7 +94,7 @@ def is_speaker_speaking(vad, data, sample_rate):
|
|||||||
for t in range(len(data)):
|
for t in range(len(data)):
|
||||||
if t!=0 and t % n_bit_each == 0:
|
if t!=0 and t % n_bit_each == 0:
|
||||||
res_list.append(vad.is_speech(data[t-n_bit_each:t], sample_rate))
|
res_list.append(vad.is_speech(data[t-n_bit_each:t], sample_rate))
|
||||||
|
|
||||||
info = ''.join(['^' if r else '.' for r in res_list])
|
info = ''.join(['^' if r else '.' for r in res_list])
|
||||||
info = info[:10]
|
info = info[:10]
|
||||||
if any(res_list):
|
if any(res_list):
|
||||||
@@ -107,14 +106,18 @@ def is_speaker_speaking(vad, data, sample_rate):
|
|||||||
class AliyunASR():
|
class AliyunASR():
|
||||||
|
|
||||||
def test_on_sentence_begin(self, message, *args):
|
def test_on_sentence_begin(self, message, *args):
|
||||||
|
# print("test_on_sentence_begin:{}".format(message))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_on_sentence_end(self, message, *args):
|
def test_on_sentence_end(self, message, *args):
|
||||||
|
# print("test_on_sentence_end:{}".format(message))
|
||||||
message = json.loads(message)
|
message = json.loads(message)
|
||||||
self.parsed_sentence = message['payload']['result']
|
self.parsed_sentence = message['payload']['result']
|
||||||
self.event_on_entence_end.set()
|
self.event_on_entence_end.set()
|
||||||
|
# print(self.parsed_sentence)
|
||||||
|
|
||||||
def test_on_start(self, message, *args):
|
def test_on_start(self, message, *args):
|
||||||
|
# print("test_on_start:{}".format(message))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_on_error(self, message, *args):
|
def test_on_error(self, message, *args):
|
||||||
@@ -126,11 +129,13 @@ class AliyunASR():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def test_on_result_chg(self, message, *args):
|
def test_on_result_chg(self, message, *args):
|
||||||
|
# print("test_on_chg:{}".format(message))
|
||||||
message = json.loads(message)
|
message = json.loads(message)
|
||||||
self.parsed_text = message['payload']['result']
|
self.parsed_text = message['payload']['result']
|
||||||
self.event_on_result_chg.set()
|
self.event_on_result_chg.set()
|
||||||
|
|
||||||
def test_on_completed(self, message, *args):
|
def test_on_completed(self, message, *args):
|
||||||
|
# print("on_completed:args=>{} message=>{}".format(args, message))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def audio_convertion_thread(self, uuid):
|
def audio_convertion_thread(self, uuid):
|
||||||
@@ -181,10 +186,10 @@ class AliyunASR():
|
|||||||
keep_alive_last_send_time = time.time()
|
keep_alive_last_send_time = time.time()
|
||||||
while not self.stop:
|
while not self.stop:
|
||||||
# time.sleep(self.capture_interval)
|
# time.sleep(self.capture_interval)
|
||||||
audio = rad.read(uuid.hex)
|
audio = rad.read(uuid.hex)
|
||||||
if audio is not None:
|
if audio is not None:
|
||||||
# convert to pcm file
|
# convert to pcm file
|
||||||
temp_file = f'{temp_folder}/{uuid.hex}.pcm' #
|
temp_file = f'{temp_folder}/{uuid.hex}.pcm' #
|
||||||
dsdata = change_sample_rate(audio, rad.rate, NEW_SAMPLERATE) # 48000 --> 16000
|
dsdata = change_sample_rate(audio, rad.rate, NEW_SAMPLERATE) # 48000 --> 16000
|
||||||
write_numpy_to_wave(temp_file, NEW_SAMPLERATE, dsdata)
|
write_numpy_to_wave(temp_file, NEW_SAMPLERATE, dsdata)
|
||||||
# read pcm binary
|
# read pcm binary
|
||||||
@@ -243,14 +248,14 @@ class AliyunASR():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = client.do_action_with_exception(request)
|
response = client.do_action_with_exception(request)
|
||||||
logging.info(response)
|
print(response)
|
||||||
jss = json.loads(response)
|
jss = json.loads(response)
|
||||||
if 'Token' in jss and 'Id' in jss['Token']:
|
if 'Token' in jss and 'Id' in jss['Token']:
|
||||||
token = jss['Token']['Id']
|
token = jss['Token']['Id']
|
||||||
expireTime = jss['Token']['ExpireTime']
|
expireTime = jss['Token']['ExpireTime']
|
||||||
logging.info("token = " + token)
|
print("token = " + token)
|
||||||
logging.info("expireTime = " + str(expireTime))
|
print("expireTime = " + str(expireTime))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
print(e)
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ from scipy import interpolate
|
|||||||
|
|
||||||
def Singleton(cls):
|
def Singleton(cls):
|
||||||
_instance = {}
|
_instance = {}
|
||||||
|
|
||||||
def _singleton(*args, **kargs):
|
def _singleton(*args, **kargs):
|
||||||
if cls not in _instance:
|
if cls not in _instance:
|
||||||
_instance[cls] = cls(*args, **kargs)
|
_instance[cls] = cls(*args, **kargs)
|
||||||
return _instance[cls]
|
return _instance[cls]
|
||||||
|
|
||||||
return _singleton
|
return _singleton
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class RealtimeAudioDistribution():
|
|||||||
else:
|
else:
|
||||||
res = None
|
res = None
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def change_sample_rate(audio, old_sr, new_sr):
|
def change_sample_rate(audio, old_sr, new_sr):
|
||||||
duration = audio.shape[0] / old_sr
|
duration = audio.shape[0] / old_sr
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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 pydantic import BaseModel, Field
|
||||||
from typing import List
|
from typing import List
|
||||||
from toolbox import update_ui_latest_msg, disable_auto_promotion
|
from toolbox import update_ui_lastest_msg, disable_auto_promotion
|
||||||
from toolbox import CatchException, update_ui, get_conf, select_api_key, get_log_folder
|
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 request_llms.bridge_all import predict_no_ui_long_connection
|
||||||
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
from crazy_functions.json_fns.pydantic_io import GptJsonIO, JsonStringError
|
||||||
@@ -40,7 +40,7 @@ class GptAcademicState():
|
|||||||
|
|
||||||
class GptAcademicGameBaseState():
|
class GptAcademicGameBaseState():
|
||||||
"""
|
"""
|
||||||
1. first init: __init__ ->
|
1. first init: __init__ ->
|
||||||
"""
|
"""
|
||||||
def init_game(self, chatbot, lock_plugin):
|
def init_game(self, chatbot, lock_plugin):
|
||||||
self.plugin_name = None
|
self.plugin_name = None
|
||||||
@@ -53,7 +53,7 @@ class GptAcademicGameBaseState():
|
|||||||
raise ValueError("callback_fn is None")
|
raise ValueError("callback_fn is None")
|
||||||
chatbot._cookies['lock_plugin'] = self.callback_fn
|
chatbot._cookies['lock_plugin'] = self.callback_fn
|
||||||
self.dump_state(chatbot)
|
self.dump_state(chatbot)
|
||||||
|
|
||||||
def get_plugin_name(self):
|
def get_plugin_name(self):
|
||||||
if self.plugin_name is None:
|
if self.plugin_name is None:
|
||||||
raise ValueError("plugin_name is None")
|
raise ValueError("plugin_name is None")
|
||||||
@@ -71,7 +71,7 @@ class GptAcademicGameBaseState():
|
|||||||
state = chatbot._cookies.get(f'plugin_state/{plugin_name}', None)
|
state = chatbot._cookies.get(f'plugin_state/{plugin_name}', None)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
state = pickle.loads(state)
|
state = pickle.loads(state)
|
||||||
else:
|
else:
|
||||||
state = cls()
|
state = cls()
|
||||||
state.init_game(chatbot, lock_plugin)
|
state.init_game(chatbot, lock_plugin)
|
||||||
state.plugin_name = plugin_name
|
state.plugin_name = plugin_name
|
||||||
@@ -79,7 +79,7 @@ class GptAcademicGameBaseState():
|
|||||||
state.chatbot = chatbot
|
state.chatbot = chatbot
|
||||||
state.callback_fn = callback_fn
|
state.callback_fn = callback_fn
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def continue_game(self, prompt, chatbot, history):
|
def continue_game(self, prompt, chatbot, history):
|
||||||
# 游戏主体
|
# 游戏主体
|
||||||
yield from self.step(prompt, chatbot, history)
|
yield from self.step(prompt, chatbot, history)
|
||||||
|
|||||||
@@ -1,386 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
from ..query_analyzer import SearchCriteria
|
|
||||||
from ..sources.github_source import GitHubSource
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class BaseHandler(ABC):
|
|
||||||
"""处理器基类"""
|
|
||||||
|
|
||||||
def __init__(self, github: GitHubSource, llm_kwargs: Dict = None):
|
|
||||||
self.github = github
|
|
||||||
self.llm_kwargs = llm_kwargs or {}
|
|
||||||
self.ranked_repos = [] # 存储排序后的仓库列表
|
|
||||||
|
|
||||||
def _get_search_params(self, plugin_kwargs: Dict) -> Dict:
|
|
||||||
"""获取搜索参数"""
|
|
||||||
return {
|
|
||||||
'max_repos': plugin_kwargs.get('max_repos', 150), # 最大仓库数量,从30改为150
|
|
||||||
'max_details': plugin_kwargs.get('max_details', 80), # 最多展示详情的仓库数量,新增参数
|
|
||||||
'search_multiplier': plugin_kwargs.get('search_multiplier', 3), # 检索倍数
|
|
||||||
'min_stars': plugin_kwargs.get('min_stars', 0), # 最少星标数
|
|
||||||
}
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
criteria: SearchCriteria,
|
|
||||||
chatbot: List[List[str]],
|
|
||||||
history: List[List[str]],
|
|
||||||
system_prompt: str,
|
|
||||||
llm_kwargs: Dict[str, Any],
|
|
||||||
plugin_kwargs: Dict[str, Any],
|
|
||||||
) -> str:
|
|
||||||
"""处理查询"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _search_repositories(self, query: str, language: str = None, min_stars: int = 0,
|
|
||||||
sort: str = "stars", per_page: int = 30) -> List[Dict]:
|
|
||||||
"""搜索仓库"""
|
|
||||||
try:
|
|
||||||
# 构建查询字符串
|
|
||||||
if min_stars > 0 and "stars:>" not in query:
|
|
||||||
query += f" stars:>{min_stars}"
|
|
||||||
|
|
||||||
if language and "language:" not in query:
|
|
||||||
query += f" language:{language}"
|
|
||||||
|
|
||||||
# 执行搜索
|
|
||||||
result = await self.github.search_repositories(
|
|
||||||
query=query,
|
|
||||||
sort=sort,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
if result and "items" in result:
|
|
||||||
return result["items"]
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"仓库搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_bilingual_repositories(self, english_query: str, chinese_query: str, language: str = None, min_stars: int = 0,
|
|
||||||
sort: str = "stars", per_page: int = 30) -> List[Dict]:
|
|
||||||
"""同时搜索中英文仓库并合并结果"""
|
|
||||||
try:
|
|
||||||
# 搜索英文仓库
|
|
||||||
english_results = await self._search_repositories(
|
|
||||||
query=english_query,
|
|
||||||
language=language,
|
|
||||||
min_stars=min_stars,
|
|
||||||
sort=sort,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 搜索中文仓库
|
|
||||||
chinese_results = await self._search_repositories(
|
|
||||||
query=chinese_query,
|
|
||||||
language=language,
|
|
||||||
min_stars=min_stars,
|
|
||||||
sort=sort,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 合并结果,去除重复项
|
|
||||||
merged_results = []
|
|
||||||
seen_repos = set()
|
|
||||||
|
|
||||||
# 优先添加英文结果
|
|
||||||
for repo in english_results:
|
|
||||||
repo_id = repo.get('id')
|
|
||||||
if repo_id and repo_id not in seen_repos:
|
|
||||||
seen_repos.add(repo_id)
|
|
||||||
merged_results.append(repo)
|
|
||||||
|
|
||||||
# 添加中文结果(排除重复)
|
|
||||||
for repo in chinese_results:
|
|
||||||
repo_id = repo.get('id')
|
|
||||||
if repo_id and repo_id not in seen_repos:
|
|
||||||
seen_repos.add(repo_id)
|
|
||||||
merged_results.append(repo)
|
|
||||||
|
|
||||||
# 按星标数重新排序
|
|
||||||
merged_results.sort(key=lambda x: x.get('stargazers_count', 0), reverse=True)
|
|
||||||
|
|
||||||
return merged_results[:per_page] # 返回合并后的前per_page个结果
|
|
||||||
except Exception as e:
|
|
||||||
print(f"双语仓库搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_code(self, query: str, language: str = None, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""搜索代码"""
|
|
||||||
try:
|
|
||||||
# 构建查询字符串
|
|
||||||
if language and "language:" not in query:
|
|
||||||
query += f" language:{language}"
|
|
||||||
|
|
||||||
# 执行搜索
|
|
||||||
result = await self.github.search_code(
|
|
||||||
query=query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
if result and "items" in result:
|
|
||||||
return result["items"]
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"代码搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_bilingual_code(self, english_query: str, chinese_query: str, language: str = None, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""同时搜索中英文代码并合并结果"""
|
|
||||||
try:
|
|
||||||
# 搜索英文代码
|
|
||||||
english_results = await self._search_code(
|
|
||||||
query=english_query,
|
|
||||||
language=language,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 搜索中文代码
|
|
||||||
chinese_results = await self._search_code(
|
|
||||||
query=chinese_query,
|
|
||||||
language=language,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 合并结果,去除重复项
|
|
||||||
merged_results = []
|
|
||||||
seen_files = set()
|
|
||||||
|
|
||||||
# 优先添加英文结果
|
|
||||||
for item in english_results:
|
|
||||||
# 使用文件URL作为唯一标识
|
|
||||||
file_url = item.get('html_url', '')
|
|
||||||
if file_url and file_url not in seen_files:
|
|
||||||
seen_files.add(file_url)
|
|
||||||
merged_results.append(item)
|
|
||||||
|
|
||||||
# 添加中文结果(排除重复)
|
|
||||||
for item in chinese_results:
|
|
||||||
file_url = item.get('html_url', '')
|
|
||||||
if file_url and file_url not in seen_files:
|
|
||||||
seen_files.add(file_url)
|
|
||||||
merged_results.append(item)
|
|
||||||
|
|
||||||
# 对结果进行排序,优先显示匹配度高的结果
|
|
||||||
# 由于无法直接获取匹配度,这里使用仓库的星标数作为替代指标
|
|
||||||
merged_results.sort(key=lambda x: x.get('repository', {}).get('stargazers_count', 0), reverse=True)
|
|
||||||
|
|
||||||
return merged_results[:per_page] # 返回合并后的前per_page个结果
|
|
||||||
except Exception as e:
|
|
||||||
print(f"双语代码搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_users(self, query: str, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""搜索用户"""
|
|
||||||
try:
|
|
||||||
result = await self.github.search_users(
|
|
||||||
query=query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
if result and "items" in result:
|
|
||||||
return result["items"]
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"用户搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_bilingual_users(self, english_query: str, chinese_query: str, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""同时搜索中英文用户并合并结果"""
|
|
||||||
try:
|
|
||||||
# 搜索英文用户
|
|
||||||
english_results = await self._search_users(
|
|
||||||
query=english_query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 搜索中文用户
|
|
||||||
chinese_results = await self._search_users(
|
|
||||||
query=chinese_query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 合并结果,去除重复项
|
|
||||||
merged_results = []
|
|
||||||
seen_users = set()
|
|
||||||
|
|
||||||
# 优先添加英文结果
|
|
||||||
for user in english_results:
|
|
||||||
user_id = user.get('id')
|
|
||||||
if user_id and user_id not in seen_users:
|
|
||||||
seen_users.add(user_id)
|
|
||||||
merged_results.append(user)
|
|
||||||
|
|
||||||
# 添加中文结果(排除重复)
|
|
||||||
for user in chinese_results:
|
|
||||||
user_id = user.get('id')
|
|
||||||
if user_id and user_id not in seen_users:
|
|
||||||
seen_users.add(user_id)
|
|
||||||
merged_results.append(user)
|
|
||||||
|
|
||||||
# 按关注者数量进行排序
|
|
||||||
merged_results.sort(key=lambda x: x.get('followers', 0), reverse=True)
|
|
||||||
|
|
||||||
return merged_results[:per_page] # 返回合并后的前per_page个结果
|
|
||||||
except Exception as e:
|
|
||||||
print(f"双语用户搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_topics(self, query: str, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""搜索主题"""
|
|
||||||
try:
|
|
||||||
result = await self.github.search_topics(
|
|
||||||
query=query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
if result and "items" in result:
|
|
||||||
return result["items"]
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"主题搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _search_bilingual_topics(self, english_query: str, chinese_query: str, per_page: int = 30) -> List[Dict]:
|
|
||||||
"""同时搜索中英文主题并合并结果"""
|
|
||||||
try:
|
|
||||||
# 搜索英文主题
|
|
||||||
english_results = await self._search_topics(
|
|
||||||
query=english_query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 搜索中文主题
|
|
||||||
chinese_results = await self._search_topics(
|
|
||||||
query=chinese_query,
|
|
||||||
per_page=per_page
|
|
||||||
)
|
|
||||||
|
|
||||||
# 合并结果,去除重复项
|
|
||||||
merged_results = []
|
|
||||||
seen_topics = set()
|
|
||||||
|
|
||||||
# 优先添加英文结果
|
|
||||||
for topic in english_results:
|
|
||||||
topic_name = topic.get('name')
|
|
||||||
if topic_name and topic_name not in seen_topics:
|
|
||||||
seen_topics.add(topic_name)
|
|
||||||
merged_results.append(topic)
|
|
||||||
|
|
||||||
# 添加中文结果(排除重复)
|
|
||||||
for topic in chinese_results:
|
|
||||||
topic_name = topic.get('name')
|
|
||||||
if topic_name and topic_name not in seen_topics:
|
|
||||||
seen_topics.add(topic_name)
|
|
||||||
merged_results.append(topic)
|
|
||||||
|
|
||||||
# 可以按流行度进行排序(如果有)
|
|
||||||
if merged_results and 'featured' in merged_results[0]:
|
|
||||||
merged_results.sort(key=lambda x: x.get('featured', False), reverse=True)
|
|
||||||
|
|
||||||
return merged_results[:per_page] # 返回合并后的前per_page个结果
|
|
||||||
except Exception as e:
|
|
||||||
print(f"双语主题搜索出错: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _get_repo_details(self, repos: List[Dict]) -> List[Dict]:
|
|
||||||
"""获取仓库详细信息"""
|
|
||||||
enhanced_repos = []
|
|
||||||
|
|
||||||
for repo in repos:
|
|
||||||
try:
|
|
||||||
# 获取README信息
|
|
||||||
owner = repo.get('owner', {}).get('login') if repo.get('owner') is not None else None
|
|
||||||
repo_name = repo.get('name')
|
|
||||||
|
|
||||||
if owner and repo_name:
|
|
||||||
readme = await self.github.get_repo_readme(owner, repo_name)
|
|
||||||
if readme and "decoded_content" in readme:
|
|
||||||
# 提取README的前1000个字符作为摘要
|
|
||||||
repo['readme_excerpt'] = readme["decoded_content"][:1000] + "..."
|
|
||||||
|
|
||||||
# 获取语言使用情况
|
|
||||||
languages = await self.github.get_repository_languages(owner, repo_name)
|
|
||||||
if languages:
|
|
||||||
repo['languages_detail'] = languages
|
|
||||||
|
|
||||||
# 获取最新发布版本
|
|
||||||
releases = await self.github.get_repo_releases(owner, repo_name, per_page=1)
|
|
||||||
if releases and len(releases) > 0:
|
|
||||||
repo['latest_release'] = releases[0]
|
|
||||||
|
|
||||||
# 获取主题标签
|
|
||||||
topics = await self.github.get_repo_topics(owner, repo_name)
|
|
||||||
if topics and "names" in topics:
|
|
||||||
repo['topics'] = topics["names"]
|
|
||||||
|
|
||||||
enhanced_repos.append(repo)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"获取仓库 {repo.get('full_name')} 详情时出错: {str(e)}")
|
|
||||||
enhanced_repos.append(repo) # 添加原始仓库信息
|
|
||||||
|
|
||||||
return enhanced_repos
|
|
||||||
|
|
||||||
def _format_repos(self, repos: List[Dict]) -> str:
|
|
||||||
"""格式化仓库列表"""
|
|
||||||
formatted = []
|
|
||||||
|
|
||||||
for i, repo in enumerate(repos, 1):
|
|
||||||
# 构建仓库URL
|
|
||||||
repo_url = repo.get('html_url', '')
|
|
||||||
|
|
||||||
# 构建完整的引用
|
|
||||||
reference = (
|
|
||||||
f"{i}. **{repo.get('full_name', '')}**\n"
|
|
||||||
f" - 描述: {repo.get('description', 'N/A')}\n"
|
|
||||||
f" - 语言: {repo.get('language', 'N/A')}\n"
|
|
||||||
f" - 星标: {repo.get('stargazers_count', 0)}\n"
|
|
||||||
f" - Fork数: {repo.get('forks_count', 0)}\n"
|
|
||||||
f" - 更新时间: {repo.get('updated_at', 'N/A')[:10]}\n"
|
|
||||||
f" - 创建时间: {repo.get('created_at', 'N/A')[:10]}\n"
|
|
||||||
f" - URL: <a href='{repo_url}' target='_blank'>{repo_url}</a>\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加主题标签(如果有)
|
|
||||||
if repo.get('topics'):
|
|
||||||
topics_str = ", ".join(repo.get('topics'))
|
|
||||||
reference += f" - 主题标签: {topics_str}\n"
|
|
||||||
|
|
||||||
# 添加最新发布版本(如果有)
|
|
||||||
if repo.get('latest_release'):
|
|
||||||
release = repo.get('latest_release')
|
|
||||||
reference += f" - 最新版本: {release.get('tag_name', 'N/A')} ({release.get('published_at', 'N/A')[:10]})\n"
|
|
||||||
|
|
||||||
# 添加README摘要(如果有)
|
|
||||||
if repo.get('readme_excerpt'):
|
|
||||||
# 截断README,只取前300个字符
|
|
||||||
readme_short = repo.get('readme_excerpt')[:300].replace('\n', ' ')
|
|
||||||
reference += f" - README摘要: {readme_short}...\n"
|
|
||||||
|
|
||||||
formatted.append(reference)
|
|
||||||
|
|
||||||
return "\n".join(formatted)
|
|
||||||
|
|
||||||
def _generate_apology_prompt(self, criteria: SearchCriteria) -> str:
|
|
||||||
"""生成道歉提示"""
|
|
||||||
return f"""很抱歉,我们未能找到与"{criteria.main_topic}"相关的GitHub项目。
|
|
||||||
|
|
||||||
可能的原因:
|
|
||||||
1. 搜索词过于具体或冷门
|
|
||||||
2. 星标数要求过高
|
|
||||||
3. 编程语言限制过于严格
|
|
||||||
|
|
||||||
建议解决方案:
|
|
||||||
1. 尝试使用更通用的关键词
|
|
||||||
2. 降低最低星标数要求
|
|
||||||
3. 移除或更改编程语言限制
|
|
||||||
请根据以上建议调整后重试。"""
|
|
||||||
|
|
||||||
def _get_current_time(self) -> str:
|
|
||||||
"""获取当前时间信息"""
|
|
||||||
now = datetime.now()
|
|
||||||
return now.strftime("%Y年%m月%d日")
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
from typing import List, Dict, Any
|
|
||||||
from .base_handler import BaseHandler
|
|
||||||
from ..query_analyzer import SearchCriteria
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class CodeSearchHandler(BaseHandler):
|
|
||||||
"""代码搜索处理器"""
|
|
||||||
|
|
||||||
def __init__(self, github, llm_kwargs=None):
|
|
||||||
super().__init__(github, llm_kwargs)
|
|
||||||
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
criteria: SearchCriteria,
|
|
||||||
chatbot: List[List[str]],
|
|
||||||
history: List[List[str]],
|
|
||||||
system_prompt: str,
|
|
||||||
llm_kwargs: Dict[str, Any],
|
|
||||||
plugin_kwargs: Dict[str, Any],
|
|
||||||
) -> str:
|
|
||||||
"""处理代码搜索请求,返回最终的prompt"""
|
|
||||||
|
|
||||||
search_params = self._get_search_params(plugin_kwargs)
|
|
||||||
|
|
||||||
# 搜索代码
|
|
||||||
code_results = await self._search_bilingual_code(
|
|
||||||
english_query=criteria.github_params["query"],
|
|
||||||
chinese_query=criteria.github_params["chinese_query"],
|
|
||||||
language=criteria.language,
|
|
||||||
per_page=search_params['max_repos']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not code_results:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 获取代码文件内容
|
|
||||||
enhanced_code_results = await self._get_code_details(code_results[:search_params['max_details']])
|
|
||||||
self.ranked_repos = [item["repository"] for item in enhanced_code_results if "repository" in item]
|
|
||||||
|
|
||||||
if not enhanced_code_results:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 构建最终的prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
基于用户对{criteria.main_topic}的查询,我找到了以下代码示例。
|
|
||||||
|
|
||||||
代码搜索结果:
|
|
||||||
{self._format_code_results(enhanced_code_results)}
|
|
||||||
|
|
||||||
请提供:
|
|
||||||
|
|
||||||
1. 对于搜索的"{criteria.main_topic}"主题的综合解释:
|
|
||||||
- 概念和原理介绍
|
|
||||||
- 常见实现方法和技术
|
|
||||||
- 最佳实践和注意事项
|
|
||||||
|
|
||||||
2. 对每个代码示例:
|
|
||||||
- 解释代码的主要功能和实现方式
|
|
||||||
- 分析代码质量、可读性和效率
|
|
||||||
- 指出代码中的亮点和潜在改进空间
|
|
||||||
- 说明代码的适用场景
|
|
||||||
|
|
||||||
3. 代码实现比较:
|
|
||||||
- 不同实现方法的优缺点
|
|
||||||
- 性能和可维护性分析
|
|
||||||
- 适用不同场景的实现建议
|
|
||||||
|
|
||||||
4. 学习建议:
|
|
||||||
- 理解和使用这些代码需要的背景知识
|
|
||||||
- 如何扩展或改进所展示的代码
|
|
||||||
- 进一步学习相关技术的资源
|
|
||||||
|
|
||||||
重要提示:
|
|
||||||
- 深入解释代码的核心逻辑和实现思路
|
|
||||||
- 提供专业、技术性的分析
|
|
||||||
- 优先关注代码的实现质量和技术价值
|
|
||||||
- 当代码实现有问题时,指出并提供改进建议
|
|
||||||
- 对于复杂代码,分解解释其组成部分
|
|
||||||
- 根据用户查询的具体问题提供针对性答案
|
|
||||||
- 所有链接请使用<a href='链接地址' target='_blank'>链接文本</a>格式,确保链接在新窗口打开
|
|
||||||
|
|
||||||
使用markdown格式提供清晰的分节回复。
|
|
||||||
"""
|
|
||||||
|
|
||||||
return final_prompt
|
|
||||||
|
|
||||||
async def _get_code_details(self, code_results: List[Dict]) -> List[Dict]:
|
|
||||||
"""获取代码详情"""
|
|
||||||
enhanced_results = []
|
|
||||||
|
|
||||||
for item in code_results:
|
|
||||||
try:
|
|
||||||
repo = item.get('repository', {})
|
|
||||||
file_path = item.get('path', '')
|
|
||||||
repo_name = repo.get('full_name', '')
|
|
||||||
|
|
||||||
if repo_name and file_path:
|
|
||||||
owner, repo_name = repo_name.split('/')
|
|
||||||
|
|
||||||
# 获取文件内容
|
|
||||||
file_content = await self.github.get_file_content(owner, repo_name, file_path)
|
|
||||||
if file_content and "decoded_content" in file_content:
|
|
||||||
item['code_content'] = file_content["decoded_content"]
|
|
||||||
|
|
||||||
# 获取仓库基本信息
|
|
||||||
repo_details = await self.github.get_repo(owner, repo_name)
|
|
||||||
if repo_details:
|
|
||||||
item['repository'] = repo_details
|
|
||||||
|
|
||||||
enhanced_results.append(item)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"获取代码详情时出错: {str(e)}")
|
|
||||||
enhanced_results.append(item) # 添加原始信息
|
|
||||||
|
|
||||||
return enhanced_results
|
|
||||||
|
|
||||||
def _format_code_results(self, code_results: List[Dict]) -> str:
|
|
||||||
"""格式化代码搜索结果"""
|
|
||||||
formatted = []
|
|
||||||
|
|
||||||
for i, item in enumerate(code_results, 1):
|
|
||||||
# 构建仓库信息
|
|
||||||
repo = item.get('repository', {})
|
|
||||||
repo_name = repo.get('full_name', 'N/A')
|
|
||||||
repo_url = repo.get('html_url', '')
|
|
||||||
stars = repo.get('stargazers_count', 0)
|
|
||||||
language = repo.get('language', 'N/A')
|
|
||||||
|
|
||||||
# 构建文件信息
|
|
||||||
file_path = item.get('path', 'N/A')
|
|
||||||
file_url = item.get('html_url', '')
|
|
||||||
|
|
||||||
# 构建代码内容
|
|
||||||
code_content = item.get('code_content', '')
|
|
||||||
if code_content:
|
|
||||||
# 只显示前30行代码
|
|
||||||
code_lines = code_content.split("\n")
|
|
||||||
if len(code_lines) > 30:
|
|
||||||
displayed_code = "\n".join(code_lines[:30]) + "\n... (代码太长已截断) ..."
|
|
||||||
else:
|
|
||||||
displayed_code = code_content
|
|
||||||
else:
|
|
||||||
displayed_code = "(代码内容获取失败)"
|
|
||||||
|
|
||||||
reference = (
|
|
||||||
f"### {i}. {file_path} (在 {repo_name} 中)\n\n"
|
|
||||||
f"- **仓库**: <a href='{repo_url}' target='_blank'>{repo_name}</a> (⭐ {stars}, 语言: {language})\n"
|
|
||||||
f"- **文件路径**: <a href='{file_url}' target='_blank'>{file_path}</a>\n\n"
|
|
||||||
f"```{language.lower()}\n{displayed_code}\n```\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
formatted.append(reference)
|
|
||||||
|
|
||||||
return "\n".join(formatted)
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
from typing import List, Dict, Any
|
|
||||||
from .base_handler import BaseHandler
|
|
||||||
from ..query_analyzer import SearchCriteria
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class RepositoryHandler(BaseHandler):
|
|
||||||
"""仓库搜索处理器"""
|
|
||||||
|
|
||||||
def __init__(self, github, llm_kwargs=None):
|
|
||||||
super().__init__(github, llm_kwargs)
|
|
||||||
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
criteria: SearchCriteria,
|
|
||||||
chatbot: List[List[str]],
|
|
||||||
history: List[List[str]],
|
|
||||||
system_prompt: str,
|
|
||||||
llm_kwargs: Dict[str, Any],
|
|
||||||
plugin_kwargs: Dict[str, Any],
|
|
||||||
) -> str:
|
|
||||||
"""处理仓库搜索请求,返回最终的prompt"""
|
|
||||||
|
|
||||||
search_params = self._get_search_params(plugin_kwargs)
|
|
||||||
|
|
||||||
# 如果是特定仓库查询
|
|
||||||
if criteria.repo_id:
|
|
||||||
try:
|
|
||||||
owner, repo = criteria.repo_id.split('/')
|
|
||||||
repo_details = await self.github.get_repo(owner, repo)
|
|
||||||
if repo_details:
|
|
||||||
# 获取推荐的相似仓库
|
|
||||||
similar_repos = await self.github.get_repo_recommendations(criteria.repo_id, limit=5)
|
|
||||||
|
|
||||||
# 添加详细信息
|
|
||||||
all_repos = [repo_details] + similar_repos
|
|
||||||
enhanced_repos = await self._get_repo_details(all_repos)
|
|
||||||
|
|
||||||
self.ranked_repos = enhanced_repos
|
|
||||||
|
|
||||||
# 构建最终的prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = self._build_repo_detail_prompt(enhanced_repos[0], enhanced_repos[1:], current_time)
|
|
||||||
return final_prompt
|
|
||||||
else:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"处理特定仓库时出错: {str(e)}")
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 一般仓库搜索
|
|
||||||
repos = await self._search_bilingual_repositories(
|
|
||||||
english_query=criteria.github_params["query"],
|
|
||||||
chinese_query=criteria.github_params["chinese_query"],
|
|
||||||
language=criteria.language,
|
|
||||||
min_stars=criteria.min_stars,
|
|
||||||
per_page=search_params['max_repos']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 获取仓库详情
|
|
||||||
enhanced_repos = await self._get_repo_details(repos[:search_params['max_details']]) # 使用max_details参数
|
|
||||||
self.ranked_repos = enhanced_repos
|
|
||||||
|
|
||||||
if not enhanced_repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 构建最终的prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
基于用户对{criteria.main_topic}的兴趣,以下是相关的GitHub仓库。
|
|
||||||
|
|
||||||
可供推荐的GitHub仓库:
|
|
||||||
{self._format_repos(enhanced_repos)}
|
|
||||||
|
|
||||||
请提供:
|
|
||||||
1. 按功能、用途或成熟度对仓库进行分组
|
|
||||||
|
|
||||||
2. 对每个仓库:
|
|
||||||
- 简要描述其主要功能和用途
|
|
||||||
- 分析其技术特点和优势
|
|
||||||
- 说明其适用场景和使用难度
|
|
||||||
- 指出其与同类产品相比的独特优势
|
|
||||||
- 解释其星标数量和活跃度代表的意义
|
|
||||||
|
|
||||||
3. 使用建议:
|
|
||||||
- 新手最适合入门的仓库
|
|
||||||
- 生产环境中最稳定可靠的选择
|
|
||||||
- 最新技术栈或创新方案的代表
|
|
||||||
- 学习特定技术的最佳资源
|
|
||||||
|
|
||||||
4. 相关资源:
|
|
||||||
- 学习这些项目需要的前置知识
|
|
||||||
- 项目间的关联和技术栈兼容性
|
|
||||||
- 可能的使用组合方案
|
|
||||||
|
|
||||||
重要提示:
|
|
||||||
- 重点解释为什么每个仓库值得关注
|
|
||||||
- 突出项目间的关联性和差异性
|
|
||||||
- 考虑用户不同水平的需求(初学者vs专业人士)
|
|
||||||
- 在介绍项目时,使用<a href='链接' target='_blank'>文本</a>格式,确保链接在新窗口打开
|
|
||||||
- 根据仓库的活跃度、更新频率、维护状态提供使用建议
|
|
||||||
- 仅基于提供的信息,不要做无根据的猜测
|
|
||||||
- 在信息缺失或不明确时,坦诚说明
|
|
||||||
|
|
||||||
使用markdown格式提供清晰的分节回复。
|
|
||||||
"""
|
|
||||||
|
|
||||||
return final_prompt
|
|
||||||
|
|
||||||
def _build_repo_detail_prompt(self, main_repo: Dict, similar_repos: List[Dict], current_time: str) -> str:
|
|
||||||
"""构建仓库详情prompt"""
|
|
||||||
|
|
||||||
# 提取README摘要
|
|
||||||
readme_content = "未提供"
|
|
||||||
if main_repo.get('readme_excerpt'):
|
|
||||||
readme_content = main_repo.get('readme_excerpt')
|
|
||||||
|
|
||||||
# 构建语言分布
|
|
||||||
languages = main_repo.get('languages_detail', {})
|
|
||||||
lang_distribution = []
|
|
||||||
if languages:
|
|
||||||
total = sum(languages.values())
|
|
||||||
for lang, bytes_val in languages.items():
|
|
||||||
percentage = (bytes_val / total) * 100
|
|
||||||
lang_distribution.append(f"{lang}: {percentage:.1f}%")
|
|
||||||
|
|
||||||
lang_str = "未知"
|
|
||||||
if lang_distribution:
|
|
||||||
lang_str = ", ".join(lang_distribution)
|
|
||||||
|
|
||||||
# 构建最终prompt
|
|
||||||
prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
## 主要仓库信息
|
|
||||||
|
|
||||||
### {main_repo.get('full_name')}
|
|
||||||
|
|
||||||
- **描述**: {main_repo.get('description', '未提供')}
|
|
||||||
- **星标数**: {main_repo.get('stargazers_count', 0)}
|
|
||||||
- **Fork数**: {main_repo.get('forks_count', 0)}
|
|
||||||
- **Watch数**: {main_repo.get('watchers_count', 0)}
|
|
||||||
- **Issues数**: {main_repo.get('open_issues_count', 0)}
|
|
||||||
- **语言分布**: {lang_str}
|
|
||||||
- **许可证**: {main_repo.get('license', {}).get('name', '未指定') if main_repo.get('license') is not None else '未指定'}
|
|
||||||
- **创建时间**: {main_repo.get('created_at', '')[:10]}
|
|
||||||
- **最近更新**: {main_repo.get('updated_at', '')[:10]}
|
|
||||||
- **主题标签**: {', '.join(main_repo.get('topics', ['无']))}
|
|
||||||
- **GitHub链接**: <a href='{main_repo.get('html_url')}' target='_blank'>链接</a>
|
|
||||||
|
|
||||||
### README摘要:
|
|
||||||
{readme_content}
|
|
||||||
|
|
||||||
## 类似仓库:
|
|
||||||
{self._format_repos(similar_repos)}
|
|
||||||
|
|
||||||
请提供以下内容:
|
|
||||||
|
|
||||||
1. **项目概述**
|
|
||||||
- 详细解释{main_repo.get('name', '')}项目的主要功能和用途
|
|
||||||
- 分析其技术特点、架构和实现原理
|
|
||||||
- 讨论其在所属领域的地位和影响力
|
|
||||||
- 评估项目成熟度和稳定性
|
|
||||||
|
|
||||||
2. **优势与特点**
|
|
||||||
- 与同类项目相比的独特优势
|
|
||||||
- 显著的技术创新或设计模式
|
|
||||||
- 值得学习或借鉴的代码实践
|
|
||||||
|
|
||||||
3. **使用场景**
|
|
||||||
- 最适合的应用场景
|
|
||||||
- 潜在的使用限制和注意事项
|
|
||||||
- 入门门槛和学习曲线评估
|
|
||||||
- 产品级应用的可行性分析
|
|
||||||
|
|
||||||
4. **资源与生态**
|
|
||||||
- 相关学习资源推荐
|
|
||||||
- 配套工具和库的建议
|
|
||||||
- 社区支持和活跃度评估
|
|
||||||
|
|
||||||
5. **类似项目对比**
|
|
||||||
- 与列出的类似项目的详细对比
|
|
||||||
- 不同场景下的最佳选择建议
|
|
||||||
- 潜在的互补使用方案
|
|
||||||
|
|
||||||
提示:所有链接请使用<a href='链接地址' target='_blank'>链接文本</a>格式,确保链接在新窗口打开。
|
|
||||||
|
|
||||||
请以专业、客观的技术分析角度回答,使用markdown格式提供结构化信息。
|
|
||||||
"""
|
|
||||||
return prompt
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
from typing import List, Dict, Any
|
|
||||||
from .base_handler import BaseHandler
|
|
||||||
from ..query_analyzer import SearchCriteria
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class TopicHandler(BaseHandler):
|
|
||||||
"""主题搜索处理器"""
|
|
||||||
|
|
||||||
def __init__(self, github, llm_kwargs=None):
|
|
||||||
super().__init__(github, llm_kwargs)
|
|
||||||
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
criteria: SearchCriteria,
|
|
||||||
chatbot: List[List[str]],
|
|
||||||
history: List[List[str]],
|
|
||||||
system_prompt: str,
|
|
||||||
llm_kwargs: Dict[str, Any],
|
|
||||||
plugin_kwargs: Dict[str, Any],
|
|
||||||
) -> str:
|
|
||||||
"""处理主题搜索请求,返回最终的prompt"""
|
|
||||||
|
|
||||||
search_params = self._get_search_params(plugin_kwargs)
|
|
||||||
|
|
||||||
# 搜索主题
|
|
||||||
topics = await self._search_bilingual_topics(
|
|
||||||
english_query=criteria.github_params["query"],
|
|
||||||
chinese_query=criteria.github_params["chinese_query"],
|
|
||||||
per_page=search_params['max_repos']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not topics:
|
|
||||||
# 尝试用主题搜索仓库
|
|
||||||
search_query = criteria.github_params["query"]
|
|
||||||
chinese_search_query = criteria.github_params["chinese_query"]
|
|
||||||
if "topic:" not in search_query:
|
|
||||||
search_query += " topic:" + criteria.main_topic.replace(" ", "-")
|
|
||||||
if "topic:" not in chinese_search_query:
|
|
||||||
chinese_search_query += " topic:" + criteria.main_topic.replace(" ", "-")
|
|
||||||
|
|
||||||
repos = await self._search_bilingual_repositories(
|
|
||||||
english_query=search_query,
|
|
||||||
chinese_query=chinese_search_query,
|
|
||||||
language=criteria.language,
|
|
||||||
min_stars=criteria.min_stars,
|
|
||||||
per_page=search_params['max_repos']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 获取仓库详情
|
|
||||||
enhanced_repos = await self._get_repo_details(repos[:10])
|
|
||||||
self.ranked_repos = enhanced_repos
|
|
||||||
|
|
||||||
if not enhanced_repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 构建基于主题的仓库列表prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
基于用户对主题"{criteria.main_topic}"的查询,我找到了以下相关GitHub仓库。
|
|
||||||
|
|
||||||
主题相关仓库:
|
|
||||||
{self._format_repos(enhanced_repos)}
|
|
||||||
|
|
||||||
请提供:
|
|
||||||
|
|
||||||
1. 主题综述:
|
|
||||||
- "{criteria.main_topic}"主题的概述和重要性
|
|
||||||
- 该主题在技术领域中的应用和发展趋势
|
|
||||||
- 主题相关的主要技术栈和知识体系
|
|
||||||
|
|
||||||
2. 仓库分析:
|
|
||||||
- 按功能、技术栈或应用场景对仓库进行分类
|
|
||||||
- 每个仓库在该主题领域的定位和贡献
|
|
||||||
- 不同仓库间的技术路线对比
|
|
||||||
|
|
||||||
3. 学习路径建议:
|
|
||||||
- 初学者入门该主题的推荐仓库和学习顺序
|
|
||||||
- 进阶学习的关键仓库和技术要点
|
|
||||||
- 实际应用中的最佳实践选择
|
|
||||||
|
|
||||||
4. 技术生态分析:
|
|
||||||
- 该主题下的主流工具和库
|
|
||||||
- 社区活跃度和维护状况
|
|
||||||
- 与其他相关技术的集成方案
|
|
||||||
|
|
||||||
重要提示:
|
|
||||||
- 主题"{criteria.main_topic}"是用户查询的核心,请围绕此主题展开分析
|
|
||||||
- 注重仓库质量评估和使用建议
|
|
||||||
- 提供基于事实的客观技术分析
|
|
||||||
- 在介绍仓库时使用<a href='链接地址' target='_blank'>链接文本</a>格式,确保链接在新窗口打开
|
|
||||||
- 考虑不同技术水平用户的需求
|
|
||||||
|
|
||||||
使用markdown格式提供清晰的分节回复。
|
|
||||||
"""
|
|
||||||
return final_prompt
|
|
||||||
|
|
||||||
# 如果找到了主题,则获取主题下的热门仓库
|
|
||||||
topic_repos = []
|
|
||||||
for topic in topics[:5]: # 增加到5个主题
|
|
||||||
topic_name = topic.get('name', '')
|
|
||||||
if topic_name:
|
|
||||||
# 搜索该主题下的仓库
|
|
||||||
repos = await self._search_repositories(
|
|
||||||
query=f"topic:{topic_name}",
|
|
||||||
language=criteria.language,
|
|
||||||
min_stars=criteria.min_stars,
|
|
||||||
per_page=20 # 每个主题最多20个仓库
|
|
||||||
)
|
|
||||||
|
|
||||||
if repos:
|
|
||||||
for repo in repos:
|
|
||||||
repo['topic_source'] = topic_name
|
|
||||||
topic_repos.append(repo)
|
|
||||||
|
|
||||||
if not topic_repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 获取前N个仓库的详情
|
|
||||||
enhanced_repos = await self._get_repo_details(topic_repos[:search_params['max_details']])
|
|
||||||
self.ranked_repos = enhanced_repos
|
|
||||||
|
|
||||||
if not enhanced_repos:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 构建最终的prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
基于用户对"{criteria.main_topic}"主题的查询,我找到了以下相关GitHub主题和仓库。
|
|
||||||
|
|
||||||
主题相关仓库:
|
|
||||||
{self._format_topic_repos(enhanced_repos)}
|
|
||||||
|
|
||||||
请提供:
|
|
||||||
|
|
||||||
1. 主题概述:
|
|
||||||
- 对"{criteria.main_topic}"相关主题的介绍和技术背景
|
|
||||||
- 这些主题在软件开发中的重要性和应用范围
|
|
||||||
- 主题间的关联性和技术演进路径
|
|
||||||
|
|
||||||
2. 精选仓库分析:
|
|
||||||
- 每个主题下最具代表性的仓库详解
|
|
||||||
- 仓库的技术亮点和创新点
|
|
||||||
- 使用场景和技术成熟度评估
|
|
||||||
|
|
||||||
3. 技术趋势分析:
|
|
||||||
- 基于主题和仓库活跃度的技术发展趋势
|
|
||||||
- 新兴解决方案和传统方案的对比
|
|
||||||
- 未来可能的技术方向预测
|
|
||||||
|
|
||||||
4. 实践建议:
|
|
||||||
- 不同应用场景下的最佳仓库选择
|
|
||||||
- 学习路径和资源推荐
|
|
||||||
- 实际项目中的应用策略
|
|
||||||
|
|
||||||
重要提示:
|
|
||||||
- 将分析重点放在主题的技术内涵和价值上
|
|
||||||
- 突出主题间的关联性和技术演进脉络
|
|
||||||
- 提供基于数据(星标数、更新频率等)的客观分析
|
|
||||||
- 考虑不同技术背景用户的需求
|
|
||||||
- 所有链接请使用<a href='链接地址' target='_blank'>链接文本</a>格式,确保链接在新窗口打开
|
|
||||||
|
|
||||||
使用markdown格式提供清晰的分节回复。
|
|
||||||
"""
|
|
||||||
|
|
||||||
return final_prompt
|
|
||||||
|
|
||||||
def _format_topic_repos(self, repos: List[Dict]) -> str:
|
|
||||||
"""按主题格式化仓库列表"""
|
|
||||||
# 按主题分组
|
|
||||||
topics_dict = {}
|
|
||||||
for repo in repos:
|
|
||||||
topic = repo.get('topic_source', '其他')
|
|
||||||
if topic not in topics_dict:
|
|
||||||
topics_dict[topic] = []
|
|
||||||
topics_dict[topic].append(repo)
|
|
||||||
|
|
||||||
# 格式化输出
|
|
||||||
formatted = []
|
|
||||||
for topic, topic_repos in topics_dict.items():
|
|
||||||
formatted.append(f"## 主题: {topic}\n")
|
|
||||||
|
|
||||||
for i, repo in enumerate(topic_repos, 1):
|
|
||||||
# 构建仓库URL
|
|
||||||
repo_url = repo.get('html_url', '')
|
|
||||||
|
|
||||||
# 构建引用
|
|
||||||
reference = (
|
|
||||||
f"{i}. **{repo.get('full_name', '')}**\n"
|
|
||||||
f" - 描述: {repo.get('description', 'N/A')}\n"
|
|
||||||
f" - 语言: {repo.get('language', 'N/A')}\n"
|
|
||||||
f" - 星标: {repo.get('stargazers_count', 0)}\n"
|
|
||||||
f" - Fork数: {repo.get('forks_count', 0)}\n"
|
|
||||||
f" - 更新时间: {repo.get('updated_at', 'N/A')[:10]}\n"
|
|
||||||
f" - URL: <a href='{repo_url}' target='_blank'>{repo_url}</a>\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加主题标签(如果有)
|
|
||||||
if repo.get('topics'):
|
|
||||||
topics_str = ", ".join(repo.get('topics'))
|
|
||||||
reference += f" - 主题标签: {topics_str}\n"
|
|
||||||
|
|
||||||
# 添加README摘要(如果有)
|
|
||||||
if repo.get('readme_excerpt'):
|
|
||||||
# 截断README,只取前200个字符
|
|
||||||
readme_short = repo.get('readme_excerpt')[:200].replace('\n', ' ')
|
|
||||||
reference += f" - README摘要: {readme_short}...\n"
|
|
||||||
|
|
||||||
formatted.append(reference)
|
|
||||||
|
|
||||||
formatted.append("\n") # 主题之间添加空行
|
|
||||||
|
|
||||||
return "\n".join(formatted)
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
from typing import List, Dict, Any
|
|
||||||
from .base_handler import BaseHandler
|
|
||||||
from ..query_analyzer import SearchCriteria
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class UserSearchHandler(BaseHandler):
|
|
||||||
"""用户搜索处理器"""
|
|
||||||
|
|
||||||
def __init__(self, github, llm_kwargs=None):
|
|
||||||
super().__init__(github, llm_kwargs)
|
|
||||||
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
criteria: SearchCriteria,
|
|
||||||
chatbot: List[List[str]],
|
|
||||||
history: List[List[str]],
|
|
||||||
system_prompt: str,
|
|
||||||
llm_kwargs: Dict[str, Any],
|
|
||||||
plugin_kwargs: Dict[str, Any],
|
|
||||||
) -> str:
|
|
||||||
"""处理用户搜索请求,返回最终的prompt"""
|
|
||||||
|
|
||||||
search_params = self._get_search_params(plugin_kwargs)
|
|
||||||
|
|
||||||
# 搜索用户
|
|
||||||
users = await self._search_bilingual_users(
|
|
||||||
english_query=criteria.github_params["query"],
|
|
||||||
chinese_query=criteria.github_params["chinese_query"],
|
|
||||||
per_page=search_params['max_repos']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not users:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 获取用户详情和仓库
|
|
||||||
enhanced_users = await self._get_user_details(users[:search_params['max_details']])
|
|
||||||
self.ranked_repos = [] # 添加用户top仓库进行展示
|
|
||||||
|
|
||||||
for user in enhanced_users:
|
|
||||||
if user.get('top_repos'):
|
|
||||||
self.ranked_repos.extend(user.get('top_repos'))
|
|
||||||
|
|
||||||
if not enhanced_users:
|
|
||||||
return self._generate_apology_prompt(criteria)
|
|
||||||
|
|
||||||
# 构建最终的prompt
|
|
||||||
current_time = self._get_current_time()
|
|
||||||
final_prompt = f"""当前时间: {current_time}
|
|
||||||
|
|
||||||
基于用户对{criteria.main_topic}的查询,我找到了以下GitHub用户。
|
|
||||||
|
|
||||||
GitHub用户搜索结果:
|
|
||||||
{self._format_users(enhanced_users)}
|
|
||||||
|
|
||||||
请提供:
|
|
||||||
|
|
||||||
1. 用户综合分析:
|
|
||||||
- 各开发者的专业领域和技术专长
|
|
||||||
- 他们在GitHub开源社区的影响力
|
|
||||||
- 技术实力和项目质量评估
|
|
||||||
|
|
||||||
2. 对每位开发者:
|
|
||||||
- 其主要贡献领域和技术栈
|
|
||||||
- 代表性项目及其价值
|
|
||||||
- 编程风格和技术特点
|
|
||||||
- 在相关领域的影响力
|
|
||||||
|
|
||||||
3. 项目推荐:
|
|
||||||
- 针对用户查询的最有价值项目
|
|
||||||
- 值得学习和借鉴的代码实践
|
|
||||||
- 不同用户项目的相互补充关系
|
|
||||||
|
|
||||||
4. 如何学习和使用:
|
|
||||||
- 如何从这些开发者项目中学习
|
|
||||||
- 最适合入门学习的项目
|
|
||||||
- 进阶学习的路径建议
|
|
||||||
|
|
||||||
重要提示:
|
|
||||||
- 关注开发者的技术专长和核心贡献
|
|
||||||
- 分析其开源项目的技术价值
|
|
||||||
- 根据用户的原始查询提供相关建议
|
|
||||||
- 避免过度赞美或主观评价
|
|
||||||
- 基于事实数据(项目数、星标数等)进行客观分析
|
|
||||||
- 所有链接请使用<a href='链接地址' target='_blank'>链接文本</a>格式,确保链接在新窗口打开
|
|
||||||
|
|
||||||
使用markdown格式提供清晰的分节回复。
|
|
||||||
"""
|
|
||||||
|
|
||||||
return final_prompt
|
|
||||||
|
|
||||||
async def _get_user_details(self, users: List[Dict]) -> List[Dict]:
|
|
||||||
"""获取用户详情和仓库"""
|
|
||||||
enhanced_users = []
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
try:
|
|
||||||
username = user.get('login')
|
|
||||||
|
|
||||||
if username:
|
|
||||||
# 获取用户详情
|
|
||||||
user_details = await self.github.get_user(username)
|
|
||||||
if user_details:
|
|
||||||
user.update(user_details)
|
|
||||||
|
|
||||||
# 获取用户仓库
|
|
||||||
repos = await self.github.get_user_repos(
|
|
||||||
username,
|
|
||||||
sort="stars",
|
|
||||||
per_page=10 # 增加到10个仓库
|
|
||||||
)
|
|
||||||
if repos:
|
|
||||||
user['top_repos'] = repos
|
|
||||||
|
|
||||||
enhanced_users.append(user)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"获取用户 {user.get('login')} 详情时出错: {str(e)}")
|
|
||||||
enhanced_users.append(user) # 添加原始信息
|
|
||||||
|
|
||||||
return enhanced_users
|
|
||||||
|
|
||||||
def _format_users(self, users: List[Dict]) -> str:
|
|
||||||
"""格式化用户列表"""
|
|
||||||
formatted = []
|
|
||||||
|
|
||||||
for i, user in enumerate(users, 1):
|
|
||||||
# 构建用户信息
|
|
||||||
username = user.get('login', 'N/A')
|
|
||||||
name = user.get('name', username)
|
|
||||||
profile_url = user.get('html_url', '')
|
|
||||||
bio = user.get('bio', '无简介')
|
|
||||||
followers = user.get('followers', 0)
|
|
||||||
public_repos = user.get('public_repos', 0)
|
|
||||||
company = user.get('company', '未指定')
|
|
||||||
location = user.get('location', '未指定')
|
|
||||||
blog = user.get('blog', '')
|
|
||||||
|
|
||||||
user_info = (
|
|
||||||
f"### {i}. {name} (@{username})\n\n"
|
|
||||||
f"- **简介**: {bio}\n"
|
|
||||||
f"- **关注者**: {followers} | **公开仓库**: {public_repos}\n"
|
|
||||||
f"- **公司**: {company} | **地点**: {location}\n"
|
|
||||||
f"- **个人网站**: {blog}\n"
|
|
||||||
f"- **GitHub**: <a href='{profile_url}' target='_blank'>{username}</a>\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加用户的热门仓库
|
|
||||||
top_repos = user.get('top_repos', [])
|
|
||||||
if top_repos:
|
|
||||||
user_info += "**热门仓库**:\n\n"
|
|
||||||
for repo in top_repos:
|
|
||||||
repo_name = repo.get('name', '')
|
|
||||||
repo_url = repo.get('html_url', '')
|
|
||||||
repo_desc = repo.get('description', '无描述')
|
|
||||||
repo_stars = repo.get('stargazers_count', 0)
|
|
||||||
repo_language = repo.get('language', '未指定')
|
|
||||||
|
|
||||||
user_info += (
|
|
||||||
f"- <a href='{repo_url}' target='_blank'>{repo_name}</a> - ⭐ {repo_stars}, {repo_language}\n"
|
|
||||||
f" {repo_desc}\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
formatted.append(user_info)
|
|
||||||
|
|
||||||
return "\n".join(formatted)
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
from typing import Dict, List
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import re
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SearchCriteria:
|
|
||||||
"""搜索条件"""
|
|
||||||
query_type: str # 查询类型: repo/code/user/topic
|
|
||||||
main_topic: str # 主题
|
|
||||||
sub_topics: List[str] # 子主题列表
|
|
||||||
language: str # 编程语言
|
|
||||||
min_stars: int # 最少星标数
|
|
||||||
github_params: Dict # GitHub搜索参数
|
|
||||||
original_query: str = "" # 原始查询字符串
|
|
||||||
repo_id: str = "" # 特定仓库ID或名称
|
|
||||||
|
|
||||||
class QueryAnalyzer:
|
|
||||||
"""查询分析器"""
|
|
||||||
|
|
||||||
# 响应索引常量
|
|
||||||
BASIC_QUERY_INDEX = 0
|
|
||||||
GITHUB_QUERY_INDEX = 1
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.valid_types = {
|
|
||||||
"repo": ["repository", "project", "library", "framework", "tool"],
|
|
||||||
"code": ["code", "snippet", "implementation", "function", "class", "algorithm"],
|
|
||||||
"user": ["user", "developer", "organization", "contributor", "maintainer"],
|
|
||||||
"topic": ["topic", "category", "tag", "field", "area", "domain"]
|
|
||||||
}
|
|
||||||
|
|
||||||
def analyze_query(self, query: str, chatbot: List, llm_kwargs: Dict):
|
|
||||||
"""分析查询意图"""
|
|
||||||
from crazy_functions.crazy_utils import \
|
|
||||||
request_gpt_model_multi_threads_with_very_awesome_ui_and_high_efficiency as request_gpt
|
|
||||||
|
|
||||||
# 1. 基本查询分析
|
|
||||||
type_prompt = f"""请分析这个与GitHub相关的查询,并严格按照以下XML格式回答:
|
|
||||||
|
|
||||||
查询: {query}
|
|
||||||
|
|
||||||
说明:
|
|
||||||
1. 你的回答必须使用下面显示的XML标签,不要有任何标签外的文本
|
|
||||||
2. 从以下选项中选择查询类型: repo/code/user/topic
|
|
||||||
- repo: 用于查找仓库、项目、框架或库
|
|
||||||
- code: 用于查找代码片段、函数实现或算法
|
|
||||||
- user: 用于查找用户、开发者或组织
|
|
||||||
- topic: 用于查找主题、类别或领域相关项目
|
|
||||||
3. 识别主题和子主题
|
|
||||||
4. 识别首选编程语言(如果有)
|
|
||||||
5. 确定最低星标数(如果适用)
|
|
||||||
|
|
||||||
必需格式:
|
|
||||||
<query_type>此处回答</query_type>
|
|
||||||
<main_topic>此处回答</main_topic>
|
|
||||||
<sub_topics>子主题1, 子主题2, ...</sub_topics>
|
|
||||||
<language>此处回答</language>
|
|
||||||
<min_stars>此处回答</min_stars>
|
|
||||||
|
|
||||||
示例回答:
|
|
||||||
|
|
||||||
1. 仓库查询:
|
|
||||||
查询: "查找有至少1000颗星的Python web框架"
|
|
||||||
<query_type>repo</query_type>
|
|
||||||
<main_topic>web框架</main_topic>
|
|
||||||
<sub_topics>后端开发, HTTP服务器, ORM</sub_topics>
|
|
||||||
<language>Python</language>
|
|
||||||
<min_stars>1000</min_stars>
|
|
||||||
|
|
||||||
2. 代码查询:
|
|
||||||
查询: "如何用JavaScript实现防抖函数"
|
|
||||||
<query_type>code</query_type>
|
|
||||||
<main_topic>防抖函数</main_topic>
|
|
||||||
<sub_topics>事件处理, 性能优化, 函数节流</sub_topics>
|
|
||||||
<language>JavaScript</language>
|
|
||||||
<min_stars>0</min_stars>"""
|
|
||||||
|
|
||||||
# 2. 生成英文搜索条件
|
|
||||||
github_prompt = f"""Optimize the following GitHub search query:
|
|
||||||
|
|
||||||
Query: {query}
|
|
||||||
|
|
||||||
Task: Convert the natural language query into an optimized GitHub search query.
|
|
||||||
Please use English, regardless of the language of the input query.
|
|
||||||
|
|
||||||
Available search fields and filters:
|
|
||||||
1. Basic fields:
|
|
||||||
- in:name - Search in repository names
|
|
||||||
- in:description - Search in repository descriptions
|
|
||||||
- in:readme - Search in README files
|
|
||||||
- in:topic - Search in topics
|
|
||||||
- language:X - Filter by programming language
|
|
||||||
- user:X - Repositories from a specific user
|
|
||||||
- org:X - Repositories from a specific organization
|
|
||||||
|
|
||||||
2. Code search fields:
|
|
||||||
- extension:X - Filter by file extension
|
|
||||||
- path:X - Filter by path
|
|
||||||
- filename:X - Filter by filename
|
|
||||||
|
|
||||||
3. Metric filters:
|
|
||||||
- stars:>X - Has more than X stars
|
|
||||||
- forks:>X - Has more than X forks
|
|
||||||
- size:>X - Size greater than X KB
|
|
||||||
- created:>YYYY-MM-DD - Created after a specific date
|
|
||||||
- pushed:>YYYY-MM-DD - Updated after a specific date
|
|
||||||
|
|
||||||
4. Other filters:
|
|
||||||
- is:public/private - Public or private repositories
|
|
||||||
- archived:true/false - Archived or not archived
|
|
||||||
- license:X - Specific license
|
|
||||||
- topic:X - Contains specific topic tag
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
1. Query: "Find Python machine learning libraries with at least 1000 stars"
|
|
||||||
<query>machine learning in:description language:python stars:>1000</query>
|
|
||||||
|
|
||||||
2. Query: "Recently updated React UI component libraries"
|
|
||||||
<query>UI components library in:readme in:description language:javascript topic:react pushed:>2023-01-01</query>
|
|
||||||
|
|
||||||
3. Query: "Open source projects developed by Facebook"
|
|
||||||
<query>org:facebook is:public</query>
|
|
||||||
|
|
||||||
4. Query: "Depth-first search implementation in JavaScript"
|
|
||||||
<query>depth first search in:file language:javascript</query>
|
|
||||||
|
|
||||||
Please analyze the query and answer using only the XML tag:
|
|
||||||
<query>Provide the optimized GitHub search query, using appropriate fields and operators</query>"""
|
|
||||||
|
|
||||||
# 3. 生成中文搜索条件
|
|
||||||
chinese_github_prompt = f"""优化以下GitHub搜索查询:
|
|
||||||
|
|
||||||
查询: {query}
|
|
||||||
|
|
||||||
任务: 将自然语言查询转换为优化的GitHub搜索查询语句。
|
|
||||||
为了搜索中文内容,请提取原始查询的关键词并使用中文形式,同时保留GitHub特定的搜索语法为英文。
|
|
||||||
|
|
||||||
可用的搜索字段和过滤器:
|
|
||||||
1. 基本字段:
|
|
||||||
- in:name - 在仓库名称中搜索
|
|
||||||
- in:description - 在仓库描述中搜索
|
|
||||||
- in:readme - 在README文件中搜索
|
|
||||||
- in:topic - 在主题中搜索
|
|
||||||
- language:X - 按编程语言筛选
|
|
||||||
- user:X - 特定用户的仓库
|
|
||||||
- org:X - 特定组织的仓库
|
|
||||||
|
|
||||||
2. 代码搜索字段:
|
|
||||||
- extension:X - 按文件扩展名筛选
|
|
||||||
- path:X - 按路径筛选
|
|
||||||
- filename:X - 按文件名筛选
|
|
||||||
|
|
||||||
3. 指标过滤器:
|
|
||||||
- stars:>X - 有超过X颗星
|
|
||||||
- forks:>X - 有超过X个分支
|
|
||||||
- size:>X - 大小超过X KB
|
|
||||||
- created:>YYYY-MM-DD - 在特定日期后创建
|
|
||||||
- pushed:>YYYY-MM-DD - 在特定日期后更新
|
|
||||||
|
|
||||||
4. 其他过滤器:
|
|
||||||
- is:public/private - 公开或私有仓库
|
|
||||||
- archived:true/false - 已归档或未归档
|
|
||||||
- license:X - 特定许可证
|
|
||||||
- topic:X - 含特定主题标签
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
1. 查询: "找有关机器学习的Python库,至少1000颗星"
|
|
||||||
<query>机器学习 in:description language:python stars:>1000</query>
|
|
||||||
|
|
||||||
2. 查询: "最近更新的React UI组件库"
|
|
||||||
<query>UI 组件库 in:readme in:description language:javascript topic:react pushed:>2023-01-01</query>
|
|
||||||
|
|
||||||
3. 查询: "微信小程序开发框架"
|
|
||||||
<query>微信小程序 开发框架 in:name in:description in:readme</query>
|
|
||||||
|
|
||||||
请分析查询并仅使用XML标签回答:
|
|
||||||
<query>提供优化的GitHub搜索查询,使用适当的字段和运算符,保留中文关键词</query>"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 构建提示数组
|
|
||||||
prompts = [
|
|
||||||
type_prompt,
|
|
||||||
github_prompt,
|
|
||||||
chinese_github_prompt,
|
|
||||||
]
|
|
||||||
|
|
||||||
show_messages = [
|
|
||||||
"分析查询类型...",
|
|
||||||
"优化英文GitHub搜索参数...",
|
|
||||||
"优化中文GitHub搜索参数...",
|
|
||||||
]
|
|
||||||
|
|
||||||
sys_prompts = [
|
|
||||||
"你是一个精通GitHub生态系统的专家,擅长分析与GitHub相关的查询。",
|
|
||||||
"You are a GitHub search expert, specialized in converting natural language queries into optimized GitHub search queries in English.",
|
|
||||||
"你是一个GitHub搜索专家,擅长处理查询并保留中文关键词进行搜索。",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 使用同步方式调用LLM
|
|
||||||
responses = yield from request_gpt(
|
|
||||||
inputs_array=prompts,
|
|
||||||
inputs_show_user_array=show_messages,
|
|
||||||
llm_kwargs=llm_kwargs,
|
|
||||||
chatbot=chatbot,
|
|
||||||
history_array=[[] for _ in prompts],
|
|
||||||
sys_prompt_array=sys_prompts,
|
|
||||||
max_workers=3
|
|
||||||
)
|
|
||||||
|
|
||||||
# 从收集的响应中提取我们需要的内容
|
|
||||||
extracted_responses = []
|
|
||||||
for i in range(len(prompts)):
|
|
||||||
if (i * 2 + 1) < len(responses):
|
|
||||||
response = responses[i * 2 + 1]
|
|
||||||
if response is None:
|
|
||||||
raise Exception(f"Response {i} is None")
|
|
||||||
if not isinstance(response, str):
|
|
||||||
try:
|
|
||||||
response = str(response)
|
|
||||||
except:
|
|
||||||
raise Exception(f"Cannot convert response {i} to string")
|
|
||||||
extracted_responses.append(response)
|
|
||||||
else:
|
|
||||||
raise Exception(f"未收到第 {i + 1} 个响应")
|
|
||||||
|
|
||||||
# 解析基本信息
|
|
||||||
query_type = self._extract_tag(extracted_responses[self.BASIC_QUERY_INDEX], "query_type")
|
|
||||||
if not query_type:
|
|
||||||
print(
|
|
||||||
f"Debug - Failed to extract query_type. Response was: {extracted_responses[self.BASIC_QUERY_INDEX]}")
|
|
||||||
raise Exception("无法提取query_type标签内容")
|
|
||||||
query_type = query_type.lower()
|
|
||||||
|
|
||||||
main_topic = self._extract_tag(extracted_responses[self.BASIC_QUERY_INDEX], "main_topic")
|
|
||||||
if not main_topic:
|
|
||||||
print(f"Debug - Failed to extract main_topic. Using query as fallback.")
|
|
||||||
main_topic = query
|
|
||||||
|
|
||||||
query_type = self._normalize_query_type(query_type, query)
|
|
||||||
|
|
||||||
# 提取子主题
|
|
||||||
sub_topics = []
|
|
||||||
sub_topics_text = self._extract_tag(extracted_responses[self.BASIC_QUERY_INDEX], "sub_topics")
|
|
||||||
if sub_topics_text:
|
|
||||||
sub_topics = [topic.strip() for topic in sub_topics_text.split(",")]
|
|
||||||
|
|
||||||
# 提取语言
|
|
||||||
language = self._extract_tag(extracted_responses[self.BASIC_QUERY_INDEX], "language")
|
|
||||||
|
|
||||||
# 提取最低星标数
|
|
||||||
min_stars = 0
|
|
||||||
min_stars_text = self._extract_tag(extracted_responses[self.BASIC_QUERY_INDEX], "min_stars")
|
|
||||||
if min_stars_text and min_stars_text.isdigit():
|
|
||||||
min_stars = int(min_stars_text)
|
|
||||||
|
|
||||||
# 解析GitHub搜索参数 - 英文
|
|
||||||
english_github_query = self._extract_tag(extracted_responses[self.GITHUB_QUERY_INDEX], "query")
|
|
||||||
|
|
||||||
# 解析GitHub搜索参数 - 中文
|
|
||||||
chinese_github_query = self._extract_tag(extracted_responses[2], "query")
|
|
||||||
|
|
||||||
# 构建GitHub参数
|
|
||||||
github_params = {
|
|
||||||
"query": english_github_query,
|
|
||||||
"chinese_query": chinese_github_query,
|
|
||||||
"sort": "stars", # 默认按星标排序
|
|
||||||
"order": "desc", # 默认降序
|
|
||||||
"per_page": 30, # 默认每页30条
|
|
||||||
"page": 1 # 默认第1页
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查是否为特定仓库查询
|
|
||||||
repo_id = ""
|
|
||||||
if "repo:" in english_github_query or "repository:" in english_github_query:
|
|
||||||
repo_match = re.search(r'(repo|repository):([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+)', english_github_query)
|
|
||||||
if repo_match:
|
|
||||||
repo_id = repo_match.group(2)
|
|
||||||
|
|
||||||
print(f"Debug - 提取的信息:")
|
|
||||||
print(f"查询类型: {query_type}")
|
|
||||||
print(f"主题: {main_topic}")
|
|
||||||
print(f"子主题: {sub_topics}")
|
|
||||||
print(f"语言: {language}")
|
|
||||||
print(f"最低星标数: {min_stars}")
|
|
||||||
print(f"英文GitHub参数: {english_github_query}")
|
|
||||||
print(f"中文GitHub参数: {chinese_github_query}")
|
|
||||||
print(f"特定仓库: {repo_id}")
|
|
||||||
|
|
||||||
# 更新返回的 SearchCriteria,包含中英文查询
|
|
||||||
return SearchCriteria(
|
|
||||||
query_type=query_type,
|
|
||||||
main_topic=main_topic,
|
|
||||||
sub_topics=sub_topics,
|
|
||||||
language=language,
|
|
||||||
min_stars=min_stars,
|
|
||||||
github_params=github_params,
|
|
||||||
original_query=query,
|
|
||||||
repo_id=repo_id
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"分析查询失败: {str(e)}")
|
|
||||||
|
|
||||||
def _normalize_query_type(self, query_type: str, query: str) -> str:
|
|
||||||
"""规范化查询类型"""
|
|
||||||
if query_type in ["repo", "code", "user", "topic"]:
|
|
||||||
return query_type
|
|
||||||
|
|
||||||
query_lower = query.lower()
|
|
||||||
for type_name, keywords in self.valid_types.items():
|
|
||||||
for keyword in keywords:
|
|
||||||
if keyword in query_lower:
|
|
||||||
return type_name
|
|
||||||
|
|
||||||
query_type_lower = query_type.lower()
|
|
||||||
for type_name, keywords in self.valid_types.items():
|
|
||||||
for keyword in keywords:
|
|
||||||
if keyword in query_type_lower:
|
|
||||||
return type_name
|
|
||||||
|
|
||||||
return "repo" # 默认返回repo类型
|
|
||||||
|
|
||||||
def _extract_tag(self, text: str, tag: str) -> str:
|
|
||||||
"""提取标记内容"""
|
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# 标准XML格式(处理多行和特殊字符)
|
|
||||||
pattern = f"<{tag}>(.*?)</{tag}>"
|
|
||||||
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
content = match.group(1).strip()
|
|
||||||
if content:
|
|
||||||
return content
|
|
||||||
|
|
||||||
# 备用模式
|
|
||||||
patterns = [
|
|
||||||
rf"<{tag}>\s*([\s\S]*?)\s*</{tag}>", # 标准XML格式
|
|
||||||
rf"<{tag}>([\s\S]*?)(?:</{tag}>|$)", # 未闭合的标签
|
|
||||||
rf"[{tag}]([\s\S]*?)[/{tag}]", # 方括号格式
|
|
||||||
rf"{tag}:\s*(.*?)(?=\n\w|$)", # 冒号格式
|
|
||||||
rf"<{tag}>\s*(.*?)(?=<|$)" # 部分闭合
|
|
||||||
]
|
|
||||||
|
|
||||||
# 尝试所有模式
|
|
||||||
for pattern in patterns:
|
|
||||||
match = re.search(pattern, text, re.IGNORECASE | re.DOTALL)
|
|
||||||
if match:
|
|
||||||
content = match.group(1).strip()
|
|
||||||
if content: # 确保提取的内容不为空
|
|
||||||
return content
|
|
||||||
|
|
||||||
# 如果所有模式都失败,返回空字符串
|
|
||||||
return ""
|
|
||||||
@@ -1,701 +0,0 @@
|
|||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List, Dict, Optional, Union, Any
|
|
||||||
|
|
||||||
class GitHubSource:
|
|
||||||
"""GitHub API实现"""
|
|
||||||
|
|
||||||
# 默认API密钥列表 - 可以放置多个GitHub令牌
|
|
||||||
API_KEYS = [
|
|
||||||
"github_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
||||||
"github_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
||||||
# "your_github_token_1",
|
|
||||||
# "your_github_token_2",
|
|
||||||
# "your_github_token_3"
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, api_key: Optional[Union[str, List[str]]] = None):
|
|
||||||
"""初始化GitHub API客户端
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_key: GitHub个人访问令牌或令牌列表
|
|
||||||
"""
|
|
||||||
if api_key is None:
|
|
||||||
self.api_keys = self.API_KEYS
|
|
||||||
elif isinstance(api_key, str):
|
|
||||||
self.api_keys = [api_key]
|
|
||||||
else:
|
|
||||||
self.api_keys = api_key
|
|
||||||
|
|
||||||
self._initialize()
|
|
||||||
|
|
||||||
def _initialize(self) -> None:
|
|
||||||
"""初始化客户端,设置默认参数"""
|
|
||||||
self.base_url = "https://api.github.com"
|
|
||||||
self.headers = {
|
|
||||||
"Accept": "application/vnd.github+json",
|
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
|
||||||
"User-Agent": "GitHub-API-Python-Client"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 如果有可用的API密钥,随机选择一个
|
|
||||||
if self.api_keys:
|
|
||||||
selected_key = random.choice(self.api_keys)
|
|
||||||
self.headers["Authorization"] = f"Bearer {selected_key}"
|
|
||||||
print(f"已随机选择API密钥进行认证")
|
|
||||||
else:
|
|
||||||
print("警告: 未提供API密钥,将受到GitHub API请求限制")
|
|
||||||
|
|
||||||
async def _request(self, method: str, endpoint: str, params: Dict = None, data: Dict = None) -> Any:
|
|
||||||
"""发送API请求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: HTTP方法 (GET, POST, PUT, DELETE等)
|
|
||||||
endpoint: API端点
|
|
||||||
params: URL参数
|
|
||||||
data: 请求体数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
解析后的响应JSON
|
|
||||||
"""
|
|
||||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
|
||||||
url = f"{self.base_url}{endpoint}"
|
|
||||||
|
|
||||||
# 为调试目的打印请求信息
|
|
||||||
print(f"请求: {method} {url}")
|
|
||||||
if params:
|
|
||||||
print(f"参数: {params}")
|
|
||||||
|
|
||||||
# 发送请求
|
|
||||||
request_kwargs = {}
|
|
||||||
if params:
|
|
||||||
request_kwargs["params"] = params
|
|
||||||
if data:
|
|
||||||
request_kwargs["json"] = data
|
|
||||||
|
|
||||||
async with session.request(method, url, **request_kwargs) as response:
|
|
||||||
response_text = await response.text()
|
|
||||||
|
|
||||||
# 检查HTTP状态码
|
|
||||||
if response.status >= 400:
|
|
||||||
print(f"API请求失败: HTTP {response.status}")
|
|
||||||
print(f"响应内容: {response_text}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 解析JSON响应
|
|
||||||
try:
|
|
||||||
return json.loads(response_text)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print(f"JSON解析错误: {response_text}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ===== 用户相关方法 =====
|
|
||||||
|
|
||||||
async def get_user(self, username: Optional[str] = None) -> Dict:
|
|
||||||
"""获取用户信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 指定用户名,不指定则获取当前授权用户
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
用户信息字典
|
|
||||||
"""
|
|
||||||
endpoint = "/user" if username is None else f"/users/{username}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_user_repos(self, username: Optional[str] = None, sort: str = "updated",
|
|
||||||
direction: str = "desc", per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取用户的仓库列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 指定用户名,不指定则获取当前授权用户
|
|
||||||
sort: 排序方式 (created, updated, pushed, full_name)
|
|
||||||
direction: 排序方向 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
仓库列表
|
|
||||||
"""
|
|
||||||
endpoint = "/user/repos" if username is None else f"/users/{username}/repos"
|
|
||||||
params = {
|
|
||||||
"sort": sort,
|
|
||||||
"direction": direction,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_user_starred(self, username: Optional[str] = None,
|
|
||||||
per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取用户星标的仓库
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 指定用户名,不指定则获取当前授权用户
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
星标仓库列表
|
|
||||||
"""
|
|
||||||
endpoint = "/user/starred" if username is None else f"/users/{username}/starred"
|
|
||||||
params = {
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
# ===== 仓库相关方法 =====
|
|
||||||
|
|
||||||
async def get_repo(self, owner: str, repo: str) -> Dict:
|
|
||||||
"""获取仓库信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
仓库信息
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_repo_branches(self, owner: str, repo: str, per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取仓库的分支列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
分支列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/branches"
|
|
||||||
params = {
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_repo_commits(self, owner: str, repo: str, sha: Optional[str] = None,
|
|
||||||
path: Optional[str] = None, per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取仓库的提交历史
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
sha: 特定提交SHA或分支名
|
|
||||||
path: 文件路径筛选
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
提交列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/commits"
|
|
||||||
params = {
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
if sha:
|
|
||||||
params["sha"] = sha
|
|
||||||
if path:
|
|
||||||
params["path"] = path
|
|
||||||
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_commit_details(self, owner: str, repo: str, commit_sha: str) -> Dict:
|
|
||||||
"""获取特定提交的详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
commit_sha: 提交SHA
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
提交详情
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/commits/{commit_sha}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
# ===== 内容相关方法 =====
|
|
||||||
|
|
||||||
async def get_file_content(self, owner: str, repo: str, path: str, ref: Optional[str] = None) -> Dict:
|
|
||||||
"""获取文件内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
path: 文件路径
|
|
||||||
ref: 分支名、标签名或提交SHA
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
文件内容信息
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/contents/{path}"
|
|
||||||
params = {}
|
|
||||||
if ref:
|
|
||||||
params["ref"] = ref
|
|
||||||
|
|
||||||
response = await self._request("GET", endpoint, params=params)
|
|
||||||
if response and isinstance(response, dict) and "content" in response:
|
|
||||||
try:
|
|
||||||
# 解码Base64编码的文件内容
|
|
||||||
content = base64.b64decode(response["content"].encode()).decode()
|
|
||||||
response["decoded_content"] = content
|
|
||||||
except Exception as e:
|
|
||||||
print(f"解码文件内容时出错: {str(e)}")
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def get_directory_content(self, owner: str, repo: str, path: str, ref: Optional[str] = None) -> List[Dict]:
|
|
||||||
"""获取目录内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
path: 目录路径
|
|
||||||
ref: 分支名、标签名或提交SHA
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
目录内容列表
|
|
||||||
"""
|
|
||||||
# 注意:此方法与get_file_content使用相同的端点,但对于目录会返回列表
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/contents/{path}"
|
|
||||||
params = {}
|
|
||||||
if ref:
|
|
||||||
params["ref"] = ref
|
|
||||||
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
# ===== Issues相关方法 =====
|
|
||||||
|
|
||||||
async def get_issues(self, owner: str, repo: str, state: str = "open",
|
|
||||||
sort: str = "created", direction: str = "desc",
|
|
||||||
per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取仓库的Issues列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
state: Issue状态 (open, closed, all)
|
|
||||||
sort: 排序方式 (created, updated, comments)
|
|
||||||
direction: 排序方向 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Issues列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/issues"
|
|
||||||
params = {
|
|
||||||
"state": state,
|
|
||||||
"sort": sort,
|
|
||||||
"direction": direction,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_issue(self, owner: str, repo: str, issue_number: int) -> Dict:
|
|
||||||
"""获取特定Issue的详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
issue_number: Issue编号
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Issue详情
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/issues/{issue_number}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_issue_comments(self, owner: str, repo: str, issue_number: int) -> List[Dict]:
|
|
||||||
"""获取Issue的评论
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
issue_number: Issue编号
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
评论列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
# ===== Pull Requests相关方法 =====
|
|
||||||
|
|
||||||
async def get_pull_requests(self, owner: str, repo: str, state: str = "open",
|
|
||||||
sort: str = "created", direction: str = "desc",
|
|
||||||
per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取仓库的Pull Request列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
state: PR状态 (open, closed, all)
|
|
||||||
sort: 排序方式 (created, updated, popularity, long-running)
|
|
||||||
direction: 排序方向 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pull Request列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/pulls"
|
|
||||||
params = {
|
|
||||||
"state": state,
|
|
||||||
"sort": sort,
|
|
||||||
"direction": direction,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_pull_request(self, owner: str, repo: str, pr_number: int) -> Dict:
|
|
||||||
"""获取特定Pull Request的详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
pr_number: Pull Request编号
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pull Request详情
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/pulls/{pr_number}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_pull_request_files(self, owner: str, repo: str, pr_number: int) -> List[Dict]:
|
|
||||||
"""获取Pull Request中修改的文件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
pr_number: Pull Request编号
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
修改文件列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/pulls/{pr_number}/files"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
# ===== 搜索相关方法 =====
|
|
||||||
|
|
||||||
async def search_repositories(self, query: str, sort: str = "stars",
|
|
||||||
order: str = "desc", per_page: int = 30, page: int = 1) -> Dict:
|
|
||||||
"""搜索仓库
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: 搜索关键词
|
|
||||||
sort: 排序方式 (stars, forks, updated)
|
|
||||||
order: 排序顺序 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
搜索结果
|
|
||||||
"""
|
|
||||||
endpoint = "/search/repositories"
|
|
||||||
params = {
|
|
||||||
"q": query,
|
|
||||||
"sort": sort,
|
|
||||||
"order": order,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def search_code(self, query: str, sort: str = "indexed",
|
|
||||||
order: str = "desc", per_page: int = 30, page: int = 1) -> Dict:
|
|
||||||
"""搜索代码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: 搜索关键词
|
|
||||||
sort: 排序方式 (indexed)
|
|
||||||
order: 排序顺序 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
搜索结果
|
|
||||||
"""
|
|
||||||
endpoint = "/search/code"
|
|
||||||
params = {
|
|
||||||
"q": query,
|
|
||||||
"sort": sort,
|
|
||||||
"order": order,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def search_issues(self, query: str, sort: str = "created",
|
|
||||||
order: str = "desc", per_page: int = 30, page: int = 1) -> Dict:
|
|
||||||
"""搜索Issues和Pull Requests
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: 搜索关键词
|
|
||||||
sort: 排序方式 (created, updated, comments)
|
|
||||||
order: 排序顺序 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
搜索结果
|
|
||||||
"""
|
|
||||||
endpoint = "/search/issues"
|
|
||||||
params = {
|
|
||||||
"q": query,
|
|
||||||
"sort": sort,
|
|
||||||
"order": order,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def search_users(self, query: str, sort: str = "followers",
|
|
||||||
order: str = "desc", per_page: int = 30, page: int = 1) -> Dict:
|
|
||||||
"""搜索用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: 搜索关键词
|
|
||||||
sort: 排序方式 (followers, repositories, joined)
|
|
||||||
order: 排序顺序 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
搜索结果
|
|
||||||
"""
|
|
||||||
endpoint = "/search/users"
|
|
||||||
params = {
|
|
||||||
"q": query,
|
|
||||||
"sort": sort,
|
|
||||||
"order": order,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
# ===== 组织相关方法 =====
|
|
||||||
|
|
||||||
async def get_organization(self, org: str) -> Dict:
|
|
||||||
"""获取组织信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
org: 组织名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
组织信息
|
|
||||||
"""
|
|
||||||
endpoint = f"/orgs/{org}"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_organization_repos(self, org: str, type: str = "all",
|
|
||||||
sort: str = "created", direction: str = "desc",
|
|
||||||
per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取组织的仓库列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
org: 组织名称
|
|
||||||
type: 仓库类型 (all, public, private, forks, sources, member, internal)
|
|
||||||
sort: 排序方式 (created, updated, pushed, full_name)
|
|
||||||
direction: 排序方向 (asc, desc)
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
仓库列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/orgs/{org}/repos"
|
|
||||||
params = {
|
|
||||||
"type": type,
|
|
||||||
"sort": sort,
|
|
||||||
"direction": direction,
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
async def get_organization_members(self, org: str, per_page: int = 30, page: int = 1) -> List[Dict]:
|
|
||||||
"""获取组织成员列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
org: 组织名称
|
|
||||||
per_page: 每页结果数量
|
|
||||||
page: 页码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
成员列表
|
|
||||||
"""
|
|
||||||
endpoint = f"/orgs/{org}/members"
|
|
||||||
params = {
|
|
||||||
"per_page": per_page,
|
|
||||||
"page": page
|
|
||||||
}
|
|
||||||
return await self._request("GET", endpoint, params=params)
|
|
||||||
|
|
||||||
# ===== 更复杂的操作 =====
|
|
||||||
|
|
||||||
async def get_repository_languages(self, owner: str, repo: str) -> Dict:
|
|
||||||
"""获取仓库使用的编程语言及其比例
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
语言使用情况
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/languages"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_repository_stats_contributors(self, owner: str, repo: str) -> List[Dict]:
|
|
||||||
"""获取仓库的贡献者统计
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
贡献者统计信息
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/stats/contributors"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def get_repository_stats_commit_activity(self, owner: str, repo: str) -> List[Dict]:
|
|
||||||
"""获取仓库的提交活动
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: 仓库所有者
|
|
||||||
repo: 仓库名
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
提交活动统计
|
|
||||||
"""
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/stats/commit_activity"
|
|
||||||
return await self._request("GET", endpoint)
|
|
||||||
|
|
||||||
async def example_usage():
|
|
||||||
"""GitHubSource使用示例"""
|
|
||||||
# 创建客户端实例(可选传入API令牌)
|
|
||||||
# github = GitHubSource(api_key="your_github_token")
|
|
||||||
github = GitHubSource()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 示例1:搜索热门Python仓库
|
|
||||||
print("\n=== 示例1:搜索热门Python仓库 ===")
|
|
||||||
repos = await github.search_repositories(
|
|
||||||
query="language:python stars:>1000",
|
|
||||||
sort="stars",
|
|
||||||
order="desc",
|
|
||||||
per_page=5
|
|
||||||
)
|
|
||||||
|
|
||||||
if repos and "items" in repos:
|
|
||||||
for i, repo in enumerate(repos["items"], 1):
|
|
||||||
print(f"\n--- 仓库 {i} ---")
|
|
||||||
print(f"名称: {repo['full_name']}")
|
|
||||||
print(f"描述: {repo['description']}")
|
|
||||||
print(f"星标数: {repo['stargazers_count']}")
|
|
||||||
print(f"Fork数: {repo['forks_count']}")
|
|
||||||
print(f"最近更新: {repo['updated_at']}")
|
|
||||||
print(f"URL: {repo['html_url']}")
|
|
||||||
|
|
||||||
# 示例2:获取特定仓库的详情
|
|
||||||
print("\n=== 示例2:获取特定仓库的详情 ===")
|
|
||||||
repo_details = await github.get_repo("microsoft", "vscode")
|
|
||||||
if repo_details:
|
|
||||||
print(f"名称: {repo_details['full_name']}")
|
|
||||||
print(f"描述: {repo_details['description']}")
|
|
||||||
print(f"星标数: {repo_details['stargazers_count']}")
|
|
||||||
print(f"Fork数: {repo_details['forks_count']}")
|
|
||||||
print(f"默认分支: {repo_details['default_branch']}")
|
|
||||||
print(f"开源许可: {repo_details.get('license', {}).get('name', '无')}")
|
|
||||||
print(f"语言: {repo_details['language']}")
|
|
||||||
print(f"Open Issues数: {repo_details['open_issues_count']}")
|
|
||||||
|
|
||||||
# 示例3:获取仓库的提交历史
|
|
||||||
print("\n=== 示例3:获取仓库的最近提交 ===")
|
|
||||||
commits = await github.get_repo_commits("tensorflow", "tensorflow", per_page=5)
|
|
||||||
if commits:
|
|
||||||
for i, commit in enumerate(commits, 1):
|
|
||||||
print(f"\n--- 提交 {i} ---")
|
|
||||||
print(f"SHA: {commit['sha'][:7]}")
|
|
||||||
print(f"作者: {commit['commit']['author']['name']}")
|
|
||||||
print(f"日期: {commit['commit']['author']['date']}")
|
|
||||||
print(f"消息: {commit['commit']['message'].splitlines()[0]}")
|
|
||||||
|
|
||||||
# 示例4:搜索代码
|
|
||||||
print("\n=== 示例4:搜索代码 ===")
|
|
||||||
code_results = await github.search_code(
|
|
||||||
query="filename:README.md language:markdown pytorch in:file",
|
|
||||||
per_page=3
|
|
||||||
)
|
|
||||||
if code_results and "items" in code_results:
|
|
||||||
print(f"共找到: {code_results['total_count']} 个结果")
|
|
||||||
for i, item in enumerate(code_results["items"], 1):
|
|
||||||
print(f"\n--- 代码 {i} ---")
|
|
||||||
print(f"仓库: {item['repository']['full_name']}")
|
|
||||||
print(f"文件: {item['path']}")
|
|
||||||
print(f"URL: {item['html_url']}")
|
|
||||||
|
|
||||||
# 示例5:获取文件内容
|
|
||||||
print("\n=== 示例5:获取文件内容 ===")
|
|
||||||
file_content = await github.get_file_content("python", "cpython", "README.rst")
|
|
||||||
if file_content and "decoded_content" in file_content:
|
|
||||||
content = file_content["decoded_content"]
|
|
||||||
print(f"文件名: {file_content['name']}")
|
|
||||||
print(f"大小: {file_content['size']} 字节")
|
|
||||||
print(f"内容预览: {content[:200]}...")
|
|
||||||
|
|
||||||
# 示例6:获取仓库使用的编程语言
|
|
||||||
print("\n=== 示例6:获取仓库使用的编程语言 ===")
|
|
||||||
languages = await github.get_repository_languages("facebook", "react")
|
|
||||||
if languages:
|
|
||||||
print(f"React仓库使用的编程语言:")
|
|
||||||
for lang, bytes_of_code in languages.items():
|
|
||||||
print(f"- {lang}: {bytes_of_code} 字节")
|
|
||||||
|
|
||||||
# 示例7:获取组织信息
|
|
||||||
print("\n=== 示例7:获取组织信息 ===")
|
|
||||||
org_info = await github.get_organization("google")
|
|
||||||
if org_info:
|
|
||||||
print(f"名称: {org_info['name']}")
|
|
||||||
print(f"描述: {org_info.get('description', '无')}")
|
|
||||||
print(f"位置: {org_info.get('location', '未指定')}")
|
|
||||||
print(f"公共仓库数: {org_info['public_repos']}")
|
|
||||||
print(f"成员数: {org_info.get('public_members', 0)}")
|
|
||||||
print(f"URL: {org_info['html_url']}")
|
|
||||||
|
|
||||||
# 示例8:获取用户信息
|
|
||||||
print("\n=== 示例8:获取用户信息 ===")
|
|
||||||
user_info = await github.get_user("torvalds")
|
|
||||||
if user_info:
|
|
||||||
print(f"名称: {user_info['name']}")
|
|
||||||
print(f"公司: {user_info.get('company', '无')}")
|
|
||||||
print(f"博客: {user_info.get('blog', '无')}")
|
|
||||||
print(f"位置: {user_info.get('location', '未指定')}")
|
|
||||||
print(f"公共仓库数: {user_info['public_repos']}")
|
|
||||||
print(f"关注者数: {user_info['followers']}")
|
|
||||||
print(f"URL: {user_info['html_url']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"发生错误: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# 运行示例
|
|
||||||
asyncio.run(example_usage())
|
|
||||||
@@ -1,593 +0,0 @@
|
|||||||
from typing import List, Dict, Optional, Tuple, Union, Any
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from crazy_functions.doc_fns.read_fns.unstructured_all.paper_structure_extractor import (
|
|
||||||
PaperStructureExtractor, PaperSection, StructuredPaper
|
|
||||||
)
|
|
||||||
from unstructured.partition.auto import partition
|
|
||||||
from unstructured.documents.elements import (
|
|
||||||
Text, Title, NarrativeText, ListItem, Table,
|
|
||||||
Footer, Header, PageBreak, Image, Address
|
|
||||||
)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DocumentSection:
|
|
||||||
"""通用文档章节数据类"""
|
|
||||||
title: str # 章节标题,如果没有标题则为空字符串
|
|
||||||
content: str # 章节内容
|
|
||||||
level: int = 0 # 标题级别,0为主标题,1为一级标题,以此类推
|
|
||||||
section_type: str = "content" # 章节类型
|
|
||||||
is_heading_only: bool = False # 是否仅包含标题
|
|
||||||
subsections: List['DocumentSection'] = field(default_factory=list) # 子章节列表
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StructuredDocument:
|
|
||||||
"""结构化文档数据类"""
|
|
||||||
title: str = "" # 文档标题
|
|
||||||
metadata: Dict[str, Any] = field(default_factory=dict) # 元数据
|
|
||||||
sections: List[DocumentSection] = field(default_factory=list) # 章节列表
|
|
||||||
full_text: str = "" # 完整文本
|
|
||||||
is_paper: bool = False # 是否为学术论文
|
|
||||||
|
|
||||||
|
|
||||||
class GenericDocumentStructureExtractor:
|
|
||||||
"""通用文档结构提取器
|
|
||||||
|
|
||||||
可以从各种文档格式中提取结构信息,包括标题和内容。
|
|
||||||
支持论文、报告、文章和一般文本文档。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 支持的文件扩展名
|
|
||||||
SUPPORTED_EXTENSIONS = [
|
|
||||||
'.pdf', '.docx', '.doc', '.pptx', '.ppt',
|
|
||||||
'.txt', '.md', '.html', '.htm', '.xml',
|
|
||||||
'.rtf', '.odt', '.epub', '.msg', '.eml'
|
|
||||||
]
|
|
||||||
|
|
||||||
# 常见的标题前缀模式
|
|
||||||
HEADING_PATTERNS = [
|
|
||||||
# 数字标题 (1., 1.1., etc.)
|
|
||||||
r'^\s*(\d+\.)+\s+',
|
|
||||||
# 中文数字标题 (一、, 二、, etc.)
|
|
||||||
r'^\s*[一二三四五六七八九十]+[、::]\s+',
|
|
||||||
# 带括号的数字标题 ((1), (2), etc.)
|
|
||||||
r'^\s*\(\s*\d+\s*\)\s+',
|
|
||||||
# 特定标记的标题 (Chapter 1, Section 1, etc.)
|
|
||||||
r'^\s*(chapter|section|part|附录|章|节)\s+\d+[\.::]\s+',
|
|
||||||
]
|
|
||||||
|
|
||||||
# 常见的文档分段标记词
|
|
||||||
SECTION_MARKERS = {
|
|
||||||
'introduction': ['简介', '导言', '引言', 'introduction', '概述', 'overview'],
|
|
||||||
'background': ['背景', '现状', 'background', '理论基础', '相关工作'],
|
|
||||||
'main_content': ['主要内容', '正文', 'main content', '分析', '讨论'],
|
|
||||||
'conclusion': ['结论', '总结', 'conclusion', '结语', '小结', 'summary'],
|
|
||||||
'reference': ['参考', '参考文献', 'references', '文献', 'bibliography'],
|
|
||||||
'appendix': ['附录', 'appendix', '补充资料', 'supplementary']
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""初始化提取器"""
|
|
||||||
self.paper_extractor = PaperStructureExtractor() # 论文专用提取器
|
|
||||||
self._setup_logging()
|
|
||||||
|
|
||||||
def _setup_logging(self):
|
|
||||||
"""配置日志"""
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def extract_document_structure(self, file_path: str, strategy: str = "fast") -> StructuredDocument:
|
|
||||||
"""提取文档结构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: 文件路径
|
|
||||||
strategy: 提取策略 ("fast" 或 "accurate")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StructuredDocument: 结构化文档对象
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.logger.info(f"正在处理文档结构: {file_path}")
|
|
||||||
|
|
||||||
# 1. 首先尝试使用论文提取器
|
|
||||||
try:
|
|
||||||
paper_result = self.paper_extractor.extract_paper_structure(file_path)
|
|
||||||
if paper_result and len(paper_result.sections) > 2: # 如果成功识别为论文结构
|
|
||||||
self.logger.info(f"成功识别为学术论文: {file_path}")
|
|
||||||
# 将论文结构转换为通用文档结构
|
|
||||||
return self._convert_paper_to_document(paper_result)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(f"论文结构提取失败,将尝试通用提取: {str(e)}")
|
|
||||||
|
|
||||||
# 2. 使用通用方法提取文档结构
|
|
||||||
elements = partition(
|
|
||||||
str(file_path),
|
|
||||||
strategy=strategy,
|
|
||||||
include_metadata=True,
|
|
||||||
nlp=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 使用通用提取器处理
|
|
||||||
doc = self._extract_generic_structure(elements)
|
|
||||||
return doc
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"文档结构提取失败: {str(e)}")
|
|
||||||
# 返回一个空的结构化文档
|
|
||||||
return StructuredDocument(
|
|
||||||
title="未能提取文档标题",
|
|
||||||
sections=[DocumentSection(
|
|
||||||
title="",
|
|
||||||
content="",
|
|
||||||
level=0,
|
|
||||||
section_type="content"
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _convert_paper_to_document(self, paper: StructuredPaper) -> StructuredDocument:
|
|
||||||
"""将论文结构转换为通用文档结构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paper: 结构化论文对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StructuredDocument: 转换后的通用文档结构
|
|
||||||
"""
|
|
||||||
doc = StructuredDocument(
|
|
||||||
title=paper.metadata.title,
|
|
||||||
is_paper=True,
|
|
||||||
full_text=paper.full_text
|
|
||||||
)
|
|
||||||
|
|
||||||
# 转换元数据
|
|
||||||
doc.metadata = {
|
|
||||||
'title': paper.metadata.title,
|
|
||||||
'authors': paper.metadata.authors,
|
|
||||||
'keywords': paper.keywords,
|
|
||||||
'abstract': paper.metadata.abstract if hasattr(paper.metadata, 'abstract') else "",
|
|
||||||
'is_paper': True
|
|
||||||
}
|
|
||||||
|
|
||||||
# 转换章节结构
|
|
||||||
doc.sections = self._convert_paper_sections(paper.sections)
|
|
||||||
|
|
||||||
return doc
|
|
||||||
|
|
||||||
def _convert_paper_sections(self, paper_sections: List[PaperSection], level: int = 0) -> List[DocumentSection]:
|
|
||||||
"""递归转换论文章节为通用文档章节
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paper_sections: 论文章节列表
|
|
||||||
level: 当前章节级别
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[DocumentSection]: 通用文档章节列表
|
|
||||||
"""
|
|
||||||
doc_sections = []
|
|
||||||
|
|
||||||
for section in paper_sections:
|
|
||||||
doc_section = DocumentSection(
|
|
||||||
title=section.title,
|
|
||||||
content=section.content,
|
|
||||||
level=section.level,
|
|
||||||
section_type=section.section_type,
|
|
||||||
is_heading_only=False if section.content else True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 递归处理子章节
|
|
||||||
if section.subsections:
|
|
||||||
doc_section.subsections = self._convert_paper_sections(
|
|
||||||
section.subsections, level + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
doc_sections.append(doc_section)
|
|
||||||
|
|
||||||
return doc_sections
|
|
||||||
|
|
||||||
def _extract_generic_structure(self, elements) -> StructuredDocument:
|
|
||||||
"""从元素列表中提取通用文档结构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
elements: 文档元素列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StructuredDocument: 结构化文档对象
|
|
||||||
"""
|
|
||||||
# 创建结构化文档对象
|
|
||||||
doc = StructuredDocument(full_text="")
|
|
||||||
|
|
||||||
# 1. 提取文档标题
|
|
||||||
title_candidates = []
|
|
||||||
for i, element in enumerate(elements[:5]): # 只检查前5个元素
|
|
||||||
if isinstance(element, Title):
|
|
||||||
title_text = str(element).strip()
|
|
||||||
title_candidates.append((i, title_text))
|
|
||||||
|
|
||||||
if title_candidates:
|
|
||||||
# 使用第一个标题作为文档标题
|
|
||||||
doc.title = title_candidates[0][1]
|
|
||||||
|
|
||||||
# 2. 识别所有标题元素和内容
|
|
||||||
title_elements = []
|
|
||||||
|
|
||||||
# 2.1 首先识别所有标题
|
|
||||||
for i, element in enumerate(elements):
|
|
||||||
is_heading = False
|
|
||||||
title_text = ""
|
|
||||||
level = 0
|
|
||||||
|
|
||||||
# 检查元素类型
|
|
||||||
if isinstance(element, Title):
|
|
||||||
is_heading = True
|
|
||||||
title_text = str(element).strip()
|
|
||||||
|
|
||||||
# 进一步检查是否为真正的标题
|
|
||||||
if self._is_likely_heading(title_text, element, i, elements):
|
|
||||||
level = self._estimate_heading_level(title_text, element)
|
|
||||||
else:
|
|
||||||
is_heading = False
|
|
||||||
|
|
||||||
# 也检查格式像标题的普通文本
|
|
||||||
elif isinstance(element, (Text, NarrativeText)) and i > 0:
|
|
||||||
text = str(element).strip()
|
|
||||||
# 检查是否匹配标题模式
|
|
||||||
if any(re.match(pattern, text) for pattern in self.HEADING_PATTERNS):
|
|
||||||
# 检查长度和后续内容以确认是否为标题
|
|
||||||
if len(text) < 100 and self._has_sufficient_following_content(i, elements):
|
|
||||||
is_heading = True
|
|
||||||
title_text = text
|
|
||||||
level = self._estimate_heading_level(title_text, element)
|
|
||||||
|
|
||||||
if is_heading:
|
|
||||||
section_type = self._identify_section_type(title_text)
|
|
||||||
title_elements.append((i, title_text, level, section_type))
|
|
||||||
|
|
||||||
# 2.2 为每个标题提取内容
|
|
||||||
sections = []
|
|
||||||
|
|
||||||
for i, (index, title_text, level, section_type) in enumerate(title_elements):
|
|
||||||
# 确定内容范围
|
|
||||||
content_start = index + 1
|
|
||||||
content_end = elements[-1] # 默认到文档结束
|
|
||||||
|
|
||||||
# 如果有下一个标题,内容到下一个标题开始
|
|
||||||
if i < len(title_elements) - 1:
|
|
||||||
content_end = title_elements[i+1][0]
|
|
||||||
else:
|
|
||||||
content_end = len(elements)
|
|
||||||
|
|
||||||
# 提取内容
|
|
||||||
content = self._extract_content_between(elements, content_start, content_end)
|
|
||||||
|
|
||||||
# 创建章节
|
|
||||||
section = DocumentSection(
|
|
||||||
title=title_text,
|
|
||||||
content=content,
|
|
||||||
level=level,
|
|
||||||
section_type=section_type,
|
|
||||||
is_heading_only=False if content.strip() else True
|
|
||||||
)
|
|
||||||
|
|
||||||
sections.append(section)
|
|
||||||
|
|
||||||
# 3. 如果没有识别到任何章节,创建一个默认章节
|
|
||||||
if not sections:
|
|
||||||
all_content = self._extract_content_between(elements, 0, len(elements))
|
|
||||||
|
|
||||||
# 尝试从内容中提取标题
|
|
||||||
first_line = all_content.split('\n')[0] if all_content else ""
|
|
||||||
if first_line and len(first_line) < 100:
|
|
||||||
doc.title = first_line
|
|
||||||
all_content = '\n'.join(all_content.split('\n')[1:])
|
|
||||||
|
|
||||||
default_section = DocumentSection(
|
|
||||||
title="",
|
|
||||||
content=all_content,
|
|
||||||
level=0,
|
|
||||||
section_type="content"
|
|
||||||
)
|
|
||||||
sections.append(default_section)
|
|
||||||
|
|
||||||
# 4. 构建层次结构
|
|
||||||
doc.sections = self._build_section_hierarchy(sections)
|
|
||||||
|
|
||||||
# 5. 提取完整文本
|
|
||||||
doc.full_text = "\n\n".join([str(element) for element in elements if isinstance(element, (Text, NarrativeText, Title, ListItem))])
|
|
||||||
|
|
||||||
return doc
|
|
||||||
|
|
||||||
def _build_section_hierarchy(self, sections: List[DocumentSection]) -> List[DocumentSection]:
|
|
||||||
"""构建章节层次结构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sections: 章节列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[DocumentSection]: 具有层次结构的章节列表
|
|
||||||
"""
|
|
||||||
if not sections:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 按层级排序
|
|
||||||
top_level_sections = []
|
|
||||||
current_parents = {0: None} # 每个层级的当前父节点
|
|
||||||
|
|
||||||
for section in sections:
|
|
||||||
# 找到当前节点的父节点
|
|
||||||
parent_level = None
|
|
||||||
for level in sorted([k for k in current_parents.keys() if k < section.level], reverse=True):
|
|
||||||
parent_level = level
|
|
||||||
break
|
|
||||||
|
|
||||||
if parent_level is None:
|
|
||||||
# 顶级章节
|
|
||||||
top_level_sections.append(section)
|
|
||||||
else:
|
|
||||||
# 子章节
|
|
||||||
parent = current_parents[parent_level]
|
|
||||||
if parent:
|
|
||||||
parent.subsections.append(section)
|
|
||||||
else:
|
|
||||||
top_level_sections.append(section)
|
|
||||||
|
|
||||||
# 更新当前层级的父节点
|
|
||||||
current_parents[section.level] = section
|
|
||||||
|
|
||||||
# 清除所有更深层级的父节点缓存
|
|
||||||
deeper_levels = [k for k in current_parents.keys() if k > section.level]
|
|
||||||
for level in deeper_levels:
|
|
||||||
current_parents.pop(level, None)
|
|
||||||
|
|
||||||
return top_level_sections
|
|
||||||
|
|
||||||
def _is_likely_heading(self, text: str, element, index: int, elements) -> bool:
|
|
||||||
"""判断文本是否可能是标题
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: 文本内容
|
|
||||||
element: 元素对象
|
|
||||||
index: 元素索引
|
|
||||||
elements: 所有元素列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否可能是标题
|
|
||||||
"""
|
|
||||||
# 1. 检查文本长度 - 标题通常不会太长
|
|
||||||
if len(text) > 150: # 标题通常不超过150个字符
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 2. 检查是否匹配标题的数字编号模式
|
|
||||||
if any(re.match(pattern, text) for pattern in self.HEADING_PATTERNS):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 3. 检查是否包含常见章节标记词
|
|
||||||
lower_text = text.lower()
|
|
||||||
for markers in self.SECTION_MARKERS.values():
|
|
||||||
if any(marker.lower() in lower_text for marker in markers):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 4. 检查后续内容数量 - 标题后通常有足够多的内容
|
|
||||||
if not self._has_sufficient_following_content(index, elements, min_chars=100):
|
|
||||||
# 但如果文本很短且以特定格式开头,仍可能是标题
|
|
||||||
if len(text) < 50 and (text.endswith(':') or text.endswith(':')):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 5. 检查格式特征
|
|
||||||
# 标题通常是元素的开头,不在段落中间
|
|
||||||
if len(text.split('\n')) > 1:
|
|
||||||
# 多行文本不太可能是标题
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 如果有元数据,检查字体特征(字体大小等)
|
|
||||||
if hasattr(element, 'metadata') and element.metadata:
|
|
||||||
try:
|
|
||||||
font_size = getattr(element.metadata, 'font_size', None)
|
|
||||||
is_bold = getattr(element.metadata, 'is_bold', False)
|
|
||||||
|
|
||||||
# 字体较大或加粗的文本更可能是标题
|
|
||||||
if font_size and font_size > 12:
|
|
||||||
return True
|
|
||||||
if is_bold:
|
|
||||||
return True
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 默认返回True,因为元素已被识别为Title类型
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _estimate_heading_level(self, text: str, element) -> int:
|
|
||||||
"""估计标题的层级
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: 标题文本
|
|
||||||
element: 元素对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: 标题层级 (0为主标题,1为一级标题, 等等)
|
|
||||||
"""
|
|
||||||
# 1. 通过编号模式判断层级
|
|
||||||
for pattern, level in [
|
|
||||||
(r'^\s*\d+\.\s+', 1), # 1. 开头 (一级标题)
|
|
||||||
(r'^\s*\d+\.\d+\.\s+', 2), # 1.1. 开头 (二级标题)
|
|
||||||
(r'^\s*\d+\.\d+\.\d+\.\s+', 3), # 1.1.1. 开头 (三级标题)
|
|
||||||
(r'^\s*\d+\.\d+\.\d+\.\d+\.\s+', 4), # 1.1.1.1. 开头 (四级标题)
|
|
||||||
]:
|
|
||||||
if re.match(pattern, text):
|
|
||||||
return level
|
|
||||||
|
|
||||||
# 2. 检查是否是常见的主要章节标题
|
|
||||||
lower_text = text.lower()
|
|
||||||
main_sections = [
|
|
||||||
'abstract', 'introduction', 'background', 'methodology',
|
|
||||||
'results', 'discussion', 'conclusion', 'references'
|
|
||||||
]
|
|
||||||
for section in main_sections:
|
|
||||||
if section in lower_text:
|
|
||||||
return 1 # 主要章节为一级标题
|
|
||||||
|
|
||||||
# 3. 根据文本特征判断
|
|
||||||
if text.isupper(): # 全大写文本可能是章标题
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# 4. 通过元数据判断层级
|
|
||||||
if hasattr(element, 'metadata') and element.metadata:
|
|
||||||
try:
|
|
||||||
# 根据字体大小判断层级
|
|
||||||
font_size = getattr(element.metadata, 'font_size', None)
|
|
||||||
if font_size is not None:
|
|
||||||
if font_size > 18: # 假设主标题字体最大
|
|
||||||
return 0
|
|
||||||
elif font_size > 16:
|
|
||||||
return 1
|
|
||||||
elif font_size > 14:
|
|
||||||
return 2
|
|
||||||
else:
|
|
||||||
return 3
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 默认为二级标题
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def _identify_section_type(self, title_text: str) -> str:
|
|
||||||
"""识别章节类型,包括参考文献部分"""
|
|
||||||
lower_text = title_text.lower()
|
|
||||||
|
|
||||||
# 特别检查是否为参考文献部分
|
|
||||||
references_patterns = [
|
|
||||||
r'references', r'参考文献', r'bibliography', r'引用文献',
|
|
||||||
r'literature cited', r'^cited\s+literature', r'^文献$', r'^引用$'
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in references_patterns:
|
|
||||||
if re.search(pattern, lower_text, re.IGNORECASE):
|
|
||||||
return "references"
|
|
||||||
|
|
||||||
# 检查是否匹配其他常见章节类型
|
|
||||||
for section_type, markers in self.SECTION_MARKERS.items():
|
|
||||||
if any(marker.lower() in lower_text for marker in markers):
|
|
||||||
return section_type
|
|
||||||
|
|
||||||
# 检查带编号的章节
|
|
||||||
if re.match(r'^\d+\.', lower_text):
|
|
||||||
return "content"
|
|
||||||
|
|
||||||
# 默认为内容章节
|
|
||||||
return "content"
|
|
||||||
|
|
||||||
def _has_sufficient_following_content(self, index: int, elements, min_chars: int = 150) -> bool:
|
|
||||||
"""检查元素后是否有足够的内容
|
|
||||||
|
|
||||||
Args:
|
|
||||||
index: 当前元素索引
|
|
||||||
elements: 所有元素列表
|
|
||||||
min_chars: 最小字符数要求
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否有足够的内容
|
|
||||||
"""
|
|
||||||
total_chars = 0
|
|
||||||
for i in range(index + 1, min(index + 5, len(elements))):
|
|
||||||
if isinstance(elements[i], Title):
|
|
||||||
# 如果紧接着是标题,就停止检查
|
|
||||||
break
|
|
||||||
if isinstance(elements[i], (Text, NarrativeText, ListItem, Table)):
|
|
||||||
total_chars += len(str(elements[i]))
|
|
||||||
if total_chars >= min_chars:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return total_chars >= min_chars
|
|
||||||
|
|
||||||
def _extract_content_between(self, elements, start_index: int, end_index: int) -> str:
|
|
||||||
"""提取指定范围内的内容文本
|
|
||||||
|
|
||||||
Args:
|
|
||||||
elements: 元素列表
|
|
||||||
start_index: 开始索引
|
|
||||||
end_index: 结束索引
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 提取的内容文本
|
|
||||||
"""
|
|
||||||
content_parts = []
|
|
||||||
|
|
||||||
for i in range(start_index, end_index):
|
|
||||||
if isinstance(elements[i], (Text, NarrativeText, ListItem, Table)):
|
|
||||||
content_parts.append(str(elements[i]).strip())
|
|
||||||
|
|
||||||
return "\n\n".join([part for part in content_parts if part])
|
|
||||||
|
|
||||||
def generate_markdown(self, doc: StructuredDocument) -> str:
|
|
||||||
"""将结构化文档转换为Markdown格式
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc: 结构化文档对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Markdown格式文本
|
|
||||||
"""
|
|
||||||
md_parts = []
|
|
||||||
|
|
||||||
# 添加标题
|
|
||||||
if doc.title:
|
|
||||||
md_parts.append(f"# {doc.title}\n")
|
|
||||||
|
|
||||||
# 添加元数据
|
|
||||||
if doc.is_paper:
|
|
||||||
# 作者信息
|
|
||||||
if 'authors' in doc.metadata and doc.metadata['authors']:
|
|
||||||
authors_str = ", ".join(doc.metadata['authors'])
|
|
||||||
md_parts.append(f"**作者:** {authors_str}\n")
|
|
||||||
|
|
||||||
# 关键词
|
|
||||||
if 'keywords' in doc.metadata and doc.metadata['keywords']:
|
|
||||||
keywords_str = ", ".join(doc.metadata['keywords'])
|
|
||||||
md_parts.append(f"**关键词:** {keywords_str}\n")
|
|
||||||
|
|
||||||
# 摘要
|
|
||||||
if 'abstract' in doc.metadata and doc.metadata['abstract']:
|
|
||||||
md_parts.append(f"## 摘要\n\n{doc.metadata['abstract']}\n")
|
|
||||||
|
|
||||||
# 添加章节内容
|
|
||||||
md_parts.append(self._format_sections_markdown(doc.sections))
|
|
||||||
|
|
||||||
return "\n".join(md_parts)
|
|
||||||
|
|
||||||
def _format_sections_markdown(self, sections: List[DocumentSection], base_level: int = 0) -> str:
|
|
||||||
"""递归格式化章节为Markdown
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sections: 章节列表
|
|
||||||
base_level: 基础层级
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Markdown格式文本
|
|
||||||
"""
|
|
||||||
md_parts = []
|
|
||||||
|
|
||||||
for section in sections:
|
|
||||||
# 计算标题级别 (确保不超过6级)
|
|
||||||
header_level = min(section.level + base_level + 1, 6)
|
|
||||||
|
|
||||||
# 添加标题和内容
|
|
||||||
if section.title:
|
|
||||||
md_parts.append(f"{'#' * header_level} {section.title}\n")
|
|
||||||
|
|
||||||
if section.content:
|
|
||||||
md_parts.append(f"{section.content}\n")
|
|
||||||
|
|
||||||
# 递归处理子章节
|
|
||||||
if section.subsections:
|
|
||||||
md_parts.append(self._format_sections_markdown(
|
|
||||||
section.subsections, base_level
|
|
||||||
))
|
|
||||||
|
|
||||||
return "\n".join(md_parts)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from .txt_doc import TxtFormatter
|
|
||||||
from .markdown_doc import MarkdownFormatter
|
|
||||||
from .html_doc import HtmlFormatter
|
|
||||||
from .word_doc import WordFormatter
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
class HtmlFormatter:
|
|
||||||
"""HTML格式文档生成器 - 保留原始文档结构"""
|
|
||||||
|
|
||||||
def __init__(self, processing_type="文本处理"):
|
|
||||||
self.processing_type = processing_type
|
|
||||||
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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
animation: fadeIn 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document-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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.document-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-type {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-date {
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document-content {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 保留文档结构的样式 */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
margin-top: 1.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: 1.8em; }
|
|
||||||
h2 { font-size: 1.5em; }
|
|
||||||
h3 { font-size: 1.3em; }
|
|
||||||
h4 { font-size: 1.1em; }
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.8em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
border-left: 4px solid var(--primary-light);
|
|
||||||
background: rgba(0,0,0,0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: monospace;
|
|
||||||
background: rgba(0,0,0,0.05);
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: rgba(0,0,0,0.05);
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre code {
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background-color: #0f172a;
|
|
||||||
--text-color: #e2e8f0;
|
|
||||||
--border-color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container, .document-content {
|
|
||||||
background: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
code, pre {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _escape_html(self, text):
|
|
||||||
"""转义HTML特殊字符"""
|
|
||||||
import html
|
|
||||||
return html.escape(text)
|
|
||||||
|
|
||||||
def _markdown_to_html(self, text):
|
|
||||||
"""将Markdown格式转换为HTML格式,保留文档结构"""
|
|
||||||
try:
|
|
||||||
import markdown
|
|
||||||
# 使用Python-Markdown库将markdown转换为HTML,启用更多扩展以支持嵌套列表
|
|
||||||
return markdown.markdown(text, extensions=['tables', 'fenced_code', 'codehilite', 'nl2br', 'sane_lists', 'smarty', 'extra'])
|
|
||||||
except ImportError:
|
|
||||||
# 如果没有markdown库,使用更复杂的替换来处理嵌套列表
|
|
||||||
import re
|
|
||||||
|
|
||||||
# 替换标题
|
|
||||||
text = re.sub(r'^# (.+)$', r'<h1>\1</h1>', text, flags=re.MULTILINE)
|
|
||||||
text = re.sub(r'^## (.+)$', r'<h2>\1</h2>', text, flags=re.MULTILINE)
|
|
||||||
text = re.sub(r'^### (.+)$', r'<h3>\1</h3>', text, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
# 预处理列表 - 在列表项之间添加空行以正确分隔
|
|
||||||
# 处理编号列表
|
|
||||||
text = re.sub(r'(\n\d+\.\s.+)(\n\d+\.\s)', r'\1\n\2', text)
|
|
||||||
# 处理项目符号列表
|
|
||||||
text = re.sub(r'(\n•\s.+)(\n•\s)', r'\1\n\2', text)
|
|
||||||
text = re.sub(r'(\n\*\s.+)(\n\*\s)', r'\1\n\2', text)
|
|
||||||
text = re.sub(r'(\n-\s.+)(\n-\s)', r'\1\n\2', text)
|
|
||||||
|
|
||||||
# 处理嵌套列表 - 确保正确的缩进和结构
|
|
||||||
lines = text.split('\n')
|
|
||||||
in_list = False
|
|
||||||
list_type = None # 'ol' 或 'ul'
|
|
||||||
list_html = []
|
|
||||||
normal_lines = []
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i]
|
|
||||||
|
|
||||||
# 匹配编号列表项
|
|
||||||
numbered_match = re.match(r'^(\d+)\.\s+(.+)$', line)
|
|
||||||
# 匹配项目符号列表项
|
|
||||||
bullet_match = re.match(r'^[•\*-]\s+(.+)$', line)
|
|
||||||
|
|
||||||
if numbered_match:
|
|
||||||
if not in_list or list_type != 'ol':
|
|
||||||
# 开始新的编号列表
|
|
||||||
if in_list:
|
|
||||||
# 关闭前一个列表
|
|
||||||
list_html.append(f'</{list_type}>')
|
|
||||||
list_html.append('<ol>')
|
|
||||||
in_list = True
|
|
||||||
list_type = 'ol'
|
|
||||||
|
|
||||||
num, content = numbered_match.groups()
|
|
||||||
list_html.append(f'<li>{content}</li>')
|
|
||||||
|
|
||||||
elif bullet_match:
|
|
||||||
if not in_list or list_type != 'ul':
|
|
||||||
# 开始新的项目符号列表
|
|
||||||
if in_list:
|
|
||||||
# 关闭前一个列表
|
|
||||||
list_html.append(f'</{list_type}>')
|
|
||||||
list_html.append('<ul>')
|
|
||||||
in_list = True
|
|
||||||
list_type = 'ul'
|
|
||||||
|
|
||||||
content = bullet_match.group(1)
|
|
||||||
list_html.append(f'<li>{content}</li>')
|
|
||||||
|
|
||||||
else:
|
|
||||||
if in_list:
|
|
||||||
# 结束当前列表
|
|
||||||
list_html.append(f'</{list_type}>')
|
|
||||||
in_list = False
|
|
||||||
# 将完成的列表添加到正常行中
|
|
||||||
normal_lines.append(''.join(list_html))
|
|
||||||
list_html = []
|
|
||||||
|
|
||||||
normal_lines.append(line)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# 如果最后还在列表中,确保关闭列表
|
|
||||||
if in_list:
|
|
||||||
list_html.append(f'</{list_type}>')
|
|
||||||
normal_lines.append(''.join(list_html))
|
|
||||||
|
|
||||||
# 重建文本
|
|
||||||
text = '\n'.join(normal_lines)
|
|
||||||
|
|
||||||
# 替换段落,但避免处理已经是HTML标签的部分
|
|
||||||
paragraphs = text.split('\n\n')
|
|
||||||
for i, p in enumerate(paragraphs):
|
|
||||||
# 如果不是以HTML标签开始且不为空
|
|
||||||
if not (p.strip().startswith('<') and p.strip().endswith('>')) and p.strip() != '':
|
|
||||||
paragraphs[i] = f'<p>{p}</p>'
|
|
||||||
|
|
||||||
return '\n'.join(paragraphs)
|
|
||||||
|
|
||||||
def create_document(self, content: str) -> str:
|
|
||||||
"""生成完整的HTML文档,保留原始文档结构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: 处理后的文档内容
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 完整的HTML文档字符串
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 将markdown内容转换为HTML
|
|
||||||
html_content = self._markdown_to_html(content)
|
|
||||||
|
|
||||||
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="document-title">文档处理结果</h1>
|
|
||||||
|
|
||||||
<div class="document-header">
|
|
||||||
<div class="processing-type">处理方式: {self._escape_html(self.processing_type)}</div>
|
|
||||||
<div class="processing-date">处理时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="document-content">
|
|
||||||
{html_content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
class MarkdownFormatter:
|
|
||||||
"""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, content: str, processing_type: str = "文本处理") -> str:
|
|
||||||
"""
|
|
||||||
创建完整的Markdown文档,保留原始文档结构
|
|
||||||
Args:
|
|
||||||
content: 处理后的文档内容
|
|
||||||
processing_type: 处理类型(润色、翻译等)
|
|
||||||
Returns:
|
|
||||||
str: 生成的Markdown文本
|
|
||||||
"""
|
|
||||||
self.content = []
|
|
||||||
|
|
||||||
# 添加标题和说明
|
|
||||||
self.content.append(f"# 文档处理结果\n")
|
|
||||||
self.content.append(f"## 处理方式: {processing_type}\n")
|
|
||||||
|
|
||||||
# 添加处理时间
|
|
||||||
from datetime import datetime
|
|
||||||
self.content.append(f"*处理时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
|
||||||
|
|
||||||
# 添加分隔线
|
|
||||||
self.content.append("---\n")
|
|
||||||
|
|
||||||
# 添加原始内容,保留结构
|
|
||||||
self.content.append(content)
|
|
||||||
|
|
||||||
# 添加结尾分隔线
|
|
||||||
self.content.append("\n---\n")
|
|
||||||
|
|
||||||
return "\n".join(self.content)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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:
|
|
||||||
"""文本格式化器 - 保留原始文档结构"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.content = []
|
|
||||||
self._setup_document()
|
|
||||||
|
|
||||||
def _setup_document(self):
|
|
||||||
"""初始化文档标题"""
|
|
||||||
self.content.append("=" * 50)
|
|
||||||
self.content.append("处理后文档".center(48))
|
|
||||||
self.content.append("=" * 50)
|
|
||||||
|
|
||||||
def _format_header(self):
|
|
||||||
"""创建文档头部信息"""
|
|
||||||
from datetime import datetime
|
|
||||||
date_str = datetime.now().strftime('%Y年%m月%d日')
|
|
||||||
return [
|
|
||||||
date_str.center(48),
|
|
||||||
"\n" # 添加空行
|
|
||||||
]
|
|
||||||
|
|
||||||
def create_document(self, content):
|
|
||||||
"""生成保留原始结构的文档"""
|
|
||||||
# 添加头部信息
|
|
||||||
self.content.extend(self._format_header())
|
|
||||||
|
|
||||||
# 处理内容,保留原始结构
|
|
||||||
processed_content = convert_markdown_to_txt(content)
|
|
||||||
|
|
||||||
# 添加处理后的内容
|
|
||||||
self.content.append(processed_content)
|
|
||||||
|
|
||||||
# 合并所有内容
|
|
||||||
return "\n".join(self.content)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
from docx2pdf import convert
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
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
|
|
||||||
if not os.system('which libreoffice') == 0:
|
|
||||||
raise RuntimeError("请先安装LibreOffice: sudo apt-get install libreoffice")
|
|
||||||
|
|
||||||
# 使用libreoffice进行转换
|
|
||||||
os.system(f'libreoffice --headless --convert-to pdf "{word_path}" --outdir "{pdf_path.parent}"')
|
|
||||||
|
|
||||||
# 如果输出路径与默认生成的不同,则重命名
|
|
||||||
default_pdf = word_path.with_suffix('.pdf')
|
|
||||||
if default_pdf != pdf_path:
|
|
||||||
os.rename(default_pdf, pdf_path)
|
|
||||||
else:
|
|
||||||
# Windows和MacOS使用docx2pdf
|
|
||||||
convert(word_path, pdf_path)
|
|
||||||
|
|
||||||
return str(pdf_path)
|
|
||||||
|
|
||||||
except Exception as 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)}")
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
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格式化器 - 保留原始文档结构"""
|
|
||||||
|
|
||||||
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("文档处理结果")
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 创建标题样式
|
|
||||||
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
|
|
||||||
|
|
||||||
# 创建标题1样式
|
|
||||||
h1_style = self.doc.styles.add_style('Heading1_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h1_style.font.name = '黑体'
|
|
||||||
h1_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
|
||||||
h1_style.font.size = Pt(18)
|
|
||||||
h1_style.font.bold = True
|
|
||||||
h1_style.paragraph_format.space_before = Pt(12)
|
|
||||||
h1_style.paragraph_format.space_after = Pt(6)
|
|
||||||
|
|
||||||
# 创建标题2样式
|
|
||||||
h2_style = self.doc.styles.add_style('Heading2_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h2_style.font.name = '黑体'
|
|
||||||
h2_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
|
||||||
h2_style.font.size = Pt(16)
|
|
||||||
h2_style.font.bold = True
|
|
||||||
h2_style.paragraph_format.space_before = Pt(10)
|
|
||||||
h2_style.paragraph_format.space_after = Pt(6)
|
|
||||||
|
|
||||||
# 创建标题3样式
|
|
||||||
h3_style = self.doc.styles.add_style('Heading3_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h3_style.font.name = '黑体'
|
|
||||||
h3_style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
|
||||||
h3_style.font.size = Pt(14)
|
|
||||||
h3_style.font.bold = True
|
|
||||||
h3_style.paragraph_format.space_before = Pt(8)
|
|
||||||
h3_style.paragraph_format.space_after = Pt(4)
|
|
||||||
|
|
||||||
# 创建代码块样式
|
|
||||||
code_style = self.doc.styles.add_style('Code_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
code_style.font.name = 'Courier New'
|
|
||||||
code_style.font.size = Pt(11)
|
|
||||||
code_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
|
||||||
code_style.paragraph_format.space_before = Pt(6)
|
|
||||||
code_style.paragraph_format.space_after = Pt(6)
|
|
||||||
code_style.paragraph_format.left_indent = Pt(36)
|
|
||||||
code_style.paragraph_format.right_indent = Pt(36)
|
|
||||||
|
|
||||||
# 创建列表样式
|
|
||||||
list_style = self.doc.styles.add_style('List_Custom', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
list_style.font.name = '仿宋'
|
|
||||||
list_style._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
|
||||||
list_style.font.size = Pt(12)
|
|
||||||
list_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
list_style.paragraph_format.left_indent = Pt(21)
|
|
||||||
list_style.paragraph_format.first_line_indent = Pt(-21)
|
|
||||||
|
|
||||||
def create_document(self, content: str, processing_type: str = "文本处理"):
|
|
||||||
"""创建文档,保留原始结构"""
|
|
||||||
# 添加标题
|
|
||||||
title_para = self.doc.add_paragraph(style='Title_Custom')
|
|
||||||
title_run = title_para.add_run('文档处理结果')
|
|
||||||
|
|
||||||
# 添加处理类型
|
|
||||||
processing_para = self.doc.add_paragraph()
|
|
||||||
processing_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
||||||
processing_run = processing_para.add_run(f"处理方式: {processing_type}")
|
|
||||||
processing_run.font.name = '仿宋'
|
|
||||||
processing_run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
|
||||||
processing_run.font.size = Pt(14)
|
|
||||||
|
|
||||||
# 添加日期
|
|
||||||
date_para = self.doc.add_paragraph()
|
|
||||||
date_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
||||||
date_run = date_para.add_run(f"处理时间: {datetime.now().strftime('%Y年%m月%d日')}")
|
|
||||||
date_run.font.name = '仿宋'
|
|
||||||
date_run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋')
|
|
||||||
date_run.font.size = Pt(14)
|
|
||||||
|
|
||||||
self.doc.add_paragraph() # 添加空行
|
|
||||||
|
|
||||||
# 预处理内容,将Markdown格式转换为适合Word的格式
|
|
||||||
processed_content = convert_markdown_to_word(content)
|
|
||||||
|
|
||||||
# 按行处理文本,保留结构
|
|
||||||
lines = processed_content.split('\n')
|
|
||||||
in_code_block = False
|
|
||||||
current_paragraph = None
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
# 检查是否为标题
|
|
||||||
header_match = re.match(r'^(#{1,6})\s+(.+)$', line)
|
|
||||||
|
|
||||||
if header_match:
|
|
||||||
# 根据#的数量确定标题级别
|
|
||||||
level = len(header_match.group(1))
|
|
||||||
title_text = header_match.group(2)
|
|
||||||
|
|
||||||
if level == 1:
|
|
||||||
style = 'Heading1_Custom'
|
|
||||||
elif level == 2:
|
|
||||||
style = 'Heading2_Custom'
|
|
||||||
else:
|
|
||||||
style = 'Heading3_Custom'
|
|
||||||
|
|
||||||
self.doc.add_paragraph(title_text, style=style)
|
|
||||||
current_paragraph = None
|
|
||||||
|
|
||||||
# 检查代码块标记
|
|
||||||
elif '[代码块]' in line:
|
|
||||||
in_code_block = True
|
|
||||||
current_paragraph = self.doc.add_paragraph(style='Code_Custom')
|
|
||||||
code_line = line.replace('[代码块]', '').strip()
|
|
||||||
if code_line:
|
|
||||||
current_paragraph.add_run(code_line)
|
|
||||||
|
|
||||||
elif '[/代码块]' in line:
|
|
||||||
in_code_block = False
|
|
||||||
code_line = line.replace('[/代码块]', '').strip()
|
|
||||||
if code_line and current_paragraph:
|
|
||||||
current_paragraph.add_run(code_line)
|
|
||||||
current_paragraph = None
|
|
||||||
|
|
||||||
# 检查列表项
|
|
||||||
elif line.strip().startswith('•'):
|
|
||||||
p = self.doc.add_paragraph(style='List_Custom')
|
|
||||||
p.add_run(line.strip())
|
|
||||||
current_paragraph = None
|
|
||||||
|
|
||||||
# 处理普通文本行
|
|
||||||
elif line.strip():
|
|
||||||
if in_code_block:
|
|
||||||
if current_paragraph:
|
|
||||||
current_paragraph.add_run('\n' + line)
|
|
||||||
else:
|
|
||||||
current_paragraph = self.doc.add_paragraph(line, style='Code_Custom')
|
|
||||||
else:
|
|
||||||
if current_paragraph is None or not current_paragraph.text:
|
|
||||||
current_paragraph = self.doc.add_paragraph(line, style='Normal_Custom')
|
|
||||||
else:
|
|
||||||
current_paragraph.add_run('\n' + line)
|
|
||||||
|
|
||||||
# 处理空行,创建新段落
|
|
||||||
elif not in_code_block:
|
|
||||||
current_paragraph = None
|
|
||||||
|
|
||||||
return self.doc
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user