Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/sensevoice-container.yml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 14 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<commit>` tags are available in the [container package](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice).

### Build with Docker
```bash
docker build -t sensevoice .
Expand All @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<commit>` タグは[コンテナパッケージ](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice)で確認できます。CPU モードでは `-e SENSEVOICE_DEVICE=cpu` を追加し、`--gpus all` を削除します。

## 微調整

### トレーニング環境のインストール
Expand Down
14 changes: 14 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<commit>` tag 见 [容器包页面](https://github.com/QwenAudio/SenseVoice/pkgs/container/sensevoice)。CPU 模式增加 `-e SENSEVOICE_DEVICE=cpu` 并移除 `--gpus all`。

## 微调

### 安装训练环境
Expand Down
54 changes: 54 additions & 0 deletions tests/test_container_contract.py
Original file line number Diff line number Diff line change
@@ -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()
Loading