Dépendances CUDA & Bot Telegram optionnel

Rapport d'analyse technique — Alfred v2 (refacto) · Juin 2026

Architecture Dépendances CUDA Telegram UV pyproject.toml

📋 Contexte

Suite au refactoring intégral d'Alfred (716 lignes effacées, 804 insérées, passage d'un monolithe de 1 600 lignes à 4 fichiers modulaires), deux problèmes techniques bloquants ont été identifiés. Ce rapport présente une analyse détaillée de chaque problème, avec les points de code concernés, l'impact, et des recommandations concrètes de correction.

💡 Objectif de ce rapport : documenter les deux problèmes identifiés (bot Telegram obligatoire, dépendances CUDA) et proposer des solutions architecturales pour rendre Alfred déployable sur toute machine, avec ou sans GPU, avec ou sans Telegram.

🔴 Problème 1 : Le bot Telegram n'est pas optionnel

Malgré le refactoring qui a introduit le pattern Channel pour abstraire les canaux de communication, le code central d'Alfred (alfred.py) conserve encore des dépendances directes à la bibliothèque python-telegram-bot qui empêchent un déploiement sans Telegram.

1.1 Points de code critiques

⛔ Point 1 : Import global au niveau du module

alfred.py ligne 18 : from telegram import Bot, Update
alfred.py ligne 19 : from telegram.request import HTTPXRequest

Impact : Ces imports sont exécutés au chargement du module, avant toute vérification de configuration. Si python-telegram-bot n'est pas installé, l'import plante immédiatement et le serveur ne démarre pas. C'est un blocage absolu.

⛔ Point 2 : Création de l'instance Bot au démarrage

alfred.py ligne 51 : bot = Bot(token=TELEGRAM_TOKEN or "", request=HTTPXRequest(connect_timeout=60.0, read_timeout=60.0))

Impact : Même avec un token vide, l'objet Bot est instancié immédiatement. Si TELEGRAM_TOKEN est None (non défini dans .env), le token passé est une chaîne vide. Le bot est créé mais inutilisable. Le vrai problème est que l'import de la bibliothèque est toujours requis.

⛔ Point 3 : Injection du bot dans le cycle de vie FastAPI

alfred.py ligne 427 : await register_tools(bot, call_llm_subagent, CONFIG, LAST_SOURCE, WS_CONNECTIONS, model_service)
alfred.py ligne 429 : async with bot:

Impact : Le bot est injecté dans mcp_server.py via register_tools(), ce qui permet aux outils MCP de l'utiliser (pour les commandes /link_xxx et /confirm_link). Le contexte async with bot: gère le cycle de vie du bot (ouverture/fermeture de la session HTTP). Si le bot n'est pas défini, ces lignes plantent.

⛔ Point 4 : Webhook Telegram monté en dur

alfred.py ligne 433 : @app.post("/webhook/telegram")

Impact : L'endpoint webhook Telegram est toujours enregistré dans FastAPI, même si TELEGRAM_TOKEN est vide. Si Telegram envoie des requêtes à cet endpoint (parce que le webhook est configuré côté Telegram), le traitement échoue silencieusement car Update.de_json() nécessite un bot valide.

⛔ Point 5 : Heartbeat et liens croisés Telegram/WebSocket

alfred.py ligne 383 : channel = TelegramChannel(bot, mcp) dans trigger_heartbeat()
alfred.py ligne 327 : await TelegramChannel(bot, mcp).send_message(...) dans /link_xxx
alfred.py ligne 335 : await WebSocketChannel(WS_CONNECTIONS[chat_id], mcp).send_message(...) dans /confirm_link

Impact : Le heartbeat crée systématiquement un TelegramChannel pour chaque utilisateur. Les commandes de liaison croisée (/link_xxx et /confirm_link) utilisent directement TelegramChannel(bot, mcp) en dur. Ces fonctionnalités sont inutiles sans Telegram, mais leur code bloque l'import si la bibliothèque n'est pas installée.

1.2 Architecture actuelle vs architecture cible

Architecture actuelle (bloquante)

Import global
telegram.Bot, telegram.Update
Création Bot
Bot(token=TELEGRAM_TOKEN or "")
Injection MCP
register_tools(bot, ...)
Webhook Telegram
@app.post("/webhook/telegram")
Heartbeat Telegram
trigger_heartbeat() → TelegramChannel

Architecture cible (optionnelle)

