H-Liu1997 commited on
Commit
b1e1a05
·
1 Parent(s): 7d73321

feat: spectator mode - all visitors see the same streaming generation

Browse files
Files changed (3) hide show
  1. app.py +39 -26
  2. model_manager.py +22 -1
  3. static/js/main.js +26 -2
app.py CHANGED
@@ -459,37 +459,50 @@ def get_frame():
459
  {"status": "error", "message": "session_id is required"}
460
  ), 400
461
 
462
- # Verify this is the active session
463
- with session_lock:
464
- if active_session_id != session_id:
465
- return jsonify(
466
- {"status": "error", "message": "Not the active session"}
467
- ), 403
468
-
469
  if model_manager is None:
470
  return jsonify({"status": "error", "message": "Model not initialized"}), 400
471
 
472
- # Get batch of frames from buffer (reduces HTTP round-trip overhead)
473
  count = min(int(request.args.get("count", 8)), 20)
474
- frames = []
475
- for _ in range(count):
476
- joints = model_manager.get_next_frame()
477
- if joints is None:
478
- break
479
- frames.append(joints.tolist())
480
-
481
- if frames:
482
- # Update last consumption time
483
- with consumption_monitor_lock:
484
- last_frame_consumed_time = time.time()
485
 
486
- return jsonify(
487
- {
488
- "status": "success",
489
- "frames": frames,
490
- "buffer_size": model_manager.frame_buffer.size(),
491
- }
492
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  else:
494
  return jsonify(
495
  {
 
459
  {"status": "error", "message": "session_id is required"}
460
  ), 400
461
 
 
 
 
 
 
 
 
462
  if model_manager is None:
463
  return jsonify({"status": "error", "message": "Model not initialized"}), 400
464
 
 
465
  count = min(int(request.args.get("count", 8)), 20)
 
 
 
 
 
 
 
 
 
 
 
466
 
467
+ # Check if this is the active session or a spectator
468
+ with session_lock:
469
+ is_active = active_session_id == session_id
470
+
471
+ if is_active:
472
+ # Active session: pop frames from generation buffer
473
+ frames = []
474
+ for _ in range(count):
475
+ joints = model_manager.get_next_frame()
476
+ if joints is None:
477
+ break
478
+ frames.append(joints.tolist())
479
+
480
+ if frames:
481
+ with consumption_monitor_lock:
482
+ last_frame_consumed_time = time.time()
483
+
484
+ return jsonify(
485
+ {
486
+ "status": "success",
487
+ "frames": frames,
488
+ "buffer_size": model_manager.frame_buffer.size(),
489
+ }
490
+ )
491
+ else:
492
+ # Spectator: read from broadcast buffer (non-destructive)
493
+ after_id = int(request.args.get("after_id", 0))
494
+ broadcast = model_manager.get_broadcast_frames(after_id, count)
495
+ if broadcast:
496
+ last_id = broadcast[-1][0]
497
+ frames = [joints.tolist() for _, joints in broadcast]
498
+ return jsonify(
499
+ {
500
+ "status": "success",
501
+ "frames": frames,
502
+ "last_id": last_id,
503
+ "buffer_size": model_manager.frame_buffer.size(),
504
+ }
505
+ )
506
  else:
507
  return jsonify(
508
  {
model_manager.py CHANGED
@@ -71,9 +71,14 @@ class ModelManager:
71
  "cfg_scale": self.model.cfg_scale,
72
  }
73
 
74
- # Frame buffer
75
  self.frame_buffer = FrameBuffer(target_buffer_size=16)
76
 
 
 
 
 
 
77
  # Stream joint recovery with smoothing
78
  self.smoothing_alpha = 0.5 # Default: medium smoothing
79
  self.stream_recovery = StreamJointRecovery263(
@@ -335,6 +340,12 @@ class ModelManager:
335
  frame_data = decoded[i].cpu().numpy()
336
  joints = self.stream_recovery.process_frame(frame_data)
337
  self.frame_buffer.add_frame(joints)
 
 
 
 
 
 
338
 
339
  step_time = time.time() - step_start
340
  total_gen_time += step_time
@@ -367,6 +378,16 @@ class ModelManager:
367
  """Get the next frame from buffer"""
368
  return self.frame_buffer.get_frame()
369
 
 
 
 
 
 
 
 
 
 
 
370
  def get_buffer_status(self):
371
  """Get buffer status"""
372
  return {
 
71
  "cfg_scale": self.model.cfg_scale,
72
  }
73
 
74
+ # Frame buffer (for active session)
75
  self.frame_buffer = FrameBuffer(target_buffer_size=16)
76
 
77
+ # Broadcast buffer (for spectators) - append-only with frame IDs
78
+ self.broadcast_frames = deque(maxlen=200)
79
+ self.broadcast_id = 0
80
+ self.broadcast_lock = threading.Lock()
81
+
82
  # Stream joint recovery with smoothing
83
  self.smoothing_alpha = 0.5 # Default: medium smoothing
84
  self.stream_recovery = StreamJointRecovery263(
 
340
  frame_data = decoded[i].cpu().numpy()
341
  joints = self.stream_recovery.process_frame(frame_data)
342
  self.frame_buffer.add_frame(joints)
343
+ # Also add to broadcast buffer for spectators
344
+ with self.broadcast_lock:
345
+ self.broadcast_id += 1
346
+ self.broadcast_frames.append(
347
+ (self.broadcast_id, joints)
348
+ )
349
 
350
  step_time = time.time() - step_start
351
  total_gen_time += step_time
 
378
  """Get the next frame from buffer"""
379
  return self.frame_buffer.get_frame()
380
 
381
+ def get_broadcast_frames(self, after_id, count=8):
382
+ """Get frames from broadcast buffer after the given ID (for spectators)."""
383
+ with self.broadcast_lock:
384
+ frames = [
385
+ (fid, joints)
386
+ for fid, joints in self.broadcast_frames
387
+ if fid > after_id
388
+ ]
389
+ return frames[:count]
390
+
391
  def get_buffer_status(self):
392
  """Get buffer status"""
393
  return {
static/js/main.js CHANGED
@@ -22,6 +22,7 @@ class MotionApp {
22
  // Local frame queue for batch fetching (reduces HTTP round-trip overhead)
23
  this.localFrameQueue = [];
24
  this.batchSize = 8; // Fetch up to 8 frames per request
 
25
 
26
  // Session management
27
  this.sessionId = this.generateSessionId();
@@ -501,14 +502,20 @@ class MotionApp {
501
  if (this.localFrameQueue.length < this.batchSize && !this.isFetchingFrame) {
502
  this.isFetchingFrame = true;
503
 
504
- fetch(`/api/get_frame?session_id=${this.sessionId}&count=${this.batchSize}`)
 
 
 
 
505
  .then(response => response.json())
506
  .then(data => {
507
  if (data.status === 'success') {
508
- // Push batch of frames into local queue
509
  for (const frame of data.frames) {
510
  this.localFrameQueue.push(frame);
511
  }
 
 
 
512
  this.consecutiveWaiting = 0;
513
  } else if (data.status === 'waiting') {
514
  this.consecutiveWaiting++;
@@ -563,6 +570,7 @@ class MotionApp {
563
  this.isFetchingFrame = false;
564
  this.consecutiveWaiting = 0;
565
  this.localFrameQueue = [];
 
566
  this.startResetBtn.textContent = 'Start';
567
  this.startResetBtn.classList.remove('btn-danger');
568
  this.startResetBtn.classList.add('btn-primary');
@@ -723,6 +731,22 @@ class MotionApp {
723
  if (data.history_length !== undefined) {
724
  this.currentHistory.textContent = data.history_length;
725
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  }
727
 
728
  // Update motion FPS (frame consumption rate)
 
22
  // Local frame queue for batch fetching (reduces HTTP round-trip overhead)
23
  this.localFrameQueue = [];
24
  this.batchSize = 8; // Fetch up to 8 frames per request
25
+ this.broadcastLastId = 0; // For spectator mode (broadcast buffer cursor)
26
 
27
  // Session management
28
  this.sessionId = this.generateSessionId();
 
502
  if (this.localFrameQueue.length < this.batchSize && !this.isFetchingFrame) {
503
  this.isFetchingFrame = true;
504
 
505
+ let url = `/api/get_frame?session_id=${this.sessionId}&count=${this.batchSize}`;
506
+ if (this.broadcastLastId > 0) {
507
+ url += `&after_id=${this.broadcastLastId}`;
508
+ }
509
+ fetch(url)
510
  .then(response => response.json())
511
  .then(data => {
512
  if (data.status === 'success') {
 
513
  for (const frame of data.frames) {
514
  this.localFrameQueue.push(frame);
515
  }
516
+ if (data.last_id !== undefined) {
517
+ this.broadcastLastId = data.last_id;
518
+ }
519
  this.consecutiveWaiting = 0;
520
  } else if (data.status === 'waiting') {
521
  this.consecutiveWaiting++;
 
570
  this.isFetchingFrame = false;
571
  this.consecutiveWaiting = 0;
572
  this.localFrameQueue = [];
573
+ this.broadcastLastId = 0;
574
  this.startResetBtn.textContent = 'Start';
575
  this.startResetBtn.classList.remove('btn-danger');
576
  this.startResetBtn.classList.add('btn-primary');
 
731
  if (data.history_length !== undefined) {
732
  this.currentHistory.textContent = data.history_length;
733
  }
734
+
735
+ // Auto-start spectator mode if someone else is generating
736
+ if (data.is_generating && !data.is_active_session && this.isIdle && !this.isRunning) {
737
+ this.isRunning = true;
738
+ this.isIdle = false;
739
+ this.statusEl.textContent = 'Watching';
740
+ this.startFrameLoop();
741
+ }
742
+ // Stop spectator mode when generation stops
743
+ if (!data.is_generating && !data.is_active_session && this.isRunning && this.statusEl.textContent === 'Watching') {
744
+ this.isRunning = false;
745
+ this.isIdle = true;
746
+ this.statusEl.textContent = 'Idle';
747
+ this.localFrameQueue = [];
748
+ this.broadcastLastId = 0;
749
+ }
750
  }
751
 
752
  // Update motion FPS (frame consumption rate)