Add ASR CPU code and image backup

This commit is contained in:
Dell 2026-06-30 14:12:33 +08:00
commit c1819b66b7
7 changed files with 286 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
docker-images/*.tar.gz filter=lfs diff=lfs merge=lfs -text

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
.pytest_cache/
.cache/
*.log
*.tmp
*.swp
.DS_Store

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM paraformer-asr:npu-310p
ENV TORCH_DEVICE_BACKEND_AUTOLOAD=0 \
OPENBLAS_NUM_THREADS=4 \
OMP_NUM_THREADS=4 \
PYTHONUNBUFFERED=1 \
ASR_PORT=9805 \
ASR_ENGINE=paraformer
WORKDIR /opt/asr-cpu
COPY app.py /opt/asr-cpu/app.py
COPY requirements.txt /opt/asr-cpu/requirements.txt
RUN python3 -m pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple -r /opt/asr-cpu/requirements.txt
CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "9805"]

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# ASR CPU 镜像和代码备份
来源设备:`192.168.1.108`
本地代码来源:`D:\202606工程\ascend\asr-cpu`
镜像名称:`wxiu/asr-cpu:latest`
## 主要内容
- `app.py`ASR 服务主程序
- `Dockerfile`:镜像构建文件
- `requirements.txt`Python 依赖
- `docker-images/asr_cpu_latest.docker-save.tar.gz``wxiu/asr-cpu:latest` 镜像备份
## 大文件下载
本仓库使用 Git LFS 保存 Docker 镜像包。
克隆仓库后执行:
```bash
git lfs install
git lfs pull
```
如果镜像文件内容是 `version https://git-lfs.github.com/spec/v1`,说明当前只是 LFS 指针文件,还需要执行 `git lfs pull` 下载真实文件。
## 恢复镜像
```bash
gunzip -c docker-images/asr_cpu_latest.docker-save.tar.gz | docker load
```

218
app.py Normal file
View File

@ -0,0 +1,218 @@
import logging
import os
import tempfile
import time
from typing import Optional
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import PlainTextResponse
logger = logging.getLogger("asr_cpu")
logging.basicConfig(level=logging.INFO)
ASR_ENGINE = os.getenv("ASR_ENGINE", "paraformer").strip().lower()
ASR_PORT = int(os.getenv("ASR_PORT", "9805"))
PARAFORMER_MODEL = os.getenv("PARAFORMER_MODEL", "/models/paraformer-mindie-common")
PARAFORMER_BATCH_SIZE_S = int(os.getenv("PARAFORMER_BATCH_SIZE_S", "300"))
WHISPER_MODEL = os.getenv(
"WHISPER_MODEL", "/models/faster-whisper/Systran/faster-whisper-base"
)
WHISPER_DEVICE = os.getenv("WHISPER_DEVICE", "cpu")
WHISPER_COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8")
WHISPER_BEAM_SIZE = int(os.getenv("WHISPER_BEAM_SIZE", "5"))
WHISPER_VAD_FILTER = os.getenv("WHISPER_VAD_FILTER", "false").lower() == "true"
WHISPER_CPU_THREADS = int(os.getenv("WHISPER_CPU_THREADS", "4"))
WHISPER_FORCE_SIMPLIFIED = os.getenv("WHISPER_FORCE_SIMPLIFIED", "true").lower() == "true"
app = FastAPI(title="Unified CPU ASR API", version="1.0.0")
model = None
to_simplified = None
def _simplifier():
global to_simplified
if to_simplified is not None:
return to_simplified
if not WHISPER_FORCE_SIMPLIFIED:
to_simplified = lambda text: text
return to_simplified
try:
from opencc import OpenCC
cc = OpenCC("t2s")
to_simplified = lambda text: cc.convert(text) if text else text
except Exception as exc:
logger.warning("OpenCC unavailable, skip t2s conversion: %s", exc)
to_simplified = lambda text: text
return to_simplified
def load_model():
global model
if model is not None:
return model
started = time.perf_counter()
if ASR_ENGINE in {"paraformer", "paraformer-cpu", "funasr"}:
from funasr import AutoModel
logger.info("Loading Paraformer CPU model: %s", PARAFORMER_MODEL)
model = AutoModel(
model=PARAFORMER_MODEL,
device="cpu",
disable_update=True,
disable_pbar=True,
)
elif ASR_ENGINE in {"faster-whisper", "whisper", "whisper-base", "whisper-tiny"}:
from faster_whisper import WhisperModel
logger.info(
"Loading faster-whisper model=%s device=%s compute=%s",
WHISPER_MODEL,
WHISPER_DEVICE,
WHISPER_COMPUTE_TYPE,
)
model = WhisperModel(
WHISPER_MODEL,
device=WHISPER_DEVICE,
compute_type=WHISPER_COMPUTE_TYPE,
local_files_only=True,
cpu_threads=WHISPER_CPU_THREADS,
)
else:
raise RuntimeError(f"Unsupported ASR_ENGINE={ASR_ENGINE!r}")
logger.info("Model loaded in %.3fs", time.perf_counter() - started)
return model
@app.on_event("startup")
async def startup_event():
load_model()
@app.get("/health")
async def health():
return {
"status": "healthy",
"engine": ASR_ENGINE,
"paraformer_model": PARAFORMER_MODEL,
"whisper_model": WHISPER_MODEL,
}
@app.get("/")
async def root():
return {"status": "ok", "engine": ASR_ENGINE}
def _transcribe_paraformer(path: str):
res = load_model().generate(input=path, batch_size_s=PARAFORMER_BATCH_SIZE_S)
text = res[0].get("text", "") if res else ""
return {
"text": text.replace(" ", ""),
"language": "zh",
"segments": [],
}
def _transcribe_whisper(path: str, language: Optional[str]):
lang = language.strip() if language and language.strip() else None
segments, info = load_model().transcribe(
path,
language=lang,
beam_size=WHISPER_BEAM_SIZE,
vad_filter=WHISPER_VAD_FILTER,
)
convert = _simplifier()
seg_list = []
texts = []
for segment in segments:
text = convert(segment.text)
seg_list.append(
{
"id": len(seg_list),
"start": round(segment.start, 3),
"end": round(segment.end, 3),
"text": text,
}
)
texts.append(text)
return {
"text": "".join(texts).strip(),
"language": info.language,
"duration": round(info.duration, 3),
"segments": seg_list,
}
async def _handle_upload(
file: UploadFile,
language: Optional[str],
response_format: str,
):
suffix = os.path.splitext(file.filename or "")[1] or ".wav"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
content = await file.read()
tmp.write(content)
tmp_path = tmp.name
started = time.perf_counter()
try:
if ASR_ENGINE in {"paraformer", "paraformer-cpu", "funasr"}:
result = _transcribe_paraformer(tmp_path)
else:
result = _transcribe_whisper(tmp_path, language)
result["elapsed"] = round(time.perf_counter() - started, 3)
if response_format == "text":
return PlainTextResponse(result["text"])
if response_format == "verbose_json":
return {
"task": "transcribe",
"language": result.get("language", "zh"),
"duration": result.get("duration"),
"text": result["text"],
"segments": result.get("segments", []),
"elapsed": result["elapsed"],
}
return {
"text": result["text"],
"language": result.get("language", "zh"),
"elapsed": result["elapsed"],
}
finally:
try:
os.unlink(tmp_path)
except FileNotFoundError:
pass
@app.post("/v1/audio/transcriptions")
async def transcribe_v1(
file: UploadFile = File(...),
language: Optional[str] = Form(None),
response_format: Optional[str] = Form("json"),
model: Optional[str] = Form(None),
temperature: Optional[float] = Form(None),
prompt: Optional[str] = Form(None),
):
try:
return await _handle_upload(file, language, response_format or "json")
except Exception as exc:
logger.exception("ASR failed")
raise HTTPException(status_code=500, detail=str(exc))
@app.post("/api/v1/audio/transcriptions")
async def transcribe_api(
file: UploadFile = File(...),
language: Optional[str] = Form(None),
response_format: Optional[str] = Form("json"),
model: Optional[str] = Form(None),
temperature: Optional[float] = Form(None),
prompt: Optional[str] = Form(None),
):
return await transcribe_v1(file, language, response_format, model, temperature, prompt)

BIN
docker-images/asr_cpu_latest.docker-save.tar.gz (Stored with Git LFS) Normal file

Binary file not shown.

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
python-multipart
requests
faster-whisper
opencc-python-reimplemented