Condition d'import
if TELEGRAM_TOKEN: import telegram
Bot conditionnel
bot = Bot(...) if TELEGRAM_TOKEN else None
MCP conditionnel
register_tools(bot, ...) if bot else register_tools(None, ...)
Webhook conditionnel
if TELEGRAM_TOKEN: app.post("/webhook/telegram")
Heartbeat multi-canal
trigger_heartbeat() → tous les canaux actifs

1.3 Recommandations

✅ Solution 1 : Imports conditionnels

Remplacer les imports globaux par des imports conditionnels :

# alfred.py — remplacer les lignes 18-19

# Imports conditionnels
if TELEGRAM_TOKEN:
    from telegram import Bot, Update
    from telegram.request import HTTPXRequest
    TELEGRAM_AVAILABLE = True
else:
    Bot = None
    Update = None
    TELEGRAM_AVAILABLE = False

Cela permet au serveur de démarrer sans python-telegram-bot installé. Les fonctionnalités Telegram sont désactivées gracieusement.

✅ Solution 2 : Bot conditionnel dans lifespan()

Modifier lifespan() pour ne gérer le bot que s'il existe :

@asynccontextmanager
async def lifespan(app: FastAPI):
    # ... chargement historique ...

    if TELEGRAM_AVAILABLE:
        await register_tools(bot, call_llm_subagent, CONFIG, LAST_SOURCE, WS_CONNECTIONS, model_service)
    else:
        await register_tools(None, call_llm_subagent, CONFIG, LAST_SOURCE, WS_CONNECTIONS, model_service)

    heartbeat_task = asyncio.create_task(heartbeat_loop())
    unload_task = asyncio.create_task(model_service.unload_loop())

    if TELEGRAM_AVAILABLE:
        async with bot:
            yield
    else:
        yield

    heartbeat_task.cancel()
    unload_task.cancel()
    await client.aclose()

✅ Solution 3 : Webhook conditionnel

Monter le webhook uniquement si le token est défini :

if TELEGRAM_AVAILABLE:
    @app.post("/webhook/telegram")
    async def telegram_endpoint(http_request: Request, background_tasks: BackgroundTasks):
        # ... code existant ...
        pass

Ou utiliser une route conditionnelle dans app.router.routes après la création de l'app.

✅ Solution 4 : Heartbeat multi-canal

Modifier trigger_heartbeat() pour utiliser tous les canaux actifs, pas seulement Telegram :

async def trigger_heartbeat(chat_id: str):
    hb_file = Path(f"alfred-data/{chat_id}/heartbeat.md")
    if not hb_file.exists():
        return

    if chat_id in ACTIVE_TASKS:
        logger.info(f"[{chat_id}] Heartbeat ignoré : une tâche est déjà active.")
        return

    channels = []
    if TELEGRAM_AVAILABLE:
        channels.append(TelegramChannel(bot, mcp))
    # Ajouter WebSocketChannel si des connections WebSocket existent

    for channel in channels:
        request = ChannelRequest(chat_id, hb_file.read_text(encoding="utf-8"))
        await process_request(channel, request)

✅ Solution 5 : Outils MCP conditionnels

Dans mcp_server.py, les commandes /link_xxx et /confirm_link doivent être enregistrées uniquement si bot est disponible. Le register_tools() doit accepter bot=None et filtrer les outils dépendants de Telegram.

🟡 Problème 2 : Dépendances CUDA bloquent uv sync sur machines sans GPU

Le deuxième problème est plus subtil mais tout aussi bloquant. Même si ModelService supporte un mode CPU, les dépendances Python liées aux modèles IA sont importées au niveau du module, ce qui force uv sync à installer des packages incompatibles avec les machines sans GPU NVIDIA.

2.1 Dépendances critiques identifiées

🔴 PyTorch (torch)

model_service.py : import torch
Par défaut, PyTorch installe le wheel CUDA (~2-3 Go). Sur une machine sans GPU NVIDIA, uv sync peut échouer si le wheel CUDA n'est pas compatible, ou installer une version CPU qui manque de fonctionnalités.

Impact : Installation bloquante sur Raspberry Pi, machines Apple Silicon, ou toute machine sans drivers NVIDIA.

🔴 Diffusers (SDXL Turbo)

model_service.py : from diffusers import AutoPipelineForText2Image
diffusers dépend de torch et de transformers. Il est lourd et n'est utile que pour la génération d'images.

Impact : Installation inutile si l'utilisateur ne veut que du texte ou de la synthèse vocale.

