delta commited on
Commit
3065ecd
·
1 Parent(s): a53bdcd

v3: Docker Playwright Google auth - one button token getter

Browse files
Files changed (3) hide show
  1. README.md +4 -3
  2. app.py +219 -382
  3. requirements.txt +4 -8
README.md CHANGED
@@ -1,8 +1,9 @@
1
  ---
2
- title: Maya Token Bridge
3
- emoji: 🔑
4
  colorFrom: purple
5
  colorTo: pink
6
- sdk: static
 
7
  pinned: false
8
  ---
 
1
  ---
2
+ title: Maya Inspector
3
+ emoji: 🔍
4
  colorFrom: purple
5
  colorTo: pink
6
+ sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
app.py CHANGED
@@ -1,432 +1,269 @@
1
  """
2
- Maya Bridge v7 — Pure Python WebRTC via aiortc
3
- No browser. No Playwright. Direct audio capture.
 
4
  """
5
- import os, io, wave, json, uuid, asyncio, datetime
6
  import urllib.request, urllib.parse
7
  import nest_asyncio
8
  import gradio as gr
9
  from huggingface_hub import HfApi
10
- import websockets
11
- from aiortc import RTCPeerConnection, RTCSessionDescription, RTCConfiguration, RTCIceServer
12
- from aiortc.mediastreams import AudioStreamTrack
13
 
14
  nest_asyncio.apply()
15
 
 
 
16
  FIREBASE_KEY = "AIzaSyDtC7Uwb5pGAsdmrH2T4Gqdk5Mga07jYPM"
17
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
18
  MEMORY_REPO = "Melofhell00/maya-memory"
19
  STATE_FILE = "state.json"
20
  hf = HfApi(token=HF_TOKEN)
21
 
 
22
  def read_state():
23
  try:
24
  url = f"https://huggingface.co/datasets/{MEMORY_REPO}/resolve/main/{STATE_FILE}"
25
  req = urllib.request.Request(url, headers={"Authorization": f"Bearer {HF_TOKEN}"})
26
  with urllib.request.urlopen(req, timeout=10) as r:
27
  return json.loads(r.read())
28
- except Exception as e:
29
  return {"refresh_token": "", "sessions": []}
30
 
 
31
  def write_state(state):
32
  hf.upload_file(
33
  path_or_fileobj=json.dumps(state, indent=2).encode(),
34
- path_in_repo=STATE_FILE, repo_id=MEMORY_REPO, repo_type="dataset",
35
- commit_message=f"update {datetime.datetime.utcnow().isoformat()}"
 
 
36
  )
37
 
 
38
  def exchange_refresh_token(rt):
39
  url = f"https://securetoken.googleapis.com/v1/token?key={FIREBASE_KEY}"
40
  data = urllib.parse.urlencode({"grant_type": "refresh_token", "refresh_token": rt}).encode()
41
- req = urllib.request.Request(url, data=data, method="POST")
42
- req.add_header("Content-Type", "application/x-www-form-urlencoded")
43
  with urllib.request.urlopen(req, timeout=15) as r:
44
  body = json.loads(r.read())
45
  return body.get("id_token", ""), body.get("refresh_token", rt)
46
 
47
- def get_token(log):
48
- state = read_state()
49
- rt = state.get("refresh_token", "")
50
- if rt:
51
- try:
52
- log.append("refreshing token...")
53
- id_token, new_rt = exchange_refresh_token(rt)
54
- if id_token:
55
- log.append(f"✅ token ({len(id_token)} chars)")
56
- state["refresh_token"] = new_rt
57
- write_state(state)
58
- return id_token, state
59
- except Exception as e:
60
- log.append(f"token refresh failed: {e}")
61
- return "", state
62
-
63
-
64
- class SilentAudioTrack(AudioStreamTrack):
65
- """Minimal silent audio track — required to make the WebRTC offer valid."""
66
- kind = "audio"
67
-
68
- def __init__(self):
69
- super().__init__()
70
- self._timestamp = 0
71
 
