commit c1819b66b7494d74769a94d810a48563034e7fb7 Author: Dell Date: Tue Jun 30 14:12:33 2026 +0800 Add ASR CPU code and image backup diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3e255f3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docker-images/*.tar.gz filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78d3654 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.cache/ +*.log +*.tmp +*.swp +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0025534 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..70e9b49 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..7a5248a --- /dev/null +++ b/app.py @@ -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) diff --git a/docker-images/asr_cpu_latest.docker-save.tar.gz b/docker-images/asr_cpu_latest.docker-save.tar.gz new file mode 100644 index 0000000..4a2a25a --- /dev/null +++ b/docker-images/asr_cpu_latest.docker-save.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87092ef06382b6125166226a56847d5bc6c0dcad1b40d2050c7b5fbea2b71d4 +size 6271213796 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fa7ee0d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +python-multipart +requests +faster-whisper +opencc-python-reimplemented