MoraxCheng commited on
Commit
86d5c5f
·
1 Parent(s): 7a6c881

Add external keep-alive script for Tranception Space and update settings

Browse files
Files changed (3) hide show
  1. .claude/settings.local.json +2 -1
  2. app.py +43 -224
  3. keep_alive_external.py +117 -0
.claude/settings.local.json CHANGED
@@ -10,7 +10,8 @@
10
  "Bash(git rm:*)",
11
  "Bash(python test:*)",
12
  "Bash(rm:*)",
13
- "Bash(chmod:*)"
 
14
  ],
15
  "deny": []
16
  }
 
10
  "Bash(git rm:*)",
11
  "Bash(python test:*)",
12
  "Bash(rm:*)",
13
+ "Bash(chmod:*)",
14
+ "Bash(cp:*)"
15
  ],
16
  "deny": []
17
  }
app.py CHANGED
@@ -1,6 +1,6 @@
1
  #!/usr/bin/env python3
2
  """
3
- Tranception Design App - Hugging Face Spaces Version
4
  """
5
  import os
6
  import sys
@@ -13,129 +13,31 @@ import matplotlib.pyplot as plt
13
  import seaborn as sns
14
  import gradio as gr
15
  from huggingface_hub import hf_hub_download
16
- import zipfile
17
  import shutil
18
  import uuid
19
  import gc
20
  import time
21
- import signal
22
- import threading
23
  import datetime
 
24
 
25
- # Check if Zero GPU should be disabled via environment variable
26
- DISABLE_ZERO_GPU = os.environ.get('DISABLE_ZERO_GPU', 'false').lower() == 'true'
27
-
28
- # Keep-alive settings
29
- KEEP_ALIVE_INTERVAL = 300 # 5 minutes
30
- last_activity_time = datetime.datetime.now()
31
- keep_alive_thread = None
32
-
33
- # Auto-refresh component to keep connection alive
34
- AUTO_REFRESH_INTERVAL = 240 # 4 minutes
35
-
36
- # Create a mock spaces module for fallback
37
- class MockSpaces:
38
- """Mock spaces module for when Zero GPU is not available"""
39
- def GPU(self, *args, **kwargs):
40
- """Mock GPU decorator that just returns the function as-is"""
41
- def decorator(func):
42
- return func
43
- return decorator
44
-
45
- # Try to import spaces for Zero GPU support
46
- if DISABLE_ZERO_GPU:
47
  SPACES_AVAILABLE = False
48
- spaces = MockSpaces()
49
- print("Zero GPU disabled via environment variable")
50
- else:
51
- try:
52
- import spaces as real_spaces
53
- # Test if spaces is working properly
54
- test_decorator = real_spaces.GPU()
55
- spaces = real_spaces
56
- SPACES_AVAILABLE = True
57
- print("Zero GPU support detected and available")
58
- except ImportError:
59
- SPACES_AVAILABLE = False
60
- spaces = MockSpaces()
61
- print("Warning: spaces module not available. Running without Zero GPU support.")
62
- except Exception as e:
63
- SPACES_AVAILABLE = False
64
- spaces = MockSpaces()
65
- print(f"Warning: Error with spaces module: {e}. Running without Zero GPU support.")
66
-
67
- # Flag to track if we should avoid Zero GPU due to initialization errors
68
- USE_ZERO_GPU = SPACES_AVAILABLE and not DISABLE_ZERO_GPU
69
 
70
- # Global flag to track initialization state
71
- INIT_FAILED = False
72
-
73
- def handle_init_error(signum, frame):
74
- """Handle initialization errors gracefully"""
75
- global INIT_FAILED
76
- INIT_FAILED = True
77
- print("Handling initialization error...")
78
- sys.exit(1)
79
-
80
- # Set up signal handler for graceful shutdown
81
- signal.signal(signal.SIGTERM, handle_init_error)
82
 
83
  def update_activity():
