diff --git a/.cursor/rules/weclone-rules.mdc b/.cursor/rules/weclone-rules.mdc index df98ef9..26b1a8b 100644 --- a/.cursor/rules/weclone-rules.mdc +++ b/.cursor/rules/weclone-rules.mdc @@ -15,9 +15,3 @@ alwaysApply: true - Unless I ask you to, code comments don't need to be excessive. - Prefer using the encapsulated logger `from weclone.utils.log import logger` for printing. - When retrieving values from a parameter dictionary read from a configuration file, the `get` method should be preferred whenever possible. - - - - - - diff --git a/.gitignore b/.gitignore index 4ffb55e..ba80f52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,167 +1,169 @@ -wandb/ -weclone_archive-my/ -**/pycache/ -events.out.tfevents.* -归档/ -*.pt -*.npz -*nohup.out -*log.txt -*cookie.bin -*.gradio/ - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - - -*.zip -LLaMA-Factory -chatglm3-6b -cache -archive -model_output* -data/test -.vscode -*-my*.* -*.csv -*test.* -*users.json -Spark-TTS-0.5B/ -uv.lock -output* -*.out - -Qwen*/ -settings.jsonc -settings.json -dataset/blocked_words.json -dataset/wechat/* -dataset/media/* -models/* \ No newline at end of file +wandb/ +weclone_archive-my/ +**/pycache/ +events.out.tfevents.* +归档/ +*.pt +*.npz +*nohup.out +*log.txt +*cookie.bin +*.gradio/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + + +*.zip +LLaMA-Factory +chatglm3-6b +cache +archive +model_output* +data/test +.vscode +*-my*.* +*.csv +*test.* +*users.json +Spark-TTS-0.5B/ +uv.lock +output* +*.out + +Qwen*/ +settings.jsonc +settings.json +dataset/blocked_words.json +dataset/wechat/* +dataset/media/* +models/* +.secrets* +.env* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ecca81f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +# .pre-commit-config.yaml +default_install_hook_types: [pre-commit, prepare-commit-msg] +ci: + autofix_commit_msg: ":balloon: auto fixes by pre-commit hooks" + autofix_prs: true + autoupdate_branch: master + autoupdate_schedule: monthly + autoupdate_commit_msg: ":balloon: pre-commit autoupdate hooks" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-ast # Python 语法检查 + - id: check-added-large-files # 防止大文件 + args: ["--maxkb=25000"] + - id: check-merge-conflict # 检查合并冲突 + - id: check-yaml # YAML 语法检查 + - id: check-toml # TOML 语法检查 + - id: debug-statements # 防止调试语句 + - id: end-of-file-fixer # 文件结尾修复 + # - id: trailing-whitespace # 移除行尾空白 + # args: [--markdown-linebreak-ext=md] + - id: no-commit-to-branch # 保护主分支 + args: ["--branch", "main", "--branch", "master"] + - id: mixed-line-ending # 检查混合行结束符 + args: ["--fix=lf"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.13 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + args: ["--profile", "black", "--line-length", "120"] + + - repo: https://github.com/PyCQA/bandit + rev: 1.8.3 + hooks: + - id: bandit + name: Python 安全检查 + args: ["-c", "pyproject.toml"] + additional_dependencies: ["bandit[toml]"] diff --git a/README.md b/README.md index 0c53917..7217a6b 100644 --- a/README.md +++ b/README.md @@ -1,285 +1,293 @@ -![download](https://github.com/user-attachments/assets/5842e84e-004f-4afd-9373-af64e9575b78) -

🚀 One-stop solution for creating your digital avatar from chat history 💡

-

🚀从聊天记录创造数字分身的一站式解决方案💡