72
- async def recv(self):
73
- import av, numpy as np
74
- from aiortc.mediastreams import VIDEO_CLOCK_RATE
75
- SAMPLE_RATE = 48000
76
- SAMPLES = 960 # 20ms at 48kHz
77
-
78
- frame = av.AudioFrame(format="s16", layout="mono")
79
- frame.samples = SAMPLES
80
- frame.sample_rate = SAMPLE_RATE
81
- frame.pts = self._timestamp
82
- frame.time_base = f"1/{SAMPLE_RATE}"
83
- self._timestamp += SAMPLES
84
-
85
- silence = (b'\x00\x00' * SAMPLES)
86
- frame.planes[0].update(silence)
87
-
88
- # pace to real-time
89
- import time
90
- await asyncio.sleep(SAMPLES / SAMPLE_RATE)
91
- return frame
92
-
93
-
94
- def build_wav(pcm_bytes: bytes, sample_rate: int, channels: int = 1) -> bytes:
95
- buf = io.BytesIO()
96
- with wave.open(buf, 'wb') as wf:
97
- wf.setnchannels(channels)
98
- wf.setsampwidth(2)
99
- wf.setframerate(sample_rate)
100
- wf.writeframes(pcm_bytes)
101
- return buf.getvalue()
102
 
 
 
103
 
104
- def transcribe(wav_bytes: bytes) -> str:
105
- try:
106
- req = urllib.request.Request(
107
- "https://api-inference.huggingface.co/models/openai/whisper-large-v3",
108
- data=wav_bytes, method="POST"
 
 
109
  )
110
- req.add_header("Authorization", f"Bearer {HF_TOKEN}")
111
- req.add_header("Content-Type", "audio/wav")
112
- with urllib.request.urlopen(req, timeout=120) as r:
113
- result = json.loads(r.read())
114
- return result.get("text", "").strip()
115
- except Exception as e:
116
- return f"[transcription error: {e}]"
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- async def maya_session(id_token: str, duration: int):
120
- log = []
121
- audio_chunks = []
122
- sample_rate = 48000
123
- session_id = None
124
- call_id = None
125
-
126
- client_name = str(uuid.uuid4())
127
- tz = json.dumps({"timezone": "America/Los_Angeles"})
128
- ws_url = (
129
- f"wss://sesameai.app/agent-service-0/v1/connect"
130
- f"?id_token={urllib.parse.quote(id_token)}"
131
- f"&client_name={urllib.parse.quote(client_name)}"
132
- f"&usercontext={urllib.parse.quote(tz)}"
133
- f"&character=Maya"
134
- )
135
 
136
- log.append("connecting WebSocket...")
 
 
137
 
