cutechicken commited on
Commit
3eab771
ยท
verified ยท
1 Parent(s): a34a371

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -102
app.py CHANGED
@@ -26,54 +26,59 @@ import os
26
  import sys
27
  from datetime import datetime
28
 
29
- # Configuration
30
  PEER_ID = f"peer-{os.getenv('COMPUTERNAME', 'unknown')}-{datetime.now().strftime('%Y%m%d%H%M%S')}"
31
- SERVER_URL = "https://your-username-your-space.hf.space" # Replace with actual Space URL
32
 
33
  def check_gpu():
34
- """Check GPU availability"""
35
  try:
36
  result = subprocess.run(['nvidia-smi', '--query-gpu=utilization.gpu', '--format=csv,noheader,nounits'],
37
  capture_output=True, text=True)
38
  if result.returncode == 0:
39
  gpu_usage = int(result.stdout.strip())
40
- return gpu_usage < 20 # GPU is idle if usage < 20%
41
  except:
42
- print("GPU not found. Running in CPU mode.")
43
  return False
44
 
45
  def register_peer():
46
- """Register peer with server"""
47
  try:
48
  response = requests.post(f"{SERVER_URL}/api/peers/register", params={"peer_id": PEER_ID})
49
  if response.status_code == 200:
50
- print(f"โœ… Peer registered: {PEER_ID}")
51
  return True
52
  except Exception as e:
53
- print(f"โŒ Server connection failed: {e}")
54
  return False
55
 
56
  def generate_image_cpu(prompt, output_path):
57
- """Generate test image using CPU"""
58
  from PIL import Image, ImageDraw, ImageFont
59
 
60
  img = Image.new('RGB', (512, 512), color='white')
61
  draw = ImageDraw.Draw(img)
62
 
63
- # Draw prompt text
64
  text = f"Prompt: {prompt[:50]}..."
65
  draw.text((10, 10), text, fill='black')
66
  draw.text((10, 40), f"Generated by: {PEER_ID}", fill='gray')
