Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd

.env
.env.*

logs
data

.git
.gitignore
docs
README.md
54 changes: 36 additions & 18 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
# Copy this file to ".env" and fill in real values.
# Never commit your real .env with secrets.
# =========================
# Telegram Bot
# =========================

# Telegram Bot token from @BotFather
BOT_TOKEN=your_telegram_bot_token_here
# Telegram bot token
BOT_TOKEN=

# Transcription backend: whisper (default) or deepgram
TRANSCRIBER_BACKEND=whisper
# =========================
# Logging
# =========================

# Log level: DEBUG | INFO | WARNING | ERROR
LOG_LEVEL=INFO

# Deepgram API key (required only if TRANSCRIBER_BACKEND=deepgram)
DG_API_KEY=your_deepgram_api_key_here
# =========================
# Audio processing
# =========================

# Optional secret path for webhook, used as /webhook/<WEBHOOK_SECRET>
WEBHOOK_SECRET=your_webhook_secret_here
# Full path to ffmpeg binary
# Required for converting Telegram audio to WAV
FFMPEG_PATH=

# Logging level: DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# =========================
# Transcription backend
# =========================

# Transcription backend to use:
# - whisper -> local Whisper model (recommended for local development)
# - deepgram -> Deepgram API (recommended for cloud deployment)
TRANSCRIBER_BACKEND=whisper

# Deepgram API key
# Required only if TRANSCRIBER_BACKEND=deepgram
DG_API_KEY=

# Optional: manual path to ffmpeg executable (useful on servers / Docker / custom installs).
# If not set, the app will try to find ffmpeg in the system PATH.
# Linux example:
# FFMPEG_PATH=/usr/local/bin/ffmpeg
# Windows example:
# FFMPEG_PATH=C:\ffmpeg\bin\ffmpeg.exe
# =========================
# Webhook / Cloud
# =========================

# Secret part of webhook URL.
# Used only in webhook mode (FastAPI + cloud deployment).
# Example: /webhook/<WEBHOOK_SECRET>
WEBHOOK_SECRET=
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Лёгкий образ с Python 3.12
FROM python:3.12-slim

# Не пишем .pyc и буферизуем stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Рабочая директория
WORKDIR /app

# Системные зависимости: ffmpeg + git (для надёжности) + build tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
build-essential \
git \
&& rm -rf /var/lib/apt/lists/*

# Сначала ставим зависимости для облака
COPY requirements-cloud.txt .

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements-cloud.txt

# Копируем остальной код
COPY . .

# Открываем порт FastAPI
EXPOSE 8000

# По умолчанию запускаем FastAPI + webhook
CMD ["uvicorn", "webapp:app", "--host", "0.0.0.0", "--port", "8000"]
73 changes: 58 additions & 15 deletions app/transcription/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging

from app.config import Settings, TranscriberBackend
from app.transcription.whisper_backend import transcribe_wav_bytes
from app.transcription.deepgram_backend import (
transcribe as deepgram_transcribe,
DeepgramError,
Expand All @@ -10,6 +9,27 @@
logger = logging.getLogger(__name__)


def _transcribe_with_whisper(wav_bytes: bytes) -> str:
"""
Ленивая обёртка над Whisper-бэкендом.

Импортирует whisper_backend только при первом вызове.
Это позволяет запускать приложение в окружениях без Whisper
(например, Docker-образ для JustRunMy.App), пока этот бэкенд
реально не используется.
"""
try:
from app.transcription.whisper_backend import transcribe_wav_bytes
except ImportError as exc:
logger.error(
"Whisper backend was requested but is not available in this environment. "
"Install Whisper packages or switch TRANSCRIBER_BACKEND to 'deepgram'."
)
raise RuntimeError("Whisper backend is not available") from exc

return transcribe_wav_bytes(wav_bytes)


async def transcribe(
wav_bytes: bytes,
*,
Expand All @@ -23,20 +43,27 @@ async def transcribe(
выбирает Whisper или Deepgram.
"""

# --- Явный выбор Whisper ---
if settings.transcriber_backend == TranscriberBackend.WHISPER:
logger.debug("Using Whisper backend for transcription: user_id=%s", user_id)
return transcribe_wav_bytes(wav_bytes)
return _transcribe_with_whisper(wav_bytes)

# --- Deepgram как облачный бэкенд ---
if settings.transcriber_backend == TranscriberBackend.DEEPGRAM:
# safety: если по каким-то причинам ключа нет в settings,
# не валимся, а откатываемся на Whisper
if not getattr(settings, "dg_api_key", None):
logger.error(
"Deepgram backend is configured but dg_api_key is missing. "
"Falling back to Whisper. user_id=%s",
"user_id=%s",
user_id,
)
return transcribe_wav_bytes(wav_bytes)
# Пытаемся откатиться на Whisper, если он вообще установлен.
try:
return _transcribe_with_whisper(wav_bytes)
except RuntimeError:
logger.error(
"Whisper fallback is not available. Returning empty transcription."
)
return ""

try:
logger.debug(
Expand All @@ -48,22 +75,38 @@ async def transcribe(
)
except DeepgramError:
logger.exception(
"Deepgram transcription failed, falling back to Whisper. user_id=%s",
"Deepgram transcription failed, attempting Whisper fallback. "
"user_id=%s",
user_id,
)
return transcribe_wav_bytes(wav_bytes)
try:
return _transcribe_with_whisper(wav_bytes)
except RuntimeError:
logger.error(
"Whisper fallback is not available. Returning empty transcription."
)
return ""
except Exception:
logger.exception(
"Unexpected error in Deepgram backend, falling back to Whisper. "
"user_id=%s",
user_id,
"Unexpected error in Deepgram backend. user_id=%s", user_id
)
return transcribe_wav_bytes(wav_bytes)
try:
return _transcribe_with_whisper(wav_bytes)
except RuntimeError:
logger.error(
"Whisper fallback is not available. Returning empty transcription."
)
return ""

# на всякий случай: если пришло что-то странное в settings.transcriber_backend
# --- Непонятный backend в настройках ---
logger.warning(
"Unknown transcriber backend %r. Falling back to Whisper. user_id=%s",
"Unknown transcriber backend %r. Falling back to Whisper if available. "
"user_id=%s",
settings.transcriber_backend,
user_id,
)
return transcribe_wav_bytes(wav_bytes)
try:
return _transcribe_with_whisper(wav_bytes)
except RuntimeError:
logger.error("Whisper backend is not available. Returning empty transcription.")
return ""
13 changes: 13 additions & 0 deletions requirements-cloud.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
aiofiles==24.1.0
aiogram==3.13.1
aiohttp==3.10.11
magic-filter==1.0.12

fastapi>=0.110
uvicorn>=0.27
httpx>=0.27

python-dotenv==1.0.1
pydantic==2.9.2
pydantic_core==2.23.4
propcache==0.4.1