🔴 OmniVoice

model_service.py : from omnivoice import OmniVoice
Package propriétaire qui peut avoir des dépendances CUDA implicites.

Impact : Installation bloquante si le package n'est pas compatible avec l'architecture cible.

🔴 Faster-Whisper

model_service.py : from faster_whisper import WhisperModel
faster-whisper dépend de ctranslate2 qui peut chercher des bibliothèques CUDA au runtime.

Impact : Installation lourde (~500 Mo) pour une fonctionnalité optionnelle.

2.2 Analyse du flux d'installation

Flux actuel (bloquant)

uv sync
pyproject.toml
torch, diffusers, omnivoice, faster-whisper
model_service.py
import torch (au chargement)
CRASH
CUDA non disponible / wheel incompatible

Flux cible (flexible)

uv sync
pyproject.toml
torch (CPU par défaut)
[extra] diffusers
[extra] omnivoice
[extra] faster-whisper
.env
ENABLE_IMAGE_GEN=true
ENABLE_AUDIO_GEN=true
ENABLE_TRANSCRIPTION=true
model_service.py
imports lazy + feature flags
✅ DÉMARRAGE
Fonctionnalités activées selon config

2.3 Recommandations

✅ Solution 1 : Extras dans pyproject.toml

Séparer les dépendances en extras optionnels. L'utilisateur installe uniquement ce dont il a besoin :

# pyproject.toml

[project]
dependencies = [
    "fastapi>=0.110.0",
    "uvicorn>=0.29.0",
    "httpx>=0.27.0",
    "python-dotenv>=1.0.0",
    "jinja2>=3.1.0",
    "asyncio>=3.4.3",
    "aiofiles>=24.1.0",
    "soundfile>=0.12.1",
    "static-ffmpeg>=2.4",
    "websockets>=13.0",
    "PyJWT>=2.8.0",
    "python-multipart>=0.0.9",
    "orjson>=3.10.0",
    "mcp>=1.0.0",
]

[project.optional-dependencies]
image = [
    "torch>=2.3.0",
    "diffusers>=0.30.0",
    "transformers>=4.41.0",
    "accelerate>=0.33.0",
    "sentencepiece>=0.2.0",
]
audio = [
    "omnivoice>=1.0.0",
    "soundfile>=0.12.1",
    "faster-whisper>=1.0.0",
    "ctranslate2>=4.3.0",
]
telegram = [
    "python-telegram-bot[httpx]>=21.7",
]
full = [
    "alfred[image,audio,telegram]",
]

[tool.uv]
# Configuration UV spécifique
compile-bytecode = true

Installation minimale : uv sync (juste le serveur web)
Installation IA : uv sync --extra full (tout)
Installation ciblée : uv sync --extra image (juste la génération d'images)

✅ Solution 2 : Imports lazy dans model_service.py

Déplacer les imports lourds à l'intérieur des méthodes qui les utilisent, pas au niveau du module :

# model_service.py

class ModelService:
    def __init__(self, device="CPU", timeout=900):
        self._models = {}
        self._last_used = {}
        self._device = device
        self.timeout = timeout
        self._loaders = {
            "image_generation": self._load_sdxl_turbo,
            "audio_generation": self._load_omnivoice,
            "audio_transcription": self._load_whisper_medium,
        }

    async def _load_sdxl_turbo(self):
        # Import lazy — ne plante que si on appelle cette méthode
        try:
            import torch
            from diffusers import AutoPipelineForText2Image
        except ImportError as e:
            raise RuntimeError("Package 'torch' et 'diffusers' requis pour la génération d'images. Installez : uv pip install alfred[image]") from e

        # ... reste du chargement ...

    async def _load_omnivoice(self):
        try:
            from omnivoice import OmniVoice
        except ImportError as e:
            raise RuntimeError("Package 'omnivoice' requis pour la synthèse vocale. Installez : uv pip install alfred[audio]") from e

    async def _load_whisper_medium(self):
        try:
            from faster_whisper import WhisperModel
        except ImportError as e:
            raise RuntimeError("Package 'faster-whisper' requis pour la transcription. Installez : uv pip install alfred[audio]") from e

✅ Solution 3 : Feature flags dans .env

Ajouter des variables d'environnement pour contrôler quelles fonctionnalités sont activées :

# .env

# Fonctionnalités
ENABLE_IMAGE_GEN=true
ENABLE_AUDIO_GENERATION=true
ENABLE_AUDIO_TRANSCRIPTION=true
ENABLE_TELEGRAM=true

# Si ENABLE_TELEGRAM=false, le bot n'est jamais instancié
# Si ENABLE_IMAGE_GEN=false, torch/diffusers ne sont jamais importés
# Si ENABLE_AUDIO_GENERATION=false, omnivoice n'est jamais importé
# Si ENABLE_AUDIO_TRANSCRIPTION=false, faster-whisper n'est jamais importé

Dans model_service.py, le constructeur vérifie ces flags :

class ModelService:
    def __init__(self, device="CPU", timeout=900):
        self._models = {}
        self._last_used = {}
        self._device = device
        self.timeout = timeout

        # Enregistrer les loaders conditionnellement
        if os.getenv("ENABLE_IMAGE_GEN", "false").lower() == "true":
            self._loaders["image_generation"] = self._load_sdxl_turbo

        if os.getenv("ENABLE_AUDIO_GENERATION", "false").lower() == "true":
            self._loaders["audio_generation"] = self._load_omnivoice

        if os.getenv("ENABLE_AUDIO_TRANSCRIPTION", "false").lower() == "true":
            self._loaders["audio_transcription"] = self._load_whisper_medium

✅ Solution 4 : PyTorch CPU par défaut

Dans pyproject.toml, utiliser PyTorch CPU par défaut et un extra CUDA :

[project.optional-dependencies]
torch-cpu = [
    "torch>=2.3.0",
]
torch-cuda = [
    "torch>=2.3.0",
    "torchvision>=0.18.0",
    "torchaudio>=2.3.0",
]

# Par défaut, on installe le CPU
[tool.uv]
default-extras = ["torch-cpu"]

Ou utiliser les indices PyTorch officiels :

# Dans pyproject.toml
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"

# Dans les dépendances
"torch>=2.3.0; extra == 'image' and not sys_platform == 'darwin'"]

