favoredone commited on
Commit
d27b88b
·
verified ·
1 Parent(s): 3a78518

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +435 -362
app.py CHANGED
@@ -1,362 +1,435 @@
1
- """
2
- FastAPI server for Bitcoin mining dashboard - Complete Integration
3
- """
4
- from fastapi import FastAPI, HTTPException, BackgroundTasks
5
- from fastapi.staticfiles import StaticFiles
6
- from fastapi.responses import FileResponse, HTMLResponse
7
- from fastapi.middleware.cors import CORSMiddleware
8
- import uvicorn
9
- import threading
10
- import time
11
- import json
12
- import asyncio
13
- from typing import Dict, Optional, List
14
- import logging
15
- import sys
16
- import os
17
-
18
- # Add the current directory to Python path to import your miner
19
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
20
-
21
- # Configure logging
22
- logging.basicConfig(
23
- level=logging.INFO,
24
- format='%(asctime)s - %(levelname)s - %(message)s'
25
- )
26
-
27
- app = FastAPI(title="Bitcoin Mining Dashboard", version="1.0.0")
28
-
29
- # Add CORS middleware
30
- app.add_middleware(
31
- CORSMiddleware,
32
- allow_origins=["*"],
33
- allow_credentials=True,
34
- allow_methods=["*"],
35
- allow_headers=["*"],
36
- )
37
-
38
- # Mount static files
39
- app.mount("/static", StaticFiles(directory="static"), name="static")
40
-
41
- # Global mining state
42
- class MiningState:
43
- def __init__(self):
44
- self.is_mining = False
45
- self.miner_instance = None
46
- self.mining_thread = None
47
- self.stats = {
48
- "status": "Stopped",
49
- "hashrate": "0 H/s",
50
- "total_hashes": "0",
51
- "blocks_found": "0",
52
- "best_hash": "None",
53
- "difficulty": "0",
54
- "network_difficulty": "0",
55
- "block_alert": "Ready to start mining",
56
- "mining_time": "0s",
57
- "cores_active": "0",
58
- "wallet_address": "1Ks4WtCEK96BaBF7HSuCGt3rEpVKPqcJKf"
59
- }
60
- self.start_time = None
61
-
62
- mining_state = MiningState()
63
-
64
- @app.get("/", response_class=HTMLResponse)
65
- async def get_index():
66
- """Serve the dashboard HTML"""
67
- try:
68
- return FileResponse("static/index.html")
69
- except:
70
- # Fallback minimal HTML
71
- return """
72
- <!DOCTYPE html>
73
- <html>
74
- <head>
75
- <title>Bitcoin Mining Dashboard</title>
76
- <style>
77
- body { font-family: Arial, sans-serif; margin: 40px; background: #f0f0f0; }
78
- .container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; }
79
- .stats { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; margin: 10px 0; }
80
- .alert { background: #e74c3c; color: white; padding: 15px; border-radius: 5px; margin: 10px 0; }
81
- .success { background: #27ae60; }
82
- .button { background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin: 5px; }
83
- .button:disabled { background: #95a5a6; }
84
- </style>
85
- </head>
86
- <body>
87
- <div class="container">
88
- <h1>⛏️ Bitcoin Mining Dashboard</h1>
89
- <div id="blockAlert" class="alert">Ready to start mining</div>
90
- <div class="stats">
91
- <h3>Mining Statistics</h3>
92
- <div id="statsDisplay"></div>
93
- </div>
94
- <button class="button" onclick="startMining()" id="startBtn">Start Mining</button>
95
- <button class="button" onclick="stopMining()" id="stopBtn" disabled>Stop Mining</button>
96
- <script>
97
- function updateStats(stats) {
98
- document.getElementById('statsDisplay').innerHTML = `
99
- <p><strong>Status:</strong> ${stats.status}</p>
100
- <p><strong>Hash Rate:</strong> ${stats.hashrate}</p>
101
- <p><strong>Total Hashes:</strong> ${stats.total_hashes}</p>
102
- <p><strong>Blocks Found:</strong> ${stats.blocks_found}</p>
103
- <p><strong>Best Hash:</strong> ${stats.best_hash}</p>
104
- <p><strong>Mining Time:</strong> ${stats.mining_time}</p>
105
- <p><strong>Wallet:</strong> ${stats.wallet_address}</p>
106
- `;
107
- document.getElementById('blockAlert').textContent = stats.block_alert;
108
- document.getElementById('blockAlert').className = stats.blocks_found > 0 ? 'alert success' : 'alert';
109
-
110
- document.getElementById('startBtn').disabled = stats.status === 'Running';
111
- document.getElementById('stopBtn').disabled = stats.status !== 'Running';
112
- }
113
-
114
- async function startMining() {
115
- const response = await fetch('/start_mining', { method: 'POST' });
116
- const result = await response.json();
117
- alert(result.message);
118
- }
119
-
120
- async function stopMining() {
121
- const response = await fetch('/stop_mining', { method: 'POST' });
122
- const result = await response.json();
123
- alert(result.message);
124
- }
125
-
126
- // Poll for stats updates
127
- setInterval(async () => {
128
- const response = await fetch('/get_stats');
129
- const stats = await response.json();
130
- updateStats(stats);
131
- }, 1000);
132
- </script>
133
- </div>
134
- </body>
135
- </html>
136
- """
137
-
138
- def mining_worker(duration=None):
139
- """Worker function to run mining in background thread"""
140
- try:
141
- # Import and initialize the miner
142
- from parallel_miner_v3 import ParallelMiner
143
-
144
- mining_state.miner_instance = ParallelMiner(
145
- num_cores=7,
146
- wallet_address="1Ks4WtCEK96BaBF7HSuCGt3rEpVKPqcJKf"
147
- )
148
- mining_state.start_time = time.time()
149
-
150
- # Start mining
151
- mining_state.miner_instance.start_mining(duration=duration)
152
-
153
- except Exception as e:
154
- logging.error(f"Mining worker error: {e}")
155
- finally:
156
- mining_state.is_mining = False
157
- mining_state.mining_thread = None
158
-
159
- @app.post("/start_mining")
160
- async def start_mining(background_tasks: BackgroundTasks):
161
- """Start the mining process"""
162
- global mining_state
163
-
164
- if mining_state.is_mining:
165
- raise HTTPException(status_code=400, detail="Mining is already running")
166
-
167
- try:
168
- mining_state.is_mining = True
169
- mining_state.stats["status"] = "Starting..."
170
- mining_state.stats["block_alert"] = "🔄 Starting mining process..."
171
-
172
- # Start mining in background thread
173
- mining_state.mining_thread = threading.Thread(
174
- target=mining_worker,
175
- kwargs={"duration": None} # Mine indefinitely
176
- )
177
- mining_state.mining_thread.daemon = True
178
- mining_state.mining_thread.start()
179
-
180
- logging.info("✅ Mining started via API")
181
- return {"message": "Mining started successfully", "status": "started"}
182
-
183
- except Exception as e:
184
- mining_state.is_mining = False
185
- logging.error(f"Error starting mining: {e}")
186
- raise HTTPException(status_code=500, detail=str(e))
187
-
188
- @app.post("/stop_mining")
189
- async def stop_mining():
190
- """Stop the mining process"""
191
- global mining_state
192
-
193
- if not mining_state.is_mining:
194
- raise HTTPException(status_code=400, detail="Mining is not running")
195
-
196
- try:
197
- if mining_state.miner_instance:
198
- mining_state.miner_instance.mining = False
199
- mining_state.is_mining = False
200
-
201
- # Log final statistics
202
- if hasattr(mining_state.miner_instance, 'total_hashes'):
203
- total_hashes = mining_state.miner_instance.total_hashes
204
- blocks_found = mining_state.miner_instance.blocks_found
205
- mining_time = time.time() - mining_state.start_time
206
-
207
- logging.info("\n" + "="*50)
208
- logging.info("⛏️ FINAL MINING STATISTICS")
209
- logging.info("="*50)
210
- logging.info(f"⏱️ Total mining time: {mining_time:.2f} seconds")
211
- logging.info(f"🔢 Total hashes: {total_hashes:,}")
212
- logging.info(f"💰 Blocks found: {blocks_found}")
213
- logging.info(f" Average hash rate: {total_hashes/max(mining_time,1)/1000:.2f} KH/s")
214
-
215
- if hasattr(mining_state.miner_instance, 'cores'):
216
- for core_idx, core in enumerate(mining_state.miner_instance.cores):
217
- logging.info(f"🔩 Core {core_idx}: {core.total_hashes:,} hashes")
218
- logging.info("="*50)
219
-
220
- mining_state.stats["status"] = "Stopped"
221
- mining_state.stats["block_alert"] = "🛑 Mining stopped"
222
-
223
- return {"message": "Mining stopped successfully", "status": "stopped"}
224
-
225
- raise HTTPException(status_code=400, detail="No active mining instance")
226
-
227
- except Exception as e:
228
- logging.error(f"Error stopping mining: {e}")
229
- raise HTTPException(status_code=500, detail=str(e))
230
-
231
- @app.get("/get_stats")
232
- async def get_mining_stats():
233
- """Get current mining statistics"""
234
- global mining_state
235
-
236
- if not mining_state.is_mining or not mining_state.miner_instance:
237
- return mining_state.stats
238
-
239
- try:
240
- miner = mining_state.miner_instance
241
-
242
- # Calculate mining time
243
- mining_time = time.time() - mining_state.start_time
244
- time_str = f"{int(mining_time//3600)}h {int((mining_time%3600)//60)}m {int(mining_time%60)}s"
245
-
246
- # Create block alert message
247
- if miner.blocks_found > 0:
248
- block_alert = f"🎉 FOUND {miner.blocks_found} BLOCK(S)! 🎉"
249
- if miner.best_hash:
250
- block_alert += f" Hash: {miner.best_hash.hex()[:16]}..."
251
- else:
252
- progress = (miner.best_hash_difficulty / max(miner.network_difficulty, 1)) * 100 if miner.best_hash_difficulty else 0
253
- block_alert = f"⛏️ Mining... Progress: {progress:.8f}%"
254
-
255
- # Update stats
256
- mining_state.stats.update({
257
- "status": "Running",
258
- "hashrate": f"{miner.current_hashrate/1000:.2f} KH/s",
259
- "total_hashes": f"{miner.total_hashes:,}",
260
- "blocks_found": str(miner.blocks_found),
261
- "best_hash": miner.best_hash.hex()[:32] + "..." if miner.best_hash else "None",
262
- "difficulty": f"{miner.best_hash_difficulty:.8f}" if miner.best_hash_difficulty else "0",
263
- "network_difficulty": f"{miner.network_difficulty:,.2f}" if hasattr(miner, 'network_difficulty') else "0",
264
- "block_alert": block_alert,
265
- "mining_time": time_str,
266
- "cores_active": f"{len(miner.cores)}" if hasattr(miner, 'cores') else "0"
267
- })
268
-
269
- return mining_state.stats
270
-
271
- except Exception as e:
272
- logging.error(f"Error getting mining stats: {e}")
273
- return {
274
- "status": "Error",
275
- "hashrate": "0 H/s",
276
- "total_hashes": "0",
277
- "blocks_found": "0",
278
- "best_hash": "Error",
279
- "difficulty": "0",
280
- "block_alert": f"Error: {str(e)}",
281
- "mining_time": "0s",
282
- "cores_active": "0"
283
- }
284
-
285
- @app.get("/get_detailed_stats")
286
- async def get_detailed_stats():
287
- """Get detailed mining statistics including per-core info"""
288
- global mining_state
289
-
290
- if not mining_state.is_mining or not mining_state.miner_instance:
291
- return {"error": "Mining not active"}
292
-
293
- try:
294
- miner = mining_state.miner_instance
295
- detailed_stats = {
296
- "overview": {
297
- "status": "Running",
298
- "total_hashes": miner.total_hashes,
299
- "blocks_found": miner.blocks_found,
300
- "current_hashrate": miner.current_hashrate,
301
- "best_hash_difficulty": miner.best_hash_difficulty,
302
- "network_difficulty": miner.network_difficulty,
303
- "mining_time": time.time() - mining_state.start_time
304
- },
305
- "cores": []
306
- }
307
-
308
- if hasattr(miner, 'cores'):
309
- for core in miner.cores:
310
- core_info = {
311
- "core_id": core.core_id,
312
- "total_hashes": core.total_hashes,
313
- "blocks_found": core.blocks_found,
314
- "units": []
315
- }
316
-
317
- for unit in core.units:
318
- core_info["units"].append({
319
- "unit_id": unit.unit_id,
320
- "total_hashes": unit.total_hashes,
321
- "blocks_found": unit.blocks_found
322
- })
323
-
324
- detailed_stats["cores"].append(core_info)
325
-
326
- return detailed_stats
327
-
328
- except Exception as e:
329
- return {"error": str(e)}
330
-
331
- @app.get("/health")
332
- async def health_check():
333
- """Health check endpoint"""
334
- return {
335
- "status": "healthy",
336
- "mining_active": mining_state.is_mining,
337
- "timestamp": time.time()
338
- }
339
-
340
- # Background task to periodically update stats
341
- @app.on_event("startup")
342
- async def startup_event():
343
- """Initialize on startup"""
344
- logging.info("🚀 Bitcoin Mining Dashboard starting up...")
345
-
346
- @app.on_event("shutdown")
347
- async def shutdown_event():
348
- """Cleanup on shutdown"""
349
- global mining_state
350
- if mining_state.is_mining and mining_state.miner_instance:
351
- mining_state.miner_instance.mining = False
352
- mining_state.is_mining = False
353
- logging.info("🛑 Bitcoin Mining Dashboard shutting down...")
354
-
355
- if __name__ == "__main__":
356
- uvicorn.run(
357
- "app:app",
358
- host="0.0.0.0",
359
- port=7868,
360
- reload=False,
361
- log_level="info"
362
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import subprocess
3
+ import torch
4
+ from PIL import Image
5
+ import requests
6
+ from io import BytesIO
7
+ import base64
8
+ from transformers import AutoProcessor, AutoModelForCausalLM
9
+ import os
10
+ import threading
11
+ import time
12
+ import urllib.parse
13
+
14
+ # Attempt to install flash-attn
15
+ try:
16
+ subprocess.run('pip install flash-attn --no-build-isolation', env={'FLASH_ATTENTION_SKIP_CUDA_BUILD': "TRUE"}, check=True, shell=True)
17
+ except subprocess.CalledProcessError as e:
18
+ print(f"Error installing flash-attn: {e}")
19
+ print("Continuing without flash-attn.")
20
+
21
+ # Determine the device to use
22
+ device = "cuda" if torch.cuda.is_available() else "cpu"
23
+
24
+ # Load the base model and processor
25
+ try:
26
+ vision_language_model_base = AutoModelForCausalLM.from_pretrained('microsoft/Florence-2-base', trust_remote_code=True).to(device).eval()
27
+ vision_language_processor_base = AutoProcessor.from_pretrained('microsoft/Florence-2-base', trust_remote_code=True)
28
+ print("✓ Base model loaded successfully")
29
+ except Exception as e:
30
+ print(f"Error loading base model: {e}")
31
+ vision_language_model_base = None
32
+ vision_language_processor_base = None
33
+
34
+ # Load the large model and processor
35
+ try:
36
+ vision_language_model_large = AutoModelForCausalLM.from_pretrained('microsoft/Florence-2-large', trust_remote_code=True).to(device).eval()
37
+ vision_language_processor_large = AutoProcessor.from_pretrained('microsoft/Florence-2-large', trust_remote_code=True)
38
+ print("✓ Large model loaded successfully")
39
+ except Exception as e:
40
+ print(f"Error loading large model: {e}")
41
+ vision_language_model_large = None
42
+ vision_language_processor_large = None
43
+
44
+ def load_image_from_url(image_url):
45
+ """Load an image from a URL."""
46
+ try:
47
+ response = requests.get(image_url, timeout=30)
48
+ response.raise_for_status()
49
+ image = Image.open(BytesIO(response.content))
50
+ return image.convert('RGB')
51
+ except Exception as e:
52
+ raise ValueError(f"Error loading image from URL: {e}")
53
+
54
+ def process_image_description(model, processor, image):
55
+ """Process an image and generate description using the specified model."""
56
+ if not isinstance(image, Image.Image):
57
+ image = Image.fromarray(image)
58
+
59
+ inputs = processor(text="<MORE_DETAILED_CAPTION>", images=image, return_tensors="pt").to(device)
60
+ with torch.no_grad():
61
+ generated_ids = model.generate(
62
+ input_ids=inputs["input_ids"],
63
+ pixel_values=inputs["pixel_values"],
64
+ max_new_tokens=1024,
65
+ early_stopping=False,
66
+ do_sample=False,
67
+ num_beams=3,
68
+ )
69
+ generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
70
+ processed_description = processor.post_process_generation(
71
+ generated_text,
72
+ task="<MORE_DETAILED_CAPTION>",
73
+ image_size=(image.width, image.height)
74
+ )
75
+ image_description = processed_description["<MORE_DETAILED_CAPTION>"]
76
+ return image_description
77
+
78
+ def describe_image(uploaded_image, model_choice):
79
+ """Generate description from uploaded image."""
80
+ if uploaded_image is None:
81
+ return "Please upload an image."
82
+
83
+ if model_choice == "Florence-2-base":
84
+ if vision_language_model_base is None:
85
+ return "Base model failed to load."
86
+ model = vision_language_model_base
87
+ processor = vision_language_processor_base
88
+ elif model_choice == "Florence-2-large":
89
+ if vision_language_model_large is None:
90
+ return "Large model failed to load."
91
+ model = vision_language_model_large
92
+ processor = vision_language_processor_large
93
+ else:
94
+ return "Invalid model choice."
95
+
96
+ try:
97
+ return process_image_description(model, processor, uploaded_image)
98
+ except Exception as e:
99
+ return f"Error generating caption: {str(e)}"
100
+
101
+ def describe_image_from_url(image_url, model_choice):
102
+ """Generate description from image URL."""
103
+ try:
104
+ if not image_url:
105
+ return {"error": "image_url is required"}
106
+
107
+ if model_choice not in ["Florence-2-base", "Florence-2-large"]:
108
+ return {"error": "Invalid model choice. Use 'Florence-2-base' or 'Florence-2-large'"}
109
+
110
+ # Load image from URL
111
+ image = load_image_from_url(image_url)
112
+
113
+ # Select model and processor
114
+ if model_choice == "Florence-2-base":
115
+ if vision_language_model_base is None:
116
+ return {"error": "Base model not available"}
117
+ model = vision_language_model_base
118
+ processor = vision_language_processor_base
119
+ else:
120
+ if vision_language_model_large is None:
121
+ return {"error": "Large model not available"}
122
+ model = vision_language_model_large
123
+ processor = vision_language_processor_large
124
+
125
+ # Generate caption
126
+ caption = process_image_description(model, processor, image)
127
+
128
+ return {
129
+ "status": "success",
130
+ "model": model_choice,
131
+ "caption": caption,
132
+ "image_size": {"width": image.width, "height": image.height}
133
+ }
134
+
135
+ except Exception as e:
136
+ return {"error": f"Error processing image: {str(e)}"}
137
+
138
+
139
+ # ---- Background captioning worker -------------------------------------------------
140
+ # This worker will start in a daemon thread before Gradio launches. It polls the
141
+ # image middleware on IMAGE_SERVER_BASE, downloads frames, captions them using
142
+ # the already-loaded Florence models, posts results to DATA_COLLECTION_BASE:/submit,
143
+ # then releases frames and courses. It uses blocking requests so it runs in a
144
+ # separate thread and will not interfere with the UI thread.
145
+
146
+ IMAGE_SERVER_BASE = os.getenv("IMAGE_SERVER_BASE", "https://fred808-vssee.hf.space")
147
+ DATA_COLLECTION_BASE = os.getenv("DATA_COLLECTION_BASE", "https://fred808-flow.hf.space")
148
+ REQUESTER_ID = os.getenv("FLO_REQUESTER_ID", f"florence-2-{os.getpid()}")
149
+ MODEL_CHOICE = os.getenv("FLO_MODEL_CHOICE", "Florence-2-base")
150
+
151
+
152
+ def _build_download_url(course: str, video: str, frame: str) -> str:
153
+ file_param = f"frame:{course}/{video}/{frame}"
154
+ return f"{IMAGE_SERVER_BASE.rstrip('/')}/download?course={urllib.parse.quote(course, safe='')}&file={urllib.parse.quote(file_param, safe='') }"
155
+
156
+
157
+ def _download_bytes(url: str, timeout: int = 30):
158
+ try:
159
+ r = requests.get(url, timeout=timeout)
160
+ r.raise_for_status()
161
+ return r.content, r.headers.get('content-type')
162
+ except Exception as e:
163
+ print(f"[BACKGROUND] download failed {url}: {e}")
164
+ return None, None
165
+
166
+
167
+ def _post_submit(caption: str, image_name: str, course: str, image_url: str, image_bytes: bytes):
168
+ submit_url = f"{DATA_COLLECTION_BASE.rstrip('/')}/submit"
169
+ files = {'image': (image_name, image_bytes, 'application/octet-stream')}
170
+ data = {'caption': caption, 'image_name': image_name, 'course': course, 'image_url': image_url}
171
+ try:
172
+ r = requests.post(submit_url, data=data, files=files, timeout=30)
173
+ try:
174
+ return r.status_code, r.json()
175
+ except Exception:
176
+ return r.status_code, r.text
177
+ except Exception as e:
178
+ print(f"[BACKGROUND] submit POST failed: {e}")
179
+ return None, None
180
+
181
+
182
+ def _release_frame(course: str, video: str, frame: str):
183
+ try:
184
+ release_url = f"{IMAGE_SERVER_BASE.rstrip('/')}/middleware/release/frame/{urllib.parse.quote(course, safe='')}/{urllib.parse.quote(video, safe='')}/{urllib.parse.quote(frame, safe='')}"
185
+ requests.post(release_url, params={"requester_id": REQUESTER_ID}, timeout=10)
186
+ except Exception as e:
187
+ print(f"[BACKGROUND] release frame failed: {e}")
188
+
189
+
190
+ def _release_course(course: str):
191
+ try:
192
+ release_url = f"{IMAGE_SERVER_BASE.rstrip('/')}/middleware/release/course/{urllib.parse.quote(course, safe='')}"
193
+ requests.post(release_url, params={"requester_id": REQUESTER_ID}, timeout=10)
194
+ except Exception as e:
195
+ print(f"[BACKGROUND] release course failed: {e}")
196
+
197
+
198
+ def background_worker():
199
+ print("[BACKGROUND] Worker waiting for model to be available...")
200
+ # wait for model(s) to load (respect existing loading logic)
201
+ waited = 0
202
+ while waited < 120:
203
+ if MODEL_CHOICE == "Florence-2-base":
204
+ if vision_language_model_base is not None and vision_language_processor_base is not None:
205
+ break
206
+ else:
207
+ if vision_language_model_large is not None and vision_language_processor_large is not None:
208
+ break
209
+ time.sleep(1)
210
+ waited += 1
211
+
212
+ if waited >= 120:
213
+ print("[BACKGROUND] Model not available after timeout; background worker exiting.")
214
+ return
215
+
216
+ print("[BACKGROUND] Model loaded; starting polling loop")
217
+
218
+ while True:
219
+ try:
220
+ # Acquire next course
221
+ try:
222
+ r = requests.get(f"{IMAGE_SERVER_BASE.rstrip('/')}/middleware/next/course", params={"requester_id": REQUESTER_ID}, timeout=15)
223
+ if r.status_code == 404:
224
+ time.sleep(3)
225
+ continue
226
+ r.raise_for_status()
227
+ course_json = r.json()
228
+ except Exception as e:
229
+ print(f"[BACKGROUND] failed to get next course: {e}")
230
+ time.sleep(3)
231
+ continue
232
+
233
+ course = course_json.get('course_id') or course_json.get('course')
234
+ if not course:
235
+ print(f"[BACKGROUND] invalid course response: {course_json}")
236
+ time.sleep(2)
237
+ continue
238
+
239
+ print(f"[BACKGROUND] processing course: {course}")
240
+
241
+ # Pull images until none left
242
+ while True:
243
+ try:
244
+ img_url = f"{IMAGE_SERVER_BASE.rstrip('/')}/middleware/next/image/{urllib.parse.quote(course, safe='')}"
245
+ rimg = requests.get(img_url, params={"requester_id": REQUESTER_ID}, timeout=15)
246
+ if rimg.status_code == 404:
247
+ print(f"[BACKGROUND] no images for course {course}")
248
+ break
249
+ rimg.raise_for_status()
250
+ img_json = rimg.json()
251
+ except Exception as e:
252
+ print(f"[BACKGROUND] failed to get next image: {e}")
253
+ time.sleep(1)
254
+ continue
255
+
256
+ video = img_json.get('video')
257
+ frame = img_json.get('frame')
258
+ file_id = img_json.get('file_id')
259
+ if not (video and frame and file_id):
260
+ print(f"[BACKGROUND] unexpected image entry: {img_json}")
261
+ time.sleep(0.5)
262
+ continue
263
+
264
+ download_url = _build_download_url(course, video, frame)
265
+ print(f"[BACKGROUND] downloading {download_url}")
266
+ img_bytes, content_type = _download_bytes(download_url)
267
+ if not img_bytes:
268
+ print(f"[BACKGROUND] failed to download image, releasing frame {file_id}")
269
+ _release_frame(course, video, frame)
270
+ time.sleep(1)
271
+ continue
272
+
273
+ try:
274
+ pil_img = Image.open(BytesIO(img_bytes)).convert('RGB')
275
+ except Exception as e:
276
+ print(f"[BACKGROUND] failed to open image bytes: {e}")
277
+ _release_frame(course, video, frame)
278
+ time.sleep(1)
279
+ continue
280
+
281
+ # Choose model and processor according to MODEL_CHOICE
282
+ if MODEL_CHOICE == "Florence-2-base":
283
+ model = vision_language_model_base
284
+ processor = vision_language_processor_base
285
+ else:
286
+ model = vision_language_model_large
287
+ processor = vision_language_processor_large
288
+
289
+ caption = ""
290
+ try:
291
+ # Reuse existing processing function: process_image_description(model, processor, image)
292
+ caption = process_image_description(model, processor, pil_img)
293
+ except Exception as e:
294
+ print(f"[BACKGROUND] captioning failed: {e}")
295
+
296
+ status, resp = _post_submit(caption, frame, course, download_url, img_bytes)
297
+ print(f"[BACKGROUND] submitted caption for {frame}: status={status}")
298
+
299
+ # release frame
300
+ _release_frame(course, video, frame)
301
+ time.sleep(0.2)
302
+
303
+ # release course
304
+ _release_course(course)
305
+ time.sleep(1)
306
+
307
+ except Exception as e:
308
+ print(f"[BACKGROUND] unexpected loop error: {e}")
309
+ time.sleep(5)
310
+
311
+ # Start background worker thread (daemon) so it doesn't block shutdown
312
+ def _start_worker_thread():
313
+ t = threading.Thread(target=background_worker, daemon=True)
314
+ t.start()
315
+
316
+
317
+ # Description for the interface
318
+ description = "> Select the model to use for generating the image description. 'Base' is smaller and faster, while 'Large' is more accurate but slower."
319
+ if device == "cpu":
320
+ description += " Note: Running on CPU, which may be slow for large models."
321
+
322
+ # Define examples - use placeholder if files don't exist
323
+ examples = [
324
+ ["https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "Florence-2-large"],
325
+ ["https://huggingface.co/spaces/Fred808/NNE/resolve/main/young-woman-doing-fencing-special-equipment.jpg", "Florence-2-base"],
326
+ ]
327
+
328
+ css = """
329
+ .submit-btn {
330
+ background-color: #4682B4 !important;
331
+ color: white !important;
332
+ }
333
+ .submit-btn:hover {
334
+ background-color: #87CEEB !important;
335
+ }
336
+ """
337
+
338
+ # Create the Gradio interface with Blocks
339
+ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
340
+ gr.Markdown("# Florence-2 Models Image Captions")
341
+ gr.Markdown(description)
342
+
343
+ with gr.Tab("Upload Image"):
344
+ with gr.Row():
345
+ with gr.Column():
346
+ image_input = gr.Image(label="Upload Image", type="pil")
347
+ generate_btn = gr.Button("Generate Caption", elem_classes="submit-btn")
348
+
349
+ with gr.Column():
350
+ model_choice = gr.Radio(
351
+ ["Florence-2-base", "Florence-2-large"],
352
+ label="Model Choice",
353
+ value="Florence-2-base"
354
+ )
355
+ output = gr.Textbox(label="Generated Caption", lines=4, show_copy_button=True)
356
+
357
+ # Examples for upload tab
358
+ gr.Examples(
359
+ examples=examples,
360
+ inputs=[image_input, model_choice],
361
+ outputs=[output],
362
+ fn=describe_image,
363
+ run_on_click=True
364
+ )
365
+
366
+ generate_btn.click(
367
+ fn=describe_image,
368
+ inputs=[image_input, model_choice],
369
+ outputs=output
370
+ )
371
+
372
+ with gr.Tab("Image from URL"):
373
+ gr.Markdown("## Generate caption from image URL")
374
+ gr.Markdown("Enter an image URL below to generate a caption.")
375
+
376
+ with gr.Row():
377
+ with gr.Column():
378
+ url_input = gr.Textbox(
379
+ label="Image URL",
380
+ placeholder="https://example.com/image.jpg",
381
+ lines=2
382
+ )
383
+ url_model_choice = gr.Radio(
384
+ ["Florence-2-base", "Florence-2-large"],
385
+ label="Model Choice",
386
+ value="Florence-2-large"
387
+ )
388
+ url_generate_btn = gr.Button("Generate Caption from URL", variant="primary")
389
+
390
+ with gr.Column():
391
+ url_output = gr.JSON(label="API Response")
392
+ url_caption = gr.Textbox(label="Caption", lines=4, show_copy_button=True)
393
+
394
+ # URL examples
395
+ url_examples = [
396
+ ["https://huggingface.co/spaces/Fred808/NNE/resolve/main/young-woman-doing-fencing-special-equipment.jpg", "Florence-2-large"],
397
+ ["https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "Florence-2-base"],
398
+ ]
399
+
400
+ gr.Examples(
401
+ examples=url_examples,
402
+ inputs=[url_input, url_model_choice],
403
+ outputs=[url_output, url_caption],
404
+ fn=describe_image_from_url,
405
+ run_on_click=True
406
+ )
407
+
408
+ def process_url_request(image_url, model_choice):
409
+ result = describe_image_from_url(image_url, model_choice)
410
+ caption = result.get("caption", "") if "caption" in result else result.get("error", "")
411
+ return result, caption
412
+
413
+ url_generate_btn.click(
414
+ fn=process_url_request,
415
+ inputs=[url_input, url_model_choice],
416
+ outputs=[url_output, url_caption]
417
+ )
418
+
419
+ # Get the port from environment variable (for Hugging Face Spaces)
420
+ port = int(os.environ.get("PORT", 7860))
421
+
422
+ # Launch the interface with simplified settings
423
+ try:
424
+ demo.launch(
425
+ server_name="0.0.0.0",
426
+ server_port=port,
427
+ share=False, # Disable share for Hugging Face Spaces
428
+ debug=False, # Disable debug mode for stability
429
+ show_error=True,
430
+ quiet=True, # Reduce verbose output
431
+ )
432
+ except Exception as e:
433
+ print(f"Error launching app: {e}")
434
+ # Fallback launch with minimal settings
435
+ demo.launch(server_name="0.0.0.0", server_port=port, share=False, quiet=True)