-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathspeech_interface.py
More file actions
265 lines (228 loc) · 11.8 KB
/
Copy pathspeech_interface.py
File metadata and controls
265 lines (228 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import os
import json
import asyncio
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
import websockets
from text_speech import generate_audio_bytes
import orchestrator
from aws import s3_client as s3
from pydantic import BaseModel
import sys
# Ajouter le dossier agents au path pour les imports
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "agents"))
from agents.calendar_agent import sync_calendar, add_event, remove_event
load_dotenv()
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# REMPLACE PAR TA CLÉ DEEPGRAM (Gratiut 200$)
DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY")
@app.websocket("/ws/stream")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
# URL Deepgram : Plus rapide et plus tolérant que ElevenLabs pour le live
dg_url = "wss://api.deepgram.com/v1/listen?language=en&model=nova-2&smart_format=true"
headers = {"Authorization": f"Token {DEEPGRAM_API_KEY}"}
try:
async with websockets.connect(dg_url, additional_headers=headers) as dg_ws:
print("🚀 Deepgram connecté (Instance STT Live)")
async def receive_from_dg():
try:
async for message in dg_ws:
data = json.loads(message)
transcript = data.get("channel", {}).get("alternatives", [{}])[0].get("transcript", "")
if transcript:
is_final = data.get("is_final", False)
try:
await websocket.send_json({"type": "stt", "text": transcript, "is_final": is_final})
except Exception:
break # Le client est déconnecté
if is_final:
pass
except websockets.exceptions.ConnectionClosed:
print("Deepgram a fermé la connexion.")
except Exception as e:
print(f"Info receive_from_dg: {e}")
async def send_to_dg():
try:
while True:
msg = await websocket.receive()
if "bytes" in msg and msg.get("bytes"):
await dg_ws.send(msg["bytes"])
elif "text" in msg and msg.get("text"):
data = json.loads(msg["text"])
if data.get("type") == "process":
print(f"🚀 Envoi vers l'Orchestrateur du texte : {data['text']}")
async def process_orchestrator_and_tts(text_input):
try:
full_text = ""
combined_sentences = []
temp_sentence = ""
import re
async for chunk in orchestrator.run_orchestrator_stream(text_input, session_id="voice-session"):
full_text += chunk
temp_sentence += chunk
try:
await websocket.send_json({
"type": "agent",
"text": full_text,
"sentences": combined_sentences + ([temp_sentence.strip()] if temp_sentence.strip() else [])
})
except Exception:
return
while any(p in temp_sentence for p in ['.', '!', '?', '\n', ':']):
match = re.search(r'([.!?\n:])', temp_sentence)
if match:
idx = match.end()
sentence_to_play = temp_sentence[:idx].strip()
temp_sentence = temp_sentence[idx:]
if sentence_to_play:
combined_sentences.append(sentence_to_play)
print(f"🔊 Génération audio pour : {sentence_to_play}")
audio_bytes = await asyncio.to_thread(generate_audio_bytes, sentence_to_play)
if audio_bytes:
try:
await websocket.send_bytes(audio_bytes)
except Exception as e:
print(f"Client déconnecté pendant TTS: {e}")
return
else:
break
if temp_sentence.strip():
sentence_to_play = temp_sentence.strip()
combined_sentences.append(sentence_to_play)
try:
await websocket.send_json({
"type": "agent",
"text": full_text,
"sentences": combined_sentences
})
except Exception:
pass
print(f"🔊 Génération audio pour : {sentence_to_play}")
audio_bytes = await asyncio.to_thread(generate_audio_bytes, sentence_to_play)
if audio_bytes:
try:
await websocket.send_bytes(audio_bytes)
except Exception:
pass
except Exception as e:
print(f"Erreur avec l'Orchestrateur: {e}")
asyncio.create_task(process_orchestrator_and_tts(data["text"]))
except Exception as e:
print(f"Info send_to_dg: {e}")
await asyncio.gather(receive_from_dg(), send_to_dg())
except Exception as e:
print(f"❌ Erreur : {e}")
@app.get("/api/summary/{course}/{filename}")
async def get_summary_api(course: str, filename: str):
s3_key = f"summaries/{course}/{filename}"
data = s3._get_json(s3_key)
if data and "summary" in data:
return {"summary": data["summary"]}
return {"error": "Summary not found"}
class EventCreate(BaseModel):
summary: str
start_time: str
end_time: str
location: str = "N/A"
class EventRemove(BaseModel):
summary: str
start_time: str
@app.post("/api/calendar/sync")
async def force_sync():
"""Force la synchronisation des sources (TUM, Salles, Manuel)."""
try:
sync_calendar.invoke({})
return {"status": "synchronized"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/calendar")
async def get_calendar():
"""Lit le fichier ICS et le convertit en JSON simple pour le front."""
ics_path = os.path.join(os.path.dirname(__file__), "agents", "agent-calendar", "local_event.ics")
if not os.path.exists(ics_path):
sync_calendar.invoke({})
from icalendar import Calendar
try:
with open(ics_path, 'rb') as f:
gcal = Calendar.from_ical(f.read())
events = []
for component in gcal.walk():
if component.name == "VEVENT":
start = component.get('dtstart').dt
end = component.get('dtend').dt
start_str = start.isoformat() if hasattr(start, 'isoformat') else str(start)
end_str = end.isoformat() if hasattr(end, 'isoformat') else str(end)
events.append({
"summary": str(component.get('summary')),
"start": start_str,
"end": end_str,
"location": str(component.get('location', 'N/A'))
})
return sorted(events, key=lambda x: x['start'])
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/calendar/add")
async def api_add_event(event: EventCreate):
try:
res = add_event.invoke({
"summary": event.summary,
"start_time": event.start_time,
"end_time": event.end_time,
"location": event.location
})
return {"status": "success", "result": res}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/calendar/remove")
async def api_remove_event(event: EventRemove):
try:
# L'outil remove_event a été corrigé pour être plus robuste sur les dates
res = remove_event.invoke({
"summary": event.summary,
"start_time": event.start_time
})
return {"status": "removed", "result": res}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
class ChatRequest(BaseModel):
message: str
session_id: str = "default"
class ChatResponse(BaseModel):
response: str
agents_called: list # pour le frontend (afficher les icônes actives)
status_events: list = [] # progression des agents pour le frontend
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
result = await orchestrator.run_orchestrator(req.message, session_id=req.session_id)
return ChatResponse(
response=result["response"],
agents_called=result.get("agents_called", []),
status_events=result.get("status_events", []),
)
@app.delete("/chat/history")
async def clear_history(session_id: str = "default"):
"""Remet la conversation à zéro."""
orchestrator.clear_conversation(session_id)
return {"status": "cleared", "session_id": session_id}
@app.get("/memory")
async def memory():
"""Retourne l'état de la mémoire Cognee (utile pour la démo)."""
from cognee_memory import get_memory_summary
return get_memory_summary()
# Serve the static React build if it exists
if os.path.exists("campus-os/build"):
app.mount("/", StaticFiles(directory="campus-os/build", html=True), name="static")
if __name__ == "__main__":
import uvicorn
print("🚀 Lancement de Speech Interface avec API Calendrier...")
uvicorn.run(app, host="0.0.0.0", port=8000)