67
  draw.text((10, 70), f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", fill='gray')
68
 
 
 
 
 
 
69
  img.save(output_path)
70
- print(f"๐Ÿ“ Test image generated: {output_path}")
71
 
72
  def main():
73
- print("๐Ÿš€ Starting P2P GPU Client...")
74
 
75
  if not register_peer():
76
- print("Server registration failed. Exiting.")
77
  return
78
 
79
  while True:
@@ -81,7 +86,7 @@ def main():
81
  # Heartbeat
82
  requests.post(f"{SERVER_URL}/api/peers/heartbeat", params={"peer_id": PEER_ID})
83
 
84
- # Request job
85
  response = requests.get(f"{SERVER_URL}/api/jobs/request", params={"peer_id": PEER_ID})
86
  if response.status_code == 200:
87
  job_data = response.json()
@@ -91,20 +96,20 @@ def main():
91
  job_id = job["id"]
92
  prompt = job["prompt"]
93
 
94
- print(f"\\n๐Ÿ“‹ New job received: {prompt}")
95
 
96
- # Generate image
97
  output_path = f"{job_id}.png"
98
 
99
  if check_gpu():
100
- print("๐ŸŽฎ Generating with GPU...")
101
- # Actual GPU generation code would go here
102
  generate_image_cpu(prompt, output_path)
103
  else:
104
- print("๐Ÿ’ป Generating with CPU...")
105
  generate_image_cpu(prompt, output_path)
106
 
107
- # Upload result
108
  with open(output_path, 'rb') as f:
109
  files = {'file': (output_path, f, 'image/png')}
110
  response = requests.post(
@@ -114,26 +119,26 @@ def main():
114
  )
115
 
116
  if response.status_code == 200:
117
- print("โœ… Result uploaded successfully")
118
 
119
- # Clean up
120
  os.remove(output_path)
121
 
122
- time.sleep(10) # Check every 10 seconds
123
 
124
  except KeyboardInterrupt:
125
- print("\\n๐Ÿ‘‹ Shutting down")
126
  break
127
  except Exception as e:
128
- print(f"โš ๏ธ Error: {e}")
129
  time.sleep(30)
130
 
131
  if __name__ == "__main__":
132
- # Check required packages
133
  try:
134
  import PIL
135
  except ImportError:
136
- print("Installing required packages...")
137
  subprocess.run([sys.executable, "-m", "pip", "install", "pillow", "requests"])
138
 
139
  main()
@@ -147,34 +152,34 @@ with open("client/requirements.txt", "w") as f:
147
  f.write("requests\npillow\n")
148
 
149
  with open("client/README.md", "w", encoding="utf-8") as f:
150
- f.write("""# P2P GPU Client for Windows
151
 
152
- ## Installation
153
- 1. Install Python 3.8+
154
- 2. Run `pip install -r requirements.txt`
155
- 3. Update SERVER_URL in `peer_agent.py` with actual Hugging Face Space URL
156
- 4. Run `python peer_agent.py`
157
 
158
- ## GPU Support
159
- - Automatically detects NVIDIA GPU if available
160
- - Falls back to CPU mode for testing
161
  """)
162
 
163
  # FastAPI app with lifespan
164
  @asynccontextmanager
165
  async def lifespan(app: FastAPI):
166
  # Startup
167
- print("Starting P2P GPU Hub...")
168
  yield
169
  # Shutdown
170
- print("Shutting down P2P GPU Hub...")
171
 
172
  app = FastAPI(lifespan=lifespan)
173
 
174
  # API endpoints
175
  @app.get("/api/status")
176
  async def get_status():
177
- """Get system status"""
178
  active_peers = sum(1 for p in peers.values()
179
  if (datetime.now() - p['last_seen']).seconds < 60)
180
  pending_jobs = sum(1 for j in jobs if j['status'] == 'pending')
@@ -194,7 +199,7 @@ async def get_status():
194
 
195
  @app.post("/api/peers/register")
196
  async def register_peer(peer_id: str):
197
- """Register a peer"""
198
  peers[peer_id] = {
199
  "status": "idle",
200
  "last_seen": datetime.now(),
@@ -204,7 +209,7 @@ async def register_peer(peer_id: str):
204
 
205
  @app.post("/api/peers/heartbeat")
206
  async def heartbeat(peer_id: str):
207
- """Update peer status"""
208
  if peer_id in peers:
209
  peers[peer_id]["last_seen"] = datetime.now()
210
  return {"status": "alive"}
@@ -212,7 +217,7 @@ async def heartbeat(peer_id: str):
212
 
213
  @app.post("/api/jobs/submit")
214
  async def submit_job(request: Request):
215
- """Submit a job"""
216
  data = await request.json()
217
  job_id = str(uuid.uuid4())
218
  job = {
@@ -226,7 +231,7 @@ async def submit_job(request: Request):
226
 
227
  @app.get("/api/jobs/request")
228
  async def request_job(peer_id: str):
229
- """Request a job for processing"""
230
  for job in jobs:
231
  if job["status"] == "pending":
232
  job["status"] = "assigned"
@@ -238,7 +243,7 @@ async def request_job(peer_id: str):
238
 
239
  @app.post("/api/jobs/result")
240
  async def submit_result(job_id: str, file: UploadFile = File(...)):
241
- """Submit job result"""
242
  filename = f"{job_id}.png"
243
  file_path = f"results/{filename}"
244
 
@@ -259,7 +264,7 @@ async def submit_result(job_id: str, file: UploadFile = File(...)):
259
 
260
  @app.get("/api/results/{filename}")
261
  async def get_result(filename: str):
262
- """Get generated image"""
263
  file_path = f"results/{filename}"
264
  if os.path.exists(file_path):
265
  return FileResponse(file_path)
@@ -267,7 +272,7 @@ async def get_result(filename: str):
267
 
268
  @app.get("/api/client/{filename}")
269
  async def get_client_file(filename: str):
270
- """Download client file"""
271
  file_path = f"client/{filename}"
272
  if os.path.exists(file_path):
273
  return FileResponse(file_path, filename=filename)
@@ -275,9 +280,9 @@ async def get_client_file(filename: str):
275
 
276
  # Gradio interface functions
277
  def gradio_submit_job(prompt):
278
- """Submit job through Gradio"""
279
  if not prompt:
280
- return "Please enter a prompt"
281
 
282
  job_id = str(uuid.uuid4())
283
  job = {
@@ -287,34 +292,40 @@ def gradio_submit_job(prompt):
287
  "created_at": datetime.now()
288
  }
289
  jobs.append(job)
290
- return f"Job submitted successfully! Job ID: {job_id}"
291
 
292
  def gradio_get_status():
293
- """Get status through Gradio"""
294
  active_peers = sum(1 for p in peers.values()
295
  if (datetime.now() - p['last_seen']).seconds < 60)
296
  pending = sum(1 for j in jobs if j['status'] == 'pending')
297
  completed = sum(1 for j in jobs if j['status'] == 'completed')
298
 
299
- status_text = f"""### System Status
300
- - Active Peers: {active_peers}
301
- - Pending Jobs: {pending}
302
- - Completed Jobs: {completed}
 
 
303
 
304
- ### Recent Jobs
305
  """
306
 
307
  # Add recent jobs
308
  recent_jobs = jobs[-5:][::-1] # Last 5 jobs, reversed
309
- for job in recent_jobs:
310
- status_text += f"\n- **{job['id'][:8]}...**: {job['prompt'][:50]}... ({job['status']})"
 
 
 
 
311
 
312
  return status_text
313
 
314
  def gradio_get_gallery():
315
- """Get completed images for gallery"""
316
  image_files = []
317
- for job in jobs[-20:]: # Last 20 jobs
318
  if job['status'] == 'completed' and 'filename' in job:
319
  file_path = f"results/{job['filename']}"
320
  if os.path.exists(file_path):
@@ -322,89 +333,152 @@ def gradio_get_gallery():
322
 
323
  return image_files
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  # Create Gradio interface
326
- with gr.Blocks(title="P2P GPU Image Generation Hub") as demo:
327
- gr.Markdown("# ๐Ÿค– P2P GPU Image Generation Hub")
328
- gr.Markdown("Distributed image generation using idle GPUs from peer nodes")
329
 
330
  with gr.Tabs():
331
- with gr.Tab("Submit Job"):
332
  with gr.Row():
333
- with gr.Column():
334
  prompt_input = gr.Textbox(
335
- label="Image Prompt",
336
- placeholder="Describe the image you want to generate...",
337
- lines=3
338
  )
339
- submit_btn = gr.Button("Submit Job", variant="primary")
340
- result_text = gr.Textbox(label="Result", interactive=False)
 
341
 
342
- submit_btn.click(
343
- fn=gradio_submit_job,
344
- inputs=prompt_input,
345
- outputs=result_text
346
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
- with gr.Tab("System Status"):
349
  status_display = gr.Markdown()
350
- refresh_btn = gr.Button("Refresh Status")
 
 
351
 
352
  refresh_btn.click(
353
  fn=gradio_get_status,
354
  outputs=status_display
355
  )
356
 
357
- # Auto-refresh status on load
358
  demo.load(fn=gradio_get_status, outputs=status_display)
359
 
360
- with gr.Tab("Gallery"):
 
361
  gallery = gr.Gallery(
362
- label="Generated Images",
363
- show_label=True,
364
  elem_id="gallery",
365
- columns=3,
366
- rows=2,
367
- height="auto"
 
 
368
  )
369
- refresh_gallery_btn = gr.Button("Refresh Gallery")
370
 
371
  refresh_gallery_btn.click(
372
  fn=gradio_get_gallery,
373
  outputs=gallery
374
  )
375
 
376
- # Auto-load gallery on tab load
377
  demo.load(fn=gradio_get_gallery, outputs=gallery)
378
 
379
- with gr.Tab("Download Client"):
380
  gr.Markdown("""
381
- ## Windows Client Setup
382
 
383
- 1. Download the client files:
384
- - [peer_agent.py](/api/client/peer_agent.py)
385
- - [requirements.txt](/api/client/requirements.txt)
386
- - [README.md](/api/client/README.md)
 
387
 
388
- 2. Install Python 3.8 or higher
 
 
389
 
390
- 3. Install requirements:
391
- ```bash
392
- pip install -r requirements.txt
393
- ```
394
 
395
- 4. Update the SERVER_URL in peer_agent.py with this Space's URL
 
 
 
 
396
 
397
- 5. Run the client:
398
- ```bash
399
- python peer_agent.py
400
- ```
401
 
402
- The client will automatically detect GPU availability and start processing jobs.
 
 
 
 
403
  """)
 
 
 
 
 
 
 
 
404
 
405
  # Mount Gradio app to FastAPI
406
  app = gr.mount_gradio_app(app, demo, path="/")
407
 
408
- # For Hugging Face Spaces
409
  if __name__ == "__main__":
410
  demo.launch()
 
26
  import sys
27
  from datetime import datetime
28
 
29
+ # ์„ค์ •
30
  PEER_ID = f"peer-{os.getenv('COMPUTERNAME', 'unknown')}-{datetime.now().strftime('%Y%m%d%H%M%S')}"
31
+ SERVER_URL = "https://your-username-your-space.hf.space" # ์‹ค์ œ Space URL๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”
32
 
33
  def check_gpu():
34
+ """GPU ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ"""
35
  try:
36
  result = subprocess.run(['nvidia-smi', '--query-gpu=utilization.gpu', '--format=csv,noheader,nounits'],
37
  capture_output=True, text=True)
38
  if result.returncode == 0:
39
  gpu_usage = int(result.stdout.strip())
40
+ return gpu_usage < 20 # GPU ์‚ฌ์šฉ๋ฅ  20% ๋ฏธ๋งŒ์ด๋ฉด idle
41
  except:
42
+ print("GPU๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. CPU ๋ชจ๋“œ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.")
43
  return False
44
 
45
  def register_peer():
46
+ """์„œ๋ฒ„์— Peer ๋“ฑ๋ก"""
47
  try:
48
  response = requests.post(f"{SERVER_URL}/api/peers/register", params={"peer_id": PEER_ID})
49
  if response.status_code == 200:
50
+ print(f"โœ… Peer ๋“ฑ๋ก ์„ฑ๊ณต: {PEER_ID}")
51
  return True
52
  except Exception as e:
53
+ print(f"โŒ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ: {e}")
54
  return False
55
 
56
  def generate_image_cpu(prompt, output_path):
57
+ """CPU๋กœ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ"""
58
  from PIL import Image, ImageDraw, ImageFont
59
 
60
  img = Image.new('RGB', (512, 512), color='white')
61
  draw = ImageDraw.Draw(img)
62
 
63
+ # ํ…์ŠคํŠธ ๊ทธ๋ฆฌ๊ธฐ
64
  text = f"Prompt: {prompt[:50]}..."
65
  draw.text((10, 10), text, fill='black')
66
  draw.text((10, 40), f"Generated by: {PEER_ID}", fill='gray')
67
  draw.text((10, 70), f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", fill='gray')
68
 
69
+ # ํ…Œ์ŠคํŠธ ํŒจํ„ด ๊ทธ๋ฆฌ๊ธฐ
70
+ for i in range(0, 512, 32):
71
+ draw.line([(i, 0), (i, 512)], fill='lightgray')
72
+ draw.line([(0, i), (512, i)], fill='lightgray')
73
+
74
  img.save(output_path)
75
+ print(f"๐Ÿ“ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ: {output_path}")
76
 
77
  def main():
78
+ print("๐Ÿš€ P2P GPU ํด๋ผ์ด์–ธํŠธ ์‹œ์ž‘...")
79
 
80
  if not register_peer():
81
+ print("์„œ๋ฒ„ ๋“ฑ๋ก ์‹คํŒจ. ํ”„๋กœ๊ทธ๋žจ์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.")
82
  return
83
 
84
  while True:
 
86
  # Heartbeat
87
  requests.post(f"{SERVER_URL}/api/peers/heartbeat", params={"peer_id": PEER_ID})
88
 
89
+ # ์ž‘์—… ์š”์ฒญ
90
  response = requests.get(f"{SERVER_URL}/api/jobs/request", params={"peer_id": PEER_ID})
91
  if response.status_code == 200:
92
  job_data = response.json()
 
96
  job_id = job["id"]
97
  prompt = job["prompt"]
98
 
99
+ print(f"\\n๐Ÿ“‹ ์ƒˆ ์ž‘์—… ์ˆ˜์‹ : {prompt}")
100
 
101
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ
102
  output_path = f"{job_id}.png"
103
 
104
  if check_gpu():
105
+ print("๐ŸŽฎ GPU ๋ชจ๋“œ๋กœ ์ƒ์„ฑ ์ค‘...")
106
+ # ์‹ค์ œ GPU ์ƒ์„ฑ ์ฝ”๋“œ๊ฐ€ ์—ฌ๊ธฐ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค
107
  generate_image_cpu(prompt, output_path)
108
  else:
109
+ print("๐Ÿ’ป CPU ๋ชจ๋“œ๋กœ ์ƒ์„ฑ ์ค‘...")
110
  generate_image_cpu(prompt, output_path)
111
 
112
+ # ๊ฒฐ๊ณผ ์—…๋กœ๋“œ
113
  with open(output_path, 'rb') as f:
114
  files = {'file': (output_path, f, 'image/png')}
115
  response = requests.post(
 
119
  )
120
 
121
  if response.status_code == 200:
122
+ print("โœ… ๊ฒฐ๊ณผ ์—…๋กœ๋“œ ์„ฑ๊ณต")
123
 
124
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
125
  os.remove(output_path)
126
 
127
+ time.sleep(10) # 10์ดˆ๋งˆ๋‹ค ํ™•์ธ
128
 
129
  except KeyboardInterrupt:
130
+ print("\\n๐Ÿ‘‹ ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ")
131
  break
132
  except Exception as e:
133
+ print(f"โš ๏ธ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
134
  time.sleep(30)
135
 
136
  if __name__ == "__main__":
137
+ # ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ํ™•์ธ
138
  try:
139
  import PIL
140
  except ImportError:
141
+ print("ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค...")
142
  subprocess.run([sys.executable, "-m", "pip", "install", "pillow", "requests"])
143
 
144
  main()
 
152
  f.write("requests\npillow\n")
153
 
154
  with open("client/README.md", "w", encoding="utf-8") as f:
155
+ f.write("""# P2P GPU ํด๋ผ์ด์–ธํŠธ (Windows)
156
 
157
+ ## ์„ค์น˜ ๋ฐฉ๋ฒ•
158
+ 1. Python 3.8 ์ด์ƒ ์„ค์น˜
159
+ 2. `pip install -r requirements.txt` ์‹คํ–‰
160
+ 3. `peer_agent.py` ํŒŒ์ผ์˜ SERVER_URL์„ ์‹ค์ œ Hugging Face Space URL๋กœ ์ˆ˜์ •
161
+ 4. `python peer_agent.py` ์‹คํ–‰
162
 
163
+ ## GPU ์ง€์›
164
+ - NVIDIA GPU๊ฐ€ ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ๊ฐ์ง€
165
+ - GPU๊ฐ€ ์—†์œผ๋ฉด CPU ๋ชจ๋“œ๋กœ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
166
  """)
167
 
168
  # FastAPI app with lifespan
169
  @asynccontextmanager
170
  async def lifespan(app: FastAPI):
171
  # Startup
172
+ print("P2P GPU Hub ์‹œ์ž‘...")
173
  yield
174
  # Shutdown
175
+ print("P2P GPU Hub ์ข…๋ฃŒ...")
176
 
177
  app = FastAPI(lifespan=lifespan)
178
 
179
  # API endpoints
180
  @app.get("/api/status")
181
  async def get_status():
182
+ """์‹œ์Šคํ…œ ์ƒํƒœ ์กฐํšŒ"""
183
  active_peers = sum(1 for p in peers.values()
184
  if (datetime.now() - p['last_seen']).seconds < 60)
185
  pending_jobs = sum(1 for j in jobs if j['status'] == 'pending')
 
199
 
200
  @app.post("/api/peers/register")
201
  async def register_peer(peer_id: str):
202
+ """Peer ๋“ฑ๋ก"""
203
  peers[peer_id] = {
204
  "status": "idle",
205
  "last_seen": datetime.now(),
 
209
 
210
  @app.post("/api/peers/heartbeat")
211
  async def heartbeat(peer_id: str):
212
+ """Peer ์ƒํƒœ ์—…๋ฐ์ดํŠธ"""
213
  if peer_id in peers:
214
  peers[peer_id]["last_seen"] = datetime.now()
215
  return {"status": "alive"}
 
217
 
218
  @app.post("/api/jobs/submit")
219
  async def submit_job(request: Request):
220
+ """์ž‘์—… ์ œ์ถœ"""
221
  data = await request.json()
222
  job_id = str(uuid.uuid4())
223
  job = {
 
231
 
232
  @app.get("/api/jobs/request")
233
  async def request_job(peer_id: str):
234
+ """์ž‘์—… ์š”์ฒญ"""
235
  for job in jobs:
236
  if job["status"] == "pending":
237
  job["status"] = "assigned"
 
243
 
244
  @app.post("/api/jobs/result")
245
  async def submit_result(job_id: str, file: UploadFile = File(...)):
246
+ """์ž‘์—… ๊ฒฐ๊ณผ ์ œ์ถœ"""
247
  filename = f"{job_id}.png"
248
  file_path = f"results/{filename}"
249
 
 
264
 
265
  @app.get("/api/results/{filename}")
266
  async def get_result(filename: str):
267
+ """์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์กฐํšŒ"""
268
  file_path = f"results/{filename}"
269
  if os.path.exists(file_path):
270
  return FileResponse(file_path)
 
272
 
273
  @app.get("/api/client/{filename}")
274
  async def get_client_file(filename: str):
275
+ """ํด๋ผ์ด์–ธํŠธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ"""
276
  file_path = f"client/{filename}"
277
  if os.path.exists(file_path):
278
  return FileResponse(file_path, filename=filename)
 
280
 
281
  # Gradio interface functions
282
  def gradio_submit_job(prompt):
283
+ """Gradio๋ฅผ ํ†ตํ•œ ์ž‘์—… ์ œ์ถœ"""
284
  if not prompt:
285
+ return "ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
286
 
287
  job_id = str(uuid.uuid4())
288
  job = {
 
292
  "created_at": datetime.now()
293
  }
294
  jobs.append(job)
295
+ return f"โœ… ์ž‘์—…์ด ์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!\n์ž‘์—… ID: {job_id[:8]}..."
296
 
297
  def gradio_get_status():
298
+ """Gradio๋ฅผ ํ†ตํ•œ ์ƒํƒœ ์กฐํšŒ"""
299
  active_peers = sum(1 for p in peers.values()
300
  if (datetime.now() - p['last_seen']).seconds < 60)
301
  pending = sum(1 for j in jobs if j['status'] == 'pending')
302
  completed = sum(1 for j in jobs if j['status'] == 'completed')
303
 
304
+ status_text = f"""## ๐Ÿ“Š ์‹œ์Šคํ…œ ์ƒํƒœ
305
+
306
+ ### ํ˜„์žฌ ์ƒํ™ฉ
307
+ - **ํ™œ์„ฑ Peer**: {active_peers}๊ฐœ
308
+ - **๋Œ€๊ธฐ์ค‘ ์ž‘์—…**: {pending}๊ฐœ
309
+ - **์™„๋ฃŒ๋œ ์ž‘์—…**: {completed}๊ฐœ
310
 
311
+ ### ์ตœ๊ทผ ์ž‘์—… ๋ชฉ๋ก
312
  """
313
 
314
  # Add recent jobs
315
  recent_jobs = jobs[-5:][::-1] # Last 5 jobs, reversed
316
+ if recent_jobs:
317
+ for job in recent_jobs:
318
+ status_emoji = "โœ…" if job['status'] == 'completed' else "โณ" if job['status'] == 'pending' else "๐Ÿ”„"
319
+ status_text += f"\n{status_emoji} **{job['id'][:8]}**: {job['prompt'][:50]}... ({job['status']})"
320
+ else:
321
+ status_text += "\n์•„์ง ์ž‘์—…์ด ์—†์Šต๋‹ˆ๋‹ค."
322
 
323
  return status_text
324
 
325
  def gradio_get_gallery():
326
+ """๊ฐค๋Ÿฌ๋ฆฌ์šฉ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ"""
327
  image_files = []
328
+ for job in jobs[-20:][::-1]: # Last 20 jobs, newest first
329
  if job['status'] == 'completed' and 'filename' in job:
330
  file_path = f"results/{job['filename']}"
331
  if os.path.exists(file_path):
 
333
 
334
  return image_files
335
 
336
+ # Custom CSS
337
+ custom_css = """
338
+ .container {
339
+ max-width: 1200px;
340
+ margin: 0 auto;
341
+ }
342
+ #gallery {
343
+ min-height: 400px;
344
+ }
345
+ .gradio-container {
346
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
347
+ }
348
+ """
349
+
350
  # Create Gradio interface
351
+ with gr.Blocks(title="P2P GPU ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ—ˆ๋ธŒ", css=custom_css, theme=gr.themes.Soft()) as demo:
352
+ gr.Markdown("# ๐Ÿค– P2P GPU ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ—ˆ๋ธŒ")
353
+ gr.Markdown("์œ ํœด GPU๋ฅผ ํ™œ์šฉํ•œ ๋ถ„์‚ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ์Šคํ…œ")
354
 
355
  with gr.Tabs():
356
+ with gr.Tab("๐ŸŽจ ์ž‘์—… ์ œ์ถœ"):
357
  with gr.Row():
358
+ with gr.Column(scale=2):
359
  prompt_input = gr.Textbox(
360
+ label="์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ",
361
+ placeholder="์ƒ์„ฑํ•˜๊ณ  ์‹ถ์€ ์ด๋ฏธ์ง€๋ฅผ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”...\n์˜ˆ: ํ‘ธ๋ฅธ ํ•˜๋Š˜ ์•„๋ž˜ ๋ฒš๊ฝƒ์ด ๋งŒ๊ฐœํ•œ ๊ณต์›",
362
+ lines=4
363
  )
364
+ with gr.Row():
365
+ submit_btn = gr.Button("์ž‘์—… ์ œ์ถœ", variant="primary", scale=2)
366
+ clear_btn = gr.Button("์ง€์šฐ๊ธฐ", scale=1)
367
 
368
+ result_text = gr.Textbox(
369
+ label="์ œ์ถœ ๊ฒฐ๊ณผ",
370
+ interactive=False,
371
+ lines=2
372
  )
373
+
374
+ with gr.Column(scale=1):
375
+ gr.Markdown("""
376
+ ### ๐Ÿ’ก ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
377
+ 1. ์›ํ•˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”
378
+ 2. '์ž‘์—… ์ œ์ถœ' ๋ฒ„ํŠผ์„ ํด๋ฆญ
379
+ 3. Peer๊ฐ€ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค
380
+ 4. ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊ฒฐ๊ณผ ํ™•์ธ
381
+
382
+ ### ๐ŸŽฏ ์ข‹์€ ํ”„๋กฌํ”„ํŠธ ์˜ˆ์‹œ
383
+ - ๊ตฌ์ฒด์ ์ธ ์„ค๋ช… ํฌํ•จ
384
+ - ์Šคํƒ€์ผ์ด๋‚˜ ๋ถ„์œ„๊ธฐ ๋ช…์‹œ
385
+ - ์ƒ‰์ƒ, ์กฐ๋ช… ๋“ฑ ์„ธ๋ถ€์‚ฌํ•ญ
386
+ """)
387
+
388
+ submit_btn.click(
389
+ fn=gradio_submit_job,
390
+ inputs=prompt_input,
391
+ outputs=result_text
392
+ )
393
+ clear_btn.click(
394
+ fn=lambda: ("", ""),
395
+ outputs=[prompt_input, result_text]
396
+ )
397
 
398
+ with gr.Tab("๐Ÿ“Š ์‹œ์Šคํ…œ ์ƒํƒœ"):
399
  status_display = gr.Markdown()
400
+ with gr.Row():
401
+ refresh_btn = gr.Button("๐Ÿ”„ ์ƒˆ๋กœ๊ณ ์นจ", variant="secondary")
402
+ auto_refresh = gr.Checkbox(label="์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ (10์ดˆ)", value=True)
403
 
404
  refresh_btn.click(
405
  fn=gradio_get_status,
406
  outputs=status_display
407
  )
408
 
409
+ # Auto-refresh status
410
  demo.load(fn=gradio_get_status, outputs=status_display)
411
 
412
+ with gr.Tab("๐Ÿ–ผ๏ธ ๊ฐค๋Ÿฌ๋ฆฌ"):
413
+ gr.Markdown("### ์ตœ๊ทผ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋“ค")
414
  gallery = gr.Gallery(
415
+ label="์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€",
416
+ show_label=False,
417
  elem_id="gallery",
418
+ columns=4,
419
+ rows=3,
420
+ height="auto",
421
+ object_fit="contain",
422
+ preview=True
423
  )
424
+ refresh_gallery_btn = gr.Button("๐Ÿ”„ ๊ฐค๋Ÿฌ๋ฆฌ ์ƒˆ๋กœ๊ณ ์นจ", variant="secondary")
425
 
426
  refresh_gallery_btn.click(
427
  fn=gradio_get_gallery,
428
  outputs=gallery
429
  )
430
 
431
+ # Auto-load gallery
432
  demo.load(fn=gradio_get_gallery, outputs=gallery)
433
 
434
+ with gr.Tab("๐Ÿ’ป ํด๋ผ์ด์–ธํŠธ ๋‹ค์šด๋กœ๋“œ"):
435
  gr.Markdown("""
436
+ ## Windows ํด๋ผ์ด์–ธํŠธ ์„ค์ • ๊ฐ€์ด๋“œ
437
 
438
+ ### ๐Ÿ“ฅ 1๋‹จ๊ณ„: ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
439
+ ๋‹ค์Œ ํŒŒ์ผ๋“ค์„ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”:
440
+ - [peer_agent.py](/api/client/peer_agent.py) - ๋ฉ”์ธ ํด๋ผ์ด์–ธํŠธ
441
+ - [requirements.txt](/api/client/requirements.txt) - ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ๋ชฉ๋ก
442
+ - [README.md](/api/client/README.md) - ์„ค๋ช…์„œ
443
 
444
+ ### ๐Ÿ 2๋‹จ๊ณ„: Python ์„ค์น˜
445
+ - Python 3.8 ์ด์ƒ ๋ฒ„์ „ ํ•„์š”
446
+ - [python.org](https://python.org)์—์„œ ๋‹ค์šด๋กœ๋“œ
447
 
448
+ ### ๐Ÿ“ฆ 3๋‹จ๊ณ„: ํŒจํ‚ค์ง€ ์„ค์น˜
449
+ ```bash
450
+ pip install -r requirements.txt
451
+ ```
452
 
453
+ ### โš™๏ธ 4๋‹จ๊ณ„: ์„ค์ •
454
+ `peer_agent.py` ํŒŒ์ผ์„ ์—ด๊ณ  SERVER_URL์„ ์ด Space์˜ URL๋กœ ๋ณ€๊ฒฝ:
455
+ ```python
456
+ SERVER_URL = "https://๋‹น์‹ ์˜-space-url.hf.space"
457
+ ```
458
 
459
+ ### ๐Ÿš€ 5๋‹จ๊ณ„: ์‹คํ–‰
460
+ ```bash
461
+ python peer_agent.py
462
+ ```
463
 
464
+ ### โœจ ํŠน์ง•
465
+ - GPU ์ž๋™ ๊ฐ์ง€
466
+ - CPU ํด๋ฐฑ ์ง€์›
467
+ - ์ž๋™ ์ž‘์—… ์ฒ˜๋ฆฌ
468
+ - 10์ดˆ๋งˆ๋‹ค ์ƒˆ ์ž‘์—… ํ™•์ธ
469
  """)
470
+
471
+ with gr.Row():
472
+ gr.Markdown("""
473
+ ### ๐ŸŽฎ GPU ์ง€์›
474
+ - NVIDIA GPU๊ฐ€ ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ํ™œ์šฉ
475
+ - GPU๊ฐ€ ์—†์–ด๋„ CPU๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
476
+ - GPU ์‚ฌ์šฉ๋ฅ  20% ๋ฏธ๋งŒ์ผ ๋•Œ๋งŒ ์ž‘์—… ์ˆ˜ํ–‰
477
+ """)
478
 
479
  # Mount Gradio app to FastAPI
480
  app = gr.mount_gradio_app(app, demo, path="/")
481
 
482
+ # For local testing
483
  if __name__ == "__main__":
484
  demo.launch()