diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe078799..2a6f55a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,6 +180,66 @@ jobs: name: audio-${{ needs.get-version.outputs.tag }}-bin-ubuntu-x64-vulkan path: build/bin/* + linux-cuda-colab: + name: Ubuntu x64 (CUDA 12.8, Colab T4) + needs: [check-release, get-version] + if: ${{ needs.check-release.outputs.should_release == 'true' }} + runs-on: ubuntu-latest + container: nvidia/cuda:12.8.1-devel-ubuntu22.04 + env: + CC: gcc-13 + CXX: g++-13 + CUDAHOSTCXX: g++-13 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install dependencies + run: | + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq software-properties-common + add-apt-repository -y ppa:ubuntu-toolchain-r/test + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gcc-13 g++-13 cmake ninja-build libssl-dev + - name: Configure + run: | + cmake -S . -B build/colab-cuda -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CUDA_ARCHITECTURES=75 \ + -DCMAKE_EXE_LINKER_FLAGS='-static-libstdc++ -static-libgcc' \ + -DENGINE_ENABLE_CUDA=ON \ + -DENGINE_ENABLE_VULKAN=OFF \ + -DENGINE_ENABLE_NATIVE_CPU=OFF \ + -DENGINE_BUILD_TESTS=OFF \ + -DAUDIOCPP_VERSION=${{ needs.get-version.outputs.version }} \ + -DAUDIOCPP_DEPLOYMENT_BUILD=ON \ + -DAUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON \ + -DAUDIOCPP_USE_SYSTEM_OPENSSL=ON \ + -DENGINE_ENABLE_LLAMAFILE=ON \ + -DENGINE_ENABLE_CUDA_GRAPHS=ON + - name: Build server + run: cmake --build build/colab-cuda --parallel "$(nproc)" --target audiocpp_server + - name: Verify portable executable + run: | + build/colab-cuda/bin/audiocpp_server --version + if ldd build/colab-cuda/bin/audiocpp_server | grep -Eq 'not found|libstdc\+\+|libgcc_s'; then + echo "::error::The Colab executable has missing or non-portable runtime dependencies." + ldd build/colab-cuda/bin/audiocpp_server + exit 1 + fi + - name: Stage bundle + run: | + mkdir -p dist/models + install -m 0755 build/colab-cuda/bin/audiocpp_server dist/audiocpp_server + install -m 0644 LICENSE dist/LICENSE + cp -r tools dist/tools + cp -r model_specs dist/model_specs + sha256sum dist/audiocpp_server > dist/SHA256SUMS + - uses: actions/upload-artifact@v4 + with: + name: audio-${{ needs.get-version.outputs.tag }}-bin-ubuntu-x64-cuda12.8-colab + path: dist/* + windows-cpu: name: Windows x64 (CPU) needs: [check-release, get-version] @@ -459,6 +519,7 @@ jobs: - macos-metal - linux-cpu - linux-vulkan + - linux-cuda-colab - windows-cpu - windows-cpu-portable - windows-vulkan diff --git a/Notebooks/colab_audio_cpp.ipynb b/Notebooks/colab_audio_cpp.ipynb new file mode 100644 index 00000000..0a54594a --- /dev/null +++ b/Notebooks/colab_audio_cpp.ipynb @@ -0,0 +1,347 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "gpuType": "T4", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open\n", + "\n", + "# audio.cpp WebUI\n", + "Run the cell below to build audio.cpp with CUDA and open its WebUI. The public link remains active while the cell is running. Anyone with the link can use the WebUI, so do not share it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "start-audio-cpp", + "cellView": "form", + "collapsed": true + }, + "outputs": [], + "source": [ + "# @title Start audio.cpp\n", + "\n", + "import json\n", + "import os\n", + "import platform\n", + "import re\n", + "import hashlib\n", + "import shutil\n", + "import socket\n", + "import subprocess\n", + "import tarfile\n", + "import time\n", + "import urllib.request\n", + "from pathlib import Path\n", + "\n", + "from IPython.display import HTML, clear_output, display\n", + "\n", + "REPO_DIR = Path(\"/content/audio.cpp\")\n", + "REPO_URL = \"https://github.com/0xShug0/audio.cpp.git\"\n", + "REPO_BRANCH = \"main\"\n", + "BUILD_DIR = REPO_DIR / \"build/colab-cuda\"\n", + "with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as port_socket:\n", + " port_socket.bind((\"127.0.0.1\", 0))\n", + " SERVER_PORT = port_socket.getsockname()[1]\n", + "BUILD_LOG = Path(\"/content/audio_cpp_build.log\")\n", + "SERVER_LOG = Path(\"/content/audio_cpp_server.log\")\n", + "TUNNEL_LOG = Path(\"/content/audio_cpp_tunnel.log\")\n", + "CLOUDFLARED = Path(\"/content/cloudflared\")\n", + "RELEASE_API_URL = \"https://api.github.com/repos/0xShug0/audio.cpp/releases/latest\"\n", + "PREBUILT_ASSET_SUFFIX = \"-bin-ubuntu-x64-cuda12.8-colab.tar.gz\"\n", + "PREBUILT_CUDA_ARCHS = {\"75\"}\n", + "PREBUILT_ROOT = Path(\"/content/audio-cpp-prebuilt\")\n", + "\n", + "def latest_colab_prebuilt():\n", + " request = urllib.request.Request(\n", + " RELEASE_API_URL,\n", + " headers={\"Accept\": \"application/vnd.github+json\", \"User-Agent\": \"audio.cpp-colab\"},\n", + " )\n", + " with urllib.request.urlopen(request, timeout=30) as response:\n", + " release = json.load(response)\n", + " matches = [asset for asset in release.get(\"assets\", []) if asset[\"name\"].endswith(PREBUILT_ASSET_SUFFIX)]\n", + " if len(matches) != 1:\n", + " raise RuntimeError(\"The latest release does not contain a Colab CUDA prebuilt.\")\n", + " asset = matches[0]\n", + " digest = asset.get(\"digest\", \"\")\n", + " if not digest.startswith(\"sha256:\"):\n", + " raise RuntimeError(\"The Colab CUDA prebuilt does not have a SHA-256 digest.\")\n", + " return asset[\"browser_download_url\"], digest.removeprefix(\"sha256:\")\n", + "\n", + "def run(command, *, cwd=None, env=None, log=None):\n", + " if log is None:\n", + " subprocess.run(command, cwd=cwd, env=env, check=True)\n", + " return\n", + " with log.open(\"a\", encoding=\"utf-8\") as output:\n", + " try:\n", + " subprocess.run(\n", + " command, cwd=cwd, env=env, check=True, stdout=output, stderr=subprocess.STDOUT\n", + " )\n", + " except subprocess.CalledProcessError as error:\n", + " command_text = \" \".join(map(str, command))\n", + " raise RuntimeError(f\"Command failed: {command_text}\\n\\n{tail(log)}\") from error\n", + "\n", + "def tail(path, line_count=40):\n", + " if not path.exists():\n", + " return \"\"\n", + " return \"\".join(path.read_text(encoding=\"utf-8\", errors=\"replace\").splitlines(True)[-line_count:])\n", + "\n", + "def run_build(command, *, cwd, env, log):\n", + " progress_pattern = re.compile(r\"\\[(\\d+)/(\\d+)\\]\")\n", + " with log.open(\"a\", encoding=\"utf-8\") as output:\n", + " process = subprocess.Popen(\n", + " command, cwd=cwd, env=env, stdout=output, stderr=subprocess.STDOUT\n", + " )\n", + " last_progress = None\n", + " while process.poll() is None:\n", + " matches = progress_pattern.findall(\n", + " log.read_text(encoding=\"utf-8\", errors=\"replace\")\n", + " )\n", + " if matches and matches[-1] != last_progress:\n", + " completed, total = map(int, matches[-1])\n", + " width = 30\n", + " filled = min(width, completed * width // total)\n", + " bar = \"#\" * filled + \"-\" * (width - filled)\n", + " percent = completed * 100 // total\n", + " clear_output(wait=True)\n", + " print(f\"Building audio.cpp with CUDA [{bar}] {percent}% ({completed}/{total})\")\n", + " last_progress = matches[-1]\n", + " time.sleep(1)\n", + " return_code = process.wait()\n", + " if return_code != 0:\n", + " command_text = \" \".join(map(str, command))\n", + " raise RuntimeError(f\"Command failed: {command_text}\\n\\n{tail(log)}\")\n", + "\n", + "def stop(process):\n", + " if process is None or process.poll() is not None:\n", + " return\n", + " process.terminate()\n", + " try:\n", + " process.wait(timeout=10)\n", + " except subprocess.TimeoutExpired:\n", + " process.kill()\n", + " process.wait()\n", + "\n", + "if subprocess.run(\n", + " [\"nvidia-smi\"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL\n", + ").returncode != 0:\n", + " raise RuntimeError(\n", + " \"No NVIDIA GPU was detected. In Colab, select Runtime > Change runtime type > T4 GPU, \"\n", + " \"restart the session, and run this cell again.\"\n", + " )\n", + "\n", + "if platform.machine() != \"x86_64\":\n", + " raise RuntimeError(f\"This notebook currently supports x86_64 Colab runtimes, not {platform.machine()}.\")\n", + "\n", + "BUILD_LOG.write_text(\"\", encoding=\"utf-8\")\n", + "SERVER_LOG.write_text(\"\", encoding=\"utf-8\")\n", + "TUNNEL_LOG.write_text(\"\", encoding=\"utf-8\")\n", + "\n", + "print(\"Preparing the Colab runtime...\", flush=True)\n", + "run([\"apt-get\", \"update\", \"-qq\"], log=BUILD_LOG)\n", + "run(\n", + " [\n", + " \"apt-get\", \"install\", \"-y\", \"-qq\",\n", + " \"ca-certificates\", \"curl\", \"ffmpeg\", \"libgomp1\", \"libssl3\",\n", + " ],\n", + " log=BUILD_LOG,\n", + ")\n", + "\n", + "compute_capability = subprocess.check_output(\n", + " [\"nvidia-smi\", \"--query-gpu=compute_cap\", \"--format=csv,noheader\"], text=True\n", + ").splitlines()[0].strip()\n", + "cuda_arch = compute_capability.replace(\".\", \"\")\n", + "if not cuda_arch.isdigit():\n", + " raise RuntimeError(f\"Could not determine the CUDA architecture from: {compute_capability!r}\")\n", + "\n", + "server_binary = None\n", + "if cuda_arch in PREBUILT_CUDA_ARCHS:\n", + " archive_path = None\n", + " try:\n", + " prebuilt_url, prebuilt_sha256 = latest_colab_prebuilt()\n", + " archive_path = Path(\"/content\") / prebuilt_url.rsplit(\"/\", 1)[-1]\n", + " print(\"Downloading the CUDA prebuilt...\", flush=True)\n", + " run([\"curl\", \"-L\", \"--fail\", \"--silent\", \"--show-error\", prebuilt_url, \"-o\", str(archive_path)], log=BUILD_LOG)\n", + " actual_sha256 = hashlib.sha256(archive_path.read_bytes()).hexdigest()\n", + " if actual_sha256 != prebuilt_sha256:\n", + " raise RuntimeError(\"The downloaded CUDA prebuilt failed SHA-256 verification.\")\n", + " if PREBUILT_ROOT.exists():\n", + " shutil.rmtree(PREBUILT_ROOT)\n", + " PREBUILT_ROOT.mkdir(parents=True)\n", + " with tarfile.open(archive_path, \"r:gz\") as archive:\n", + " destination = PREBUILT_ROOT.resolve()\n", + " for member in archive.getmembers():\n", + " member_path = (PREBUILT_ROOT / member.name).resolve()\n", + " if destination not in member_path.parents and member_path != destination:\n", + " raise RuntimeError(\"The CUDA prebuilt contains an unsafe path.\")\n", + " archive.extractall(PREBUILT_ROOT, filter=\"data\")\n", + " matches = list(PREBUILT_ROOT.rglob(\"audiocpp_server\"))\n", + " if len(matches) != 1:\n", + " raise RuntimeError(\"The CUDA prebuilt does not contain one audiocpp_server binary.\")\n", + " server_binary = matches[0]\n", + " server_binary.chmod(0o755)\n", + " except Exception as error:\n", + " print(f\"The CUDA prebuilt could not be used; building from source instead. ({error})\", flush=True)\n", + " server_binary = None\n", + " finally:\n", + " if archive_path is not None:\n", + " archive_path.unlink(missing_ok=True)\n", + "\n", + "if server_binary is None:\n", + " run(\n", + " [\n", + " \"apt-get\", \"install\", \"-y\", \"-qq\", \"build-essential\", \"cmake\",\n", + " \"libssl-dev\", \"ninja-build\", \"software-properties-common\",\n", + " ],\n", + " log=BUILD_LOG,\n", + " )\n", + " if subprocess.run([\"bash\", \"-lc\", \"command -v gcc-13 && command -v g++-13\"], stdout=subprocess.DEVNULL).returncode != 0:\n", + " run([\"add-apt-repository\", \"-y\", \"ppa:ubuntu-toolchain-r/test\"], log=BUILD_LOG)\n", + " run([\"apt-get\", \"update\", \"-qq\"], log=BUILD_LOG)\n", + " run([\"apt-get\", \"install\", \"-y\", \"-qq\", \"gcc-13\", \"g++-13\"], log=BUILD_LOG)\n", + "\n", + " if not (REPO_DIR / \".git\").exists():\n", + " if REPO_DIR.exists():\n", + " raise RuntimeError(f\"{REPO_DIR} exists but is not a Git checkout. Restart the runtime and try again.\")\n", + " print(\"Downloading audio.cpp source...\", flush=True)\n", + " run([\"git\", \"clone\", \"--depth=1\", \"--branch\", REPO_BRANCH, REPO_URL, str(REPO_DIR)], log=BUILD_LOG)\n", + " else:\n", + " print(\"Using the existing audio.cpp checkout.\", flush=True)\n", + "\n", + " build_env = os.environ.copy()\n", + " build_env.update({\"CC\": \"gcc-13\", \"CXX\": \"g++-13\", \"CUDAHOSTCXX\": \"g++-13\"})\n", + " jobs = max(1, min(os.cpu_count() or 2, 4))\n", + " server_binary = BUILD_DIR / \"bin/audiocpp_server\"\n", + " print(\"Building audio.cpp with CUDA. This can take several minutes...\", flush=True)\n", + " run_build(\n", + " [\n", + " \"bash\", \"scripts/build_linux.sh\",\n", + " \"--backend\", \"cuda\",\n", + " \"--build-dir\", str(BUILD_DIR),\n", + " \"--build-type\", \"Release\",\n", + " \"--cuda-arch\", cuda_arch,\n", + " \"--deployment-build\",\n", + " \"--native-model-manager\",\n", + " \"--system-openssl\",\n", + " \"--target\", \"audiocpp_server\",\n", + " \"--jobs\", str(jobs),\n", + " ],\n", + " cwd=REPO_DIR,\n", + " env=build_env,\n", + " log=BUILD_LOG,\n", + " )\n", + "if not CLOUDFLARED.exists():\n", + " print(\"Installing the public tunnel...\", flush=True)\n", + " run(\n", + " [\n", + " \"curl\", \"-L\", \"--fail\", \"--silent\", \"--show-error\",\n", + " \"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64\",\n", + " \"-o\", str(CLOUDFLARED),\n", + " ],\n", + " log=BUILD_LOG,\n", + " )\n", + " CLOUDFLARED.chmod(0o755)\n", + "\n", + "server_process = None\n", + "tunnel_process = None\n", + "server_output = None\n", + "tunnel_output = None\n", + "\n", + "try:\n", + " print(\"Starting the audio.cpp WebUI...\", flush=True)\n", + " server_output = SERVER_LOG.open(\"w\", encoding=\"utf-8\")\n", + " server_process = subprocess.Popen(\n", + " [\n", + " str(server_binary), \"--ui\", \"--ui-management\",\n", + " \"--backend\", \"cuda\", \"--host\", \"127.0.0.1\",\n", + " \"--port\", str(SERVER_PORT),\n", + " ],\n", + " cwd=server_binary.parent,\n", + " stdout=server_output,\n", + " stderr=subprocess.STDOUT,\n", + " text=True,\n", + " )\n", + "\n", + " health_url = f\"http://127.0.0.1:{SERVER_PORT}/health\"\n", + " deadline = time.monotonic() + 120\n", + " while time.monotonic() < deadline:\n", + " if server_process.poll() is not None:\n", + " raise RuntimeError(\"audio.cpp stopped during startup.\\n\\n\" + tail(SERVER_LOG))\n", + " try:\n", + " with urllib.request.urlopen(health_url, timeout=2) as response:\n", + " if response.status == 200:\n", + " break\n", + " except Exception:\n", + " time.sleep(1)\n", + " else:\n", + " raise RuntimeError(\"audio.cpp did not become ready.\\n\\n\" + tail(SERVER_LOG))\n", + "\n", + " tunnel_output = TUNNEL_LOG.open(\"w\", encoding=\"utf-8\")\n", + " tunnel_process = subprocess.Popen(\n", + " [str(CLOUDFLARED), \"tunnel\", \"--url\", f\"http://127.0.0.1:{SERVER_PORT}\", \"--no-autoupdate\"],\n", + " stdout=tunnel_output,\n", + " stderr=subprocess.STDOUT,\n", + " text=True,\n", + " )\n", + "\n", + " public_url = None\n", + " deadline = time.monotonic() + 90\n", + " while time.monotonic() < deadline:\n", + " if tunnel_process.poll() is not None:\n", + " raise RuntimeError(\"The public tunnel stopped during startup.\\n\\n\" + tail(TUNNEL_LOG))\n", + " match = re.search(r\"https://[a-z0-9-]+\\.trycloudflare\\.com\", TUNNEL_LOG.read_text(encoding=\"utf-8\", errors=\"replace\"))\n", + " if match:\n", + " public_url = match.group(0)\n", + " break\n", + " time.sleep(1)\n", + " if public_url is None:\n", + " raise RuntimeError(\"The public tunnel did not provide a URL.\\n\\n\" + tail(TUNNEL_LOG))\n", + "\n", + " clear_output(wait=True)\n", + " display(HTML(\n", + " f'

audio.cpp is running

'\n", + " f'

Open the audio.cpp WebUI

'\n", + " '

Keep this cell running while you use the WebUI. Anyone with the link can access this session.

'\n", + " ))\n", + "\n", + " while True:\n", + " if server_process.poll() is not None:\n", + " raise RuntimeError(\"audio.cpp stopped unexpectedly.\\n\\n\" + tail(SERVER_LOG))\n", + " if tunnel_process.poll() is not None:\n", + " raise RuntimeError(\"The public tunnel stopped unexpectedly.\\n\\n\" + tail(TUNNEL_LOG))\n", + " time.sleep(5)\n", + "except KeyboardInterrupt:\n", + " clear_output(wait=True)\n", + " print(\"audio.cpp stopped.\")\n", + "finally:\n", + " stop(tunnel_process)\n", + " stop(server_process)\n", + " if tunnel_output is not None:\n", + " tunnel_output.close()\n", + " if server_output is not None:\n", + " server_output.close()\n" + ] + } + ] +} diff --git a/README.md b/README.md index 3f019d74..15fb7f9c 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,8 @@ package notes. ## WebUI ![Maintained by contributors](https://img.shields.io/badge/maintained%20by-contributors-brightgreen) +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/0xShug0/audio.cpp/blob/main/Notebooks/colab_audio_cpp.ipynb) + `audiocpp_server` includes an embedded SvelteKit/TypeScript WebUI for running local TTS, cloning, ASR, generation, conversion, separation, VAD, diarization, and alignment workflows. The production UI is compiled into the server binary, so using it requires neither Python nor separate frontend files: