| 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
|
| sep: ";"
|
| shell: pwsh
|
| - os: windows-latest
|
| variant: GPU
|
| platform: Windows
|
| pytorch_url: https://download.pytorch.org/whl/cu121
|
| sep: ";"
|
| shell: pwsh
|
| - os: ubuntu-latest
|
| variant: CPU
|
| platform: Linux
|
| pytorch_url: https://download.pytorch.org/whl/cpu
|
| sep: ":"
|
| shell: bash
|
| - os: ubuntu-latest
|
| variant: GPU
|
| platform: Linux
|
| pytorch_url: https://download.pytorch.org/whl/cu121
|
| 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 "jinja2<3.1.5"
|
| pip install gradio==3.50.2 librosa soundfile scipy numpy praat-parselmouth pyworld faiss-cpu tqdm requests python-dotenv colorama huggingface_hub pedalboard ffmpeg-python av imageio-ffmpeg
|
| 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 onnxruntime
|
|
|
| - 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 (HuBERT, RMVPE, UVR5, DeEcho, pretrained) ==="
|
| python tools/download_models.py --all
|
|
|
| echo "=== Download Roformer separator models ==="
|
| 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)
|
| # Vocal separation model
|
| sep = Separator(output_dir='.', model_file_dir=model_dir)
|
| sep.load_model('model_bs_roformer_ep_317_sdr_12.9755.ckpt')
|
| del sep
|
| # Karaoke SOTA ensemble models
|
| for karaoke_model in [
|
| 'mel_band_roformer_karaoke_aufr33_viperx_sdr_10.1956.ckpt',
|
| 'mel_band_roformer_karaoke_gabox_v2.ckpt',
|
| 'mel_band_roformer_karaoke_becruily.ckpt',
|
| ]:
|
| sep2 = Separator(output_dir='.', model_file_dir=model_dir)
|
| sep2.load_model(karaoke_model)
|
| del sep2
|
| 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 "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}"
|
|
|
| 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
|
|
|
| 7z a -tzip "${PKG}.zip" "${PKG}"
|
|
|
| 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 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
|
|
|
| rm "${PKG}.7z"
|
| 7z a -v1900m "${PKG}.7z" "${PKG}"
|
| fi
|
| fi
|
| else
|
| tar -czf "${PKG}.tar.gz" "${PKG}"
|
|
|
| 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*
|
|
|
| upload-release:
|
| needs: [build]
|
| runs-on: ubuntu-latest
|
|
|
| steps:
|
| - name: Download all artifacts
|
| uses: actions/download-artifact@v4
|
| with:
|
| merge-multiple: true
|
|
|
| - name: List artifacts
|
| run: ls -lh AI-RVC-*
|
|
|
| - name: Determine target tag
|
| id: tag
|
| 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
|
| run: |
|
| TAG="${{ steps.tag.outputs.tag }}"
|
| echo "上传资源到 Release: ${TAG}"
|
|
|
| mapfile -t FILES < <(find . -maxdepth 1 -name "AI-RVC-*-Portable*" -type f | sort)
|
| 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 timeout 30m 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 }}
|
|
|