eye_tracking.py
1 """ 2 Eye Tracking Integration (Tobii via Talon) 3 4 Integration point for hardware eye tracking on macOS. 5 6 Architecture: 7 Tobii 5 → Talon (Mac driver) → Unix Socket → TalonIntegration → AttentionTracker 8 9 Since Tobii doesn't officially support macOS, we use Talon as the 10 hardware interface. Talon provides: 11 - Mac-compatible Tobii driver 12 - Raw gaze data streaming 13 - Calibration UI 14 15 This module provides: 16 1. TalonIntegration - connects to Talon's gaze bridge 17 2. Fixation detection using I-DT algorithm 18 3. Semantic mapping (screen regions to concepts) 19 4. AttentionEvent generation for the attention tracker 20 21 The hierarchy of attention signals (from inferred to direct): 22 23 INFERRED DIRECT 24 ◄────────────────────────────────────────────────► 25 26 Session topics Podcast plays Highlights Eye gaze 27 (weak signal) (medium) (strong) (ground truth) 28 29 Eye tracking provides: 30 - Fixation duration → depth of attention 31 - Saccade patterns → scanning vs reading 32 - Pupil dilation → cognitive load / interest 33 - Gaze velocity → searching vs focused 34 35 Integration points: 36 - AttentionEvent.modality = 'gaze' 37 - AttentionEvent.intensity = fixation_duration / baseline 38 - Real-time trajectory updates (sub-second) 39 40 "Attention is all you need" - both the transformer insight and literally true. 41 """ 42 43 from dataclasses import dataclass, field 44 from datetime import datetime 45 from typing import Optional, List, Dict, Tuple, Callable, Union 46 from enum import Enum 47 from pathlib import Path 48 import json 49 import socket 50 import threading 51 import time 52 53 54 class GazeEventType(Enum): 55 """Types of gaze events.""" 56 FIXATION = "fixation" # Sustained gaze on target 57 SACCADE = "saccade" # Rapid movement between targets 58 SMOOTH_PURSUIT = "smooth" # Tracking moving target 59 BLINK = "blink" # Eyes closed 60 61 62 @dataclass 63 class GazePoint: 64 """A single gaze sample from the eye tracker.""" 65 timestamp: datetime 66 x: float # Screen x coordinate (0-1 normalized) 67 y: float # Screen y coordinate (0-1 normalized) 68 left_valid: bool = True 69 right_valid: bool = True 70 pupil_diameter: Optional[float] = None # mm, indicates cognitive load 71 72 73 @dataclass 74 class Fixation: 75 """A fixation event - sustained attention on a point.""" 76 start_time: datetime 77 end_time: datetime 78 x: float # Centroid x 79 y: float # Centroid y 80 duration_ms: float 81 dispersion: float # How spread out the gaze points were 82 avg_pupil: Optional[float] = None 83 84 @property 85 def intensity(self) -> float: 86 """ 87 Compute attention intensity from fixation properties. 88 89 Longer fixations with smaller dispersion = higher intensity. 90 Larger pupil = higher cognitive engagement. 91 """ 92 # Base intensity from duration (saturates around 1 second) 93 duration_factor = min(1.0, self.duration_ms / 1000.0) 94 95 # Dispersion penalty (tight gaze = more focused) 96 dispersion_factor = max(0.5, 1.0 - self.dispersion * 2) 97 98 # Pupil boost (if available) 99 pupil_factor = 1.0 100 if self.avg_pupil: 101 # Assume baseline ~4mm, larger = more engaged 102 pupil_factor = min(1.2, self.avg_pupil / 4.0) 103 104 return duration_factor * dispersion_factor * pupil_factor 105 106 107 @dataclass 108 class ScreenRegion: 109 """A region of the screen mapped to a semantic target.""" 110 name: str 111 x_min: float 112 y_min: float 113 x_max: float 114 y_max: float 115 target_id: Optional[str] = None # UUID if mapped to a bullet/concept 116 target_type: Optional[str] = None # 'bullet', 'code', 'ui_element' 117 118 def contains(self, x: float, y: float) -> bool: 119 """Check if a point is in this region.""" 120 return (self.x_min <= x <= self.x_max and 121 self.y_min <= y <= self.y_max) 122 123 124 class TobiiIntegration: 125 """ 126 Integration with Tobii eye tracker. 127 128 This is a placeholder for when the hardware arrives. 129 The actual implementation will use the Tobii SDK. 130 131 Usage (future): 132 tobii = TobiiIntegration() 133 tobii.connect() 134 135 # Register screen regions 136 tobii.register_region(ScreenRegion( 137 name="editor", 138 x_min=0.0, y_min=0.0, 139 x_max=0.7, y_max=1.0, 140 target_type="code" 141 )) 142 143 # Start tracking 144 tobii.start() 145 146 # Get fixations 147 for fixation in tobii.get_fixations(): 148 print(f"Looked at {fixation.x}, {fixation.y} for {fixation.duration_ms}ms") 149 """ 150 151 def __init__( 152 self, 153 fixation_threshold_ms: float = 100, # Minimum duration for fixation 154 dispersion_threshold: float = 0.03, # Maximum dispersion (normalized) 155 ): 156 self.fixation_threshold_ms = fixation_threshold_ms 157 self.dispersion_threshold = dispersion_threshold 158 159 # State 160 self._connected = False 161 self._tracking = False 162 self._gaze_buffer: List[GazePoint] = [] 163 self._fixations: List[Fixation] = [] 164 self._regions: List[ScreenRegion] = [] 165 166 # Callbacks 167 self._on_fixation: List[Callable[[Fixation], None]] = [] 168 self._on_region_enter: List[Callable[[ScreenRegion, Fixation], None]] = [] 169 170 def connect(self) -> bool: 171 """ 172 Connect to the Tobii eye tracker. 173 174 Returns True if connected successfully. 175 """ 176 # TODO: Implement with Tobii SDK 177 # from tobii_research import find_all_eyetrackers 178 # eyetrackers = find_all_eyetrackers() 179 # if eyetrackers: 180 # self._tracker = eyetrackers[0] 181 # self._connected = True 182 # return True 183 184 print("[TobiiIntegration] Hardware not connected (placeholder mode)") 185 self._connected = False 186 return False 187 188 def start(self) -> None: 189 """Start tracking gaze.""" 190 if not self._connected: 191 print("[TobiiIntegration] Not connected, cannot start") 192 return 193 194 # TODO: Subscribe to gaze data 195 # self._tracker.subscribe_to(tr.EYETRACKER_GAZE_DATA, self._gaze_callback) 196 self._tracking = True 197 198 def stop(self) -> None: 199 """Stop tracking gaze.""" 200 # TODO: Unsubscribe from gaze data 201 self._tracking = False 202 203 def register_region(self, region: ScreenRegion) -> None: 204 """Register a screen region for semantic mapping.""" 205 self._regions.append(region) 206 207 def on_fixation(self, callback: Callable[[Fixation], None]) -> None: 208 """Register callback for fixation events.""" 209 self._on_fixation.append(callback) 210 211 def on_region_enter( 212 self, 213 callback: Callable[[ScreenRegion, Fixation], None] 214 ) -> None: 215 """Register callback for when gaze enters a region.""" 216 self._on_region_enter.append(callback) 217 218 def get_fixations(self, since: datetime = None) -> List[Fixation]: 219 """Get recent fixations.""" 220 if since: 221 return [f for f in self._fixations if f.start_time >= since] 222 return self._fixations 223 224 def get_attention_heatmap( 225 self, 226 width: int = 100, 227 height: int = 100 228 ) -> List[List[float]]: 229 """ 230 Generate an attention heatmap from recent fixations. 231 232 Returns a 2D grid of attention intensity values. 233 """ 234 heatmap = [[0.0] * width for _ in range(height)] 235 236 for fixation in self._fixations[-100:]: # Last 100 fixations 237 x_idx = int(fixation.x * (width - 1)) 238 y_idx = int(fixation.y * (height - 1)) 239 240 # Add intensity with gaussian spread 241 for dx in range(-3, 4): 242 for dy in range(-3, 4): 243 nx, ny = x_idx + dx, y_idx + dy 244 if 0 <= nx < width and 0 <= ny < height: 245 dist = (dx**2 + dy**2) ** 0.5 246 spread = fixation.intensity * max(0, 1 - dist / 3) 247 heatmap[ny][nx] += spread 248 249 return heatmap 250 251 def _gaze_callback(self, gaze_data) -> None: 252 """ 253 Callback for raw gaze data from Tobii SDK. 254 255 Converts raw samples to fixations using I-DT algorithm 256 (dispersion-threshold identification). 257 """ 258 # TODO: Implement with real Tobii data structure 259 # point = GazePoint( 260 # timestamp=datetime.now(), 261 # x=gaze_data['left_gaze_point_on_display_area'][0], 262 # y=gaze_data['left_gaze_point_on_display_area'][1], 263 # left_valid=gaze_data['left_gaze_point_validity'], 264 # right_valid=gaze_data['right_gaze_point_validity'], 265 # pupil_diameter=gaze_data['left_pupil_diameter'] 266 # ) 267 # self._gaze_buffer.append(point) 268 # self._process_buffer() 269 pass 270 271 def _process_buffer(self) -> None: 272 """ 273 Process gaze buffer to detect fixations. 274 275 Uses I-DT (dispersion-threshold) algorithm: 276 1. Maintain a sliding window of gaze points 277 2. If dispersion < threshold and duration > threshold, it's a fixation 278 3. Emit fixation event 279 """ 280 if len(self._gaze_buffer) < 3: 281 return 282 283 # Check if recent points form a fixation 284 window = self._gaze_buffer[-10:] # Last 10 samples 285 if not window: 286 return 287 288 # Calculate dispersion (max distance from centroid) 289 xs = [p.x for p in window if p.left_valid or p.right_valid] 290 ys = [p.y for p in window if p.left_valid or p.right_valid] 291 292 if not xs: 293 return 294 295 cx, cy = sum(xs) / len(xs), sum(ys) / len(ys) 296 dispersion = max( 297 max(abs(x - cx) for x in xs), 298 max(abs(y - cy) for y in ys) 299 ) 300 301 # Check if this is a fixation 302 if dispersion < self.dispersion_threshold: 303 duration_ms = ( 304 window[-1].timestamp - window[0].timestamp 305 ).total_seconds() * 1000 306 307 if duration_ms >= self.fixation_threshold_ms: 308 # Create fixation 309 avg_pupil = None 310 pupils = [p.pupil_diameter for p in window if p.pupil_diameter] 311 if pupils: 312 avg_pupil = sum(pupils) / len(pupils) 313 314 fixation = Fixation( 315 start_time=window[0].timestamp, 316 end_time=window[-1].timestamp, 317 x=cx, 318 y=cy, 319 duration_ms=duration_ms, 320 dispersion=dispersion, 321 avg_pupil=avg_pupil 322 ) 323 324 self._fixations.append(fixation) 325 326 # Trigger callbacks 327 for callback in self._on_fixation: 328 callback(fixation) 329 330 # Check region entry 331 for region in self._regions: 332 if region.contains(cx, cy): 333 for callback in self._on_region_enter: 334 callback(region, fixation) 335 336 # Clear buffer 337 self._gaze_buffer = self._gaze_buffer[-3:] 338 339 340 class TalonIntegration: 341 """ 342 Integration with Tobii eye tracker via Talon on macOS. 343 344 Talon provides the Mac driver for Tobii 5. This class connects 345 to Talon's gaze bridge (sovereign_gaze_bridge.py) via Unix socket 346 and processes the raw gaze stream. 347 348 Usage: 349 talon = TalonIntegration() 350 talon.connect() 351 352 # Register screen regions 353 talon.register_region(ScreenRegion( 354 name="editor", 355 x_min=0.0, y_min=0.0, 356 x_max=0.7, y_max=1.0, 357 target_type="code" 358 )) 359 360 # Start tracking (runs in background thread) 361 talon.start() 362 363 # Get fixations 364 for fixation in talon.get_fixations(): 365 print(f"Looked at {fixation.x}, {fixation.y} for {fixation.duration_ms}ms") 366 367 # Cleanup 368 talon.stop() 369 """ 370 371 SOCKET_PATH = Path.home() / ".sovereign" / "gaze.sock" 372 FALLBACK_PORT = 9876 373 374 def __init__( 375 self, 376 fixation_threshold_ms: float = 100, 377 dispersion_threshold: float = 0.03, 378 ): 379 self.fixation_threshold_ms = fixation_threshold_ms 380 self.dispersion_threshold = dispersion_threshold 381 382 # State 383 self._connected = False 384 self._tracking = False 385 self._socket: Optional[socket.socket] = None 386 self._receiver_thread: Optional[threading.Thread] = None 387 self._stop_event = threading.Event() 388 389 # Data buffers 390 self._gaze_buffer: List[GazePoint] = [] 391 self._fixations: List[Fixation] = [] 392 self._regions: List[ScreenRegion] = [] 393 self._buffer_lock = threading.Lock() 394 395 # Callbacks 396 self._on_fixation: List[Callable[[Fixation], None]] = [] 397 self._on_region_enter: List[Callable[[ScreenRegion, Fixation], None]] = [] 398 self._on_gaze_point: List[Callable[[GazePoint], None]] = [] 399 400 def connect(self) -> bool: 401 """ 402 Connect to Talon's gaze bridge. 403 404 Returns True if connected successfully. 405 """ 406 # Try Unix socket first 407 if self.SOCKET_PATH.exists(): 408 try: 409 self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 410 self._socket.connect(str(self.SOCKET_PATH)) 411 self._socket.setblocking(False) 412 self._connected = True 413 print(f"[TalonIntegration] Connected via Unix socket") 414 return True 415 except Exception as e: 416 print(f"[TalonIntegration] Unix socket failed: {e}") 417 418 # Fall back to TCP 419 try: 420 self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 421 self._socket.connect(('127.0.0.1', self.FALLBACK_PORT)) 422 self._socket.setblocking(False) 423 self._connected = True 424 print(f"[TalonIntegration] Connected via TCP port {self.FALLBACK_PORT}") 425 return True 426 except Exception as e: 427 print(f"[TalonIntegration] TCP connection failed: {e}") 428 print("[TalonIntegration] Is Talon running with sovereign_gaze_bridge?") 429 self._connected = False 430 return False 431 432 def start(self) -> None: 433 """Start receiving gaze data in background thread.""" 434 if not self._connected: 435 print("[TalonIntegration] Not connected, cannot start") 436 return 437 438 if self._tracking: 439 return 440 441 self._tracking = True 442 self._stop_event.clear() 443 self._receiver_thread = threading.Thread( 444 target=self._receive_loop, 445 daemon=True, 446 name="TalonGazeReceiver" 447 ) 448 self._receiver_thread.start() 449 print("[TalonIntegration] Started gaze tracking") 450 451 def stop(self) -> None: 452 """Stop receiving gaze data.""" 453 if not self._tracking: 454 return 455 456 self._tracking = False 457 self._stop_event.set() 458 459 if self._receiver_thread: 460 self._receiver_thread.join(timeout=1.0) 461 self._receiver_thread = None 462 463 if self._socket: 464 try: 465 self._socket.close() 466 except: 467 pass 468 self._socket = None 469 470 self._connected = False 471 print("[TalonIntegration] Stopped gaze tracking") 472 473 def register_region(self, region: ScreenRegion) -> None: 474 """Register a screen region for semantic mapping.""" 475 self._regions.append(region) 476 477 def on_fixation(self, callback: Callable[[Fixation], None]) -> None: 478 """Register callback for fixation events.""" 479 self._on_fixation.append(callback) 480 481 def on_region_enter( 482 self, 483 callback: Callable[[ScreenRegion, Fixation], None] 484 ) -> None: 485 """Register callback for when gaze enters a region.""" 486 self._on_region_enter.append(callback) 487 488 def on_gaze_point(self, callback: Callable[[GazePoint], None]) -> None: 489 """Register callback for raw gaze points.""" 490 self._on_gaze_point.append(callback) 491 492 def get_fixations(self, since: datetime = None) -> List[Fixation]: 493 """Get recent fixations.""" 494 with self._buffer_lock: 495 if since: 496 return [f for f in self._fixations if f.start_time >= since] 497 return list(self._fixations) 498 499 def get_attention_heatmap( 500 self, 501 width: int = 100, 502 height: int = 100 503 ) -> List[List[float]]: 504 """Generate an attention heatmap from recent fixations.""" 505 heatmap = [[0.0] * width for _ in range(height)] 506 507 with self._buffer_lock: 508 fixations = self._fixations[-100:] 509 510 for fixation in fixations: 511 x_idx = int(fixation.x * (width - 1)) 512 y_idx = int(fixation.y * (height - 1)) 513 514 for dx in range(-3, 4): 515 for dy in range(-3, 4): 516 nx, ny = x_idx + dx, y_idx + dy 517 if 0 <= nx < width and 0 <= ny < height: 518 dist = (dx**2 + dy**2) ** 0.5 519 spread = fixation.intensity * max(0, 1 - dist / 3) 520 heatmap[ny][nx] += spread 521 522 return heatmap 523 524 def _receive_loop(self) -> None: 525 """Background thread that receives gaze data from Talon.""" 526 buffer = "" 527 528 while not self._stop_event.is_set(): 529 try: 530 data = self._socket.recv(4096).decode('utf-8') 531 if not data: 532 time.sleep(0.01) 533 continue 534 535 buffer += data 536 537 # Process complete JSON lines 538 while '\n' in buffer: 539 line, buffer = buffer.split('\n', 1) 540 if line.strip(): 541 self._process_message(line) 542 543 except BlockingIOError: 544 time.sleep(0.01) 545 except Exception as e: 546 if not self._stop_event.is_set(): 547 print(f"[TalonIntegration] Receive error: {e}") 548 break 549 550 def _process_message(self, message: str) -> None: 551 """Process a JSON message from Talon.""" 552 try: 553 data = json.loads(message) 554 555 if data.get("type") == "gaze_point": 556 point = GazePoint( 557 timestamp=datetime.fromisoformat(data["timestamp"]), 558 x=data["x"], 559 y=data["y"], 560 left_valid=data.get("left_valid", True), 561 right_valid=data.get("right_valid", True), 562 pupil_diameter=data.get("pupil_diameter"), 563 ) 564 565 # Notify raw gaze callbacks 566 for callback in self._on_gaze_point: 567 try: 568 callback(point) 569 except Exception as e: 570 print(f"[TalonIntegration] Gaze callback error: {e}") 571 572 # Add to buffer and process fixations 573 with self._buffer_lock: 574 self._gaze_buffer.append(point) 575 # Keep buffer bounded 576 if len(self._gaze_buffer) > 200: 577 self._gaze_buffer = self._gaze_buffer[-100:] 578 579 self._process_buffer() 580 581 except json.JSONDecodeError as e: 582 print(f"[TalonIntegration] JSON parse error: {e}") 583 except Exception as e: 584 print(f"[TalonIntegration] Message processing error: {e}") 585 586 def _process_buffer(self) -> None: 587 """ 588 Process gaze buffer to detect fixations using I-DT algorithm. 589 590 I-DT (dispersion-threshold identification): 591 1. Maintain a sliding window of gaze points 592 2. If dispersion < threshold and duration > threshold, it's a fixation 593 3. Emit fixation event and clear the window 594 """ 595 with self._buffer_lock: 596 if len(self._gaze_buffer) < 3: 597 return 598 599 window = self._gaze_buffer[-10:] 600 if not window: 601 return 602 603 # Calculate dispersion (max distance from centroid) 604 xs = [p.x for p in window if p.left_valid or p.right_valid] 605 ys = [p.y for p in window if p.left_valid or p.right_valid] 606 607 if not xs: 608 return 609 610 cx, cy = sum(xs) / len(xs), sum(ys) / len(ys) 611 dispersion = max( 612 max(abs(x - cx) for x in xs), 613 max(abs(y - cy) for y in ys) 614 ) 615 616 # Check if this is a fixation 617 if dispersion < self.dispersion_threshold: 618 duration_ms = ( 619 window[-1].timestamp - window[0].timestamp 620 ).total_seconds() * 1000 621 622 if duration_ms >= self.fixation_threshold_ms: 623 avg_pupil = None 624 pupils = [p.pupil_diameter for p in window if p.pupil_diameter] 625 if pupils: 626 avg_pupil = sum(pupils) / len(pupils) 627 628 fixation = Fixation( 629 start_time=window[0].timestamp, 630 end_time=window[-1].timestamp, 631 x=cx, 632 y=cy, 633 duration_ms=duration_ms, 634 dispersion=dispersion, 635 avg_pupil=avg_pupil 636 ) 637 638 self._fixations.append(fixation) 639 # Keep fixations bounded 640 if len(self._fixations) > 1000: 641 self._fixations = self._fixations[-500:] 642 643 # Clear processed points, keep last few for continuity 644 self._gaze_buffer = self._gaze_buffer[-3:] 645 646 # Trigger callbacks outside the lock 647 if 'fixation' in dir(): 648 for callback in self._on_fixation: 649 try: 650 callback(fixation) 651 except Exception as e: 652 print(f"[TalonIntegration] Fixation callback error: {e}") 653 654 # Check region entry 655 for region in self._regions: 656 if region.contains(fixation.x, fixation.y): 657 for callback in self._on_region_enter: 658 try: 659 callback(region, fixation) 660 except Exception as e: 661 print(f"[TalonIntegration] Region callback error: {e}") 662 663 664 class EyeTrackingAttentionBridge: 665 """ 666 Bridges eye tracking data to the attention system. 667 668 Converts fixations into AttentionEvents that feed 669 the main attention tracker. 670 671 Works with either TalonIntegration (Mac) or TobiiIntegration (Windows/Linux). 672 """ 673 674 def __init__(self, tracker: Union[TalonIntegration, TobiiIntegration]): 675 self.tracker = tracker 676 self._screen_to_target: Dict[Tuple[float, float], str] = {} 677 678 def register_target( 679 self, 680 region: ScreenRegion, 681 target_id: str, 682 target_type: str 683 ) -> None: 684 """Register a screen region as a semantic target.""" 685 region.target_id = target_id 686 region.target_type = target_type 687 self.tracker.register_region(region) 688 689 def fixation_to_attention_event(self, fixation: Fixation): 690 """ 691 Convert a fixation to an AttentionEvent. 692 693 Returns an AttentionEvent or None if the fixation 694 doesn't map to a known target. 695 """ 696 from .tracker import AttentionEvent 697 698 # Find which region this fixation is in 699 for region in self.tracker._regions: 700 if region.contains(fixation.x, fixation.y): 701 if region.target_id: 702 source = 'talon' if isinstance(self.tracker, TalonIntegration) else 'tobii' 703 return AttentionEvent( 704 timestamp=fixation.start_time, 705 target_id=region.target_id, 706 target_type=region.target_type or 'unknown', 707 modality='gaze', 708 duration_seconds=fixation.duration_ms / 1000.0, 709 intensity=fixation.intensity, 710 source=source 711 ) 712 713 return None 714 715 716 def create_eye_tracking_system(prefer_talon: bool = True): 717 """ 718 Create the eye tracking system. 719 720 On macOS, uses Talon as the Tobii driver. 721 On Windows/Linux, can use direct Tobii SDK. 722 723 Args: 724 prefer_talon: If True, try Talon first (recommended for Mac) 725 726 Returns: 727 (tracker, bridge) tuple, or (None, None) if unavailable 728 """ 729 import platform 730 731 # On Mac, always use Talon 732 if platform.system() == "Darwin" or prefer_talon: 733 talon = TalonIntegration() 734 if talon.connect(): 735 bridge = EyeTrackingAttentionBridge(talon) 736 return talon, bridge 737 print("[EyeTracking] Talon bridge not available") 738 print(" 1. Install Talon from talonvoice.com") 739 print(" 2. Copy sovereign_gaze_bridge.py to ~/.talon/user/") 740 print(" 3. Connect your Tobii 5 and run calibration") 741 742 # Fall back to direct Tobii (Windows/Linux) 743 tobii = TobiiIntegration() 744 if tobii.connect(): 745 bridge = EyeTrackingAttentionBridge(tobii) 746 return tobii, bridge 747 748 print("[EyeTracking] No eye tracking available") 749 return None, None 750 751 752 if __name__ == "__main__": 753 import platform 754 755 print("=== Eye Tracking Module ===\n") 756 print(f"Platform: {platform.system()}") 757 758 if platform.system() == "Darwin": 759 print("\nMac detected - using Talon integration") 760 print("\nSetup steps:") 761 print(" 1. Install Talon from https://talonvoice.com") 762 print(" 2. Copy these files to ~/.talon/user/:") 763 print(" - integrations/talon/sovereign_gaze_bridge.py") 764 print(" - integrations/talon/sovereign_gaze.talon") 765 print(" 3. Connect Tobii Eye Tracker 5") 766 print(" 4. In Talon, run calibration: 'run calibration'") 767 print(" 5. Start this script to receive gaze data") 768 else: 769 print("\nWindows/Linux detected - can use direct Tobii SDK") 770 print(" pip install tobii-research") 771 772 print("\nArchitecture:") 773 print(" Tobii 5 → Talon → Unix Socket → TalonIntegration → AttentionTracker") 774 print("\nData flow:") 775 print(" GazePoint (raw) → Fixation (I-DT) → AttentionEvent → Trajectory") 776 print("\n'Attention is all you need'")