📊 Résumé des actions requises

Priorité Action Fichier(s) Complexité Impact
🔴 Critique Imports conditionnels Telegram alfred.py (lignes 18-19) Facile Permet le démarrage sans Telegram
🔴 Critique Bot conditionnel dans lifespan() alfred.py (lignes 427-431) Facile Évite le crash au démarrage
🔴 Critique Extras pyproject.toml (torch, diffusers, omnivoice, faster-whisper) pyproject.toml Moyen Permet uv sync sans GPU
🟡 Important Imports lazy dans model_service.py model_service.py Moyen Évite le crash à l'exécution
🟡 Important Feature flags .env .env + model_service.py Facile Contrôle granulaire des fonctionnalités
🟢 Secondaire Heartbeat multi-canal alfred.py (trigger_heartbeat) Moyen Heartbeat sur tous les canaux actifs
🟢 Secondaire Webhook conditionnel alfred.py (telegram_endpoint) Facile Webhook uniquement si Telegram activé
🟢 Secondaire Outils MCP conditionnels mcp_server.py (register_tools) Facile Outils Telegram uniquement si bot disponible

🎯 Recommandation finale

💡 Approche recommandée : Combiner les solutions 1 (extras pyproject.toml) + 2 (imports lazy) + 3 (feature flags). Cela donne un contrôle total : l'utilisateur choisit ce qu'il installe (uv sync, uv sync --extra image, uv sync --extra full) et ce qu'il active (.env), et le serveur démarre toujours, même avec zéro fonctionnalité IA activée.

⚠️ Point d'attention : Le ModelService doit gérer gracieusement le cas où aucun modèle n'est disponible (retourner une erreur claire au lieu de planter). Le heartbeat doit aussi vérifier qu'au moins un canal est actif avant de tenter d'envoyer un message.

📚 Sources

📖 Documentation uv

astral.sh — uv optional-dependencies astral.sh — uv project dependencies astral.sh — uv virtual environments

📖 Documentation PyTorch

pytorch.org — Get Started Locally (CPU vs CUDA) pytorch.org — Previous Versions

📖 Documentation python-telegram-bot

docs.python-telegram-bot.org docs.python-telegram-bot.org/en/stable/

📖 Documentation FastAPI

fastapi.tiangolo.com — Using Request directly fastapi.tiangolo.com — Lifespan events

📖 Code source Alfred

alfred-data/8235286081/alfred-source/alfred.py alfred-data/8235286081/alfred-source/model_service.py alfred-data/8235286081/alfred-source/mcp_server.py alfred-data/8235286081/alfred-source/channel.py