84
- """Update last activity time"""
85
- global last_activity_time
86
- last_activity_time = datetime.datetime.now()
87
-
88
- def keep_alive_worker():
89
- """Background thread to keep the Space alive"""
90
- while True:
91
- try:
92
- time.sleep(KEEP_ALIVE_INTERVAL)
93
- current_time = datetime.datetime.now()
94
- time_since_activity = (current_time - last_activity_time).total_seconds()
95
-
96
- print(f"Keep-alive check: Last activity {time_since_activity:.0f} seconds ago")
97
-
98
- # Update activity timestamp
99
- if time_since_activity > KEEP_ALIVE_INTERVAL:
100
- print("Updating activity timestamp...")
101
- update_activity()
102
-
103
- # Create a dummy inference to keep Zero GPU warm
104
- try:
105
- if USE_ZERO_GPU and hasattr(score_and_create_matrix_all_singles, '__wrapped__'):
106
- print("Triggering keep-alive inference...")
107
- # Use the smallest possible input
108
- dummy_result = score_and_create_matrix_all_singles(
109
- sequence="MSKGE",
110
- mutation_range_start=1,
111
- mutation_range_end=2,
112
- model_type="Small",
113
- scoring_mirror=False,
114
- batch_size_inference=1
115
- )
116
- print("Keep-alive inference completed")
117
- # Clean up results
118
- if dummy_result and len(dummy_result) > 2:
119
- for file in dummy_result[2]: # CSV files
120
- try:
121
- os.remove(file)
122
- except:
123
- pass
124
- except Exception as e:
125
- print(f"Keep-alive inference error (non-critical): {e}")
126
- except Exception as e:
127
- print(f"Keep-alive thread error: {e}")
128
- time.sleep(60) # Wait a bit before retrying
129
-
130
- def warm_up_zero_gpu():
131
- """Warm up Zero GPU after idle period"""
132
- if not USE_ZERO_GPU:
133
- return False
134
-
135
- print("Warming up Zero GPU...")
136
- # Note: Cannot reliably warm up Zero GPU outside of decorated functions
137
- # This is a limitation of the Zero GPU system
138
- return False
139
 
140
  # Add current directory to path
141
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -315,7 +217,6 @@ def check_valid_mutant(sequence,mutant,AA_vocab=AA_vocab):
315
  def cleanup_old_files(max_age_minutes=30):
316
  """Clean up old inference files"""
317
  import glob
318
- import time
319
  current_time = time.time()
