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}" # 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* 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 }}