Spaces:
Running
Running
Update src/ux_agent.py
Browse files- src/ux_agent.py +127 -44
src/ux_agent.py
CHANGED
|
@@ -61,8 +61,10 @@ class AgentUX:
|
|
| 61 |
self.last_audio_path = None
|
| 62 |
self.last_pending_count = 0
|
| 63 |
|
| 64 |
-
# 🟢 NEW:
|
| 65 |
-
self.
|
|
|
|
|
|
|
| 66 |
|
| 67 |
self.alert_sound = None
|
| 68 |
|
|
@@ -179,51 +181,122 @@ class AgentUX:
|
|
| 179 |
print(f"Peer Translation Error: {e}")
|
| 180 |
return text # Fallback to original text if API fails
|
| 181 |
|
| 182 |
-
#
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
translation = ""
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
"translation": translation,
|
| 212 |
-
"dialect": dialect,
|
| 213 |
-
"id": str(uuid.uuid4())[:8]
|
| 214 |
-
}
|
| 215 |
self.live_rooms[room_code].append(msg)
|
| 216 |
return json.dumps({"status": "success"})
|
| 217 |
|
| 218 |
-
# 🟢 NEW: Allows device B to pull messages sent by device A
|
| 219 |
def api_remote_poll(self, room_code, last_index):
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
idx = int(last_index)
|
| 224 |
-
|
| 225 |
-
return json.dumps(new_msgs)
|
| 226 |
-
|
| 227 |
# ==========================================
|
| 228 |
# SOCIOLINGUISTIC PIPELINE
|
| 229 |
# ==========================================
|
|
@@ -1196,20 +1269,30 @@ class AgentUX:
|
|
| 1196 |
api_name="translate_peer"
|
| 1197 |
)
|
| 1198 |
|
| 1199 |
-
# 🟢 NEW: Dialect Relay
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
api_btn_relay_send = gr.Button()
|
| 1201 |
api_relay_room = gr.Textbox()
|
| 1202 |
-
api_relay_sender = gr.Textbox()
|
| 1203 |
api_relay_text = gr.Textbox()
|
| 1204 |
-
|
| 1205 |
api_relay_out = gr.Textbox()
|
| 1206 |
-
api_btn_relay_send.click(fn=self.api_remote_eval_and_send, inputs=[api_relay_room,
|
| 1207 |
|
| 1208 |
api_btn_relay_poll = gr.Button()
|
| 1209 |
-
api_poll_room = gr.Textbox()
|
| 1210 |
api_poll_idx = gr.Number()
|
| 1211 |
-
|
| 1212 |
-
api_btn_relay_poll.click(fn=self.api_remote_poll, inputs=[api_poll_room, api_poll_idx], outputs=[api_poll_out], api_name="relay_poll")
|
| 1213 |
|
| 1214 |
# 7. Secure Feedback Endpoint
|
| 1215 |
api_fb_btn = gr.Button()
|
|
|
|
| 61 |
self.last_audio_path = None
|
| 62 |
self.last_pending_count = 0
|
| 63 |
|
| 64 |
+
# 🟢 NEW: Matchmaking & Live Room Memory
|
| 65 |
+
self.waiting_pool = [] # List of operators looking for a match
|
| 66 |
+
self.active_matches = {} # Maps Operator ID -> Room Code
|
| 67 |
+
self.live_rooms = {} # Stores the actual chat logs per room
|
| 68 |
|
| 69 |
self.alert_sound = None
|
| 70 |
|
|
|
|
| 181 |
print(f"Peer Translation Error: {e}")
|
| 182 |
return text # Fallback to original text if API fails
|
| 183 |
|
| 184 |
+
# ==========================================
|
| 185 |
+
# DIALECT RELAY (MATCHMAKING & CHAT)
|
| 186 |
+
# ==========================================
|
| 187 |
+
def api_join_queue(self, operator_id, dialect):
|
| 188 |
+
import uuid, json, time
|
| 189 |
+
# Clean up any old ghost matches
|
| 190 |
+
if operator_id in self.active_matches:
|
| 191 |
+
del self.active_matches[operator_id]
|
| 192 |
+
|
| 193 |
+
# 1. Check if someone else is waiting
|
| 194 |
+
partner = None
|
| 195 |
+
for p in self.waiting_pool:
|
| 196 |
+
if p['operator_id'] != operator_id:
|
| 197 |
+
partner = p
|
| 198 |
+
break
|
| 199 |
+
|
| 200 |
+
if partner:
|
| 201 |
+
# 2. Match found! Create a room.
|
| 202 |
+
self.waiting_pool.remove(partner)
|
| 203 |
+
room_id = f"FREQ-{uuid.uuid4().hex[:6].upper()}"
|
| 204 |
+
|
| 205 |
+
self.active_matches[operator_id] = {"room_id": room_id, "partner_dialect": partner['dialect']}
|
| 206 |
+
self.active_matches[partner['operator_id']] = {"room_id": room_id, "partner_dialect": dialect}
|
| 207 |
+
self.live_rooms[room_id] = []
|
| 208 |
+
|
| 209 |
+
return json.dumps({"status": "matched", "room_id": room_id, "partner_dialect": partner['dialect']})
|
| 210 |
+
else:
|
| 211 |
+
# 3. No one is waiting. Add self to pool.
|
| 212 |
+
self.waiting_pool = [p for p in self.waiting_pool if p['operator_id'] != operator_id] # Prevent duplicates
|
| 213 |
+
self.waiting_pool.append({"operator_id": operator_id, "dialect": dialect, "time": time.time()})
|
| 214 |
+
return json.dumps({"status": "waiting"})
|
| 215 |
+
|
| 216 |
+
def api_check_match(self, operator_id):
|
| 217 |
+
import json
|
| 218 |
+
if operator_id in self.active_matches:
|
| 219 |
+
match = self.active_matches[operator_id]
|
| 220 |
+
return json.dumps({"status": "matched", "room_id": match["room_id"], "partner_dialect": match["partner_dialect"]})
|
| 221 |
+
return json.dumps({"status": "waiting"})
|
| 222 |
|
| 223 |
+
def api_leave_queue(self, operator_id):
|
| 224 |
+
import json
|
| 225 |
+
self.waiting_pool = [p for p in self.waiting_pool if p['operator_id'] != operator_id]
|
| 226 |
+
if operator_id in self.active_matches:
|
| 227 |
+
del self.active_matches[operator_id]
|
| 228 |
+
return json.dumps({"status": "left"})
|
| 229 |
+
|
| 230 |
+
def api_remote_eval_and_send(self, room_code, sender_id, text, source_dialect, target_dialect):
|
| 231 |
+
import json, os, re, glob, uuid, threading
|
| 232 |
+
import pandas as pd
|
| 233 |
+
|
| 234 |
+
if not text: return json.dumps({"status": "error", "msg": "Empty text"})
|
| 235 |
|
| 236 |
translation = ""
|
| 237 |
+
needs_help = False
|
| 238 |
+
|
| 239 |
+
# 1. Forward Lookup (Understand Source)
|
| 240 |
+
eval_result = self.brain.search_local_dataset(text)
|
| 241 |
+
if not eval_result: eval_result = self.brain.search_personas(text)
|
| 242 |
+
standard_meaning = eval_result.get("clarification", text) if eval_result else text
|
| 243 |
+
|
| 244 |
+
if standard_meaning.lower() != text.lower():
|
| 245 |
+
needs_help = True
|
| 246 |
|
| 247 |
+
# 2. Reverse Lookup (Translate to Target)
|
| 248 |
+
target_utterance = None
|
| 249 |
+
if target_dialect and target_dialect != "General English" and needs_help:
|
| 250 |
+
try:
|
| 251 |
+
if hasattr(self.brain, 'config') and os.path.exists(self.brain.config.DATASET_DIR):
|
| 252 |
+
target_file = os.path.join(self.brain.config.DATASET_DIR, f"{target_dialect}.csv")
|
| 253 |
+
if os.path.exists(target_file):
|
| 254 |
+
df = pd.read_csv(target_file)
|
| 255 |
+
if 'Clarification' in df.columns and 'Utterance' in df.columns:
|
| 256 |
+
pattern = re.compile(f"^{re.escape(standard_meaning)}$", re.IGNORECASE)
|
| 257 |
+
matches = df[df['Clarification'].astype(str).str.match(pattern, na=False)]
|
| 258 |
+
if not matches.empty: target_utterance = str(matches.iloc[0]['Utterance'])
|
| 259 |
+
except Exception as e: pass
|
| 260 |
+
|
| 261 |
+
# 3. Gemini Fallback
|
| 262 |
+
if not target_utterance:
|
| 263 |
+
prompt = f"""
|
| 264 |
+
Analyze this utterance from a speaker of '{source_dialect}': "{text}"
|
| 265 |
+
Is this mutually intelligible Standard English?
|
| 266 |
+
If YES, return EXACTLY: {{"needs_help": false, "translation": ""}}
|
| 267 |
+
If NO, translate it to '{target_dialect}' and return EXACTLY: {{"needs_help": true, "translation": "The translated text here"}}
|
| 268 |
+
"""
|
| 269 |
+
try:
|
| 270 |
+
res = self.brain.gemini_manager.client.models.generate_content(model='gemini-2.0-flash', contents=prompt)
|
| 271 |
+
clean_json = res.text.replace("```json", "").replace("```", "").strip()
|
| 272 |
+
data = json.loads(clean_json)
|
| 273 |
+
if data.get("needs_help"): translation = data.get("translation", "")
|
| 274 |
+
except: translation = standard_meaning if needs_help else ""
|
| 275 |
+
else:
|
| 276 |
+
translation = target_utterance
|
| 277 |
+
|
| 278 |
+
# 🟢 THE DATA COLLECTION HOOK (Silent Background Saving)
|
| 279 |
+
clarification_to_save = translation if needs_help and translation else standard_meaning
|
| 280 |
+
try:
|
| 281 |
+
threading.Thread(
|
| 282 |
+
target=self.check_and_submit_logic,
|
| 283 |
+
args=(text, source_dialect, "", clarification_to_save, "Conversational", "Relay Peer-to-Peer Chat", "Automated Relay Extraction", "Game: Relay Pair", "AI Relay Bouncer", sender_id, None, False),
|
| 284 |
+
daemon=True
|
| 285 |
+
).start()
|
| 286 |
+
except Exception as save_err:
|
| 287 |
+
print(f"Data Hook Error: {save_err}")
|
| 288 |
|
| 289 |
+
# 4. Route to Room
|
| 290 |
+
if room_code not in self.live_rooms: self.live_rooms[room_code] = []
|
| 291 |
+
msg = {"sender": sender_id, "original": text, "translation": translation, "dialect": source_dialect, "target_dialect": target_dialect, "id": str(uuid.uuid4())[:8]}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
self.live_rooms[room_code].append(msg)
|
| 293 |
return json.dumps({"status": "success"})
|
| 294 |
|
|
|
|
| 295 |
def api_remote_poll(self, room_code, last_index):
|
| 296 |
+
import json
|
| 297 |
+
if room_code not in self.live_rooms: return json.dumps([])
|
|
|
|
| 298 |
idx = int(last_index)
|
| 299 |
+
return json.dumps(self.live_rooms[room_code][idx:])
|
|
|
|
|
|
|
| 300 |
# ==========================================
|
| 301 |
# SOCIOLINGUISTIC PIPELINE
|
| 302 |
# ==========================================
|
|
|
|
| 1269 |
api_name="translate_peer"
|
| 1270 |
)
|
| 1271 |
|
| 1272 |
+
# 🟢 NEW: Dialect Relay Endpoints
|
| 1273 |
+
api_btn_join = gr.Button()
|
| 1274 |
+
api_q_op = gr.Textbox()
|
| 1275 |
+
api_q_dialect = gr.Textbox()
|
| 1276 |
+
api_q_out = gr.Textbox()
|
| 1277 |
+
api_btn_join.click(fn=self.api_join_queue, inputs=[api_q_op, api_q_dialect], outputs=[api_q_out], api_name="join_queue")
|
| 1278 |
+
|
| 1279 |
+
api_btn_check = gr.Button()
|
| 1280 |
+
api_c_out = gr.Textbox()
|
| 1281 |
+
api_btn_check.click(fn=self.api_check_match, inputs=[api_q_op], outputs=[api_c_out], api_name="check_match")
|
| 1282 |
+
|
| 1283 |
+
api_btn_leave = gr.Button()
|
| 1284 |
+
api_btn_leave.click(fn=self.api_leave_queue, inputs=[api_q_op], outputs=[api_c_out], api_name="leave_queue")
|
| 1285 |
+
|
| 1286 |
api_btn_relay_send = gr.Button()
|
| 1287 |
api_relay_room = gr.Textbox()
|
|
|
|
| 1288 |
api_relay_text = gr.Textbox()
|
| 1289 |
+
api_relay_target = gr.Textbox()
|
| 1290 |
api_relay_out = gr.Textbox()
|
| 1291 |
+
api_btn_relay_send.click(fn=self.api_remote_eval_and_send, inputs=[api_relay_room, api_q_op, api_relay_text, api_q_dialect, api_relay_target], outputs=[api_relay_out], api_name="relay_send")
|
| 1292 |
|
| 1293 |
api_btn_relay_poll = gr.Button()
|
|
|
|
| 1294 |
api_poll_idx = gr.Number()
|
| 1295 |
+
api_btn_relay_poll.click(fn=self.api_remote_poll, inputs=[api_relay_room, api_poll_idx], outputs=[api_c_out], api_name="relay_poll")
|
|
|
|
| 1296 |
|
| 1297 |
# 7. Secure Feedback Endpoint
|
| 1298 |
api_fb_btn = gr.Button()
|