- - -
- -[![GitHub stars](https://img.shields.io/github/stars/xming521/WeClone?style=for-the-badge&logo=github&label=Stars&logoColor=white&color=ffda65)](https://github.com/xming521/WeClone/stargazers) -[![GitHub release](https://img.shields.io/github/v/release/xming521/WeClone?style=for-the-badge&logo=github&label=Release&logoColor=white&color=06d094)](https://github.com/xming521/WeClone/releases) - - WeClone① - -[![Twitter](https://img.shields.io/badge/Twitter-@weclone567-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/weclone567) -[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/+JEdak4m0XEQ3NGNl) - -Featured|HelloGitHub -xming521%2FWeClone | Trendshift -Ask DeepWiki -
- -

- 项目主页 | - 项目文档 | - Windows部署指南 | - Linux部署指南【保姆级】 -

- -> [!IMPORTANT] -> ### WhatsApp and Telegram chat logs integration for digital avatar creation is coming ! - -## ✨核心功能 -- 💫 涵盖打造数字分身的全链路方案,包括聊天数据导出、预处理、模型训练、部署 -- 💬 使用微信聊天记录微调LLM,让大模型有"那味儿" -- 🔗 绑定到微信、QQ、Telegram、企微、飞书机器人,实现自己的数字分身 -- 🛡️ 隐私信息过滤,本地化微调部署,数据安全可控 - -## 📋特性与说明 - -> [!IMPORTANT] -> ### WeClone现在支持图片模态数据微调了!并且包含了更全的上下文,记得拉取最新代码并更新依赖。 - -> [!IMPORTANT] -> - WeClone仍在快速迭代期,当前效果不代表最终效果。 -> - 微调LLM效果很大程度取决于模型大小、聊天数据的数量和质量,理论上模型越大,数据越多,效果越好。 -> - Windows环境未进行严格测试,可以使用WSL作为运行环境。详细教程可点击[Windows部署指南](https://blog.051088.xyz/2025/05/14/WeClone-%E7%94%A8%E5%BE%AE%E4%BF%A1%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E6%89%93%E9%80%A0%E8%87%AA%E5%B7%B1%E7%9A%84AI%E6%95%B0%E5%AD%97%E5%88%86%E8%BA%AB/)查看。 - -### 硬件要求 - -项目默认使用Qwen2.5-7B-Instruct模型,LoRA方法对sft阶段微调,大约需要16GB显存。也可以使用[LLaMA Factory](https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md#%E6%A8%A1%E5%9E%8B)支持的其他模型和方法。 - -需要显存的估算值: -| 方法 | 精度 | 7B | 14B | 30B | 70B | `x`B | -| ------------------------------- | ---- | ----- | ----- | ----- | ------ | ------- | -| Full (`bf16` or `fp16`) | 32 | 120GB | 240GB | 600GB | 1200GB | `18x`GB | -| Full (`pure_bf16`) | 16 | 60GB | 120GB | 300GB | 600GB | `8x`GB | -| Freeze/LoRA/GaLore/APOLLO/BAdam | 16 | 16GB | 32GB | 64GB | 160GB | `2x`GB | -| QLoRA | 8 | 10GB | 20GB | 40GB | 80GB | `x`GB | -| QLoRA | 4 | 6GB | 12GB | 24GB | 48GB | `x/2`GB | -| QLoRA | 2 | 4GB | 8GB | 16GB | 24GB | `x/4`GB | - - -## 环境搭建 -1.cuda安装(已安装可跳过,**要求版本12.4及以上**):[LLaMA Factory](https://llamafactory.readthedocs.io/zh-cn/latest/getting_started/installation.html#cuda) - -2.建议使用 [uv](https://docs.astral.sh/uv/)安装依赖,这是一个非常快速的 Python 环境管理器。安装uv后,您可以使用以下命令创建一个新的Python环境并安装依赖项,注意这不包含音频克隆功能的依赖: -```bash -git clone https://github.com/xming521/WeClone.git -cd WeClone -uv venv .venv --python=3.10 -source .venv/bin/activate # windows下执行 .venv\Scripts\activate -uv pip install --group main -e . -``` -> [!TIP] -> 如果要使用最新的模型进行微调,需要手动安装最新版LLaMA Factory:`uv pip install --upgrade git+https://github.com/hiyouga/LLaMA-Factory.git`,同时其他依赖版本也可能需要修改,例如vllm pytorch transforms - -3.将配置文件模板复制一份并重命名为`settings.jsonc`,后续配置修改在此文件进行: -```bash -cp settings.template.jsonc settings.jsonc -``` -- 微调**多模态模型**时,请使用[examples/mllm.template.jsonc](https://github.com/xming521/WeClone/blob/master/examples/mllm.template.jsonc)作为配置文件。 - -> [!NOTE] -> 训练以及推理相关配置统一在文件`settings.jsonc` - -4.使用以下命令测试CUDA环境是否正确配置并可被PyTorch识别,Mac不需要: -```bash -python -c "import torch; print('CUDA是否可用:', torch.cuda.is_available());" -``` - -5.(可选)安装FlashAttention,加速训练和推理:`uv pip install flash-attn --no-build-isolation` - -## 模型下载 -```bash -git lfs install -git clone https://www.modelscope.cn/Qwen/Qwen2.5-7B-Instruct.git -``` -下载有问题使用其他方式下载:[模型的下载](https://www.modelscope.cn/docs/models/download) - - -## 数据准备 - -请使用[PyWxDump](https://github.com/xaoyaoo/PyWxDump)提取微信聊天记录(不支持4.0版本微信)。可以先将手机的聊天记录迁移(备份)到电脑,数据量更多一些。下载软件并解密数据库后,点击聊天备份,导出类型为CSV,可以导出多个联系人(不建议使用群聊记录),然后将导出的位于`wxdump_tmp/export` 的 `csv` 文件夹放在`./dataset`目录即可,也就是不同人聊天记录的文件夹一起放在 `./dataset/csv`。 - -### 图片数据准备 -在能进入微信个人文件夹的环境执行,如果没有环境创建环境并安装基础依赖即可(`uv pip install -e .`),然后执行以下命令,将微信图片数据保存到`./dataset/wechat/dat`目录下。 -```bash -python weclone/data/chat_parsers/wechat_parser.py --wechat-data-dir "微信个人文件夹路径 例如 C:\Users\user\Documents\WeChat Files\wxid_d68wiru2zseo22" -``` -之后使用[微信图片解密工具](https://github.com/Evil0ctal/WeChat-image-decryption)解密图片数据,解密后的图片数据保存到`dataset/media/images`目录下。 - -## 数据预处理 - -- 项目默认去除了数据中的手机号、身份证号、邮箱、网址。还在`settings.jsonc`中提供了一个禁用词词库`blocked_words`,可以自行添加需要过滤的词句(会默认去掉包括禁用词的整句)。 -> [!IMPORTANT] -> 🚨 请一定注意保护个人隐私,不要泄露个人信息! - - -- 执行以下命令对数据进行处理,可以根据自己的聊天风格修改settings.jsonc的`make_dataset_args`。 -```bash -weclone-cli make-dataset -``` -- 目前仅支持时间窗口策略,根据`single_combine_time_window`将单人连续消息通过逗号连接合并为一句,根据`qa_match_time_window`匹配问答对。 -- 多模态数据可以通过`image_max_pixels`和`max_image_num`参数控制图片数量和大小,减少显存占用。 -- 可以启用`clean_dataset`中的`enable_clean`选项,对数据进行清洗,以达到更好效果(多模态数据暂不支持)。* 当前系统支持使用 `llm judge` 对聊天记录进行打分,提供 **vllm 离线推理** 和 **API 在线推理** 两种方式。可通过将 `settings.jsonc` 文件中的 `"online_llm_clear": false` 修改为 `true` 来启用 API 在线推理模式,并配置相应的 `base_url`、`llm_api_key`、`model_name` 等参数。所有兼容 OpenAI 接口的模型均可接入。 -- 在获得 `llm 打分分数分布情况` 后,可通过设置 `accept_score` 参数筛选可接受的分数区间,同时可适当降低 `train_sft_args` 中的 `lora_dropout` 参数,以提升模型的拟合效果。 - -## 配置参数并微调模型 - -- (可选)修改 `settings.jsonc` 的 `model_name_or_path` 和 `template` 选择本地下载好的其他模型。 -- 修改`per_device_train_batch_size`以及`gradient_accumulation_steps`来调整显存占用。 -- 可以根据自己数据集的数量和质量修改`train_sft_args`的`num_train_epochs`、`lora_rank`、`lora_dropout`等参数。 - -### 单卡训练 -```bash -weclone-cli train-sft -``` -多卡环境单卡训练,需要先执行 `export CUDA_VISIBLE_DEVICES=0` - -### 多卡训练 -取消`settings.jsonc`中`deepspeed`行代码注释,使用以下命令多卡训练: -```bash -uv pip install deepspeed -deepspeed --num_gpus=使用显卡数量 weclone/train/train_sft.py -``` - -### 使用浏览器demo简单推理 -可以在这一步测试出合适的temperature、top_p值,修改settings.jsonc的`infer_args`后,供后续推理时使用。 -```bash -weclone-cli webchat-demo -``` - -### 使用接口进行推理 - -```bash -weclone-cli server -``` - -### 使用常见聊天问题测试 -不包含询问个人信息的问题,仅有日常聊天。测试结果在test_result-my.txt。 -```bash -weclone-cli server -weclone-cli test-model -``` - -## 🖼️ 微调效果 -使用Qwen2.5-14B-Instruct模型,大概3万条处理后的有效数据,loss降到了3.5左右的效果。 -
-截图 -
- alt text - alt text - alt text - alt text -
-
- - -## 🤖 部署到聊天机器人 - -### AstrBot - -[AstrBot](https://github.com/AstrBotDevs/AstrBot) 是易上手的多平台 LLM 聊天机器人及开发框架 ✨ 平台支持 QQ、QQ频道、Telegram、微信、企微、飞书。 - -使用步骤: -1. 部署 AstrBot -2. 在 AstrBot 中部署消息平台 -3. 执行 `weclone-cli server` 启动api服务 -4. 在 AstrBot 中新增服务提供商,类型选择OpenAI,API Base URL 根据AstrBot部署方式填写(例如docker部署可能为http://172.17.0.1:8005/v1) ,模型填写gpt-3.5-turbo,API Key随意填写一个 -5. 微调后不支持工具调用,请先关掉默认的工具,消息平台发送指令: `/tool off all`,否则会没有微调后的效果。 -6. 根据微调时使用的default_system,在 AstrBot 中设置系统提示词。 -![5](https://github.com/user-attachments/assets/19de7072-076a-4cdf-8ae6-46b9b89f536a) -> [!IMPORTANT] -> 检查api_service的日志,尽量保证大模型服务请求的参数和微调时一致,tool插件能力都关掉。 -7. 调整采样参数,例如temperature、top_p、top_k等 -[配置自定义的模型参数](https://astrbot.app/config/model-config.html#%E9%85%8D%E7%BD%AE%E8%87%AA%E5%AE%9A%E4%B9%89%E7%9A%84%E6%A8%A1%E5%9E%8B%E5%8F%82%E6%95%B0) - -### LangBot - -[LangBot](https://github.com/RockChinQ/LangBot) 是一个开源的接入全球多种即时通信平台的 LLM 机器人平台,适合各种场景使用。 - -1. [部署 LangBot](https://github.com/RockChinQ/LangBot#-%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8) -2. 在 LangBot 中添加一个机器人 -4. 在模型页添加新模型,名称`gpt-3.5-turbo`,供应商选择 OpenAI,填写 请求 URL 为 WeClone 的地址,详细连接方式可以参考[文档](https://docs.langbot.app/zh/workshop/network-details.html),API Key 任意填写。 - -image - -6. 在流水线配置中选择刚才添加的模型,或修改提示词配置 - -image - -## 📌 路线图 -- [ ] 更丰富的上下文:包括上下文对话、聊天对象信息、时间等 + 思考 -- [ ] Memory 支持 -- [ ] 支持多模态 -- [ ] 数据增强 -- [ ] 支持GUI - -## 问题解决 -- 微调问题:[LLaMA-Factory| FAQs | 常见问题](https://github.com/hiyouga/LLaMA-Factory/issues/4614) 或者更方便的 [![更方便的Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hiyouga/LLaMA-Factory) - -## ❤️ 贡献代码 - -欢迎任何 Issues/Pull Requests! - -你可以通过查看Issues或帮助审核 PR(拉取请求)来贡献。对于新功能的添加,请先通过 Issue 讨论。 -运行`uv pip install --group dev -e .`安装开发依赖。 -项目使用`pytest`测试(测试脚本待完善),`pyright`检查类型,`ruff`检查代码格式。 - - -## ⚠️ 免责声明 -> [!CAUTION] -> 请勿用于非法用途,否则后果自负。 -
-1. 使用目的 - -* 本项目仅供学习交流使用,**请勿用于非法用途**,**请勿用于非法用途**,**请勿用于非法用途**,否则后果自负。 -* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。 - -2. 使用期限 - -* 您应该在下载保存使用本项目的24小时内,删除本项目的源代码和程序;超出此期限的任何使用行为,一概与本项目及其开发者无关。 - -3. 操作规范 - -* 本项目仅允许在授权情况下使用数据训练,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。 -* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任。 - -4. 免责声明接受 - -* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它; - -5. 禁止用于非法测试或渗透 - -* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关。 -* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担。 - -6. 免责声明修改 - -* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。 - -7. 其他 - -* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任。 - -* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。 - -
-请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。 - -
-
-
- -## ⭐ Star History -> [!TIP] -> 如果本项目对您有帮助,或者您关注本项目的未来发展,请给项目 Star,谢谢 - -
- -[![Star History Chart](https://api.star-history.com/svg?repos=xming521/WeClone&type=Date)](https://www.star-history.com/#xming521/WeClone&Date) - -
- - -
克隆我们,保留灵魂的芬芳
+![download](https://github.com/user-attachments/assets/5842e84e-004f-4afd-9373-af64e9575b78) +

🚀 One-stop solution for creating your digital avatar from chat history 💡

+

🚀从聊天记录创造数字分身的一站式解决方案💡

+ + +
+ +[![GitHub stars](https://img.shields.io/github/stars/xming521/WeClone?style=for-the-badge&logo=github&label=Stars&logoColor=white&color=ffda65)](https://github.com/xming521/WeClone/stargazers) +[![GitHub release](https://img.shields.io/github/v/release/xming521/WeClone?style=for-the-badge&logo=github&label=Release&logoColor=white&color=06d094)](https://github.com/xming521/WeClone/releases) + + WeClone① + +[![Twitter](https://img.shields.io/badge/Twitter-@weclone567-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/weclone567) +[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/+JEdak4m0XEQ3NGNl) + +Featured|HelloGitHub +xming521%2FWeClone | Trendshift +Ask DeepWiki +
+ +

+ 项目主页 | + 项目文档 | + Windows部署指南 | + Linux部署指南【保姆级】 +

+ +> [!IMPORTANT] +> ### WhatsApp and Telegram chat logs integration for digital avatar creation is coming ! + +## ✨核心功能 +- 💫 涵盖打造数字分身的全链路方案,包括聊天数据导出、预处理、模型训练、部署 +- 💬 使用微信聊天记录微调LLM,让大模型有"那味儿" +- 🔗 绑定到微信、QQ、Telegram、企微、飞书机器人,实现自己的数字分身 +- 🛡️ 隐私信息过滤,本地化微调部署,数据安全可控 + +## 📋特性与说明 + +> [!IMPORTANT] +> ### WeClone现在支持图片模态数据微调了!并且包含了更全的上下文,记得拉取最新代码并更新依赖。 + +> [!IMPORTANT] +> - WeClone仍在快速迭代期,当前效果不代表最终效果。 +> - 微调LLM效果很大程度取决于模型大小、聊天数据的数量和质量,理论上模型越大,数据越多,效果越好。 +> - Windows环境未进行严格测试,可以使用WSL作为运行环境。详细教程可点击[Windows部署指南](https://blog.051088.xyz/2025/05/14/WeClone-%E7%94%A8%E5%BE%AE%E4%BF%A1%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E6%89%93%E9%80%A0%E8%87%AA%E5%B7%B1%E7%9A%84AI%E6%95%B0%E5%AD%97%E5%88%86%E8%BA%AB/)查看。 + +### 更新日志 +[25/06/05]支持图片模态数据微调 + +### 硬件要求 + +项目默认使用Qwen2.5-7B-Instruct模型,LoRA方法对sft阶段微调,大约需要16GB显存。也可以使用[LLaMA Factory](https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md#%E6%A8%A1%E5%9E%8B)支持的其他模型和方法。 + +需要显存的估算值: +| 方法 | 精度 | 7B | 14B | 30B | 70B | `x`B | +| ------------------------------- | ---- | ----- | ----- | ----- | ------ | ------- | +| Full (`bf16` or `fp16`) | 32 | 120GB | 240GB | 600GB | 1200GB | `18x`GB | +| Full (`pure_bf16`) | 16 | 60GB | 120GB | 300GB | 600GB | `8x`GB | +| Freeze/LoRA/GaLore/APOLLO/BAdam | 16 | 16GB | 32GB | 64GB | 160GB | `2x`GB | +| QLoRA | 8 | 10GB | 20GB | 40GB | 80GB | `x`GB | +| QLoRA | 4 | 6GB | 12GB | 24GB | 48GB | `x/2`GB | +| QLoRA | 2 | 4GB | 8GB | 16GB | 24GB | `x/4`GB | + + +## 环境搭建 +1.cuda安装(已安装可跳过,**要求版本12.4及以上**):[LLaMA Factory](https://llamafactory.readthedocs.io/zh-cn/latest/getting_started/installation.html#cuda) + +2.建议使用 [uv](https://docs.astral.sh/uv/)安装依赖,这是一个非常快速的 Python 环境管理器。安装uv后,您可以使用以下命令创建一个新的Python环境并安装依赖项,注意这不包含音频克隆功能的依赖: +```bash +git clone https://github.com/xming521/WeClone.git +cd WeClone +uv venv .venv --python=3.10 +source .venv/bin/activate # windows下执行 .venv\Scripts\activate +uv pip install --group main -e . +``` +> [!TIP] +> 如果要使用最新的模型进行微调,需要手动安装最新版LLaMA Factory:`uv pip install --upgrade git+https://github.com/hiyouga/LLaMA-Factory.git`,同时其他依赖版本也可能需要修改,例如vllm pytorch transforms + +3.将配置文件模板复制一份并重命名为`settings.jsonc`,后续配置修改在此文件进行: +```bash +cp settings.template.jsonc settings.jsonc +``` +- 微调**多模态模型**时,请使用[examples/mllm.template.jsonc](https://github.com/xming521/WeClone/blob/master/examples/mllm.template.jsonc)作为配置文件。 + +> [!NOTE] +> 训练以及推理相关配置统一在文件`settings.jsonc` + +4.使用以下命令测试CUDA环境是否正确配置并可被PyTorch识别,Mac不需要: +```bash +python -c "import torch; print('CUDA是否可用:', torch.cuda.is_available());" +``` + +5.(可选)安装FlashAttention,加速训练和推理:`uv pip install flash-attn --no-build-isolation` + +## 模型下载 +```bash +git lfs install +git clone https://www.modelscope.cn/Qwen/Qwen2.5-7B-Instruct.git +``` +下载有问题使用其他方式下载:[模型的下载](https://www.modelscope.cn/docs/models/download) + + +## 数据准备 + +请使用[PyWxDump](https://github.com/xaoyaoo/PyWxDump)提取微信聊天记录(不支持4.0版本微信)。可以先将手机的聊天记录迁移(备份)到电脑,数据量更多一些。下载软件并解密数据库后,点击聊天备份,导出类型为CSV,可以导出多个联系人(不建议使用群聊记录),然后将导出的位于`wxdump_tmp/export` 的 `csv` 文件夹放在`./dataset`目录即可,也就是不同人聊天记录的文件夹一起放在 `./dataset/csv`。 + +### 图片数据准备 +在能进入微信个人文件夹的环境执行,如果没有环境创建环境并安装基础依赖即可(`uv pip install -e .`),然后执行以下命令,将微信图片数据保存到`./dataset/wechat/dat`目录下。 +```bash +python weclone/data/chat_parsers/wechat_parser.py --wechat-data-dir "微信个人文件夹路径 例如 C:\Users\user\Documents\WeChat Files\wxid_d68wiru2zseo22" +``` +之后使用[微信图片解密工具](https://github.com/Evil0ctal/WeChat-image-decryption)解密图片数据,解密后的图片数据保存到`dataset/media/images`目录下。 + +## 数据预处理 + +- 项目默认去除了数据中的手机号、身份证号、邮箱、网址。还在`settings.jsonc`中提供了一个禁用词词库`blocked_words`,可以自行添加需要过滤的词句(会默认去掉包括禁用词的整句)。 +> [!IMPORTANT] +> 🚨 请一定注意保护个人隐私,不要泄露个人信息! + + +- 执行以下命令对数据进行处理,可以根据自己的聊天风格修改settings.jsonc的`make_dataset_args`。 +```bash +weclone-cli make-dataset +``` +- 目前仅支持时间窗口策略,根据`single_combine_time_window`将单人连续消息通过逗号连接合并为一句,根据`qa_match_time_window`匹配问答对。 +- 多模态数据可以通过`image_max_pixels`和`max_image_num`参数控制图片数量和大小,减少显存占用。 +- 可以启用`clean_dataset`中的`enable_clean`选项,对数据进行清洗,以达到更好效果(多模态数据暂不支持)。* 当前系统支持使用 `llm judge` 对聊天记录进行打分,提供 **vllm 离线推理** 和 **API 在线推理** 两种方式。可通过将 `settings.jsonc` 文件中的 `"online_llm_clear": false` 修改为 `true` 来启用 API 在线推理模式,并配置相应的 `base_url`、`llm_api_key`、`model_name` 等参数。所有兼容 OpenAI 接口的模型均可接入。 +- 在获得 `llm 打分分数分布情况` 后,可通过设置 `accept_score` 参数筛选可接受的分数区间,同时可适当降低 `train_sft_args` 中的 `lora_dropout` 参数,以提升模型的拟合效果。 + +## 配置参数并微调模型 + +- (可选)修改 `settings.jsonc` 的 `model_name_or_path` 和 `template` 选择本地下载好的其他模型。 +- 修改`per_device_train_batch_size`以及`gradient_accumulation_steps`来调整显存占用。 +- 可以根据自己数据集的数量和质量修改`train_sft_args`的`num_train_epochs`、`lora_rank`、`lora_dropout`等参数。 + +### 单卡训练 +```bash +weclone-cli train-sft +``` +多卡环境单卡训练,需要先执行 `export CUDA_VISIBLE_DEVICES=0` + +### 多卡训练 +取消`settings.jsonc`中`deepspeed`行代码注释,使用以下命令多卡训练: +```bash +uv pip install deepspeed +deepspeed --num_gpus=使用显卡数量 weclone/train/train_sft.py +``` + +### 使用浏览器demo简单推理 +可以在这一步测试出合适的temperature、top_p值,修改settings.jsonc的`infer_args`后,供后续推理时使用。 +```bash +weclone-cli webchat-demo +``` + +### 使用接口进行推理 + +```bash +weclone-cli server +``` + +### 使用常见聊天问题测试 +不包含询问个人信息的问题,仅有日常聊天。测试结果在test_result-my.txt。 +```bash +weclone-cli server +weclone-cli test-model +``` + +## 🖼️ 微调效果 +使用Qwen2.5-14B-Instruct模型,大概3万条处理后的有效数据,loss降到了3.5左右的效果。 +
+截图 +
+ alt text + alt text + alt text + alt text +
+
+ + +## 🤖 部署到聊天机器人 + +### AstrBot + +[AstrBot](https://github.com/AstrBotDevs/AstrBot) 是易上手的多平台 LLM 聊天机器人及开发框架 ✨ 平台支持 QQ、QQ频道、Telegram、微信、企微、飞书。 + +使用步骤: +1. 部署 AstrBot +2. 在 AstrBot 中部署消息平台 +3. 执行 `weclone-cli server` 启动api服务 +4. 在 AstrBot 中新增服务提供商,类型选择OpenAI,API Base URL 根据AstrBot部署方式填写(例如docker部署可能为http://172.17.0.1:8005/v1) ,模型填写gpt-3.5-turbo,API Key随意填写一个 +5. 微调后不支持工具调用,请先关掉默认的工具,消息平台发送指令: `/tool off all`,否则会没有微调后的效果。 +6. 根据微调时使用的default_system,在 AstrBot 中设置系统提示词。 +![5](https://github.com/user-attachments/assets/19de7072-076a-4cdf-8ae6-46b9b89f536a) +> [!IMPORTANT] +> 检查api_service的日志,尽量保证大模型服务请求的参数和微调时一致,tool插件能力都关掉。 +7. 调整采样参数,例如temperature、top_p、top_k等 +[配置自定义的模型参数](https://astrbot.app/config/model-config.html#%E9%85%8D%E7%BD%AE%E8%87%AA%E5%AE%9A%E4%B9%89%E7%9A%84%E6%A8%A1%E5%9E%8B%E5%8F%82%E6%95%B0) + +### LangBot + +[LangBot](https://github.com/RockChinQ/LangBot) 是一个开源的接入全球多种即时通信平台的 LLM 机器人平台,适合各种场景使用。 + +1. [部署 LangBot](https://github.com/RockChinQ/LangBot#-%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8) +2. 在 LangBot 中添加一个机器人 +4. 在模型页添加新模型,名称`gpt-3.5-turbo`,供应商选择 OpenAI,填写 请求 URL 为 WeClone 的地址,详细连接方式可以参考[文档](https://docs.langbot.app/zh/workshop/network-details.html),API Key 任意填写。 + +image + +6. 在流水线配置中选择刚才添加的模型,或修改提示词配置 + +image + +## 📌 路线图 +- [ ] 更丰富的上下文:包括上下文对话、聊天对象信息、时间等 + 思考 +- [ ] Memory 支持 +- [ ] 支持多模态 +- [ ] 数据增强 +- [ ] 支持GUI + +## 问题解决 +- 微调问题:[LLaMA-Factory| FAQs | 常见问题](https://github.com/hiyouga/LLaMA-Factory/issues/4614) 或者更方便的 [![更方便的Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hiyouga/LLaMA-Factory) + +## ❤️ 贡献代码 + +欢迎任何 Issues/Pull Requests! + +你可以通过查看Issues或帮助审核 PR(拉取请求)来贡献。对于新功能的添加,请先通过 Issue 讨论。 +开发环境: +```bash +uv pip install --group dev -e . +pre-commit install +``` + +项目使用`pytest`测试,`pyright`检查类型,`ruff`检查代码格式。 + + +## ⚠️ 免责声明 +> [!CAUTION] +> 请勿用于非法用途,否则后果自负。 +
+1. 使用目的 + +* 本项目仅供学习交流使用,**请勿用于非法用途**,**请勿用于非法用途**,**请勿用于非法用途**,否则后果自负。 +* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。 + +2. 使用期限 + +* 您应该在下载保存使用本项目的24小时内,删除本项目的源代码和程序;超出此期限的任何使用行为,一概与本项目及其开发者无关。 + +3. 操作规范 + +* 本项目仅允许在授权情况下使用数据训练,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。 +* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任。 + +4. 免责声明接受 + +* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它; + +5. 禁止用于非法测试或渗透 + +* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关。 +* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担。 + +6. 免责声明修改 + +* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。 + +7. 其他 + +* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任。 + +* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。 + +
+请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。 + +
+
+
+ +## ⭐ Star History +> [!TIP] +> 如果本项目对您有帮助,或者您关注本项目的未来发展,请给项目 Star,谢谢 + +
+ +[![Star History Chart](https://api.star-history.com/svg?repos=xming521/WeClone&type=Date)](https://www.star-history.com/#xming521/WeClone&Date) + +
+ + +
克隆我们,保留灵魂的芬芳
diff --git a/dataset/res_csv/pt/dataset_info.json b/dataset/res_csv/pt/dataset_info.json index e1ee546..cdd436c 100644 --- a/dataset/res_csv/pt/dataset_info.json +++ b/dataset/res_csv/pt/dataset_info.json @@ -3,4 +3,4 @@ "columns": { "prompt": "c" } -}} \ No newline at end of file +}} diff --git a/dataset/res_csv/sft/dataset_info.json b/dataset/res_csv/sft/dataset_info.json index 3055c42..f1133df 100644 --- a/dataset/res_csv/sft/dataset_info.json +++ b/dataset/res_csv/sft/dataset_info.json @@ -31,4 +31,4 @@ "assistant_tag": "assistant" } } -} \ No newline at end of file +} diff --git a/dataset/test_data-privacy.json b/dataset/test_data-privacy.json index 2ac3119..02461af 100644 --- a/dataset/test_data-privacy.json +++ b/dataset/test_data-privacy.json @@ -221,4 +221,4 @@ "有没有什么特别的收藏?" ] ] -} \ No newline at end of file +} diff --git a/dataset/test_data.json b/dataset/test_data.json index 6f9c386..7358806 100644 --- a/dataset/test_data.json +++ b/dataset/test_data.json @@ -154,4 +154,4 @@ "注意安全啊。" ] ] -} \ No newline at end of file +} diff --git a/ds_config.json b/ds_config.json index 92ba1a4..8a06442 100644 --- a/ds_config.json +++ b/ds_config.json @@ -25,4 +25,4 @@ "train_batch_size": "auto", "train_micro_batch_size_per_gpu": "auto", "wall_clock_breakdown": false -} \ No newline at end of file +} diff --git a/examples/mllm.template.jsonc b/examples/mllm.template.jsonc index 6d1502f..cb5ba94 100644 --- a/examples/mllm.template.jsonc +++ b/examples/mllm.template.jsonc @@ -82,4 +82,4 @@ "max_length": 50, "top_p": 0.65 } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 4614e96..b08ec77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ sparktts = [ "tqdm>=4.66.5", ] -dev = ["pytest", "pytest-order", "pyright", "ruff"] +dev = ["pytest", "pytest-order", "pyright", "ruff", "pre-commit"] [project.scripts] weclone-cli = "weclone.cli:cli" diff --git a/tests/full_pipe.jsonc b/tests/full_pipe.jsonc index e0270a4..2f43e62 100644 --- a/tests/full_pipe.jsonc +++ b/tests/full_pipe.jsonc @@ -86,4 +86,4 @@ "max_length": 50, "top_p": 0.65 } -} \ No newline at end of file +} diff --git a/tests/test_full_pipe.py b/tests/test_full_pipe.py index ea06739..c6c4f0b 100644 --- a/tests/test_full_pipe.py +++ b/tests/test_full_pipe.py @@ -1,12 +1,14 @@ -import pytest -from unittest import mock -import sys +import functools import os import shutil -import functools import subprocess +import sys import time -from typing import Union, Optional, cast +from typing import Optional, Union, cast +from unittest import mock + +import pytest + from weclone.utils.log import logger sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) diff --git a/weclone-audio/src/Llasa/infer.py b/weclone-audio/src/Llasa/infer.py index bde7a4a..1053e80 100644 --- a/weclone-audio/src/Llasa/infer.py +++ b/weclone-audio/src/Llasa/infer.py @@ -1,12 +1,11 @@ import os + import soundfile as sf from text_to_speech import TextToSpeech - sample_audio_text = "对,这就是我万人敬仰的太乙真人,虽然有点婴儿肥,但也掩不住我逼人的帅气。" # 示例音频文本 sample_audio_path = os.path.join(os.path.dirname(__file__), "sample.wav") # 示例音频路径 tts = TextToSpeech(sample_audio_path, sample_audio_text) target_text = "晚上好啊" # 生成目标文本 result = tts.infer(target_text) sf.write(os.path.join(os.path.dirname(__file__), "output.wav"), result[1], result[0]) # 保存生成音频 - diff --git a/weclone-audio/src/Llasa/text_to_speech.py b/weclone-audio/src/Llasa/text_to_speech.py index 2bb468b..9a81d78 100644 --- a/weclone-audio/src/Llasa/text_to_speech.py +++ b/weclone-audio/src/Llasa/text_to_speech.py @@ -1,131 +1,125 @@ -import os -from transformers import AutoTokenizer, AutoModelForCausalLM -import torch -import soundfile as sf -from xcodec2.modeling_xcodec2 import XCodec2Model -import torchaudio - - -class TextToSpeech: - def __init__(self, sample_audio_path, sample_audio_text): - self.sample_audio_text = sample_audio_text - # 初始化模型 - llasa_3b = "HKUSTAudio/Llasa-3B" - xcodec2 = "HKUSTAudio/xcodec2" - - self.tokenizer = AutoTokenizer.from_pretrained(llasa_3b) - self.llasa_3b_model = AutoModelForCausalLM.from_pretrained( - llasa_3b, - trust_remote_code=True, - device_map="auto", - ) - self.llasa_3b_model.eval() - - self.xcodec_model = XCodec2Model.from_pretrained(xcodec2) - self.xcodec_model.eval().cuda() - - # 处理音频 - waveform, sample_rate = torchaudio.load(sample_audio_path) - if len(waveform[0]) / sample_rate > 15: - print("已将音频裁剪至前15秒。") - waveform = waveform[:, : sample_rate * 15] - - # 检查音频是否为立体声 - if waveform.size(0) > 1: - waveform_mono = torch.mean(waveform, dim=0, keepdim=True) - else: - waveform_mono = waveform - - self.prompt_wav = torchaudio.transforms.Resample( - orig_freq=sample_rate, new_freq=16000 - )(waveform_mono) - - # Encode the prompt wav - vq_code_prompt = self.xcodec_model.encode_code(input_waveform=self.prompt_wav) - vq_code_prompt = vq_code_prompt[0, 0, :] - self.speech_ids_prefix = self.ids_to_speech_tokens(vq_code_prompt) - self.speech_end_id = self.tokenizer.convert_tokens_to_ids("<|SPEECH_GENERATION_END|>") - - def ids_to_speech_tokens(self, speech_ids): - speech_tokens_str = [] - for speech_id in speech_ids: - speech_tokens_str.append(f"<|s_{speech_id}|>") - return speech_tokens_str - - def extract_speech_ids(self, speech_tokens_str): - speech_ids = [] - for token_str in speech_tokens_str: - if token_str.startswith("<|s_") and token_str.endswith("|>"): - num_str = token_str[4:-2] - num = int(num_str) - speech_ids.append(num) - else: - print(f"Unexpected token: {token_str}") - return speech_ids - - @torch.inference_mode() - def infer(self, target_text): - if len(target_text) == 0: - return None - elif len(target_text) > 300: - print("文本过长,请保持在300字符以内。") - target_text = target_text[:300] - - input_text = self.sample_audio_text + " " + target_text - - formatted_text = ( - f"<|TEXT_UNDERSTANDING_START|>{input_text}<|TEXT_UNDERSTANDING_END|>" - ) - - chat = [ - { - "role": "user", - "content": "Convert the text to speech:" + formatted_text, - }, - { - "role": "assistant", - "content": "<|SPEECH_GENERATION_START|>" - + "".join(self.speech_ids_prefix), - }, - ] - - input_ids = self.tokenizer.apply_chat_template( - chat, tokenize=True, return_tensors="pt", continue_final_message=True - ) - input_ids = input_ids.to("cuda") - - outputs = self.llasa_3b_model.generate( - input_ids, - max_length=2048, - eos_token_id=self.speech_end_id, - do_sample=True, - top_p=1, - temperature=0.8, - ) - generated_ids = outputs[0][input_ids.shape[1] - len(self.speech_ids_prefix): -1] - - speech_tokens = self.tokenizer.batch_decode( - generated_ids, skip_special_tokens=True - ) - - speech_tokens = self.extract_speech_ids(speech_tokens) - speech_tokens = torch.tensor(speech_tokens).cuda().unsqueeze(0).unsqueeze(0) - - gen_wav = self.xcodec_model.decode_code(speech_tokens) - gen_wav = gen_wav[:, :, self.prompt_wav.shape[1]:] - - return (16000, gen_wav[0, 0, :].cpu().numpy()) - - -if __name__ == "__main__": - # 如果遇到问题,请尝试将参考音频转换为WAV或MP3格式,将其裁剪至15秒以内,并缩短提示文本。 - sample_audio_text = "对,这就是我万人敬仰的太乙真人,虽然有点婴儿肥,但也掩不住我逼人的帅气。" - sample_audio_path = os.path.join(os.path.dirname(__file__), "sample.wav") - - tts = TextToSpeech(sample_audio_path, sample_audio_text) - target_text = "晚上好啊,吃了吗您" - result = tts.infer(target_text) - sf.write(os.path.join(os.path.dirname(__file__), "output.wav"), result[1], result[0]) - target_text = "我是老北京正黄旗!" - result = tts.infer(target_text) - sf.write(os.path.join(os.path.dirname(__file__), "output1.wav"), result[1], result[0]) +import os + +import soundfile as sf +import torch +import torchaudio +from transformers import AutoModelForCausalLM, AutoTokenizer +from xcodec2.modeling_xcodec2 import XCodec2Model + + +class TextToSpeech: + def __init__(self, sample_audio_path, sample_audio_text): + self.sample_audio_text = sample_audio_text + # 初始化模型 + llasa_3b = "HKUSTAudio/Llasa-3B" + xcodec2 = "HKUSTAudio/xcodec2" + + self.tokenizer = AutoTokenizer.from_pretrained(llasa_3b) + self.llasa_3b_model = AutoModelForCausalLM.from_pretrained( + llasa_3b, + trust_remote_code=True, + device_map="auto", + ) + self.llasa_3b_model.eval() + + self.xcodec_model = XCodec2Model.from_pretrained(xcodec2) + self.xcodec_model.eval().cuda() + + # 处理音频 + waveform, sample_rate = torchaudio.load(sample_audio_path) + if len(waveform[0]) / sample_rate > 15: + print("已将音频裁剪至前15秒。") + waveform = waveform[:, : sample_rate * 15] + + # 检查音频是否为立体声 + if waveform.size(0) > 1: + waveform_mono = torch.mean(waveform, dim=0, keepdim=True) + else: + waveform_mono = waveform + + self.prompt_wav = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)(waveform_mono) + + # Encode the prompt wav + vq_code_prompt = self.xcodec_model.encode_code(input_waveform=self.prompt_wav) + vq_code_prompt = vq_code_prompt[0, 0, :] + self.speech_ids_prefix = self.ids_to_speech_tokens(vq_code_prompt) + self.speech_end_id = self.tokenizer.convert_tokens_to_ids("<|SPEECH_GENERATION_END|>") + + def ids_to_speech_tokens(self, speech_ids): + speech_tokens_str = [] + for speech_id in speech_ids: + speech_tokens_str.append(f"<|s_{speech_id}|>") + return speech_tokens_str + + def extract_speech_ids(self, speech_tokens_str): + speech_ids = [] + for token_str in speech_tokens_str: + if token_str.startswith("<|s_") and token_str.endswith("|>"): + num_str = token_str[4:-2] + num = int(num_str) + speech_ids.append(num) + else: + print(f"Unexpected token: {token_str}") + return speech_ids + + @torch.inference_mode() + def infer(self, target_text): + if len(target_text) == 0: + return None + elif len(target_text) > 300: + print("文本过长,请保持在300字符以内。") + target_text = target_text[:300] + + input_text = self.sample_audio_text + " " + target_text + + formatted_text = f"<|TEXT_UNDERSTANDING_START|>{input_text}<|TEXT_UNDERSTANDING_END|>" + + chat = [ + { + "role": "user", + "content": "Convert the text to speech:" + formatted_text, + }, + { + "role": "assistant", + "content": "<|SPEECH_GENERATION_START|>" + "".join(self.speech_ids_prefix), + }, + ] + + input_ids = self.tokenizer.apply_chat_template( + chat, tokenize=True, return_tensors="pt", continue_final_message=True + ) + input_ids = input_ids.to("cuda") + + outputs = self.llasa_3b_model.generate( + input_ids, + max_length=2048, + eos_token_id=self.speech_end_id, + do_sample=True, + top_p=1, + temperature=0.8, + ) + generated_ids = outputs[0][input_ids.shape[1] - len(self.speech_ids_prefix) : -1] + + speech_tokens = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True) + + speech_tokens = self.extract_speech_ids(speech_tokens) + speech_tokens = torch.tensor(speech_tokens).cuda().unsqueeze(0).unsqueeze(0) + + gen_wav = self.xcodec_model.decode_code(speech_tokens) + gen_wav = gen_wav[:, :, self.prompt_wav.shape[1] :] + + return (16000, gen_wav[0, 0, :].cpu().numpy()) + + +if __name__ == "__main__": + # 如果遇到问题,请尝试将参考音频转换为WAV或MP3格式,将其裁剪至15秒以内,并缩短提示文本。 + sample_audio_text = "对,这就是我万人敬仰的太乙真人,虽然有点婴儿肥,但也掩不住我逼人的帅气。" + sample_audio_path = os.path.join(os.path.dirname(__file__), "sample.wav") + + tts = TextToSpeech(sample_audio_path, sample_audio_text) + target_text = "晚上好啊,吃了吗您" + result = tts.infer(target_text) + sf.write(os.path.join(os.path.dirname(__file__), "output.wav"), result[1], result[0]) + target_text = "我是老北京正黄旗!" + result = tts.infer(target_text) + sf.write(os.path.join(os.path.dirname(__file__), "output1.wav"), result[1], result[0]) diff --git a/weclone-audio/src/SparkTTS.py b/weclone-audio/src/SparkTTS.py index e12bb17..948ae42 100644 --- a/weclone-audio/src/SparkTTS.py +++ b/weclone-audio/src/SparkTTS.py @@ -1,14 +1,16 @@ -import re -import torch -from typing import Tuple -from pathlib import Path -from transformers import AutoTokenizer, AutoModelForCausalLM import os +import re import sys +from pathlib import Path +from typing import Tuple + +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "./Spark-TTS"))) -from sparktts.utils.file import load_config from sparktts.models.audio_tokenizer import BiCodecTokenizer -from sparktts.utils.token_parser import LEVELS_MAP, GENDER_MAP, TASK_TOKEN_MAP +from sparktts.utils.file import load_config +from sparktts.utils.token_parser import GENDER_MAP, LEVELS_MAP, TASK_TOKEN_MAP class SparkTTS: @@ -55,18 +57,12 @@ class SparkTTS: Tuple[str, torch.Tensor]: Input prompt; global tokens """ - global_token_ids, semantic_token_ids = self.audio_tokenizer.tokenize( - prompt_speech_path - ) - global_tokens = "".join( - [f"<|bicodec_global_{i}|>" for i in global_token_ids.squeeze()] - ) + global_token_ids, semantic_token_ids = self.audio_tokenizer.tokenize(prompt_speech_path) + global_tokens = "".join([f"<|bicodec_global_{i}|>" for i in global_token_ids.squeeze()]) # Prepare the input tokens for the model if prompt_text is not None: - semantic_tokens = "".join( - [f"<|bicodec_semantic_{i}|>" for i in semantic_token_ids.squeeze()] - ) + semantic_tokens = "".join([f"<|bicodec_semantic_{i}|>" for i in semantic_token_ids.squeeze()]) inputs = [ TASK_TOKEN_MAP["tts"], "<|start_content|>", @@ -125,9 +121,7 @@ class SparkTTS: speed_label_tokens = f"<|speed_label_{speed_level_id}|>" gender_tokens = f"<|gender_{gender_id}|>" - attribte_tokens = "".join( - [gender_tokens, pitch_label_tokens, speed_label_tokens] - ) + attribte_tokens = "".join([gender_tokens, pitch_label_tokens, speed_label_tokens]) control_tts_inputs = [ TASK_TOKEN_MAP["controllable_tts"], @@ -175,9 +169,7 @@ class SparkTTS: prompt = self.process_prompt_control(gender, pitch, speed, text) else: - prompt, global_token_ids = self.process_prompt( - text, prompt_speech_path, prompt_text - ) + prompt, global_token_ids = self.process_prompt(text, prompt_speech_path, prompt_text) model_inputs = self.tokenizer([prompt], return_tensors="pt").to(self.device) # Generate speech using the model @@ -192,8 +184,7 @@ class SparkTTS: # Trim the output tokens to remove the input tokens generated_ids = [ - output_ids[len(input_ids):] - for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + output_ids[len(input_ids) :] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] # Decode the generated tokens into text @@ -201,9 +192,7 @@ class SparkTTS: # Extract semantic token IDs from the generated text pred_semantic_ids = ( - torch.tensor([int(token) for token in re.findall(r"bicodec_semantic_(\d+)", predicts)]) - .long() - .unsqueeze(0) + torch.tensor([int(token) for token in re.findall(r"bicodec_semantic_(\d+)", predicts)]).long().unsqueeze(0) ) if gender is not None: diff --git a/weclone-audio/src/get_sample_audio.py b/weclone-audio/src/get_sample_audio.py index 8e53541..4d2d89d 100644 --- a/weclone-audio/src/get_sample_audio.py +++ b/weclone-audio/src/get_sample_audio.py @@ -1,18 +1,20 @@ -import os import argparse +import os + from pywxdump.db import MediaHandler + def main(): parser = argparse.ArgumentParser(description="Extract audio from WeChat database") - parser.add_argument("--db-path", type=str, required=True, - help="Path to WeChat database file") - parser.add_argument("--MsgSvrID", type=str, required=True, - help="Message server ID of the audio") - parser.add_argument("--save-path", type=str, - default=os.path.join(os.path.dirname(__file__), "sample.wav"), - help="Path to save the audio file (default: sample.wav in script directory)") - parser.add_argument("--rate", type=int, default=24000, - help="Sample rate for audio conversion (default: 24000)") + parser.add_argument("--db-path", type=str, required=True, help="Path to WeChat database file") + parser.add_argument("--MsgSvrID", type=str, required=True, help="Message server ID of the audio") + parser.add_argument( + "--save-path", + type=str, + default=os.path.join(os.path.dirname(__file__), "sample.wav"), + help="Path to save the audio file (default: sample.wav in script directory)", + ) + parser.add_argument("--rate", type=int, default=24000, help="Sample rate for audio conversion (default: 24000)") args = parser.parse_args() @@ -31,5 +33,6 @@ def main(): rate=args.rate, ) + if __name__ == "__main__": main() diff --git a/weclone-audio/src/infer.py b/weclone-audio/src/infer.py index 55094d5..10e3f9d 100644 --- a/weclone-audio/src/infer.py +++ b/weclone-audio/src/infer.py @@ -1,7 +1,7 @@ import os + import soundfile as sf import torch - from SparkTTS import SparkTTS model = SparkTTS("weclone-audio/pretrained_models/Spark-TTS-0.5B", "cuda") diff --git a/weclone-audio/src/server未完工/.env.example b/weclone-audio/src/server未完工/.env.example index 627d413..71ea56e 100644 --- a/weclone-audio/src/server未完工/.env.example +++ b/weclone-audio/src/server未完工/.env.example @@ -11,4 +11,4 @@ REQUIRE_API_KEY=True REMOVE_FILTER=False -EXPAND_API=True \ No newline at end of file +EXPAND_API=True diff --git a/weclone-audio/src/server未完工/handle_text.py b/weclone-audio/src/server未完工/handle_text.py index c4fba21..167b3bf 100644 --- a/weclone-audio/src/server未完工/handle_text.py +++ b/weclone-audio/src/server未完工/handle_text.py @@ -1,6 +1,8 @@ import re + import emoji + def prepare_tts_input_with_context(text: str) -> str: """ Prepares text for a TTS API by cleaning Markdown and adding minimal contextual hints diff --git a/weclone-audio/src/server未完工/requirements.txt b/weclone-audio/src/server未完工/requirements.txt index 4019693..f08da66 100644 --- a/weclone-audio/src/server未完工/requirements.txt +++ b/weclone-audio/src/server未完工/requirements.txt @@ -2,4 +2,4 @@ flask gevent python-dotenv edge-tts -emoji \ No newline at end of file +emoji diff --git a/weclone-audio/src/server未完工/server.py b/weclone-audio/src/server未完工/server.py index 486ffd3..b11533e 100644 --- a/weclone-audio/src/server未完工/server.py +++ b/weclone-audio/src/server未完工/server.py @@ -1,13 +1,13 @@ # server.py -from flask import Flask, request, send_file, jsonify -from gevent.pywsgi import WSGIServer -from dotenv import load_dotenv import os +from dotenv import load_dotenv +from flask import Flask, jsonify, request, send_file +from gevent.pywsgi import WSGIServer from handle_text import prepare_tts_input_with_context from tts_handler import generate_speech, get_models, get_voices -from utils import getenv_bool, require_api_key, AUDIO_FORMAT_MIME_TYPES +from utils import AUDIO_FORMAT_MIME_TYPES, getenv_bool, require_api_key app = Flask(__name__) load_dotenv() diff --git a/weclone-audio/src/server未完工/tts_handler.py b/weclone-audio/src/server未完工/tts_handler.py index f243acc..39dafef 100644 --- a/weclone-audio/src/server未完工/tts_handler.py +++ b/weclone-audio/src/server未完工/tts_handler.py @@ -1,10 +1,11 @@ -import edge_tts import asyncio -import tempfile -import subprocess import os +import subprocess +import tempfile from pathlib import Path +import edge_tts + # Language default (environment variable) DEFAULT_LANGUAGE = os.getenv('DEFAULT_LANGUAGE', 'en-US') diff --git a/weclone-audio/src/server未完工/utils.py b/weclone-audio/src/server未完工/utils.py index 7fe9845..c0f3ca3 100644 --- a/weclone-audio/src/server未完工/utils.py +++ b/weclone-audio/src/server未完工/utils.py @@ -1,9 +1,10 @@ # utils.py -from flask import request, jsonify -from functools import wraps import os +from functools import wraps + from dotenv import load_dotenv +from flask import jsonify, request load_dotenv() diff --git a/weclone/cli.py b/weclone/cli.py index 78e97c0..59d6550 100644 --- a/weclone/cli.py +++ b/weclone/cli.py @@ -1,12 +1,13 @@ -import click -import commentjson -from pathlib import Path +import functools import os import sys -import functools +from pathlib import Path + +import click +import commentjson -from weclone.utils.log import logger, capture_output from weclone.utils.config import load_config +from weclone.utils.log import capture_output, logger cli_config: dict | None = None diff --git a/weclone/core/inference/offline_infer.py b/weclone/core/inference/offline_infer.py index be7affa..e417834 100644 --- a/weclone/core/inference/offline_infer.py +++ b/weclone/core/inference/offline_infer.py @@ -1,20 +1,13 @@ -import json from typing import List, Optional, Union - -from llamafactory.data import get_dataset, get_template_and_fix_tokenizer -from llamafactory.extras.constants import IGNORE_INDEX +from llamafactory.data import get_template_and_fix_tokenizer from llamafactory.extras.misc import get_device_count -from llamafactory.extras.packages import is_vllm_available from llamafactory.hparams import get_infer_args from llamafactory.model import load_tokenizer from pydantic import BaseModel -from vllm.sampling_params import GuidedDecodingParams - - from vllm import LLM, SamplingParams from vllm.lora.request import LoRARequest - +from vllm.sampling_params import GuidedDecodingParams # 这里不需要写太好,transforms库后续更新自带vllm diff --git a/weclone/core/inference/online_infer.py b/weclone/core/inference/online_infer.py index 518cf71..8868d5e 100644 --- a/weclone/core/inference/online_infer.py +++ b/weclone/core/inference/online_infer.py @@ -1,26 +1,23 @@ -import json -import time -import requests from openai import OpenAI + class OnlineLLM: - def __init__(self, api_key: str, base_url: str,model_name: str,default_system: str): + def __init__(self, api_key: str, base_url: str, model_name: str, default_system: str): self.api_key = api_key self.base_url = base_url self.model_name = model_name self.default_system = default_system - self.client = OpenAI( - api_key=self.api_key, - base_url=self.base_url - ) + self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) - - def chat(self,prompt_text, - temperature: float = 0.7, - max_tokens: int = 1024, - top_p: float = 0.95, - stream: bool = False, - enable_thinking: bool = False): + def chat( + self, + prompt_text, + temperature: float = 0.7, + max_tokens: int = 1024, + top_p: float = 0.95, + stream: bool = False, + enable_thinking: bool = False, + ): messages = [ {"role": "system", "content": self.default_system}, {"role": "user", "content": prompt_text}, @@ -29,12 +26,10 @@ class OnlineLLM: model=self.model_name, messages=messages, stream=stream, - temperature = temperature, + temperature=temperature, max_tokens=max_tokens, top_p=top_p, - # enable_thinking=enable_thinking 适配Qwen3动态开启推理 - + # enable_thinking=enable_thinking 适配Qwen3动态开启推理 ) return response - diff --git a/weclone/data/chat_parsers/wechat_parser.py b/weclone/data/chat_parsers/wechat_parser.py index 5bdb857..8fc4522 100644 --- a/weclone/data/chat_parsers/wechat_parser.py +++ b/weclone/data/chat_parsers/wechat_parser.py @@ -1,7 +1,8 @@ -import os import argparse -from pathlib import Path +import os import shutil +from pathlib import Path + import pandas as pd from tqdm import tqdm diff --git a/weclone/data/clean/strategies.py b/weclone/data/clean/strategies.py index 81c37df..d540979 100644 --- a/weclone/data/clean/strategies.py +++ b/weclone/data/clean/strategies.py @@ -1,12 +1,14 @@ import json -import pandas as pd +import os from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Dict, List, Union +from typing import Any, Dict, List + +import pandas as pd from langchain_core.prompts import PromptTemplate -from weclone.data.models import QaPair, CutMessage, QaPairScore + +from weclone.data.models import QaPair, QaPairScore from weclone.prompts.clean_data import CLEAN_PROMPT -import os from weclone.utils.log import logger diff --git a/weclone/data/clean/strategies_online.py b/weclone/data/clean/strategies_online.py index f2ede9c..64b9265 100644 --- a/weclone/data/clean/strategies_online.py +++ b/weclone/data/clean/strategies_online.py @@ -1,16 +1,19 @@ -import re import json -import pandas as pd -from tqdm import tqdm +import os +import re from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Dict, List + +import pandas as pd from langchain_core.prompts import PromptTemplate -from weclone.data.models import QaPair, QaPairScore -from weclone.prompts.clean_data import CLEAN_PROMPT,ONLINE_LLM_CLEAN_PROMPT +from tqdm import tqdm + from weclone.core.inference.online_infer import OnlineLLM +from weclone.data.models import QaPair, QaPairScore +from weclone.prompts.clean_data import ONLINE_LLM_CLEAN_PROMPT from weclone.utils.log import logger -import os + @dataclass class CleaningStrategy(ABC): @@ -22,6 +25,7 @@ class CleaningStrategy(ABC): def clean(self, data: Any) -> Any: pass + @dataclass class OlineLLMCleaningStrategy(CleaningStrategy): """使用大模型进行数据清洗的策略""" @@ -32,27 +36,22 @@ class OlineLLMCleaningStrategy(CleaningStrategy): logger.info(f"使用模型 {self.make_dataset_config.get('model_name', '')}") client = OnlineLLM( - api_key = self.make_dataset_config.get("llm_api_key"), - base_url = self.make_dataset_config.get("base_url"), - model_name = self.make_dataset_config.get("model_name"), - default_system = self.make_dataset_config.get("default_system") + api_key=self.make_dataset_config.get("llm_api_key"), # type: ignore + base_url=self.make_dataset_config.get("base_url"), + model_name=self.make_dataset_config.get("model_name"), + default_system=self.make_dataset_config.get("default_system"), ) prompt_template = PromptTemplate.from_template(ONLINE_LLM_CLEAN_PROMPT) parsed_scores = [] - clean_batch_size = int(self.make_dataset_config.get("clean_batch_size", 10)) + clean_batch_size = int(self.make_dataset_config.get("clean_batch_size", 10)) for i in tqdm(range(0, len(data), clean_batch_size), desc="在线模型评分进度"): batch = data[i : i + clean_batch_size] # 构造当前批次的 qa_list - qa_list = [ - {"id": qa.id, "Q": qa.instruction, "A": qa.output} - for qa in batch - ] + qa_list = [{"id": qa.id, "Q": qa.instruction, "A": qa.output} for qa in batch] qa_list_json = json.dumps(qa_list, ensure_ascii=False) # 填充模板 - prompt_text = prompt_template.invoke({ - "qa_list": qa_list_json - }).text + prompt_text = prompt_template.invoke({"qa_list": qa_list_json}).text try: response = client.chat(prompt_text) result_text = response.choices[0].message.content @@ -68,7 +67,7 @@ class OlineLLMCleaningStrategy(CleaningStrategy): except json.JSONDecodeError as e: logger.error(f"JSON 解析失败,跳过本批次: {e}\n内容:{result_text}") continue - + for item in score_list: parsed_scores.append(QaPairScore(**item)) except Exception as e: @@ -89,14 +88,16 @@ class OlineLLMCleaningStrategy(CleaningStrategy): score_counts = score_series.value_counts().sort_index() score_percentages = score_series.value_counts(normalize=True).sort_index() * 100 pd.set_option("display.unicode.east_asian_width", True) - distribution_df = pd.DataFrame({ - "数量": score_counts, - "占比(%)": score_percentages.round(2), - }) + distribution_df = pd.DataFrame( + { + "数量": score_counts, + "占比(%)": score_percentages.round(2), + } + ) distribution_df.index.name = "分数" printable_df_str = distribution_df.reset_index().to_string(index=False) logger.success(f"在线模型打分分数分布情况:\n{printable_df_str}") - + def clean(self) -> str: """ 清洗 SFT 数据并返回清洗后的文件路径。 @@ -116,11 +117,11 @@ class OlineLLMCleaningStrategy(CleaningStrategy): return sft_json_path try: - with open(sft_json_path, 'r', encoding='utf-8') as f: + with open(sft_json_path, "r", encoding="utf-8") as f: data = json.load(f) filtered_data = [item for item in data if item.get("score", 0) >= accept_score] - with open(output_json_path, 'w', encoding='utf-8') as f: + with open(output_json_path, "w", encoding="utf-8") as f: json.dump(filtered_data, f, ensure_ascii=False, indent=4) logger.success(f"已筛出低于{accept_score}分的数据,共保留 {len(filtered_data)} 条数据") diff --git a/weclone/data/models.py b/weclone/data/models.py index 20c9d63..f19370d 100644 --- a/weclone/data/models.py +++ b/weclone/data/models.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from enum import Enum -from typing import Union, Optional + from pandas import Timestamp from pydantic import BaseModel + from weclone.utils.i18n import MultiLangList diff --git a/weclone/data/qa_generator.py b/weclone/data/qa_generator.py index 5696299..1caf5db 100644 --- a/weclone/data/qa_generator.py +++ b/weclone/data/qa_generator.py @@ -1,20 +1,20 @@ +import json import os -import sys -import subprocess -from typing import Dict, List, Union import re +import subprocess +import sys +from typing import List, Union import pandas as pd -import json -from pandas import Timestamp from llamafactory.extras.packages import is_vllm_available +from pandas import Timestamp from weclone.data.clean.strategies import LLMCleaningStrategy from weclone.data.clean.strategies_online import OlineLLMCleaningStrategy +from weclone.data.models import ChatMessage, CutMessage, QaPair, skip_type_list +from weclone.data.strategies import LLMStrategy, TimeWindowStrategy from weclone.utils.config import load_config from weclone.utils.log import logger -from weclone.data.models import ChatMessage, CutMessage, skip_type_list, QaPair -from weclone.data.strategies import TimeWindowStrategy, LLMStrategy class DataProcessor: diff --git a/weclone/data/qa_generatorV2.py b/weclone/data/qa_generatorV2.py index 59cc74a..f827e67 100644 --- a/weclone/data/qa_generatorV2.py +++ b/weclone/data/qa_generatorV2.py @@ -1,27 +1,27 @@ +import json import os -import sys -import subprocess -from typing import Dict, List, Union import re +import subprocess +import sys +from typing import List, Union import pandas as pd -import json from pandas import Timestamp from weclone.data.clean.strategies import LLMCleaningStrategy from weclone.data.clean.strategies_online import OlineLLMCleaningStrategy -from weclone.utils.config import load_config -from weclone.utils.log import logger from weclone.data.models import ( ChatMessage, CutMessage, - skip_type_list, - cut_type_list, - QaPairV2, Message, + QaPairV2, + cut_type_list, + skip_type_list, ) -from weclone.data.strategies import TimeWindowStrategy, LLMStrategy +from weclone.data.strategies import LLMStrategy, TimeWindowStrategy from weclone.data.utils import check_image_file_exists +from weclone.utils.config import load_config +from weclone.utils.log import logger class DataProcessor: diff --git a/weclone/data/strategies.py b/weclone/data/strategies.py index 4a457f1..2476f0a 100644 --- a/weclone/data/strategies.py +++ b/weclone/data/strategies.py @@ -1,7 +1,8 @@ +from abc import ABC, abstractmethod from dataclasses import dataclass from typing import List + from .models import ChatMessage -from abc import ABC, abstractmethod @dataclass @@ -11,9 +12,7 @@ class ConversationStrategy(ABC): is_single_chat: bool @abstractmethod - def is_same_conversation( - self, history_msg: List[ChatMessage], current_msg: ChatMessage - ) -> bool: + def is_same_conversation(self, history_msg: List[ChatMessage], current_msg: ChatMessage) -> bool: """判断两条消息是否属于同一个对话""" pass @@ -24,12 +23,8 @@ class TimeWindowStrategy(ConversationStrategy): time_window: int # 时间窗口(分钟) - def is_same_conversation( - self, history_msg: List[ChatMessage], current_msg: ChatMessage - ) -> bool: - time_diff = abs( - (current_msg.CreateTime - history_msg[-1].CreateTime) - ).total_seconds() + def is_same_conversation(self, history_msg: List[ChatMessage], current_msg: ChatMessage) -> bool: + time_diff = abs((current_msg.CreateTime - history_msg[-1].CreateTime)).total_seconds() return time_diff <= self.time_window @@ -37,9 +32,7 @@ class TimeWindowStrategy(ConversationStrategy): class LLMStrategy(ConversationStrategy): """基于大模型判断策略""" - def is_same_conversation( - self, history_msg: List[ChatMessage], current_msg: ChatMessage - ) -> bool: + def is_same_conversation(self, history_msg: List[ChatMessage], current_msg: ChatMessage) -> bool: # 修复user_id错误,使用talker字段代替user_id return current_msg.talker == history_msg[-1].talker if history_msg else False @@ -51,10 +44,6 @@ class CompositeStrategy(ConversationStrategy): strategies: List[ConversationStrategy] require_all: bool = True # True表示所有策略都满足,False表示任一策略满足即可 - def is_same_conversation( - self, history_msg: List[ChatMessage], current_msg: ChatMessage - ) -> bool: - results = [ - s.is_same_conversation(history_msg, current_msg) for s in self.strategies - ] + def is_same_conversation(self, history_msg: List[ChatMessage], current_msg: ChatMessage) -> bool: + results = [s.is_same_conversation(history_msg, current_msg) for s in self.strategies] return all(results) if self.require_all else any(results) diff --git a/weclone/data/utils.py b/weclone/data/utils.py index 46f91df..085312f 100644 --- a/weclone/data/utils.py +++ b/weclone/data/utils.py @@ -1,5 +1,6 @@ import os from pathlib import Path + from weclone.utils.log import logger @@ -34,5 +35,5 @@ def check_image_file_exists(file_path: str) -> str | bool: if __name__ == "__main__": - path = "Storage\Image\2021-08\6ce3f785b4230246639c3dd0d4a8848c.dat" + path = "Storage\\Image\2021-08\6ce3f785b4230246639c3dd0d4a8848c.dat" print(check_image_file_exists(path)) diff --git a/weclone/eval/test_model.py b/weclone/eval/test_model.py index 59eeb2f..9dc35e6 100644 --- a/weclone/eval/test_model.py +++ b/weclone/eval/test_model.py @@ -1,10 +1,10 @@ import json +from typing import List, cast # 导入 cast + import openai from openai import OpenAI # 导入 OpenAI 类 - +from openai.types.chat import ChatCompletionMessageParam # 导入消息参数类型 from tqdm import tqdm -from typing import List, Dict, cast # 导入 cast -from openai.types.chat import ChatCompletionMessageParam # 导入消息参数类型 from weclone.utils.config import load_config @@ -19,10 +19,7 @@ config = { config = type("Config", (object,), config)() # 初始化 OpenAI 客户端 -client = OpenAI( - api_key="""sk-test""", - base_url="http://127.0.0.1:8005/v1" -) +client = OpenAI(api_key="""sk-test""", base_url="http://127.0.0.1:8005/v1") def handler_text(content: str, history: list, config): @@ -37,14 +34,14 @@ def handler_text(content: str, history: list, config): typed_messages = cast(List[ChatCompletionMessageParam], messages) response = client.chat.completions.create( model=config.model, - messages=typed_messages, # 传递转换后的列表 - max_tokens=50 + messages=typed_messages, # 传递转换后的列表 + max_tokens=50, ) except openai.APIError as e: history.pop() return "AI接口出错,请重试\n" + str(e) - resp = str(response.choices[0].message.content) # type: ignore + resp = str(response.choices[0].message.content) # type: ignore resp = resp.replace("\n ", "") history.append({"role": "assistant", "content": resp}) return resp diff --git a/weclone/eval/web_demo.py b/weclone/eval/web_demo.py index 98ec03e..63af1f0 100644 --- a/weclone/eval/web_demo.py +++ b/weclone/eval/web_demo.py @@ -1,4 +1,5 @@ from llamafactory.webui.interface import create_web_demo + from weclone.utils.config import load_config diff --git a/weclone/prompts/clean_data.py b/weclone/prompts/clean_data.py index efc2688..2cddca2 100644 --- a/weclone/prompts/clean_data.py +++ b/weclone/prompts/clean_data.py @@ -37,6 +37,47 @@ CLEAN_PROMPT = """ }} """ + +CLEAN_PROMPT_V2 = """ +# 角色 +你是一个数据质量评估员。 + +# 任务 +你的任务是评估下面提供的对话内容的**逻辑性**和**相关性**。目标是识别并帮助过滤掉那些回答与问题**明显不匹配**、**逻辑严重混乱**的数据对。请根据以下核心评估点给出一个1到5的整数分数,并将该分数与原始 `id` 一起输出。 + +**重要考量:** +1. **简短回答的有效性:** 请注意,诸如“好的”、“是的”、“收到”、“嗯”、“知道了”等简短的肯定、确认或应答,在合适的语境下是完全**有逻辑且相关的**。**不要仅仅因为回答简短就将其评为低分。** 只有当这类简短回答与【问题/上下文 Q】**明显不符**时,才应考虑低分。 +2. **处理错别字和自我纠正:** 聊天记录中可能包含常见的打字错误(错别字)或用户先打错字随后又自行纠正的情况(例如,发送“我想去1楼”紧接着又发送“*2楼”进行更正)。在评估时,请**聚焦于用户想要表达的最终意图和信息的核心内容**,而**不应仅仅因为存在错别字或纠正过程就判定为低质量**。。 + + +# 核心评估点 (请在心中衡量) +1. **相关性 (Relevance):** 【回答 A】是否直接回应或恰当地衔接了【问题/上下文 Q】?它是在回答问题,还是完全跑题了?只有当【回答 A】与【问题/上下文 Q】**明显矛盾**、**完全不着边际**(即使考虑上下文也无法合理化),或简短回答**明显不适用于**该【问题/上下文 Q】时,才给予低分。 +2. **逻辑性 (Coherence):** 【回答 A】本身是否符合基本的逻辑?结合【问题/上下文 Q】来看,这个问答对是否构成了一个符合逻辑的交流片段?是否存在明显的矛盾、混乱的内容?只有当【回答 A】**自身逻辑混乱**、**与Q存在无法解释的矛盾**时,才给予低分。 + +# 评分标准 (1-5分) +* **1分 (极差):** 完全不相关;逻辑严重混乱/矛盾。 +* **2分 (差):** 相关性很低;存在明显的逻辑问题或不连贯。 +* **3分 (中等):** 相关性一般(可能部分跑题或回应不充分);逻辑上勉强说得通但不够流畅或有瑕疵。 +* **4分 (良好):** 相关性好,回答了问题或恰当衔接;逻辑清晰。 +* **5分 (优秀):** 相关性强,回应精准;逻辑严谨流畅。 + +# 输入数据 +```json +{{ + "id": "{id}", + "Q": "{Q}", + "A": "{A}" +}} + +# 输出要求 +请严格按照以下 JSON 格式输出,包含原始的 id 和你给出的1到5的整数评分 score,不要包含任何其他文字、解释或标签。 +{{ + "id": "<这里填入输入数据中的id值>", + "score": <这里填入1到5的整数评分> +}} +""" + + ONLINE_LLM_CLEAN_PROMPT = """ # 角色 你是一个数据质量评估员。 @@ -77,4 +118,4 @@ ONLINE_LLM_CLEAN_PROMPT = """ }} … ] -""" \ No newline at end of file +""" diff --git a/weclone/server/api_service.py b/weclone/server/api_service.py index 29d8efa..b07d027 100644 --- a/weclone/server/api_service.py +++ b/weclone/server/api_service.py @@ -1,9 +1,10 @@ import os -import uvicorn -from llamafactory.chat import ChatModel -from llamafactory.api.app import create_app -from weclone.utils.config import load_config +import uvicorn +from llamafactory.api.app import create_app +from llamafactory.chat import ChatModel + +from weclone.utils.config import load_config def main(): diff --git a/weclone/train/train_pt.py b/weclone/train/train_pt.py index b7fd547..d4ab046 100644 --- a/weclone/train/train_pt.py +++ b/weclone/train/train_pt.py @@ -1,4 +1,5 @@ from llamafactory.train.tuner import run_exp + from weclone.utils.config import load_config config = load_config("train_pt") diff --git a/weclone/train/train_sft.py b/weclone/train/train_sft.py index 78483d7..c7c5826 100644 --- a/weclone/train/train_sft.py +++ b/weclone/train/train_sft.py @@ -1,11 +1,14 @@ +import json import os import sys -import json -from llamafactory.train.tuner import run_exp + from llamafactory.extras.misc import get_current_device +from llamafactory.train.tuner import run_exp + +from weclone.data.clean.strategies import LLMCleaningStrategy from weclone.utils.config import load_config from weclone.utils.log import logger -from weclone.data.clean.strategies import LLMCleaningStrategy + def main(): train_config = load_config(arg_type="train_sft") diff --git a/weclone/utils/config.py b/weclone/utils/config.py index 06ef6af..2582f1a 100644 --- a/weclone/utils/config.py +++ b/weclone/utils/config.py @@ -1,7 +1,8 @@ import os -import commentjson import sys +import commentjson + from .log import logger from .tools import dict_to_argv diff --git a/weclone/utils/i18n.py b/weclone/utils/i18n.py index 9efee32..78502e4 100644 --- a/weclone/utils/i18n.py +++ b/weclone/utils/i18n.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Optional, Tuple +from typing import Dict, List, Optional class MultiLangList: diff --git a/weclone/utils/length_cdf.py b/weclone/utils/length_cdf.py index f4f8fd5..7d2e17b 100644 --- a/weclone/utils/length_cdf.py +++ b/weclone/utils/length_cdf.py @@ -15,12 +15,12 @@ from collections import defaultdict import fire -from tqdm import tqdm -from weclone.utils.log import logger - from llamafactory.data import get_dataset, get_template_and_fix_tokenizer from llamafactory.hparams import get_train_args from llamafactory.model import load_tokenizer +from tqdm import tqdm + +from weclone.utils.log import logger def length_cdf( diff --git a/weclone/utils/log.py b/weclone/utils/log.py index ad164a8..1ebc71d 100644 --- a/weclone/utils/log.py +++ b/weclone/utils/log.py @@ -1,7 +1,8 @@ -from loguru import logger import sys from functools import wraps +from loguru import logger + logger.remove() logger.add( diff --git a/weclone/utils/tools.py b/weclone/utils/tools.py index 28bc29d..3c2fb11 100644 --- a/weclone/utils/tools.py +++ b/weclone/utils/tools.py @@ -5,5 +5,3 @@ def dict_to_argv(d): if v is not None: argv.append(str(v)) return argv - -