Merge pull request #149 from xming521/dev

Add pre-commit, format code with ruff, update .gitignore, update pyproject.toml, update README.md.
This commit is contained in:
小铭
2025-06-09 14:52:07 +08:00
committed by GitHub
46 changed files with 853 additions and 782 deletions
-6
View File
@@ -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.
+169 -167
View File
@@ -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/*
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*
+48
View File
@@ -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]"]
+293 -285
View File
@@ -1,285 +1,293 @@
![download](https://github.com/user-attachments/assets/5842e84e-004f-4afd-9373-af64e9575b78)
<h3 align="center">🚀 One-stop solution for creating your digital avatar from chat history 💡</h3>
<h3 align="center">🚀从聊天记录创造数字分身的一站式解决方案💡</h3>
<div align="center">
[![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)
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wNdgbOVT6oFOJ2wlMLsolUXErW9ESLpk&jump_from=webapi&authKey=z/reOp6YLyvR4Tl2k2nYMsLoMC3w9/99ucgKMX0oRGlxDV/WbYnvq2QxODoIkfxn" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/badge/QQ群-708067078-12B7F5?style=for-the-badge&logo=qq&logoColor=white" alt="WeClone①" title="WeClone①">
</a>
[![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)
<a href="https://hellogithub.com/repository/12ab209b56cb4cfd885c8cfd4cfdd53e" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=12ab209b56cb4cfd885c8cfd4cfdd53e&claim_uid=RThlPDoGrFvdMY5" alt="FeaturedHelloGitHub" style="width: 150px; height: 28px;" /></a>
<a href="https://trendshift.io/repositories/13759" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13759" alt="xming521%2FWeClone | Trendshift" style="width: 220px; height: 50px;" /></a>
<a href="https://deepwiki.com/xming521/WeClone"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" style="width: 134px; height: 23px;margin-bottom: 3px;"></a>
</div>
<p align="center">
<a href="https://www.weclone.love/" target="_blank"> 项目主页 </a>
<a href="https://www.weclone.love/what-is-weclone.html" target="_blank"> 项目文档 </a>
<a href="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/" target="_blank">Windows部署指南</a>
<a href="https://blog.051088.xyz/posts/weclone-linux-tutorial/" target="_blank"> Linux部署指南【保姆级】</a>
</p>
> [!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左右的效果。
<details>
<summary>截图</summary>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="https://github.com/user-attachments/assets/0775ec52-452b-485f-9785-c6eb7b277132" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/8c7628b5-da70-4c37-9e51-fdfb0eadd2df" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/523aa742-2aa3-40e9-bd67-b98b336e83a8" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/dabf0603-dcc4-4a47-b5c3-2bbc036820d9" alt="alt text" style="width: 48%; min-width: 150px;">
</div>
</details>
## 🤖 部署到聊天机器人
### AstrBot
[AstrBot](https://github.com/AstrBotDevs/AstrBot) 是易上手的多平台 LLM 聊天机器人及开发框架 ✨ 平台支持 QQ、QQ频道、Telegram、微信、企微、飞书。
使用步骤:
1. 部署 AstrBot
2. 在 AstrBot 中部署消息平台
3. 执行 `weclone-cli server` 启动api服务
4. 在 AstrBot 中新增服务提供商,类型选择OpenAIAPI 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 任意填写。
<img width="400px" alt="image" src="https://github.com/user-attachments/assets/fc167dea-7c93-4d94-9c5f-db709d0320ba" />
6. 在流水线配置中选择刚才添加的模型,或修改提示词配置
<img width="400px" alt="image" src="https://github.com/user-attachments/assets/dbb0fd0a-f760-42db-acd0-bb99c859b52e" />
## 📌 路线图
- [ ] 更丰富的上下文:包括上下文对话、聊天对象信息、时间等 + 思考
- [ ] 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]
> 请勿用于非法用途,否则后果自负。
<details>
<summary>1. 使用目的</summary>
* 本项目仅供学习交流使用,**请勿用于非法用途**,**请勿用于非法用途**,**请勿用于非法用途**,否则后果自负。
* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。
2. 使用期限
* 您应该在下载保存使用本项目的24小时内,删除本项目的源代码和程序;超出此期限的任何使用行为,一概与本项目及其开发者无关。
3. 操作规范
* 本项目仅允许在授权情况下使用数据训练,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。
* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任。
4. 免责声明接受
* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它;
5. 禁止用于非法测试或渗透
* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关
* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担。
6. 免责声明修改
* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。
7. 其他
* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任
* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
</details>
请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
<br>
<br>
<br>
## ⭐ Star History
> [!TIP]
> 如果本项目对您有帮助,或者您关注本项目的未来发展,请给项目 Star,谢谢
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=xming521/WeClone&type=Date)](https://www.star-history.com/#xming521/WeClone&Date)
</div>
<div align="center"> 克隆我们,保留灵魂的芬芳 </div>
![download](https://github.com/user-attachments/assets/5842e84e-004f-4afd-9373-af64e9575b78)
<h3 align="center">🚀 One-stop solution for creating your digital avatar from chat history 💡</h3>
<h3 align="center">🚀从聊天记录创造数字分身的一站式解决方案💡</h3>
<div align="center">
[![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)
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wNdgbOVT6oFOJ2wlMLsolUXErW9ESLpk&jump_from=webapi&authKey=z/reOp6YLyvR4Tl2k2nYMsLoMC3w9/99ucgKMX0oRGlxDV/WbYnvq2QxODoIkfxn" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/badge/QQ群-708067078-12B7F5?style=for-the-badge&logo=qq&logoColor=white" alt="WeClone①" title="WeClone①">
</a>
[![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)
<a href="https://hellogithub.com/repository/12ab209b56cb4cfd885c8cfd4cfdd53e" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=12ab209b56cb4cfd885c8cfd4cfdd53e&claim_uid=RThlPDoGrFvdMY5" alt="FeaturedHelloGitHub" style="width: 150px; height: 28px;" /></a>
<a href="https://trendshift.io/repositories/13759" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13759" alt="xming521%2FWeClone | Trendshift" style="width: 220px; height: 50px;" /></a>
<a href="https://deepwiki.com/xming521/WeClone"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" style="width: 134px; height: 23px;margin-bottom: 3px;"></a>
</div>
<p align="center">
<a href="https://www.weclone.love/" target="_blank"> 项目主页 </a>
<a href="https://www.weclone.love/what-is-weclone.html" target="_blank"> 项目文档 </a>
<a href="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/" target="_blank">Windows部署指南</a>
<a href="https://blog.051088.xyz/posts/weclone-linux-tutorial/" target="_blank"> Linux部署指南【保姆级】</a>
</p>
> [!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左右的效果。
<details>
<summary>截图</summary>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="https://github.com/user-attachments/assets/0775ec52-452b-485f-9785-c6eb7b277132" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/8c7628b5-da70-4c37-9e51-fdfb0eadd2df" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/523aa742-2aa3-40e9-bd67-b98b336e83a8" alt="alt text" style="width: 48%; min-width: 150px;">
<img src="https://github.com/user-attachments/assets/dabf0603-dcc4-4a47-b5c3-2bbc036820d9" alt="alt text" style="width: 48%; min-width: 150px;">
</div>
</details>
## 🤖 部署到聊天机器人
### AstrBot
[AstrBot](https://github.com/AstrBotDevs/AstrBot) 是易上手的多平台 LLM 聊天机器人及开发框架 ✨ 平台支持 QQ、QQ频道、Telegram、微信、企微、飞书。
使用步骤:
1. 部署 AstrBot
2. 在 AstrBot 中部署消息平台
3. 执行 `weclone-cli server` 启动api服务
4. 在 AstrBot 中新增服务提供商,类型选择OpenAIAPI 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 任意填写。
<img width="400px" alt="image" src="https://github.com/user-attachments/assets/fc167dea-7c93-4d94-9c5f-db709d0320ba" />
6. 在流水线配置中选择刚才添加的模型,或修改提示词配置
<img width="400px" alt="image" src="https://github.com/user-attachments/assets/dbb0fd0a-f760-42db-acd0-bb99c859b52e" />
## 📌 路线图
- [ ] 更丰富的上下文:包括上下文对话、聊天对象信息、时间等 + 思考
- [ ] 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]
> 请勿用于非法用途,否则后果自负。
<details>
<summary>1. 使用目的</summary>
* 本项目仅供学习交流使用,**请勿用于非法用途**,**请勿用于非法用途**,**请勿用于非法用途**,否则后果自负。
* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担
2. 使用期限
* 您应该在下载保存使用本项目的24小时内,删除本项目的源代码和程序;超出此期限的任何使用行为,一概与本项目及其开发者无关。
3. 操作规范
* 本项目仅允许在授权情况下使用数据训练,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。
* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任
4. 免责声明接受
* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它;
5. 禁止用于非法测试或渗透
* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关。
* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担
6. 免责声明修改
* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。
7. 其他
* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任。
* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
</details>
请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
<br>
<br>
<br>
## ⭐ Star History
> [!TIP]
> 如果本项目对您有帮助,或者您关注本项目的未来发展,请给项目 Star,谢谢
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=xming521/WeClone&type=Date)](https://www.star-history.com/#xming521/WeClone&Date)
</div>
<div align="center"> 克隆我们,保留灵魂的芬芳 </div>
+1 -1
View File
@@ -3,4 +3,4 @@
"columns": {
"prompt": "c"
}
}}
}}
+1 -1
View File
@@ -31,4 +31,4 @@
"assistant_tag": "assistant"
}
}
}
}
+1 -1
View File
@@ -221,4 +221,4 @@
"有没有什么特别的收藏?"
]
]
}
}
+1 -1
View File
@@ -154,4 +154,4 @@
"注意安全啊。"
]
]
}
}
+1 -1
View File
@@ -25,4 +25,4 @@
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
}
+1 -1
View File
@@ -82,4 +82,4 @@
"max_length": 50,
"top_p": 0.65
}
}
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -86,4 +86,4 @@
"max_length": 50,
"top_p": 0.65
}
}
}
+7 -5
View File
@@ -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__), '..')))
+1 -2
View File
@@ -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]) # 保存生成音频
+125 -131
View File
@@ -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])
+16 -27
View File
@@ -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:
+13 -10
View File
@@ -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()
+1 -1
View File
@@ -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")
@@ -11,4 +11,4 @@ REQUIRE_API_KEY=True
REMOVE_FILTER=False
EXPAND_API=True
EXPAND_API=True
@@ -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
@@ -2,4 +2,4 @@ flask
gevent
python-dotenv
edge-tts
emoji
emoji
+4 -4
View File
@@ -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()
@@ -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')
+3 -2
View File
@@ -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()
+6 -5
View File
@@ -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
+2 -9
View File
@@ -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
+14 -19
View File
@@ -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
+3 -2
View File
@@ -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
+6 -4
View File
@@ -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
+27 -26
View File
@@ -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)} 条数据")
+2 -1
View File
@@ -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
+7 -7
View File
@@ -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:
+10 -10
View File
@@ -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:
+8 -19
View File
@@ -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)
+2 -1
View File
@@ -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))
+7 -10
View File
@@ -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
+1
View File
@@ -1,4 +1,5 @@
from llamafactory.webui.interface import create_web_demo
from weclone.utils.config import load_config
+42 -1
View File
@@ -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 = """
}}
]
"""
"""
+5 -4
View File
@@ -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():
+1
View File
@@ -1,4 +1,5 @@
from llamafactory.train.tuner import run_exp
from weclone.utils.config import load_config
config = load_config("train_pt")
+6 -3
View File
@@ -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")
+2 -1
View File
@@ -1,7 +1,8 @@
import os
import commentjson
import sys
import commentjson
from .log import logger
from .tools import dict_to_argv
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import List, Dict, Optional, Tuple
from typing import Dict, List, Optional
class MultiLangList:
+3 -3
View File
@@ -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(
+2 -1
View File
@@ -1,7 +1,8 @@
from loguru import logger
import sys
from functools import wraps
from loguru import logger
logger.remove()
logger.add(
-2
View File
@@ -5,5 +5,3 @@ def dict_to_argv(d):
if v is not None:
argv.append(str(v))
return argv