cutechicken commited on
Commit
1f143e8
ยท
verified ยท
1 Parent(s): 27b5398

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +410 -0
app.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Request
2
+ from fastapi.responses import FileResponse, HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import uuid
5
+ import os
6
+ import json
7
+ from datetime import datetime
8
+ from typing import Dict, List
9
+ import shutil
10
+
11
+ app = FastAPI()
12
+
13
+ # ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ
14
+ peers: Dict[str, Dict] = {}
15
+ jobs: List[Dict] = []
16
+
17
+ # ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
18
+ os.makedirs("results", exist_ok=True)
19
+ os.makedirs("client", exist_ok=True)
20
+
21
+ # ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ์ƒ์„ฑ
22
+ CLIENT_CODE = '''import requests
23
+ import subprocess
24
+ import time
25
+ import os
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}/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
+ img.save(output_path)
70
+ print(f"๐Ÿ“ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ: {output_path}")
71
+
72
+ def main():
73
+ print("๐Ÿš€ P2P GPU Client ์‹œ์ž‘...")
74
+
75
+ if not register_peer():
76
+ print("์„œ๋ฒ„ ๋“ฑ๋ก ์‹คํŒจ. ํ”„๋กœ๊ทธ๋žจ์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.")
77
+ return
78
+
79
+ while True:
80
+ try:
81
+ # Heartbeat
82
+ requests.post(f"{SERVER_URL}/peers/heartbeat", params={"peer_id": PEER_ID})
83
+
84
+ # GPU ์ฒดํฌ (Windows์—์„œ๋Š” ๋Œ€๋ถ€๋ถ„ False ๋ฐ˜ํ™˜)
85
+ if True: # ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ํ•ญ์ƒ ์ž‘์—… ์š”์ฒญ
86
+ # ์ž‘์—… ์š”์ฒญ
87
+ response = requests.get(f"{SERVER_URL}/jobs/request", params={"peer_id": PEER_ID})
88
+ if response.status_code == 200:
89
+ job_data = response.json()
90
+
91
+ if job_data.get("job"):
92
+ job = job_data["job"]
93
+ job_id = job["id"]
94
+ prompt = job["prompt"]
95
+
96
+ print(f"\\n๐Ÿ“‹ ์ƒˆ ์ž‘์—… ์ˆ˜์‹ : {prompt}")
97
+
98
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ
99
+ output_path = f"{job_id}.png"
100
+
101
+ if check_gpu():
102
+ print("๐ŸŽฎ GPU ๋ชจ๋“œ๋กœ ์ƒ์„ฑ ์ค‘...")
103
+ # ์‹ค์ œ GPU ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ฝ”๋“œ
104
+ # ์—ฌ๊ธฐ์„œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด CPU ํ•จ์ˆ˜ ํ˜ธ์ถœ
105
+ generate_image_cpu(prompt, output_path)
106
+ else:
107
+ print("๐Ÿ’ป CPU ๋ชจ๋“œ๋กœ ์ƒ์„ฑ ์ค‘...")
108
+ generate_image_cpu(prompt, output_path)
109
+
110
+ # ๊ฒฐ๊ณผ ์—…๋กœ๋“œ
111
+ with open(output_path, 'rb') as f:
112
+ files = {'file': (output_path, f, 'image/png')}
113
+ response = requests.post(
114
+ f"{SERVER_URL}/jobs/result",
115
+ params={"job_id": job_id},
116
+ files=files
117
+ )
118
+
119
+ if response.status_code == 200:
120
+ print("โœ… ๊ฒฐ๊ณผ ์—…๋กœ๋“œ ์„ฑ๊ณต")
121
+
122
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
123
+ os.remove(output_path)
124
+
125
+ time.sleep(10) # 10์ดˆ๋งˆ๋‹ค ์ฒดํฌ
126
+
127
+ except KeyboardInterrupt:
128
+ print("\\n๐Ÿ‘‹ ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ")
129
+ break
130
+ except Exception as e:
131
+ print(f"โš ๏ธ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
132
+ time.sleep(30)
133
+
134
+ if __name__ == "__main__":
135
+ # ํ•„์š”๏ฟฝ๏ฟฝ๏ฟฝ ํŒจํ‚ค์ง€ ์„ค์น˜ ํ™•์ธ
136
+ try:
137
+ import PIL
138
+ except ImportError:
139
+ print("ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค...")
140
+ subprocess.run([sys.executable, "-m", "pip", "install", "pillow", "requests"])
141
+
142
+ main()
143
+ '''
144
+
145
+ # ํด๋ผ์ด์–ธํŠธ ํŒŒ์ผ ์ƒ์„ฑ
146
+ with open("client/peer_agent.py", "w", encoding="utf-8") as f:
147
+ f.write(CLIENT_CODE)
148
+
149
+ with open("client/requirements.txt", "w") as f:
150
+ f.write("requests\npillow\n")
151
+
152
+ with open("client/README.md", "w", encoding="utf-8") as f:
153
+ f.write("""# P2P GPU Client for Windows
154
+
155
+ ## ์„ค์น˜ ๋ฐฉ๋ฒ•
156
+ 1. Python 3.8+ ์„ค์น˜
157
+ 2. `pip install -r requirements.txt`
158
+ 3. `peer_agent.py` ํŒŒ์ผ์˜ SERVER_URL์„ ์‹ค์ œ Hugging Face Space URL๋กœ ์ˆ˜์ •
159
+ 4. `python peer_agent.py` ์‹คํ–‰
160
+
161
+ ## GPU ์ง€์›
162
+ - NVIDIA GPU๊ฐ€ ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ๊ฐ์ง€
163
+ - ์—†์œผ๋ฉด CPU ๋ชจ๋“œ๋กœ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
164
+ """)
165
+
166
+ # === API ์—”๋“œํฌ์ธํŠธ ===
167
+
168
+ @app.get("/")
169
+ async def home():
170
+ """์›น ์ธํ„ฐํŽ˜์ด์Šค"""
171
+ html = """
172
+ <html>
173
+ <head>
174
+ <title>P2P GPU Image Generation Hub</title>
175
+ <style>
176
+ body { font-family: Arial; max-width: 1200px; margin: 0 auto; padding: 20px; }
177
+ .status { background: #f0f0f0; padding: 20px; margin: 20px 0; border-radius: 10px; }
178
+ .job-form { background: #e8f4f8; padding: 20px; border-radius: 10px; }
179
+ input, button { padding: 10px; margin: 5px; }
180
+ .results img { max-width: 200px; margin: 10px; }
181
+ </style>
182
+ </head>
183
+ <body>
184
+ <h1>๐Ÿค– P2P GPU Image Generation Hub</h1>
185
+
186
+ <div class="status">
187
+ <h2>๐Ÿ“Š ์‹œ์Šคํ…œ ์ƒํƒœ</h2>
188
+ <p>ํ™œ์„ฑ Peer: <span id="peer-count">0</span></p>
189
+ <p>๋Œ€๊ธฐ์ค‘ ์ž‘์—…: <span id="pending-jobs">0</span></p>
190
+ <p>์™„๋ฃŒ๋œ ์ž‘์—…: <span id="completed-jobs">0</span></p>
191
+ </div>
192
+
193
+ <div class="job-form">
194
+ <h2>๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ</h2>
195
+ <input type="text" id="prompt" placeholder="์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ..." style="width: 400px;">
196
+ <button onclick="submitJob()">์ƒ์„ฑ ์š”์ฒญ</button>
197
+ </div>
198
+
199
+ <div class="results">
200
+ <h2>๐Ÿ–ผ๏ธ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€</h2>
201
+ <div id="results"></div>
202
+ </div>
203
+
204
+ <div style="margin-top: 40px;">
205
+ <h2>๐Ÿ’ป ํด๋ผ์ด์–ธํŠธ ๋‹ค์šด๋กœ๋“œ</h2>
206
+ <a href="/client/peer_agent.py" download>peer_agent.py ๋‹ค์šด๋กœ๋“œ</a>
207
+ </div>
208
+
209
+ <script>
210
+ async function updateStatus() {
211
+ const status = await fetch('/status').then(r => r.json());
212
+ document.getElementById('peer-count').textContent = status.active_peers;
213
+ document.getElementById('pending-jobs').textContent = status.pending_jobs;
214
+ document.getElementById('completed-jobs').textContent = status.completed_jobs;
215
+
216
+ // ์™„๋ฃŒ๋œ ์ž‘์—… ํ‘œ์‹œ
217
+ const resultsDiv = document.getElementById('results');
218
+ resultsDiv.innerHTML = '';
219
+ status.recent_results.forEach(result => {
220
+ const img = document.createElement('img');
221
+ img.src = `/results/${result.filename}`;
222
+ img.title = result.prompt;
223
+ resultsDiv.appendChild(img);
224
+ });
225
+ }
226
+
227
+ async function submitJob() {
228
+ const prompt = document.getElementById('prompt').value;
229
+ if (!prompt) return;
230
+
231
+ const response = await fetch('/jobs/submit', {
232
+ method: 'POST',
233
+ headers: {'Content-Type': 'application/json'},
234
+ body: JSON.stringify({prompt: prompt})
235
+ });
236
+
237
+ if (response.ok) {
238
+ alert('์ž‘์—…์ด ์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
239
+ document.getElementById('prompt').value = '';
240
+ }
241
+ }
242
+
243
+ setInterval(updateStatus, 2000);
244
+ updateStatus();
245
+ </script>
246
+ </body>
247
+ </html>
248
+ """
249
+ return HTMLResponse(content=html)
250
+
251
+ @app.get("/status")
252
+ async def get_status():
253
+ """์‹œ์Šคํ…œ ์ƒํƒœ ์กฐํšŒ"""
254
+ active_peers = sum(1 for p in peers.values()
255
+ if (datetime.now() - p['last_seen']).seconds < 60)
256
+ pending_jobs = sum(1 for j in jobs if j['status'] == 'pending')
257
+ completed_jobs = sum(1 for j in jobs if j['status'] == 'completed')
258
+
259
+ recent_results = [
260
+ {"filename": j['filename'], "prompt": j['prompt']}
261
+ for j in jobs[-10:] if j['status'] == 'completed' and 'filename' in j
262
+ ]
263
+
264
+ return {
265
+ "active_peers": active_peers,
266
+ "pending_jobs": pending_jobs,
267
+ "completed_jobs": completed_jobs,
268
+ "recent_results": recent_results
269
+ }
270
+
271
+ # === Peer ๊ด€๋ฆฌ ===
272
+
273
+ @app.post("/peers/register")
274
+ async def register_peer(peer_id: str):
275
+ """Peer ๋“ฑ๋ก"""
276
+ peers[peer_id] = {
277
+ "status": "idle",
278
+ "last_seen": datetime.now(),
279
+ "jobs_completed": 0
280
+ }
281
+ return {"status": "registered", "peer_id": peer_id}
282
+
283
+ @app.post("/peers/heartbeat")
284
+ async def heartbeat(peer_id: str):
285
+ """Peer ์ƒํƒœ ์—…๋ฐ์ดํŠธ"""
286
+ if peer_id in peers:
287
+ peers[peer_id]["last_seen"] = datetime.now()
288
+ return {"status": "alive"}
289
+ return {"status": "unregistered"}
290
+
291
+ # === ์ž‘์—… ๊ด€๋ฆฌ ===
292
+
293
+ @app.post("/jobs/submit")
294
+ async def submit_job(request: Request):
295
+ """์ž‘์—… ์ œ์ถœ"""
296
+ data = await request.json()
297
+ job_id = str(uuid.uuid4())
298
+ job = {
299
+ "id": job_id,
300
+ "prompt": data.get("prompt", ""),
301
+ "status": "pending",
302
+ "created_at": datetime.now()
303
+ }
304
+ jobs.append(job)
305
+ return {"job_id": job_id, "status": "submitted"}
306
+
307
+ @app.get("/jobs/request")
308
+ async def request_job(peer_id: str):
309
+ """Peer๊ฐ€ ์ž‘์—… ์š”์ฒญ"""
310
+ # ๋Œ€๊ธฐ์ค‘์ธ ์ž‘์—… ์ฐพ๊ธฐ
311
+ for job in jobs:
312
+ if job["status"] == "pending":
313
+ job["status"] = "assigned"
314
+ job["peer_id"] = peer_id
315
+ job["assigned_at"] = datetime.now()
316
+ return {"job": job}
317
+
318
+ return {"job": None}
319
+
320
+ @app.post("/jobs/result")
321
+ async def submit_result(job_id: str, file: UploadFile = File(...)):
322
+ """์ž‘์—… ๊ฒฐ๊ณผ ์ œ์ถœ"""
323
+ # ํŒŒ์ผ ์ €์žฅ
324
+ filename = f"{job_id}.png"
325
+ file_path = f"results/{filename}"
326
+
327
+ with open(file_path, "wb") as buffer:
328
+ shutil.copyfileobj(file.file, buffer)
329
+
330
+ # ์ž‘์—… ์ƒํƒœ ์—…๋ฐ์ดํŠธ
331
+ for job in jobs:
332
+ if job["id"] == job_id:
333
+ job["status"] = "completed"
334
+ job["filename"] = filename
335
+ job["completed_at"] = datetime.now()
336
+
337
+ # Peer ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
338
+ if "peer_id" in job and job["peer_id"] in peers:
339
+ peers[job["peer_id"]]["jobs_completed"] += 1
340
+ break
341
+
342
+ return {"status": "success", "filename": filename}
343
+
344
+ # === ํŒŒ์ผ ์„œ๋น™ ===
345
+
346
+ @app.get("/results/{filename}")
347
+ async def get_result(filename: str):
348
+ """์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์กฐํšŒ"""
349
+ file_path = f"results/{filename}"
350
+ if os.path.exists(file_path):
351
+ return FileResponse(file_path)
352
+ return {"error": "File not found"}
353
+
354
+ @app.get("/client/{filename}")
355
+ async def get_client_file(filename: str):
356
+ """ํด๋ผ์ด์–ธํŠธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ"""
357
+ file_path = f"client/{filename}"
358
+ if os.path.exists(file_path):
359
+ return FileResponse(file_path, filename=filename)
360
+ return {"error": "File not found"}
361
+
362
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค (์˜ต์…˜)
363
+ # Hugging Face Space์—์„œ๋Š” Gradio๊ฐ€ ๊ธฐ๋ณธ ์ง€์›๋ฉ๋‹ˆ๋‹ค
364
+ import gradio as gr
365
+
366
+ def gradio_submit_job(prompt):
367
+ """Gradio์—์„œ ์ž‘์—… ์ œ์ถœ"""
368
+ job_id = str(uuid.uuid4())
369
+ job = {
370
+ "id": job_id,
371
+ "prompt": prompt,
372
+ "status": "pending",
373
+ "created_at": datetime.now()
374
+ }
375
+ jobs.append(job)
376
+ return f"์ž‘์—…์ด ์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Job ID: {job_id}"
377
+
378
+ def gradio_get_status():
379
+ """Gradio์—์„œ ์ƒํƒœ ์กฐํšŒ"""
380
+ active_peers = sum(1 for p in peers.values()
381
+ if (datetime.now() - p['last_seen']).seconds < 60)
382
+ pending = sum(1 for j in jobs if j['status'] == 'pending')
383
+ completed = sum(1 for j in jobs if j['status'] == 'completed')
384
+
385
+ return f"""
386
+ ### ์‹œ์Šคํ…œ ์ƒํƒœ
387
+ - ํ™œ์„ฑ Peer: {active_peers}
388
+ - ๋Œ€๊ธฐ์ค‘ ์ž‘์—…: {pending}
389
+ - ์™„๋ฃŒ๋œ ์ž‘์—…: {completed}
390
+ """
391
+
392
+ # Gradio ์•ฑ ์ƒ์„ฑ
393
+ with gr.Blocks() as gradio_app:
394
+ gr.Markdown("# ๐Ÿค– P2P GPU Image Generation Hub")
395
+
396
+ with gr.Tab("์ž‘์—… ์ œ์ถœ"):
397
+ prompt_input = gr.Textbox(label="์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ", placeholder="์ƒ์„ฑํ•˜๊ณ  ์‹ถ์€ ์ด๋ฏธ์ง€๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”...")
398
+ submit_btn = gr.Button("์ œ์ถœ")
399
+ result_text = gr.Textbox(label="๊ฒฐ๊ณผ")
400
+
401
+ submit_btn.click(gradio_submit_job, inputs=prompt_input, outputs=result_text)
402
+
403
+ with gr.Tab("์‹œ์Šคํ…œ ์ƒํƒœ"):
404
+ status_text = gr.Markdown()
405
+ refresh_btn = gr.Button("์ƒˆ๋กœ๊ณ ์นจ")
406
+
407
+ refresh_btn.click(gradio_get_status, outputs=status_text)
408
+
409
+ # FastAPI์™€ Gradio ํ†ตํ•ฉ
410
+ app = gr.mount_gradio_app(app, gradio_app, path="/gradio")