320
  patterns = ["fitness_scoring_substitution_matrix_*.png",
321
  "fitness_scoring_substitution_matrix_*.csv",
@@ -344,7 +245,7 @@ def get_mutated_protein(sequence,mutant):
344
  return ''.join(mutated_sequence)
345
 
346
  def score_and_create_matrix_all_singles_impl(sequence,mutation_range_start=None,mutation_range_end=None,model_type="Large",scoring_mirror=False,batch_size_inference=20,max_number_positions_per_heatmap=50,num_workers=0,AA_vocab=AA_vocab):
347
- # Update activity time
348
  update_activity()
349
 
350
  # Clean up old files periodically
@@ -384,10 +285,6 @@ def score_and_create_matrix_all_singles_impl(sequence,mutation_range_start=None,
384
  # Device selection - Zero GPU will provide CUDA when decorated with @spaces.GPU
385
  print(f"GPU Available: {torch.cuda.is_available()}")
386
 
387
- # Try to ensure GPU is available when using Zero GPU
388
- if USE_ZERO_GPU and not torch.cuda.is_available():
389
- print("Zero GPU enabled but CUDA not available - this is expected before GPU allocation")
390
-
391
  if torch.cuda.is_available():
392
  device = torch.device("cuda")
393
  model = model.to(device)
@@ -401,9 +298,6 @@ def score_and_create_matrix_all_singles_impl(sequence,mutation_range_start=None,
401
  device = torch.device("cpu")
402
  model = model.to(device)
403
  print("Inference will take place on CPU")
404
- if USE_ZERO_GPU:
405
- print("WARNING: Zero GPU is enabled but CUDA is not available!")
406
- print("The Space may need to be restarted from the Hugging Face interface.")
407
  # Reduce batch size for CPU inference
408
  batch_size_inference = min(batch_size_inference, 10)
409
 
@@ -460,8 +354,8 @@ def score_and_create_matrix_all_singles_impl(sequence,mutation_range_start=None,
460
  if torch.cuda.is_available():
461
  torch.cuda.empty_cache()
462
 
463
- # Apply Zero GPU decorator - will use real decorator if available, mock otherwise
464
- if USE_ZERO_GPU:
465
  score_and_create_matrix_all_singles = spaces.GPU(duration=300)(score_and_create_matrix_all_singles_impl)
466
  else:
467
  score_and_create_matrix_all_singles = score_and_create_matrix_all_singles_impl
@@ -475,19 +369,6 @@ def clear_inputs(protein_sequence_input,mutation_range_start,mutation_range_end)
475
  mutation_range_end = None
476
  return protein_sequence_input,mutation_range_start,mutation_range_end
477
 
478
- # Health check endpoint
479
- def health_check():
480
- """Simple health check that returns current status"""
481
- update_activity()
482
- status = {
483
- "status": "healthy",
484
- "zero_gpu": USE_ZERO_GPU,
485
- "cuda_available": torch.cuda.is_available(),
486
- "last_activity": last_activity_time.isoformat(),
487
- "timestamp": datetime.datetime.now().isoformat()
488
- }
489
- return status
490
-
491
  # Create Gradio app
492
  tranception_design = gr.Blocks()
493
 
@@ -496,21 +377,29 @@ with tranception_design:
496
  gr.Markdown("## 🧬 BASIS-China iGEM Team 2025 - Protein Engineering Platform")
497
  gr.Markdown("### Welcome to BASIS-China's implementation of Tranception on Hugging Face Spaces!")
498
  gr.Markdown("We are the BASIS-China iGEM team, and we're excited to present our deployment of the Tranception model for protein fitness prediction. This tool enables in silico directed evolution to iteratively improve protein fitness through single amino acid substitutions. At each step, Tranception computes log likelihood ratios for all possible mutations compared to the starting sequence, generating fitness heatmaps and recommendations to guide protein engineering.")
499
- gr.Markdown("**Technical Details**: This deployment leverages Hugging Face's Zero GPU infrastructure, which dynamically allocates H200 GPU resources when available. This allows for efficient inference while managing computational resources effectively. The system includes automatic keep-alive mechanisms to maintain GPU availability.")
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
  # Status indicator
502
  with gr.Row():
503
  with gr.Column(scale=1):
504
  def get_gpu_status():
505
- time_since = (datetime.datetime.now() - last_activity_time).total_seconds()
506
- if USE_ZERO_GPU:
507
- if torch.cuda.is_available():
508
- gpu_name = torch.cuda.get_device_name(0)
509
- return f"🔥 Zero GPU Active: {gpu_name} | Last activity: {int(time_since)}s ago"
510
- else:
511
- return f"⚠️ Zero GPU: Ready | Last activity: {int(time_since)}s ago"
512
- else:
513
- return "💻 Running on CPU"
514
 
515
  gpu_status = gr.Textbox(
516
  label="Compute Status",
@@ -520,19 +409,6 @@ with tranception_design:
520
  elem_id="gpu_status"
521
  )
522
 
523
- # Hidden components for keep-alive
524
- with gr.Row(visible=False):
525
- # Auto-refresh component to maintain WebSocket connection
526
- keep_alive_refresh = gr.Number(value=0, visible=False)
527
-
528
- def increment_counter():
529
- update_activity()
530
- return gr.update(value=time.time())
531
-
532
- # This will trigger every 4 minutes to keep the connection alive
533
- keep_alive_timer = gr.Timer(value=AUTO_REFRESH_INTERVAL)
534
- keep_alive_timer.tick(increment_counter, outputs=[keep_alive_refresh])
535
-
536
  with gr.Tabs():
537
  with gr.TabItem("Input"):
538
  with gr.Row():
@@ -622,66 +498,9 @@ with tranception_design:
622
  gr.Markdown("Links: <a href='https://proceedings.mlr.press/v162/notin22a.html' target='_blank'>Paper</a> <a href='https://github.com/OATML-Markslab/Tranception' target='_blank'>Code</a> <a href='https://sites.google.com/view/proteingym/substitutions' target='_blank'>ProteinGym</a> <a href='https://igem.org/teams/5247' target='_blank'>BASIS-China iGEM Team</a>")
623
 
624
  if __name__ == "__main__":
625
- # Start keep-alive thread
626
- if USE_ZERO_GPU:
627
- print("Starting keep-alive thread for Zero GPU...")
628
- keep_alive_thread = threading.Thread(target=keep_alive_worker, daemon=True)
629
- keep_alive_thread.start()
630
-
631
- # Schedule periodic dummy inferences to keep alive
632
- print("Keep-alive system activated - will perform dummy inferences every 5 minutes")
633
-
634
- # Configure queue for better resource management
635
- tranception_design.queue(
636
- max_size=10, # Limit queue size
637
- status_update_rate="auto", # Show status updates
638
- api_open=False # Disable API to prevent external requests
639
- )
640
-
641
- # Launch with appropriate settings for HF Spaces
642
- # Wrap launch in try-except to handle Zero GPU initialization errors gracefully
643
- launch_retries = 0
644
- max_launch_retries = 3
645
-
646
- while launch_retries < max_launch_retries:
647
- try:
648
- # Add a small delay on retries to allow system to stabilize
649
- if launch_retries > 0:
650
- print(f"Retry attempt {launch_retries}/{max_launch_retries}...")
651
- time.sleep(5)
652
-
653
- tranception_design.launch(
654
- max_threads=2, # Limit concurrent threads
655
- show_error=True,
656
- server_name="0.0.0.0",
657
- server_port=7860,
658
- quiet=False, # Show all logs
659
- prevent_thread_lock=True, # Prevent thread locking issues
660
- share=False, # Don't create public link
661
- inbrowser=False # Don't open browser
662
- )
663
- break # If successful, exit the retry loop
664
-
665
- except RuntimeError as e:
666
- error_msg = str(e)
667
- if "ZeroGPU" in error_msg or "Unknown" in error_msg:
668
- print(f"Zero GPU initialization error: {e}")
669
- launch_retries += 1
670
-
671
- if launch_retries < max_launch_retries:
672
- print("Retrying with Zero GPU after warm-up...")
673
- # Wait longer before retry
674
- time.sleep(10)
675
- else:
676
- print("Max retries reached. The Space may need to be restarted.")
677
- print("Note: Zero GPU containers can crash after idle periods.")
678
- print("Consider restarting the Space from the Hugging Face interface.")
679
- sys.exit(1)
680
- else:
681
- # Non-Zero GPU error, re-raise
682
- raise
683
- except Exception as e:
684
- print(f"Unexpected error during launch: {e}")
685
- launch_retries += 1
686
- if launch_retries >= max_launch_retries:
687
- raise
 
1
  #!/usr/bin/env python3
2
  """
3
+ Tranception Design App - Hugging Face Spaces Version (Zero GPU Fixed)
4
  """
5
  import os
6
  import sys
 
13
  import seaborn as sns
14
  import gradio as gr
15
  from huggingface_hub import hf_hub_download
 
16
  import shutil
17
  import uuid
18
  import gc
19
  import time
 
 
20
  import datetime
21
+ import threading
22
 
23
+ # Simplified Zero GPU handling
24
+ try:
25
+ import spaces
26
+ SPACES_AVAILABLE = True
27
+ print("Zero GPU support detected")
28
+ except ImportError:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  SPACES_AVAILABLE = False
30
+ print("Running without Zero GPU support")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # Keep-alive state
33
+ last_activity = datetime.datetime.now()
34
+ activity_lock = threading.Lock()
 
 
 
 
 
 
 
 
 
35
 
36
  def update_activity():
37
+ """Update last activity timestamp"""
38
+ global last_activity
39
+ with activity_lock:
40
+ last_activity = datetime.datetime.now()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  # Add current directory to path
43
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
217
  def cleanup_old_files(max_age_minutes=30):
218
  """Clean up old inference files"""
219
  import glob
 
220
  current_time = time.time()
221
  patterns = ["fitness_scoring_substitution_matrix_*.png",
222
  "fitness_scoring_substitution_matrix_*.csv",
 
245
  return ''.join(mutated_sequence)
246
 
247
  def score_and_create_matrix_all_singles_impl(sequence,mutation_range_start=None,mutation_range_end=None,model_type="Large",scoring_mirror=False,batch_size_inference=20,max_number_positions_per_heatmap=50,num_workers=0,AA_vocab=AA_vocab):
248
+ # Update activity
249
  update_activity()
250
 
251
  # Clean up old files periodically
 
285
  # Device selection - Zero GPU will provide CUDA when decorated with @spaces.GPU
286
  print(f"GPU Available: {torch.cuda.is_available()}")
287
 
 
 
 
 
288
  if torch.cuda.is_available():
289
  device = torch.device("cuda")
290
  model = model.to(device)
 
298
  device = torch.device("cpu")
299
  model = model.to(device)
300
  print("Inference will take place on CPU")
 
 
 
301
  # Reduce batch size for CPU inference
302
  batch_size_inference = min(batch_size_inference, 10)
303
 
 
354
  if torch.cuda.is_available():
355
  torch.cuda.empty_cache()
356
 
357
+ # Apply Zero GPU decorator if available
358
+ if SPACES_AVAILABLE:
359
  score_and_create_matrix_all_singles = spaces.GPU(duration=300)(score_and_create_matrix_all_singles_impl)
360
  else:
361
  score_and_create_matrix_all_singles = score_and_create_matrix_all_singles_impl
 
369
  mutation_range_end = None
370
  return protein_sequence_input,mutation_range_start,mutation_range_end
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  # Create Gradio app
373
  tranception_design = gr.Blocks()
374
 
 
377
  gr.Markdown("## 🧬 BASIS-China iGEM Team 2025 - Protein Engineering Platform")
378
  gr.Markdown("### Welcome to BASIS-China's implementation of Tranception on Hugging Face Spaces!")
379
  gr.Markdown("We are the BASIS-China iGEM team, and we're excited to present our deployment of the Tranception model for protein fitness prediction. This tool enables in silico directed evolution to iteratively improve protein fitness through single amino acid substitutions. At each step, Tranception computes log likelihood ratios for all possible mutations compared to the starting sequence, generating fitness heatmaps and recommendations to guide protein engineering.")
380
+ gr.Markdown("**Technical Details**: This deployment leverages Hugging Face's Zero GPU infrastructure, which dynamically allocates H200 GPU resources when available. This allows for efficient inference while managing computational resources effectively.")
381
+
382
+ # Hidden keep-alive component
383
+ with gr.Row(visible=False):
384
+ keep_alive_component = gr.Number(value=0, visible=False)
385
+
386
+ def keep_alive_update():
387
+ update_activity()
388
+ return time.time()
389
+
390
+ # Update every 2 minutes to keep websocket alive
391
+ keep_alive_timer = gr.Timer(value=120)
392
+ keep_alive_timer.tick(keep_alive_update, outputs=[keep_alive_component])
393
 
394
  # Status indicator
395
  with gr.Row():
396
  with gr.Column(scale=1):
397
  def get_gpu_status():
398
+ with activity_lock:
399
+ time_since = (datetime.datetime.now() - last_activity).total_seconds()
400
+
401
+ status = "🔥 Zero GPU" if SPACES_AVAILABLE else "💻 CPU Mode"
402
+ return f"{status} | Last activity: {int(time_since)}s ago"
 
 
 
 
403
 
404
  gpu_status = gr.Textbox(
405
  label="Compute Status",
 
409
  elem_id="gpu_status"
410
  )
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  with gr.Tabs():
413
  with gr.TabItem("Input"):
414
  with gr.Row():
 
498
  gr.Markdown("Links: <a href='https://proceedings.mlr.press/v162/notin22a.html' target='_blank'>Paper</a> <a href='https://github.com/OATML-Markslab/Tranception' target='_blank'>Code</a> <a href='https://sites.google.com/view/proteingym/substitutions' target='_blank'>ProteinGym</a> <a href='https://igem.org/teams/5247' target='_blank'>BASIS-China iGEM Team</a>")
499
 
500
  if __name__ == "__main__":
501
+ # Simple launch without queue to avoid Zero GPU conflicts
502
+ tranception_design.launch(
503
+ server_name="0.0.0.0",
504
+ server_port=7860,
505
+ show_error=True
506
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
keep_alive_external.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ External Keep-Alive for Tranception Space
4
+ This script runs on your local machine to keep the Space active
5
+ """
6
+ import requests
7
+ import time
8
+ from datetime import datetime
9
+ import sys
10
+
11
+ # Space configuration
12
+ SPACE_EMBED_URL = "https://moraxcheng-transeption-igem-basischina-2025.hf.space"
13
+ GRADIO_API_URL = f"{SPACE_EMBED_URL}/run/predict"
14
+
15
+ # Keep-alive settings
16
+ PING_INTERVAL = 240 # 4 minutes
17
+ WAKE_UP_RETRIES = 3
18
+
19
+ def simple_ping():
20
+ """Simple HTTP GET to keep connection alive"""
21
+ try:
22
+ response = requests.get(SPACE_EMBED_URL, timeout=30)
23
+ return response.status_code == 200
24
+ except:
25
+ return False
26
+
27
+ def gradio_keep_alive():
28
+ """Send minimal Gradio API request"""
29
+ try:
30
+ # Minimal prediction request
31
+ payload = {
32
+ "data": [
33
+ "MSKGE", # 5 amino acids
34
+ 1, # start
35
+ 3, # end
36
+ "Small", # smallest model
37
+ False, # no mirror
38
+ 1 # batch size 1
39
+ ]
40
+ }
41
+
42
+ # First, initiate the prediction
43
+ response = requests.post(
44
+ GRADIO_API_URL,
45
+ json=payload,
46
+ timeout=60
47
+ )
48
+
49
+ if response.status_code == 200:
50
+ return True
51
+ except Exception as e:
52
+ print(f"API request error: {e}")
53
+
54
+ return False
55
+
56
+ def keep_alive_loop():
57
+ """Main keep-alive loop"""
58
+ print("="*60)
59
+ print("Tranception Space Keep-Alive")
60
+ print(f"Space: {SPACE_EMBED_URL}")
61
+ print(f"Ping interval: {PING_INTERVAL} seconds")
62
+ print("Press Ctrl+C to stop")
63
+ print("="*60)
64
+ print()
65
+
66
+ consecutive_failures = 0
67
+
68
+ while True:
69
+ try:
70
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
71
+
72
+ # Try simple ping first
73
+ print(f"[{timestamp}] Pinging Space...", end=" ", flush=True)
74
+
75
+ if simple_ping():
76
+ print("✓ Online")
77
+ consecutive_failures = 0
78
+ else:
79
+ print("✗ No response")
80
+ consecutive_failures += 1
81
+
82
+ # Try Gradio API as fallback
83
+ print(f"[{timestamp}] Trying Gradio API...", end=" ", flush=True)
84
+ if gradio_keep_alive():
85
+ print("✓ Success")
86
+ consecutive_failures = 0
87
+ else:
88
+ print("✗ Failed")
89
+
90
+ if consecutive_failures >= 3:
91
+ print("\n⚠️ WARNING: Space appears to be down!")
92
+ print("You may need to manually restart it at:")
93
+ print(f"https://huggingface.co/spaces/MoraxCheng/Transeption_iGEM_BASISCHINA_2025/settings\n")
94
+
95
+ # Wait for next ping
96
+ print(f"Next ping in {PING_INTERVAL} seconds...\n")
97
+ time.sleep(PING_INTERVAL)
98
+
99
+ except KeyboardInterrupt:
100
+ print("\n\nKeep-alive stopped by user")
101
+ break
102
+ except Exception as e:
103
+ print(f"\nUnexpected error: {e}")
104
+ print("Continuing...\n")
105
+ time.sleep(60)
106
+
107
+ if __name__ == "__main__":
108
+ # Test connection
109
+ print("Testing connection...")
110
+ if simple_ping():
111
+ print("✓ Space is online\n")
112
+ else:
113
+ print("⚠ Space appears to be offline")
114
+ print("Starting keep-alive anyway...\n")
115
+
116
+ # Start keep-alive
117
+ keep_alive_loop()