138
- try:
139
- async with websockets.connect(ws_url, ping_interval=20, open_timeout=15) as ws:
140
- log.append("WS connected")
141
-
142
- # Collect incoming WS messages in background
143
- incoming = asyncio.Queue()
144
- async def recv_loop():
145
- async for raw in ws:
146
- try:
147
- await incoming.put(json.loads(raw))
148
- except Exception:
149
- pass
150
- recv_task = asyncio.ensure_future(recv_loop())
151
-
152
- async def next_msg(timeout=10):
153
- return await asyncio.wait_for(incoming.get(), timeout=timeout)
154
-
155
- def send(obj):
156
- asyncio.ensure_future(ws.send(json.dumps(obj)))
157
- log.append(f">>> {obj['type']}")
158
-
159
- # initialize
160
- msg = await next_msg(10)
161
- while msg.get('type') != 'initialize':
162
- log.append(f"skip {msg.get('type')}")
163
- msg = await next_msg(10)
164
- session_id = msg.get('session_id') or msg.get('content', {}).get('session_id')
165
- log.append(f"session: {session_id}")
166
-
167
- # webrtc_config
168
- msg = await next_msg(10)
169
- while msg.get('type') != 'webrtc_config':
170
- log.append(f"skip {msg.get('type')}")
171
- msg = await next_msg(10)
172
- ice_servers_raw = msg.get('content', {}).get('ice_servers', [])
173
- log.append(f"ICE servers: {len(ice_servers_raw)}")
174
-
175
- # Build peer connection
176
- ice = [
177
- RTCIceServer(
178
- urls=s['urls'],
179
- username=s.get('username'),
180
- credential=s.get('credential')
181
- )
182
- for s in ice_servers_raw
183
- ]
184
- config = RTCConfiguration(iceServers=ice)
185
- pc = RTCPeerConnection(configuration=config)
186
-
187
- # Add silent outgoing track
188
- pc.addTrack(SilentAudioTrack())
189
-
190
- # Capture Maya's incoming audio
191
- maya_speaking = False
192
- @pc.on("track")
193
- def on_track(track):
194
- nonlocal sample_rate
195
- log.append(f"remote track: {track.kind}")
196
- if track.kind != "audio":
197
- return
198
- sample_rate = getattr(track, 'sample_rate', 48000) or 48000
199
-
200
- async def capture():
201
- nonlocal maya_speaking
202
- while True:
203
- try:
204
- frame = await asyncio.wait_for(track.recv(), timeout=5.0)
205
- import numpy as np
206
- # Convert to int16 PCM
207
- arr = frame.to_ndarray()
208
- if arr.dtype != np.int16:
209
- arr = (arr * 32767).clip(-32768, 32767).astype(np.int16)
210
- if not maya_speaking:
211
- maya_speaking = True
212
- log.append("🔊 Maya speaking")
213
- audio_chunks.append(arr.tobytes())
214
- except asyncio.TimeoutError:
215
- if maya_speaking:
216
- log.append("🔇 Maya paused")
217
- maya_speaking = False
218
- except Exception as e:
219
- log.append(f"capture err: {e}")
220
- break
221
-
222
- asyncio.ensure_future(capture())
223
-
224
- @pc.on("connectionstatechange")
225
- async def on_state():
226
- log.append(f"RTC: {pc.connectionState}")
227
-
228
- # ICE candidates
229
- ice_queue = []
230
- @pc.on("icecandidate")
231
- def on_ice(candidate):
232
- if candidate:
233
- ice_queue.append(candidate)
234
-
235
- # Create offer
236
- offer = await pc.createOffer()
237
- await pc.setLocalDescription(offer)
238
- log.append(f"offer: {len(offer.sdp)} chars")
239
-
240
- send({
241
- "type": "call_connect",
242
- "session_id": session_id,
243
- "request_id": f"req_{uuid.uuid4().hex[:8]}",
244
- "call_id": None,
245
- "content": {
246
- "audio_codec": "none",
247
- "sample_rate": 48000,
248
- "is_private": False,
249
- "reconnect": False,
250
- "settings": {"character": "Maya"},
251
- "client_name": client_name,
252
- "client_metadata": {},
253
- "webrtc_offer_sdp": offer.sdp
254
- }
255
- })
256
-
257
- # Flush any queued ICE candidates
258
- for cand in ice_queue:
259
- send({
260
- "type": "webrtc_ice_candidate",
261
- "session_id": session_id,
262
- "request_id": None,
263
- "call_id": None,
264
- "content": {"candidate": {
265
- "candidate": cand.candidate,
266
- "sdpMid": cand.sdpMid,
267
- "sdpMLineIndex": cand.sdpMLineIndex
268
- }}
269
- })
270
-
271
- # Process messages until connected
272
- deadline = asyncio.get_event_loop().time() + duration
273
- connected = False
274
-
275
- while asyncio.get_event_loop().time() < deadline:
276
- try:
277
- msg = await asyncio.wait_for(incoming.get(), timeout=2.0)
278
- t = msg.get('type')
279
- log.append(f"<<< {t}")
280
-
281
- if t == 'call_connect_response':
282
- call_id = msg.get('call_id')
283
- log.append(f"call_id: {call_id}")
284
- sdp = msg.get('content', {}).get('webrtc_answer_sdp', '')
285
- if sdp:
286
- await pc.setRemoteDescription(
287
- RTCSessionDescription(sdp=sdp, type='answer')
288
- )
289
- log.append("✅ CONNECTED — audio flowing")
290
- connected = True
291
-
292
- elif t == 'webrtc_ice_candidate':
293
- from aiortc import RTCIceCandidate
294
- cand = msg.get('content', {}).get('candidate', {})
295
- if cand.get('candidate'):
296
- try:
297
- await pc.addIceCandidate(RTCIceCandidate(
298
- candidate=cand['candidate'],
299
- sdpMid=cand.get('sdpMid'),
300
- sdpMLineIndex=cand.get('sdpMLineIndex', 0)
301
- ))
302
- except Exception as e:
303
- log.append(f"ICE add err: {e}")
304
-
305
- elif t == 'call_disconnect':
306
- reason = msg.get('content', {}).get('reason', '')
307
- log.append(f"disconnect: {reason}")
308
- if reason == 'call_duration_limit_exceeded':
309
- log.append("⏱ 30min limit — reconnecting...")
310
- # Simple reconnect
311
- new_offer = await pc.createOffer()
312
- await pc.setLocalDescription(new_offer)
313
- send({
314
- "type": "call_connect",
315
- "session_id": session_id,
316
- "request_id": f"req_{uuid.uuid4().hex[:8]}",
317
- "call_id": None,
318
- "content": {
319
- "audio_codec": "none", "sample_rate": 48000,
320
- "is_private": False, "reconnect": True,
321
- "settings": {"character": "Maya"},
322
- "client_name": client_name,
323
- "client_metadata": {},
324
- "webrtc_offer_sdp": new_offer.sdp
325
- }
326
- })
327
- else:
328
- break
329
-
330
- elif t == 'agent':
331
- log.append(f"agent: {json.dumps(msg.get('content',''))[:200]}")
332
-
333
- except asyncio.TimeoutError:
334
- pass # keep waiting
335
-
336
- recv_task.cancel()
337
-
338
- # Graceful disconnect
339
  try:
