This commit is contained in:
2026-04-24 21:49:58 +02:00
parent e592ed44af
commit 8cd28873e8
6 changed files with 265 additions and 66 deletions

View File

@@ -1,5 +1,5 @@
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
from fastapi import Depends, FastAPI, File, HTTPException, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from .database import Base, engine, get_db
@@ -9,8 +9,10 @@ from .services import (
build_llm_reply,
ensure_student_mastery,
evaluate_answer,
list_tts_profiles,
pick_next_skill,
seed_skills,
synthesize_speech,
transcribe_audio,
)
@@ -68,7 +70,7 @@ def start_session(student_id: int, db: Session = Depends(get_db)):
ensure_student_mastery(db, student)
message = (
f"Bonjour {student.first_name} ! Je suis ton professeur virtuel. "
f"Bonjour {student.first_name} ! Je suis Professeur TOP, ton professeur virtuel. "
"Aujourd'hui, on va apprendre pas à pas et faire un petit test pour voir ce que tu maîtrises déjà."
)
db.add(models.Message(student_id=student.id, role="assistant", content=message))
@@ -109,6 +111,23 @@ async def transcribe(file: UploadFile = File(...)):
return {"text": text}
@app.get("/tts/profiles", response_model=schemas.TTSProfilesResponse)
def get_tts_profiles():
return schemas.TTSProfilesResponse(profiles=list_tts_profiles())
@app.post("/tts")
def text_to_speech(payload: schemas.TTSRequest):
try:
audio_bytes = synthesize_speech(payload.text, payload.profile_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Erreur de synthèse vocale: {exc}") from exc
return Response(content=audio_bytes, media_type="audio/mpeg")
@app.get("/progress/{student_id}", response_model=schemas.ProgressResponse)
def get_progress(student_id: int, db: Session = Depends(get_db)):
student = db.query(models.Student).filter_by(id=student_id).first()

View File

@@ -28,6 +28,21 @@ class ChatResponse(BaseModel):
should_speak: bool = True
class TTSProfile(BaseModel):
id: str
label: str
description: str
class TTSProfilesResponse(BaseModel):
profiles: List[TTSProfile]
class TTSRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=4096)
profile_id: str = Field(..., min_length=1)
class SkillProgress(BaseModel):
code: str
subject: str

View File

@@ -9,7 +9,7 @@ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
SYSTEM_PROMPT = """
Tu es ProfAmi, un professeur virtuel français pour enfants de 8 à 12 ans.
Tu es Professeur TOP, un professeur virtuel français pour enfants de 8 à 12 ans.
Règles :
- Tu parles toujours en français simple et chaleureux.
- Tu donnes des explications très courtes, puis un mini exemple.
@@ -20,6 +20,58 @@ Règles :
- Tu enseignes principalement le programme national français niveau primaire/cycle 3.
""".strip()
TTS_PROFILES = [
{
"id": "rigolote",
"label": "Rigolote",
"description": "La plus joueuse et cartoon.",
"voice": "coral",
"speed": 1.08,
"instructions": (
"Parle en français avec une énergie joyeuse, malicieuse et très expressive. "
"Tu es Professeur TOP, un professeur amusant, chaleureux et un peu théâtral. "
"Le ton doit rester clair pour un enfant, avec des intonations souriantes, "
"des fins de phrases dynamiques et une diction très vivante."
),
},
{
"id": "petillante",
"label": "Pétillante",
"description": "Enjouée, dynamique et encourageante.",
"voice": "shimmer",
"speed": 1.03,
"instructions": (
"Parle en français avec une voix lumineuse, motivante et positive. "
"Tu es Professeur TOP: enthousiaste, rassurant et très engageant, "
"sans caricature excessive. Garde une diction nette et chaleureuse."
),
},
{
"id": "douce",
"label": "Douce",
"description": "Calme, rassurante et patiente.",
"voice": "sage",
"speed": 0.98,
"instructions": (
"Parle en français avec une voix douce, calme et très rassurante. "
"Tu es Professeur TOP dans une version posée, bienveillante et patiente. "
"Le rythme est fluide, jamais pressé, avec une intonation apaisante."
),
},
{
"id": "sobre",
"label": "Sobre",
"description": "La plus neutre et sérieuse.",
"voice": "alloy",
"speed": 1.0,
"instructions": (
"Parle en français avec une voix claire, naturelle et sobre. "
"Tu es Professeur TOP dans une version très lisible, professionnelle et mesurée. "
"Reste chaleureux, mais sans effet théâtral."
),
},
]
def seed_skills(db: Session) -> None:
for skill in SKILLS:
@@ -88,6 +140,37 @@ def build_llm_reply(db: Session, student_id: int, user_message: str) -> str:
return response.output_text.strip()
def list_tts_profiles() -> list[dict]:
return [
{
"id": profile["id"],
"label": profile["label"],
"description": profile["description"],
}
for profile in TTS_PROFILES
]
def synthesize_speech(text: str, profile_id: str) -> bytes:
clean_text = text.strip()
if not clean_text:
raise ValueError("Texte à synthétiser manquant")
profile = next((item for item in TTS_PROFILES if item["id"] == profile_id), None)
if not profile:
raise ValueError("Profil de voix inconnu")
response = client.audio.speech.create(
model="gpt-4o-mini-tts",
voice=profile["voice"],
input=clean_text[:4096],
instructions=profile["instructions"],
response_format="mp3",
speed=profile["speed"],
)
return response.content
def transcribe_audio(filename: str, audio_bytes: bytes, content_type: str | None = None) -> str:
file_payload = (filename, audio_bytes, content_type or "application/octet-stream")
transcript = client.audio.transcriptions.create(