add a simple python fastapi server to serve whisper models for speech-to-text and piper for text-to-speech

This commit is contained in:
milan
2026-04-08 23:54:48 +02:00
parent 1895e1ec66
commit 5def79b4ca
13 changed files with 193 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
temporary_audio/
whisper_models/
piper_models/
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+1
View File
@@ -0,0 +1 @@
audio_server
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.14 (audio_server)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,23 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IncorrectFormatting" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N803" />
<option value="N806" />
</list>
</option>
<option name="ignoredBaseClasses">
<list>
<option value="unittest.TestCase" />
<option value="unittest.case.TestCase" />
<option value="todo_extract.Task" />
</list>
</option>
</inspection_tool>
<inspection_tool class="TodoComment" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14 (transcription_server)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (audio_server)" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/audio_server.iml" filepath="$PROJECT_DIR$/.idea/audio_server.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+19
View File
@@ -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"]
+20
View File
@@ -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']
+8
View File
@@ -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
+76
View File
@@ -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")