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:
@@ -0,0 +1,3 @@
|
||||
temporary_audio/
|
||||
whisper_models/
|
||||
piper_models/
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
audio_server
|
||||
Generated
+11
@@ -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>
|
||||
Generated
+7
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
@@ -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"]
|
||||
@@ -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']
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user