From cdb9fe28066e17e8cb5b6ff9bd9e75b3f35bf614 Mon Sep 17 00:00:00 2001 From: LauraGPT <18321252+LauraGPT@users.noreply.github.com> Date: Sun, 30 Aug 2026 04:51:51 +0000 Subject: [PATCH] ci(container): publish SenseVoice runtime to GHCR Signed-off-by: LauraGPT <18321252+LauraGPT@users.noreply.github.com> --- .github/workflows/sensevoice-container.yml | 78 ++++++++++++++++++++++ Dockerfile | 19 ++++-- README.md | 16 ++++- README_ja.md | 14 ++++ README_zh.md | 14 ++++ tests/test_container_contract.py | 54 +++++++++++++++ 6 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/sensevoice-container.yml create mode 100644 tests/test_container_contract.py diff --git a/.github/workflows/sensevoice-container.yml b/.github/workflows/sensevoice-container.yml new file mode 100644 index 0000000..7826355 --- /dev/null +++ b/.github/workflows/sensevoice-container.yml @@ -0,0 +1,78 @@ +name: SenseVoice container + +on: + pull_request: + paths: + - Dockerfile + - requirements.txt + - api.py + - model.py + - .github/workflows/sensevoice-container.yml + - tests/test_container_contract.py + push: + branches: + - main + tags: + - "v*" + paths: + - Dockerfile + - requirements.txt + - api.py + - model.py + - .github/workflows/sensevoice-container.yml + - tests/test_container_contract.py + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: sensevoice-container-${{ github.ref }} + cancel-in-progress: true + +jobs: + contract: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate container contract + run: python -m unittest tests.test_container_contract + + build: + needs: contract + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/sensevoice + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=sha + + - name: Build and publish + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true diff --git a/Dockerfile b/Dockerfile index 3e95698..f4c354d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,26 @@ # ====================================================== # FunASR SenseVoiceSmall Inference Server # ====================================================== -FROM pytorch/pytorch:2.3.1-cuda12.1-cudnn8-runtime +FROM pytorch/pytorch:2.12.1-cuda12.6-cudnn9-runtime@sha256:79c5599719e0b1afdb56ac2d14588b530283752d7ae6ec3c36e18ec9deb8b229 + +LABEL org.opencontainers.image.source="https://github.com/QwenAudio/SenseVoice" \ + org.opencontainers.image.description="SenseVoiceSmall FastAPI inference server" \ + org.opencontainers.image.licenses="Apache-2.0" # Install system dependencies -RUN apt-get update && apt-get install -y \ - ffmpeg libsndfile1 git && \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg libsndfile1 git python3-venv && \ rm -rf /var/lib/apt/lists/* WORKDIR /app -RUN ls -la /app +RUN python -m venv --system-site-packages /opt/sensevoice-venv +ENV PATH=/opt/sensevoice-venv/bin:$PATH + # Copy only requirements first COPY requirements.txt /app/ # Install dependencies (cached if requirements.txt didn't change) -RUN pip install --no-cache-dir -r requirements.txt +RUN python -m pip install --no-cache-dir -r requirements.txt # Now copy the rest of your code COPY . /app @@ -34,5 +40,8 @@ ENV MODELSCOPE_CACHE=/models # Create model cache directory (helps reuse between restarts) RUN mkdir -p /models +HEALTHCHECK --interval=30s --timeout=5s --start-period=5m --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:50000/', timeout=3)" || exit 1 + # Start FastAPI app CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "50000"] diff --git a/README.md b/README.md index 61af53c..1cb8aba 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,20 @@ pip3 install -e ./ SenseVoice can be built and run using Docker to simplify setup, ensure reproducibility, and support both CPU and GPU inference. +### Official image + +The official `linux/amd64` runtime image is published to GitHub Container Registry. Model weights are not embedded in the image; they are downloaded into the mounted `/models` cache on first start. + +```bash +docker pull ghcr.io/qwenaudio/sensevoice:latest +docker run --rm --gpus all \ + -p 50000:50000 \ + -v sensevoice-models:/models \ + ghcr.io/qwenaudio/sensevoice:latest +``` + +Open `http://127.0.0.1:50000/docs` after the container becomes healthy. Release tags and immutable `sha-` tags are available in the [container package](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice). + ### Build with Docker ```bash docker build -t sensevoice . @@ -314,7 +328,7 @@ docker run --gpus all -p 50000:50000 sensevoice ``` ### Run (CPU-only) ```bash -docker run -e SENSEVOICE_DEVICE=cpu -p 50000:50000 sensevoice +docker run --rm -e SENSEVOICE_DEVICE=cpu -p 50000:50000 -v sensevoice-models:/models ghcr.io/qwenaudio/sensevoice:latest ``` ### Docker Compose Docker Compose provides an easier way to run SenseVoice with persistent model caching, networking etc. diff --git a/README_ja.md b/README_ja.md index 1877f8f..754ba1a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -246,6 +246,20 @@ export SENSEVOICE_DEVICE=cuda:0 fastapi run --port 50000 ``` +### 公式 Docker イメージ + +公式の `linux/amd64` ランタイムイメージは GitHub Container Registry で公開されます。モデル重みはイメージに含まれず、初回起動時にマウントした `/models` キャッシュへダウンロードされます。 + +```bash +docker pull ghcr.io/qwenaudio/sensevoice:latest +docker run --rm --gpus all \ + -p 50000:50000 \ + -v sensevoice-models:/models \ + ghcr.io/qwenaudio/sensevoice:latest +``` + +コンテナが healthy になったら `http://127.0.0.1:50000/docs` を開いてください。リリースタグと不変の `sha-` タグは[コンテナパッケージ](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice)で確認できます。CPU モードでは `-e SENSEVOICE_DEVICE=cpu` を追加し、`--gpus all` を削除します。 + ## 微調整 ### トレーニング環境のインストール diff --git a/README_zh.md b/README_zh.md index a69b882..ce0e7fe 100644 --- a/README_zh.md +++ b/README_zh.md @@ -274,6 +274,20 @@ export SENSEVOICE_DEVICE=cuda:0 fastapi run --port 50000 ``` +### Docker 官方镜像 + +官方 `linux/amd64` 运行时镜像发布在 GitHub Container Registry。镜像不内置模型权重;首次启动会把权重下载到挂载的 `/models` 缓存。 + +```bash +docker pull ghcr.io/qwenaudio/sensevoice:latest +docker run --rm --gpus all \ + -p 50000:50000 \ + -v sensevoice-models:/models \ + ghcr.io/qwenaudio/sensevoice:latest +``` + +容器健康后访问 `http://127.0.0.1:50000/docs`。版本 tag 和不可变的 `sha-` tag 见 [容器包页面](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice)。CPU 模式增加 `-e SENSEVOICE_DEVICE=cpu` 并移除 `--gpus all`。 + ## 微调 ### 安装训练环境 diff --git a/tests/test_container_contract.py b/tests/test_container_contract.py new file mode 100644 index 0000000..082b4c5 --- /dev/null +++ b/tests/test_container_contract.py @@ -0,0 +1,54 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class ContainerContractTest(unittest.TestCase): + def test_container_base_matches_repository_torch_floor(self): + dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") + + self.assertIn( + "pytorch/pytorch:2.12.1-cuda12.6-cudnn9-runtime@sha256:", + dockerfile, + ) + self.assertNotIn("pytorch/pytorch:2.3.1", dockerfile) + + def test_container_installs_dependencies_in_an_isolated_venv(self): + dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") + + self.assertIn( + "python -m venv --system-site-packages /opt/sensevoice-venv", + dockerfile, + ) + self.assertIn("ENV PATH=/opt/sensevoice-venv/bin:$PATH", dockerfile) + self.assertNotIn("--break-system-packages", dockerfile) + + def test_container_workflow_builds_prs_and_only_pushes_trusted_refs(self): + workflow = ( + ROOT / ".github" / "workflows" / "sensevoice-container.yml" + ).read_text(encoding="utf-8") + + self.assertIn("pull_request:", workflow) + self.assertIn("packages: write", workflow) + self.assertIn("platforms: linux/amd64", workflow) + self.assertIn("push: ${{ github.event_name != 'pull_request' }}", workflow) + self.assertIn( + "ghcr.io/${{ github.repository_owner }}/sensevoice", + workflow, + ) + + def test_readme_uses_public_ghcr_image_and_real_service_port(self): + readme = (ROOT / "README.md").read_text(encoding="utf-8") + + self.assertIn("ghcr.io/qwenaudio/sensevoice", readme.lower()) + self.assertIn("-p 50000:50000", readme) + self.assertNotIn( + "registry.cn-hangzhou.aliyuncs.com/funasr/sensevoice", + readme, + ) + + +if __name__ == "__main__": + unittest.main()