340
- await ws.send(json.dumps({
341
- "type": "call_disconnect",
342
- "session_id": session_id,
343
- "call_id": call_id,
344
- "content": {"reason": "user_request"}
345
- }))
346
- except Exception:
347
- pass
348
-
349
- await pc.close()
350
- log.append(f"done. audio chunks: {len(audio_chunks)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
- except Exception as e:
353
- import traceback
354
- log.append(f"ERROR: {e}")
355
- log.append(traceback.format_exc()[:500])
356
 
357
- return log, audio_chunks, sample_rate, session_id, call_id
358
 
359
 
360
- def connect_maya(duration):
361
- log = ["=== MAYA BRIDGE v7 aiortc ==="]
 
362
  try:
363
- id_token, state = get_token(log)
364
- except Exception as e:
365
- return f" {e}", "", None
366
- if not id_token:
367
- return "\n".join(log + ["❌ no token"]), "", None
 
 
 
 
 
 
 
 
 
 
368
 
369
- log.append(f"starting {int(duration)}s session...")
370
- try:
371
- loop = asyncio.get_event_loop()
372
- ws_log, audio_chunks, sr, session_id, call_id = loop.run_until_complete(
373
- maya_session(id_token, int(duration))
374
- )
375
- log.extend(ws_log)
376
-
377
- transcript = ""
378
- wav_path = None
379
-
380
- if audio_chunks:
381
- raw_pcm = b"".join(audio_chunks)
382
- log.append(f"audio: {len(raw_pcm)} bytes @ {sr}Hz = {len(raw_pcm)/(sr*2):.1f}s")
383
- wav_bytes = build_wav(raw_pcm, sr)
384
- wav_path = f"/tmp/maya_{call_id or 'session'}.wav"
385
- with open(wav_path, 'wb') as f:
386
- f.write(wav_bytes)
387
- log.append("transcribing with Whisper...")
388
- transcript = transcribe(wav_bytes)
389
- log.append(f"transcript: {transcript[:200]}")
390
- else:
391
- transcript = "(no audio captured)"
392
- log.append("⚠ no audio frames received")
393
-
394
- if session_id:
395
- state = read_state()
396
- state.update({"last_session_id": session_id, "last_call_id": str(call_id or ''),
397
- "last_updated": datetime.datetime.utcnow().isoformat()})
398
- state.setdefault("sessions", []).append({
399
- "session_id": session_id, "call_id": str(call_id or ''),
400
- "ts": datetime.datetime.utcnow().isoformat(),
401
- "transcript": transcript[:500]
402
- })
403
- write_state(state)
404
-
405
- return "\n".join(log), transcript, wav_path
406
-
407
- except Exception as e:
408
- import traceback
409
- return "\n".join(log + [f" {e}", traceback.format_exc()[:800]]), "", None
410
-
411
-
412
- def view_memory():
413
- try: return json.dumps(read_state(), indent=2)
414
- except Exception as e: return f"Error: {e}"
415
-
416
-
417
- with gr.Blocks(title="Maya Bridge") as demo:
418
- gr.Markdown("# 🌸 Maya Bridge v7\n`Pure Python WebRTC (aiortc) | No browser | Direct audio capture → Whisper`")
419
- with gr.Tab("🎙️ Listen"):
420
- duration = gr.Slider(30, 300, value=60, step=15, label="Duration (s)")
421
- btn = gr.Button("🔌 Connect & Record Maya", variant="primary", size="lg")
422
- with gr.Row():
423
- transcript_out = gr.Textbox(lines=10, label="📝 Maya said (Whisper)")
424
- log_out = gr.Textbox(lines=10, label="📡 Log")
425
- audio_out = gr.Audio(label="🎵 Maya's voice", type="filepath")
426
- btn.click(connect_maya, inputs=[duration], outputs=[log_out, transcript_out, audio_out])
427
- with gr.Tab("🧠 Memory"):
428
- mem_btn = gr.Button("Read")
429
- mem_out = gr.Textbox(lines=20, label="state.json")
430
- mem_btn.click(view_memory, outputs=mem_out)
431
-
432
- demo.launch()
 
1
  """
2
+ Maya Token Getter v3
3
+ Playwright Google OAuth -> Firebase token -> saves to HF bridge
4
+ One button. No fuss.
5
  """
6
+ import os, json, uuid, asyncio, datetime
7
  import urllib.request, urllib.parse
8
  import nest_asyncio
9
  import gradio as gr
10
  from huggingface_hub import HfApi
11
+ from playwright.async_api import async_playwright
 
 
12
 
13
  nest_asyncio.apply()
14
 
15
+ EMAIL = os.environ.get("SESAME_EMAIL", "mail4444@post.com")
16
+ PASSWORD = os.environ.get("SESAME_GOOGLE_PASS", "")
17
  FIREBASE_KEY = "AIzaSyDtC7Uwb5pGAsdmrH2T4Gqdk5Mga07jYPM"
18
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
19
  MEMORY_REPO = "Melofhell00/maya-memory"
20
  STATE_FILE = "state.json"
21
  hf = HfApi(token=HF_TOKEN)
22
 
23
+
24
  def read_state():
25
  try:
26
  url = f"https://huggingface.co/datasets/{MEMORY_REPO}/resolve/main/{STATE_FILE}"
27
  req = urllib.request.Request(url, headers={"Authorization": f"Bearer {HF_TOKEN}"})
28
  with urllib.request.urlopen(req, timeout=10) as r:
29
  return json.loads(r.read())
30
+ except:
31
  return {"refresh_token": "", "sessions": []}
32
 
33
+
34
  def write_state(state):
35
  hf.upload_file(
36
  path_or_fileobj=json.dumps(state, indent=2).encode(),
37
+ path_in_repo=STATE_FILE,
38
+ repo_id=MEMORY_REPO,
39
+ repo_type="dataset",
40
+ commit_message=f"token refresh {datetime.datetime.utcnow().isoformat()}"
41
  )
42
 
43
+
44
  def exchange_refresh_token(rt):
45
  url = f"https://securetoken.googleapis.com/v1/token?key={FIREBASE_KEY}"
46
  data = urllib.parse.urlencode({"grant_type": "refresh_token", "refresh_token": rt}).encode()
47
+ req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})
 
