diff --git a/audio_server/.gitignore b/audio_server/.gitignore new file mode 100644 index 0000000..aa09c5e --- /dev/null +++ b/audio_server/.gitignore @@ -0,0 +1,3 @@ +temporary_audio/ +whisper_models/ +piper_models/ \ No newline at end of file diff --git a/audio_server/.idea/.gitignore b/audio_server/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/audio_server/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/audio_server/.idea/.name b/audio_server/.idea/.name new file mode 100644 index 0000000..f71cdab --- /dev/null +++ b/audio_server/.idea/.name @@ -0,0 +1 @@ +audio_server \ No newline at end of file diff --git a/audio_server/.idea/audio_server.iml b/audio_server/.idea/audio_server.iml new file mode 100644 index 0000000..847b460 --- /dev/null +++ b/audio_server/.idea/audio_server.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/audio_server/.idea/inspectionProfiles/Project_Default.xml b/audio_server/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7898187 --- /dev/null +++ b/audio_server/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/audio_server/.idea/inspectionProfiles/profiles_settings.xml b/audio_server/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/audio_server/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/audio_server/.idea/misc.xml b/audio_server/.idea/misc.xml new file mode 100644 index 0000000..1b83d63 --- /dev/null +++ b/audio_server/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/audio_server/.idea/modules.xml b/audio_server/.idea/modules.xml new file mode 100644 index 0000000..e934ccb --- /dev/null +++ b/audio_server/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/audio_server/.idea/vcs.xml b/audio_server/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/audio_server/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/audio_server/Containerfile b/audio_server/Containerfile new file mode 100644 index 0000000..eae642a --- /dev/null +++ b/audio_server/Containerfile @@ -0,0 +1,19 @@ +FROM python:3.14.4-slim AS builder + +WORKDIR /app +COPY pyproject.toml requirements.txt ./ +COPY src src + +RUN pip wheel --no-cache-dir --no-deps --wheel-dir wheels . + +FROM python:3.14.4-slim AS runner + +COPY --from=builder /app/wheels /wheels +RUN pip install --no-cache /wheels/* && rm -rf /wheels +RUN apt update +RUN apt -y install ffmpeg + +WORKDIR /app + +EXPOSE 8000 +CMD ["uvicorn", "audio_server:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/audio_server/pyproject.toml b/audio_server/pyproject.toml new file mode 100644 index 0000000..c6568cb --- /dev/null +++ b/audio_server/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=82.0.1", "wheel"] + +[project] +name = "audio_server" +description = "a fastapi server serving whisper and piper" +version = "1.0.0" +authors = [ + { name = 'Milan', email = 'milan.boemer@gmail.com' } +] + +requires-python = '>=3.12' + +dynamic = ["dependencies"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } + +[tool.setuptools.packages.find] +where = ['src'] \ No newline at end of file diff --git a/audio_server/requirements.txt b/audio_server/requirements.txt new file mode 100644 index 0000000..99d547f --- /dev/null +++ b/audio_server/requirements.txt @@ -0,0 +1,8 @@ +openai-whisper==20250625 +fastapi~=0.135.3 +psutil~=7.2.2 +uvicorn~=0.44.0 +starlette~=1.0.0 +python-multipart~=0.0.24 +aiofiles~=25.1.0 +piper-tts~=1.4.2 \ No newline at end of file diff --git a/audio_server/src/audio_server/__init__.py b/audio_server/src/audio_server/__init__.py new file mode 100644 index 0000000..67d4590 --- /dev/null +++ b/audio_server/src/audio_server/__init__.py @@ -0,0 +1,76 @@ +import logging +import os +import uuid +from wave import Wave_write +from pathlib import Path + +import aiofiles +from piper import PiperVoice, SynthesisConfig +from piper.download_voices import download_voice +import psutil +from fastapi import FastAPI, UploadFile +import whisper +from pydantic import BaseModel +from fastapi.responses import FileResponse, JSONResponse + +app = FastAPI() + +whisper_model_name = os.getenv("WHISPER_MODEL", default="small") +whisper_path = Path("whisper_models") +whisper_path.mkdir(parents=True, exist_ok=True) +whisper_model = whisper.load_model(whisper_model_name, download_root=whisper_path.as_posix()) + +piper_model_name = os.getenv("PIPER_MODEL", default="de_DE-karlsson-low") +piper_path = Path("piper_models") +piper_path.mkdir(parents=True, exist_ok=True) +download_voice(piper_model_name, piper_path) + +temporary_audio_path = Path("temporary_audio") +temporary_audio_path.mkdir(parents=True, exist_ok=True) + +logger = logging.getLogger(__name__) + +@app.get("/") +async def index(): + return { + "memory_usage": psutil.Process(os.getpid()).memory_info().rss, + "piper_model": piper_model_name, + "whisper_model": whisper_model_name, + } + +@app.get("/transcribe") +async def transcribe(audio_file: UploadFile): + file_extensions = { + "audio/wave": "wav", + "audio/mpeg": "mp3", + } + + if audio_file.content_type not in file_extensions.keys(): + logger.error("%s is not supported", audio_file.content_type) + return JSONResponse({"detail": "wrong file type"}, status_code=400) + + file_path = temporary_audio_path / f"{uuid.uuid4()}.{file_extensions[audio_file.content_type]}" + + async with aiofiles.open(file_path, "wb") as f: + content = await audio_file.read() + await f.write(content) + + # noinspection PyArgumentList + transcription = whisper_model.transcribe(file_path.as_posix()) + + return JSONResponse(transcription) + +class VoiceRequest(BaseModel): + text: str + config: SynthesisConfig | None + +@app.get("/tts") +async def tts(voice_request: VoiceRequest): + voice = PiperVoice.load(piper_path / f"{piper_model_name}.onnx") + + audio_file_path = temporary_audio_path / f"{uuid.uuid4()}.wav" + + with Wave_write(audio_file_path.as_posix()) as writer: + voice.synthesize_wav(voice_request.text, writer, syn_config=voice_request.config) + + return FileResponse(audio_file_path, media_type="audio/wav") \ No newline at end of file