Bladeren bron

Passer en 3.4.1 et separer le service TTS dans son depot

Andy Esnard 1 dag geleden
bovenliggende
commit
ae4e579487

+ 1 - 1
chatbot.py

@@ -54,7 +54,7 @@ if WEB_SEARCH_RESULT_COUNT < 1:
 # Initialiser le client OpenAI asynchrone ici
 openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=URL_OPENAI_API)
 
-BOT_VERSION = "3.4.0"
+BOT_VERSION = "3.4.1"
 
 # Vérifier la version de Discord
 if not sys.version_info[:2] >= (3, 15):

+ 0 - 13
tts-service/Dockerfile

@@ -1,13 +0,0 @@
-FROM python:3.12-slim
-WORKDIR /app
-ENV PYTHONUNBUFFERED=1 HF_HOME=/opt/models HOME=/tmp OMP_NUM_THREADS=4 MKL_NUM_THREADS=4
-RUN apt-get update && apt-get install -y --no-install-recommends espeak-ng libsndfile1 && rm -rf /var/lib/apt/lists/*
-COPY requirements.txt .
-RUN pip install --no-cache-dir torch==2.7.1 --index-url https://download.pytorch.org/whl/cpu && pip install --no-cache-dir -r requirements.txt
-# Modèle et voix embarqués : aucun téléchargement au démarrage du pod.
-RUN python -c "from kokoro import KPipeline; p=KPipeline(lang_code='f', device='cpu'); list(p('Bonjour.', voice='ff_siwis'))" && chmod -R a+rX /opt/models
-ENV HF_HUB_OFFLINE=1
-COPY app.py .
-USER 1001
-EXPOSE 8000
-CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]

+ 0 - 34
tts-service/README.md

@@ -1,34 +0,0 @@
-# Synthèse vocale Kokoro sur CPU
-
-Service interne `kokoro-tts:8000`, voix française féminine `ff_siwis`.
-Les poids sont embarqués dans l'image ; le modèle est chargé et préchauffé au
-démarrage. Aucun GPU n'est demandé. Le manifeste cible le nœud
-`bureau.local.pentou.ovh` dans le namespace `localai` (à adapter ailleurs).
-
-Depuis la racine du dépôt :
-
-```sh
-oc apply -f tts-service/openshift.yaml
-oc start-build kokoro-tts -n localai --from-dir=tts-service --follow
-```
-
-Après réussite du build, mettre à jour le Deployment `kokoro-tts`, conteneur
-`tts`, avec le digest indiqué par l'ImageStreamTag `kokoro-tts:cpu`, puis vérifier
-son rollout. Le manifeste initial utilise le tag `cpu` ; il ne configure pas
-de déclencheur automatique pour les builds suivants.
-
-API : POST `/v1/audio/speech`, JSON `input` (1 à 300 caractères), `voice`
-(`ff_siwis`), réponse WAV PCM16 mono 24 kHz. GET `/healthz` devient disponible
-après préchauffage. Une génération à la fois ; HTTP 429 si occupé.
-
-`check_service.py` peut être exécuté via stdin dans le pod Satoko. Il vérifie
-la connectivité, le format WAV, la validation de voix et mesure la génération
-de trois phrases sans publier sur Discord.
-
-Mesures sur Ryzen 9 5950X, limite 4 CPU : 1,44 / 2,53 / 2,69 secondes de requête
-pour 3,13 / 5,75 / 5,53 secondes d'audio. Elles excluent STT, LLM et Discord.
-`requirements-deployed.txt` est l'inventaire de l'image testée ; le Dockerfile
-installe PyTorch CPU explicitement puis les dépendances de `requirements.txt`.
-
-La NetworkPolicy autorise seulement les pods `app=satoko-chatbot` du même
-namespace à accéder au service. Aucune route publique n'est créée.

+ 0 - 74
tts-service/app.py

@@ -1,74 +0,0 @@
-"""Service local Kokoro CPU : API WAV compatible avec le client vocal."""
-import asyncio
-import logging
-import os
-import time
-from contextlib import asynccontextmanager
-from io import BytesIO
-
-import numpy as np
-import soundfile as sf
-import torch
-from fastapi import FastAPI, HTTPException
-from fastapi.responses import Response
-from kokoro import KPipeline
-from pydantic import BaseModel, Field
-
-logger = logging.getLogger('uvicorn.error')
-pipeline = None
-generation_lock = asyncio.Lock()
-
-
-def generate(text, voice):
-    with torch.inference_mode():
-        chunks = [result.audio.cpu().numpy() for result in pipeline(text, voice=voice)]
-    if not chunks:
-        raise ValueError('Aucun audio généré')
-    audio = np.concatenate(chunks)
-    output = BytesIO()
-    sf.write(output, audio, 24000, format='WAV', subtype='PCM_16')
-    return output.getvalue(), len(audio) / 24000
-
-
-@asynccontextmanager
-async def lifespan(app):
-    global pipeline
-    torch.set_num_threads(int(os.getenv('TTS_THREADS', '4')))
-    torch.set_num_interop_threads(1)
-    pipeline = KPipeline(lang_code='f', repo_id='hexgrad/Kokoro-82M', device='cpu')
-    await asyncio.to_thread(generate, 'Bonjour, je suis prête.', 'ff_siwis')
-    yield
-
-
-app = FastAPI(lifespan=lifespan)
-
-
-class SpeechRequest(BaseModel):
-    input: str = Field(min_length=1, max_length=300)
-    voice: str = 'ff_siwis'
-
-
-@app.get('/healthz')
-def health():
-    return {'ready': pipeline is not None, 'model': 'Kokoro-82M', 'voice': 'ff_siwis', 'device': 'cpu'}
-
-
-@app.post('/v1/audio/speech')
-async def speech(request: SpeechRequest):
-    if request.voice != 'ff_siwis' or not request.input.strip():
-        raise HTTPException(422, 'Voix inconnue ou texte vide')
-    if generation_lock.locked():
-        raise HTTPException(429, 'Synthèse occupée')
-    async with generation_lock:
-        started = time.monotonic()
-        # Shield : ne pas libérer le verrou pendant une génération annulée par le client.
-        task = asyncio.create_task(asyncio.to_thread(generate, request.input, request.voice))
-        try:
-            payload, duration = await asyncio.shield(task)
-        except asyncio.CancelledError:
-            await task
-            raise
-        elapsed = time.monotonic() - started
-        logger.info('Synthèse : caractères=%d génération=%.3fs audio=%.3fs', len(request.input), elapsed, duration)
-        return Response(payload, media_type='audio/wav', headers={
-            'X-Generation-Seconds': f'{elapsed:.3f}', 'X-Audio-Seconds': f'{duration:.3f}'})

+ 0 - 28
tts-service/check_service.py

@@ -1,28 +0,0 @@
-"""À exécuter depuis le pod Satoko ; aucun appel à Discord."""
-import io
-import json
-import time
-import wave
-import httpx
-
-with httpx.Client(timeout=90) as http:
-    health = http.get('http://kokoro-tts:8000/healthz')
-    health.raise_for_status()
-    print(health.json())
-    for text in (
-        'Bonjour, je suis prête à discuter avec vous.',
-        'Tu peux maintenant me parler dans le salon vocal, et je te répondrai directement à voix haute.',
-        'Les trois serveurs fonctionnent correctement. Nous pouvons continuer notre partie tranquillement.',
-    ):
-        started = time.monotonic()
-        response = http.post('http://kokoro-tts:8000/v1/audio/speech', json={'input': text, 'voice': 'ff_siwis'})
-        response.raise_for_status()
-        with wave.open(io.BytesIO(response.content)) as wav:
-            assert wav.getframerate() == 24000 and wav.getnchannels() == 1 and wav.getsampwidth() == 2
-            duration = wav.getnframes() / wav.getframerate()
-        print(json.dumps({'characters': len(text), 'request_seconds': round(time.monotonic()-started, 3),
-                          'generation_seconds': response.headers.get('X-Generation-Seconds'),
-                          'audio_seconds': round(duration, 3)}, ensure_ascii=False))
-    invalid = http.post('http://kokoro-tts:8000/v1/audio/speech', json={'input': 'bonjour', 'voice': '../unknown'})
-    assert invalid.status_code == 422
-    print('WAV et validation de voix : OK')

+ 0 - 118
tts-service/openshift.yaml

@@ -1,118 +0,0 @@
-apiVersion: image.openshift.io/v1
-kind: ImageStream
-metadata:
-  name: kokoro-tts
-  namespace: localai
----
-apiVersion: build.openshift.io/v1
-kind: BuildConfig
-metadata:
-  name: kokoro-tts
-  namespace: localai
-spec:
-  runPolicy: Serial
-  source:
-    type: Binary
-    binary: {}
-  strategy:
-    type: Docker
-    dockerStrategy: {}
-  output:
-    to:
-      kind: ImageStreamTag
-      name: kokoro-tts:cpu
-  resources:
-    requests:
-      cpu: '1'
-      memory: 2Gi
-    limits:
-      cpu: '4'
-      memory: 6Gi
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: kokoro-tts
-  namespace: localai
-spec:
-  replicas: 1
-  strategy:
-    type: Recreate
-  selector:
-    matchLabels:
-      app: kokoro-tts
-  template:
-    metadata:
-      labels:
-        app: kokoro-tts
-    spec:
-      nodeSelector:
-        kubernetes.io/hostname: bureau.local.pentou.ovh
-      automountServiceAccountToken: false
-      containers:
-        - name: tts
-          image: image-registry.openshift-image-registry.svc:5000/localai/kokoro-tts:cpu
-          ports:
-            - containerPort: 8000
-          resources:
-            requests:
-              cpu: '2'
-              memory: 2Gi
-            limits:
-              cpu: '4'
-              memory: 4Gi
-          securityContext:
-            allowPrivilegeEscalation: false
-            capabilities:
-              drop: [ALL]
-            runAsNonRoot: true
-            seccompProfile:
-              type: RuntimeDefault
-          startupProbe:
-            httpGet:
-              path: /healthz
-              port: 8000
-            periodSeconds: 5
-            failureThreshold: 60
-          readinessProbe:
-            httpGet:
-              path: /healthz
-              port: 8000
-            periodSeconds: 5
-          livenessProbe:
-            httpGet:
-              path: /healthz
-              port: 8000
-            periodSeconds: 20
-            timeoutSeconds: 5
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: kokoro-tts
-  namespace: localai
-spec:
-  selector:
-    app: kokoro-tts
-  ports:
-    - port: 8000
-      targetPort: 8000
----
-apiVersion: networking.k8s.io/v1
-kind: NetworkPolicy
-metadata:
-  name: kokoro-tts-ingress
-  namespace: localai
-spec:
-  podSelector:
-    matchLabels:
-      app: kokoro-tts
-  policyTypes: [Ingress]
-  ingress:
-    - from:
-        - podSelector:
-            matchLabels:
-              app: satoko-chatbot
-      ports:
-        - protocol: TCP
-          port: 8000

+ 0 - 97
tts-service/requirements-deployed.txt

@@ -1,97 +0,0 @@
-addict==2.4.0
-annotated-doc==0.0.5
-annotated-types==0.8.0
-anyio==4.16.0
-attrs==26.1.0
-babel==2.18.0
-blis==1.3.4
-catalogue==2.0.10
-certifi==2026.10.23
-cffi==2.1.1
-charset-normalizer==3.5.2
-click==8.5.0
-cloudpathlib==0.26.0
-cloudpickle==3.1.2
-confection==1.3.4
-csvw==4.1.1
-curated-tokenizers==0.0.10
-curated-transformers==0.1.1
-cymem==2.0.16
-dlinfo==2.0.0
-docopt==0.6.2
-espeakng-loader==0.2.4
-fastapi==0.115.12
-filelock==4.2.4
-fsspec==2026.9.0
-h11==0.16.0
-hf-xet==1.8.0
-httpcore==1.0.9
-httpcore2==2.16.0
-httpx==0.28.1
-httpx2==2.16.0
-huggingface_hub==2.4.0
-idna==3.20
-isodate==0.7.2
-Jinja2==3.1.6
-joblib==1.6.0
-jsonschema==4.26.0
-jsonschema-specifications==2025.9.1
-kokoro==0.9.4
-language-tags==1.3.2
-loguru==0.7.3
-markdown-it-py==4.2.0
-MarkupSafe==3.0.3
-mdurl==0.1.2
-misaki==0.9.4
-mpmath==1.3.0
-murmurhash==1.0.16
-networkx==3.7
-num2words==0.5.14
-numpy==2.5.4
-packaging==26.3
-phonemizer-fork==3.3.2
-preshed==3.0.15
-pycparser==3.12
-pydantic==2.14.2
-pydantic_core==2.50.2
-Pygments==2.21.0
-pyparsing==3.3.3
-python-dateutil==4.0.0
-PyYAML==6.0.3
-rdflib==7.6.0
-referencing==0.37.0
-regex==2026.9.29
-requests==2.34.2
-rfc3986==1.5.0
-rich==15.0.0
-rpds-py==2026.9.1
-safetensors==0.8.0
-segments==2.4.0
-setuptools==78.1.0
-shellingham==1.5.4
-smart_open==9.0.2
-soundfile==0.13.1
-spacy==3.8.18
-spacy-curated-transformers==0.3.1
-spacy-legacy==3.0.12
-spacy-loggers==1.0.5
-srsly==2.5.6
-starlette==0.46.2
-sympy==1.14.0
-termcolor==3.3.0
-thinc==8.3.14
-tokenizers==0.23.3
-torch==2.7.1+cpu
-tqdm==4.70.1
-transformers==5.22.1
-truststore==0.10.5
-typer==0.27.3
-typing-inspection==0.4.5
-typing_extensions==4.16.0
-tzdata==2026.6
-uritemplate==4.2.0
-urllib3==2.8.0
-uvicorn==0.34.2
-wasabi==1.1.3
-weasel==1.0.1
-wrapt==2.5.2

+ 0 - 4
tts-service/requirements.txt

@@ -1,4 +0,0 @@
-kokoro==0.9.4
-soundfile==0.13.1
-fastapi==0.115.12
-uvicorn==0.34.2