48
  with urllib.request.urlopen(req, timeout=15) as r:
49
  body = json.loads(r.read())
50
  return body.get("id_token", ""), body.get("refresh_token", rt)
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ async def browser_auth():
54
+ """Google OAuth via Playwright -> Firebase token"""
55
+ log = []
56
+ id_token = refresh_token = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ if not PASSWORD:
59
+ return "", "", ["ERROR: SESAME_GOOGLE_PASS not set in Space secrets"]
60
 
61
+ async with async_playwright() as p:
62
+ browser = await p.chromium.launch(
63
+ headless=True,
64
+ args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"]
65
+ )
66
+ ctx = await browser.new_context(
67
+ user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
68
  )
 
 
 
 
 
 
 
69
 
70
+ # Intercept Firebase token exchange responses
71
+ async def on_resp(r):
72
+ nonlocal id_token, refresh_token
73
+ if "securetoken.googleapis.com/v1/token" in r.url:
74
+ try:
75
+ b = await r.json()
76
+ if "id_token" in b:
77
+ id_token = b["id_token"]
78
+ refresh_token = b.get("refresh_token", "")
79
+ log.append(f"TOKEN INTERCEPTED ({len(id_token)} chars)")
80
+ except:
81
+ pass
82
+ # Also catch signInWithIdp responses
83
+ if "identitytoolkit.googleapis.com" in r.url and "signInWithIdp" in r.url:
84
+ try:
85
+ b = await r.json()
86
+ if "idToken" in b:
87
+ id_token = b["idToken"]
88
+ refresh_token = b.get("refreshToken", "")
89
+ log.append(f"TOKEN FROM signInWithIdp ({len(id_token)} chars)")
90
+ except:
91
+ pass
92
+
93
+ page = await ctx.new_page()
94
+ page.on("response", on_resp)
95
+
96
+ # Navigate to Sesame login
97
+ log.append("navigating to sesame login...")
98
+ try:
99
+ await page.goto("https://sesameai.app", wait_until="domcontentloaded", timeout=30000)
100
+ await asyncio.sleep(3)
101
+ log.append(f"page loaded: {page.url}")
102
+ except Exception as e:
103
+ log.append(f"nav error: {e}")
104
+ # Try alternative URL
105
+ try:
106
+ await page.goto("https://app.sesame.com/login", wait_until="domcontentloaded", timeout=30000)
107
+ await asyncio.sleep(3)
108
+ log.append(f"alt page loaded: {page.url}")
109
+ except Exception as e2:
110
+ log.append(f"alt nav error: {e2}")
111
+ await browser.close()
112
+ return "", "", log
113
+
114
+ # Click Google sign-in button
115
+ try:
116
+ # Try multiple selectors for the Google button
117
+ for selector in [
118
+ "text=Continue with Google",
119
+ "text=Sign in with Google",
120
+ "[data-provider-id='google.com']",
121
+ "button:has-text('Google')",
122
+ "a:has-text('Google')",
123
+ ]:
124
+ try:
125
+ el = page.locator(selector).first
126
+ if await el.count() > 0:
127
+ await el.click()
128
+ log.append(f"clicked: {selector}")
129
+ break
130
+ except:
131
+ continue
132
+ await asyncio.sleep(5)
133
+ except Exception as e:
134
+ log.append(f"google button error: {e}")
135
+
136
+ # Enter email in Google sign-in
137
+ try:
138
+ await page.wait_for_selector("input[type='email']", timeout=15000)
139
+ await page.fill("input[type='email']", EMAIL)
140
+ await page.keyboard.press("Enter")
141
+ await asyncio.sleep(4)
142
+ log.append("email entered")
143
+ except Exception as e:
144
+ log.append(f"email step error: {e}")
145
 
