ci: stream release assets from matrix builds #29
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Executables | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: '要上传资源的 Release 标签(如 v1.2.0),留空则上传到最新 Release' | |
| required: false | |
| default: '' | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| variant: CPU | |
| platform: Windows | |
| pytorch_url: https://download.pytorch.org/whl/cpu | |
| audio_separator_extra: cpu | |
| sep: ";" | |
| shell: pwsh | |
| - os: windows-latest | |
| variant: GPU | |
| platform: Windows | |
| pytorch_url: https://download.pytorch.org/whl/cu121 | |
| audio_separator_extra: gpu | |
| sep: ";" | |
| shell: pwsh | |
| - os: ubuntu-latest | |
| variant: CPU | |
| platform: Linux | |
| pytorch_url: https://download.pytorch.org/whl/cpu | |
| audio_separator_extra: cpu | |
| sep: ":" | |
| shell: bash | |
| - os: ubuntu-latest | |
| variant: GPU | |
| platform: Linux | |
| pytorch_url: https://download.pytorch.org/whl/cu121 | |
| audio_separator_extra: gpu | |
| sep: ":" | |
| shell: bash | |
| runs-on: ${{ matrix.os }} | |
| name: Build ${{ matrix.platform }}-${{ matrix.variant }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential libsndfile1 ffmpeg portaudio19-dev | |
| - name: Install FFmpeg (Windows) | |
| if: runner.os == 'Windows' | |
| run: choco install ffmpeg -y | |
| - name: Install Python dependencies | |
| run: | | |
| python -m pip install --upgrade "pip<24.1" | |
| pip install pyinstaller | |
| pip install torch torchaudio --index-url ${{ matrix.pytorch_url }} | |
| pip install omegaconf==2.0.6 --no-deps | |
| pip install PyYAML antlr4-python3-runtime hydra-core | |
| pip install fairseq==0.12.2 --no-deps | |
| pip install "gradio==5.49.1" "fastapi>=0.115,<1" "anyio>=3,<5" "pydantic>=2,<3" "jinja2>=3.1,<4" "pandas>=2,<3" | |
| pip install "librosa>=0.10.0" "soundfile>=0.12.0" "scipy>=1.10.0" "numpy>=2,<3" "praat-parselmouth>=0.4.3" "pyworld>=0.3.4" "faiss-cpu>=1.7.4" "tqdm>=4.65.0" "requests>=2.28.0" "python-dotenv>=1.0.0" "colorama>=0.4.6" "huggingface_hub>=0.19.0,<1.0" "pedalboard>=0.7.0" "ffmpeg-python>=0.2.0" "av>=10.0.0" "imageio-ffmpeg>=0.5.1" | |
| pip install demucs torchcrepe --no-deps | |
| pip install julius dora-search lameenc openunmix treetable | |
| python -c "import torch; print(f'PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}')" | |
| - name: Install audio-separator | |
| run: pip install "audio-separator[${{ matrix.audio_separator_extra }}]==0.44.1" | |
| - name: Bundle FFmpeg runtime | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| import os | |
| import shutil | |
| import stat | |
| import subprocess | |
| from pathlib import Path | |
| import imageio_ffmpeg | |
| def check_executable(executable: Path) -> None: | |
| result = subprocess.run([str(executable), "-version"], text=True, capture_output=True) | |
| if result.returncode != 0: | |
| details = "\n".join( | |
| part.strip() | |
| for part in (result.stdout, result.stderr) | |
| if part and part.strip() | |
| ) | |
| raise RuntimeError(f"{executable} failed -version:\n{details}") | |
| def resolve_ffprobe() -> Path: | |
| ffprobe_src = shutil.which("ffprobe") | |
| if not ffprobe_src: | |
| raise RuntimeError("ffprobe not found after installing FFmpeg") | |
| ffprobe_path = Path(ffprobe_src) | |
| if os.name == "nt": | |
| chocolatey_root = Path(os.environ.get("ChocolateyInstall", r"C:\ProgramData\chocolatey")) | |
| chocolatey_shim_dir = chocolatey_root / "bin" | |
| if ffprobe_path.parent.resolve() == chocolatey_shim_dir.resolve(): | |
| real_ffprobe = chocolatey_root / "lib" / "ffmpeg" / "tools" / "ffmpeg" / "bin" / "ffprobe.exe" | |
| if not real_ffprobe.exists(): | |
| raise RuntimeError(f"Chocolatey ffprobe shim found, but real binary is missing: {real_ffprobe}") | |
| return real_ffprobe | |
| return ffprobe_path | |
| bundle_dir = Path("tools/ffmpeg/bin") | |
| bundle_dir.mkdir(parents=True, exist_ok=True) | |
| ffmpeg_src = Path(imageio_ffmpeg.get_ffmpeg_exe()) | |
| ffmpeg_name = "ffmpeg.exe" if os.name == "nt" else "ffmpeg" | |
| ffmpeg_dest = bundle_dir / ffmpeg_name | |
| shutil.copy2(ffmpeg_src, ffmpeg_dest) | |
| ffmpeg_dest.chmod(ffmpeg_dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | |
| check_executable(ffmpeg_dest) | |
| print(f"Bundled ffmpeg: {ffmpeg_dest}") | |
| ffprobe_src = resolve_ffprobe() | |
| ffprobe_name = "ffprobe.exe" if os.name == "nt" else "ffprobe" | |
| ffprobe_dest = bundle_dir / ffprobe_name | |
| shutil.copy2(ffprobe_src, ffprobe_dest) | |
| ffprobe_dest.chmod(ffprobe_dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | |
| subprocess.run([str(ffprobe_dest), "-version"], text=True, capture_output=True, check=True) | |
| print(f"Bundled ffprobe: {ffprobe_dest}") | |
| PY | |
| - name: Download all AI models | |
| shell: bash | |
| env: | |
| PYTHONIOENCODING: utf-8 | |
| run: | | |
| echo "=== Download all base models and official RVC source ===" | |
| python tools/download_models.py --all | |
| echo "=== Download current RoFormer separator defaults ===" | |
| python -c " | |
| from audio_separator.separator import Separator | |
| import os | |
| model_dir = os.path.join('assets', 'separator_models') | |
| os.makedirs(model_dir, exist_ok=True) | |
| for model_name in [ | |
| 'melband_roformer_big_beta6x.ckpt', | |
| 'mel_band_roformer_vocals_fv4_gabox.ckpt', | |
| 'dereverb_mel_band_roformer_anvuew_sdr_19.1729.ckpt', | |
| 'mel_band_roformer_karaoke_aufr33_viperx_sdr_10.1956.ckpt', | |
| 'mel_band_roformer_karaoke_gabox_v2.ckpt', | |
| 'mel_band_roformer_karaoke_becruily.ckpt', | |
| ]: | |
| sep = Separator(output_dir='.', model_file_dir=model_dir) | |
| sep.load_model(model_name) | |
| del sep | |
| print('Roformer models downloaded.') | |
| " | |
| echo "=== Verify downloaded models ===" | |
| find assets/ -name "*.pt" -o -name "*.pth" -o -name "*.ckpt" -o -name "*.onnx" | while read f; do | |
| SIZE=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f") | |
| echo " $f ($(( SIZE / 1048576 )) MB)" | |
| done | |
| - name: Build executable | |
| shell: bash | |
| run: | | |
| SEP="${{ matrix.sep }}" | |
| NAME="AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}" | |
| pyinstaller --name "${NAME}" \ | |
| --onedir \ | |
| --add-data "ui${SEP}ui" \ | |
| --add-data "infer${SEP}infer" \ | |
| --add-data "lib${SEP}lib" \ | |
| --add-data "models${SEP}models" \ | |
| --add-data "tools${SEP}tools" \ | |
| --add-data "i18n${SEP}i18n" \ | |
| --add-data "configs${SEP}configs" \ | |
| --add-data "_official_rvc${SEP}_official_rvc" \ | |
| --add-data "assets/hubert${SEP}assets/hubert" \ | |
| --add-data "assets/rmvpe${SEP}assets/rmvpe" \ | |
| --add-data "assets/uvr5_weights${SEP}assets/uvr5_weights" \ | |
| --add-data "assets/pretrained_v2${SEP}assets/pretrained_v2" \ | |
| --add-data "assets/separator_models${SEP}assets/separator_models" \ | |
| --hidden-import=torch \ | |
| --hidden-import=torchaudio \ | |
| --hidden-import=gradio \ | |
| --hidden-import=librosa \ | |
| --hidden-import=soundfile \ | |
| --hidden-import=fairseq \ | |
| --hidden-import=audio_separator \ | |
| --hidden-import=demucs \ | |
| --hidden-import=pedalboard \ | |
| --collect-all torch \ | |
| --collect-all torchaudio \ | |
| --collect-all gradio \ | |
| --collect-all gradio_client \ | |
| run.py | |
| - name: Create portable package | |
| shell: bash | |
| run: | | |
| NAME="AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}" | |
| PKG="${NAME}-Portable" | |
| mkdir -p "${PKG}" | |
| # onedir 输出在 dist/NAME/ 目录下,复制全部内容 | |
| cp -r dist/${NAME}/* "${PKG}/" | |
| cp README.md "${PKG}/" | |
| [ -f LICENSE ] && cp LICENSE "${PKG}/" | |
| if [ "${{ matrix.variant }}" = "GPU" ]; then | |
| VARIANT_NOTE="GPU 版(CUDA 12.1),支持 NVIDIA 显卡加速 | |
| 如果没有 NVIDIA 显卡,程序会自动回退到 CPU 推理" | |
| else | |
| VARIANT_NOTE="CPU 版,无需显卡即可运行 | |
| 如需 GPU 加速,请下载 GPU 版本或使用本地安装方式:python install.py" | |
| fi | |
| if [ "${{ matrix.platform }}" = "Windows" ]; then | |
| EXE_NAME="${NAME}.exe" | |
| cat > "${PKG}/使用说明.txt" << HEREDOC | |
| AI-RVC ${{ matrix.platform }} 便携版(${{ matrix.variant }}) | |
| 使用方法: | |
| 双击 ${EXE_NAME} 启动 | |
| 浏览器访问 http://127.0.0.1:7860 | |
| ${VARIANT_NOTE} | |
| AI 模型已内置,无需额外下载 | |
| 无需安装 Python,解压即用 | |
| HEREDOC | |
| else | |
| EXE_NAME="${NAME}" | |
| chmod +x "${PKG}/${EXE_NAME}" | |
| cat > "${PKG}/使用说明.txt" << HEREDOC | |
| AI-RVC ${{ matrix.platform }} 便携版(${{ matrix.variant }}) | |
| 使用方法: | |
| chmod +x ${EXE_NAME} | |
| ./${EXE_NAME} | |
| 浏览器访问 http://127.0.0.1:7860 | |
| ${VARIANT_NOTE} | |
| AI 模型已内置,无需额外下载 | |
| 无需安装 Python,解压即用 | |
| HEREDOC | |
| fi | |
| - name: Compress package | |
| shell: bash | |
| run: | | |
| NAME="AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}" | |
| PKG="${NAME}-Portable" | |
| if [ "${{ matrix.platform }}" = "Windows" ]; then | |
| # 先压缩成 zip | |
| 7z a -tzip "${PKG}.zip" "${PKG}" | |
| # 超过 1.9GB 时删除 zip 改用 7z(压缩率更高) | |
| FILE_SIZE=$(stat -c%s "${PKG}.zip" 2>/dev/null || wc -c < "${PKG}.zip" | tr -d ' ') | |
| if [ "$FILE_SIZE" -gt 1900000000 ]; then | |
| rm "${PKG}.zip" | |
| # 先尝试不分卷的 7z | |
| 7z a "${PKG}.7z" "${PKG}" | |
| SEVENZ_SIZE=$(stat -c%s "${PKG}.7z" 2>/dev/null || wc -c < "${PKG}.7z" | tr -d ' ') | |
| if [ "$SEVENZ_SIZE" -gt 1900000000 ]; then | |
| # 7z 也超限,改用分卷 | |
| rm "${PKG}.7z" | |
| 7z a -v1900m "${PKG}.7z" "${PKG}" | |
| fi | |
| fi | |
| else | |
| tar -czf "${PKG}.tar.gz" "${PKG}" | |
| # 超过 1.9GB 时拆分 | |
| FILE_SIZE=$(stat -c%s "${PKG}.tar.gz" 2>/dev/null || stat -f%z "${PKG}.tar.gz") | |
| if [ "$FILE_SIZE" -gt 1900000000 ]; then | |
| split -b 1900M "${PKG}.tar.gz" "${PKG}.tar.gz.part" | |
| rm "${PKG}.tar.gz" | |
| fi | |
| fi | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: AI-RVC-${{ matrix.platform }}-${{ matrix.variant }} | |
| path: | | |
| AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}-Portable.zip | |
| AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}-Portable.7z | |
| AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}-Portable.7z.* | |
| AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}-Portable.tar.gz | |
| AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}-Portable.tar.gz.part* | |
| if-no-files-found: error | |
| - name: Determine target tag | |
| id: tag | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| elif [ -n "${{ github.event.inputs.tag }}" ]; then | |
| echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT | |
| else | |
| LATEST=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName') | |
| echo "tag=${LATEST}" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload assets to release | |
| shell: bash | |
| run: | | |
| NAME="AI-RVC-${{ matrix.platform }}-${{ matrix.variant }}" | |
| PKG="${NAME}-Portable" | |
| TAG="${{ steps.tag.outputs.tag }}" | |
| echo "上传资源到 Release: ${TAG}" | |
| mapfile -t FILES < <( | |
| find . -maxdepth 1 -type f \( \ | |
| -name "${PKG}.zip" -o \ | |
| -name "${PKG}.7z" -o \ | |
| -name "${PKG}.7z.*" -o \ | |
| -name "${PKG}.tar.gz" -o \ | |
| -name "${PKG}.tar.gz.part*" \ | |
| \) | sort | |
| ) | |
| if [ "${#FILES[@]}" -eq 0 ]; then | |
| echo "没有找到可上传的压缩产物: ${PKG}" | |
| exit 1 | |
| fi | |
| upload_asset_with_retry() { | |
| local tag="$1" | |
| local file="$2" | |
| local attempt | |
| local exit_code | |
| for attempt in 1 2 3; do | |
| echo "上传(${attempt}/3): ${file}" | |
| if gh release upload "${tag}" "${file}" \ | |
| --repo ${{ github.repository }} \ | |
| --clobber; then | |
| return 0 | |
| fi | |
| exit_code=$? | |
| echo "上传失败(退出码=${exit_code}): ${file}" | |
| if [ "${attempt}" -lt 3 ]; then | |
| sleep $(( attempt * 20 )) | |
| fi | |
| done | |
| echo "上传最终失败: ${file}" | |
| return 1 | |
| } | |
| echo "找到以下文件:" | |
| for f in "${FILES[@]}"; do | |
| SIZE=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f") | |
| echo " $f ($(( SIZE / 1048576 )) MB)" | |
| done | |
| # 逐个上传 | |
| for f in "${FILES[@]}"; do | |
| upload_asset_with_retry "${TAG}" "$f" | |
| done | |
| echo "全部上传完成" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |