Voices
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user