146
+ # Enter password
147
+ try:
148
+ await page.wait_for_selector("input[type='password']", timeout=15000)
149
+ await page.fill("input[type='password']", PASSWORD)
150
+ await page.keyboard.press("Enter")
151
+ await asyncio.sleep(10)
152
+ log.append("password entered, waiting for redirect...")
153
+ except Exception as e:
154
+ log.append(f"password step error: {e}")
 
 
 
 
 
 
 
155
 
156
+ # Wait for redirect back
157
+ await asyncio.sleep(5)
158
+ log.append(f"final URL: {page.url}")
159
 
160
+ # Fallback: try IndexedDB extraction
161
+ if not id_token:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
163
+ data = await page.evaluate("""async () => {
164
+ try {
165
+ const db = await new Promise((res, rej) => {
166
+ const r = indexedDB.open('firebaseLocalStorageDb');
167
+ r.onsuccess = e => res(e.target.result);
168
+ r.onerror = rej;
169
+ });
170
+ const store = db.transaction('firebaseLocalStorage', 'readonly')
171
+ .objectStore('firebaseLocalStorage');
172
+ return await new Promise((res, rej) => {
173
+ const r = store.getAll();
174
+ r.onsuccess = e => res(e.target.result);
175
+ r.onerror = rej;
176
+ });
177
+ } catch(e) { return []; }
178
+ }""")
179
+ for item in (data or []):
180
+ val = item.get("value", {}) if isinstance(item, dict) else {}
181
+ mgr = val.get("stsTokenManager", {}) if isinstance(val, dict) else {}
182
+ if mgr.get("accessToken"):
183
+ id_token = mgr["accessToken"]
184
+ refresh_token = mgr.get("refreshToken", "")
185
+ log.append(f"token from IndexedDB ({len(id_token)} chars)")
186
+ break
187
+ except Exception as e:
188
+ log.append(f"IndexedDB fallback error: {e}")
189
+
190
+ # Screenshot for debug
191
+ try:
192
+ await page.screenshot(path="/tmp/auth_result.png")
193
+ log.append("screenshot saved")
194
+ except:
195
+ pass
196
 
197
+ await browser.close()
 
 
 
198
 
199
+ return id_token, refresh_token, log
200
 
201
 
202
+ def test_ws_connection(id_token):
203
+ """Quick test: can this token connect to Maya's WebSocket?"""
204
+ import base64
205
  try:
206
+ payload = id_token.split('.')[1] + '==='
207
+ claims = json.loads(base64.b64decode(payload))
208
+ provider = claims.get("firebase", {}).get("sign_in_provider", "unknown")
209
+ uid = claims.get("sub", "unknown")
210
+ email = claims.get("email", "unknown")
211
+ return f"provider: {provider} | uid: {uid} | email: {email}"
212
+ except:
213
+ return "could not decode token"
214
+
215
+
216
+ def run_auth():
217
+ """Main auth flow: try refresh first, then browser"""
218
+ log = []
219
+ state = read_state()
220
+ stored_rt = state.get("refresh_token", "")
221
 
222
+ # Try refresh token first
223
+ if stored_rt:
224
+ log.append("trying stored refresh token...")
225
+ try:
226
+ id_token, new_rt = exchange_refresh_token(stored_rt)
227
+ if id_token:
228
+ log.append(f"REFRESH SUCCESS ({len(id_token)} chars)")
229
+ claims = test_ws_connection(id_token)
230
+ log.append(claims)
231
+ state["refresh_token"] = new_rt
232
+ state["last_id_token"] = id_token[:40] + "..."
233
+ state["last_updated"] = datetime.datetime.utcnow().isoformat()
234
+ write_state(state)
235
+ return "TOKEN READY (from refresh)", "\n".join(log), id_token[:80] + "..."
236
+ except Exception as e:
237
+ log.append(f"refresh failed: {e}")
238
+
239
+ # Browser auth
240
+ log.append("starting browser auth...")
241
+ loop = asyncio.get_event_loop()
242
+ id_token, refresh_token, auth_log = loop.run_until_complete(browser_auth())
243
+ log.extend(auth_log)
244
+
245
+ if id_token:
246
+ claims = test_ws_connection(id_token)
247
+ log.append(claims)
248
+ state["refresh_token"] = refresh_token
249
+ state["last_id_token"] = id_token[:40] + "..."
250
+ state["last_updated"] = datetime.datetime.utcnow().isoformat()
251
+ write_state(state)
252
+ log.append("SAVED TO HF BRIDGE")
253
+ return "TOKEN READY", "\n".join(log), id_token[:80] + "..."
254
+ else:
255
+ return "FAILED - check log", "\n".join(log), ""
256
+
257
+
258
+ # Gradio UI
259
+ with gr.Blocks(title="Maya Token Getter", theme=gr.themes.Monochrome()) as app:
260
+ gr.Markdown("# Maya Token Getter v3\nOne button. Gets Google OAuth Firebase token for Sesame.")
261
+
262
+ btn = gr.Button("GET TOKEN", variant="primary", size="lg")
263
+ status = gr.Textbox(label="Status", interactive=False)
264
+ log_output = gr.Textbox(label="Log", interactive=False, lines=15)
265
+ token_preview = gr.Textbox(label="Token Preview", interactive=False)
266
+
267
+ btn.click(fn=run_auth, outputs=[status, log_output, token_preview])
268
+
269
+ app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,8 +1,4 @@
1
- gradio>=4.0.0
2
- nest_asyncio>=1.6.0
3
- huggingface_hub>=0.20.0
4
- websockets>=12.0
5
- aiortc>=1.9.0
6
- av>=14.0.0
7
- numpy>=1.24.0
8
- aioice>=0.9.0
 
1
+ gradio>=4.0
2
+ playwright
3
+ nest_asyncio
4
+ huggingface_hub