Update app.py
Browse files
app.py
CHANGED
|
@@ -8,25 +8,13 @@ from werkzeug.utils import secure_filename
|
|
| 8 |
from itertools import cycle
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
|
| 11 |
-
# بارگذاری متغیرهای محیطی از فایل .env (برای تست لوکال)
|
| 12 |
load_dotenv()
|
| 13 |
|
| 14 |
-
# --- 1. تنظیمات اصلی ---
|
| 15 |
-
|
| 16 |
-
# دریافت لیست کارگرها از متغیر محیطی
|
| 17 |
-
# آدرسها باید با کاما (,) جدا شده باشند
|
| 18 |
workers_env = os.getenv("WORKER_NODES", "")
|
| 19 |
-
|
| 20 |
if not workers_env:
|
| 21 |
-
|
| 22 |
-
raise ValueError("خطا: متغیر محیطی WORKER_NODES تنظیم نشده است. لطفاً آدرس کارگرها را در تنظیمات اضافه کنید.")
|
| 23 |
|
| 24 |
-
# تبدیل رشته به لیست و حذف فاصلههای اضافی
|
| 25 |
WORKER_URLS = [url.strip() for url in workers_env.split(',') if url.strip()]
|
| 26 |
-
|
| 27 |
-
if not WORKER_URLS:
|
| 28 |
-
raise ValueError("خطا: لیست کارگرها خالی است.")
|
| 29 |
-
|
| 30 |
worker_pool = cycle(WORKER_URLS)
|
| 31 |
|
| 32 |
UPLOADER_API_URL = "https://hamed744-uploadfile.hf.space/upload"
|
|
@@ -40,11 +28,10 @@ lock = threading.Lock()
|
|
| 40 |
def get_next_worker_url():
|
| 41 |
return next(worker_pool)
|
| 42 |
|
| 43 |
-
# --- 2. توابع پسزمینه ---
|
| 44 |
def get_permanent_link(job_id, temp_render_url, worker_url):
|
| 45 |
try:
|
| 46 |
with lock: jobs[job_id]["status"] = "در حال دائمیسازی لینک..."
|
| 47 |
-
if not HF_TOKEN: raise Exception("
|
| 48 |
|
| 49 |
video_full_url = f"{worker_url}{temp_render_url}"
|
| 50 |
|
|
@@ -55,7 +42,7 @@ def get_permanent_link(job_id, temp_render_url, worker_url):
|
|
| 55 |
data = response.json()
|
| 56 |
|
| 57 |
uploader_proxy_url = data.get("hf_url")
|
| 58 |
-
if not uploader_proxy_url: raise Exception("
|
| 59 |
|
| 60 |
path_part = uploader_proxy_url.split('/proxy/')[1]
|
| 61 |
final_proxy_url = f"/proxy/{path_part}"
|
|
@@ -65,22 +52,21 @@ def get_permanent_link(job_id, temp_render_url, worker_url):
|
|
| 65 |
jobs[job_id]["result"] = final_proxy_url
|
| 66 |
|
| 67 |
except Exception as e:
|
| 68 |
-
print(f"
|
| 69 |
with lock:
|
| 70 |
jobs[job_id]["status"] = "error"
|
| 71 |
-
jobs[job_id]["result"] = f"
|
| 72 |
|
| 73 |
-
# <<< تابع اصلاح شده: حذف تایماوت و افزایش مقاومت در برابر خطای ارتباطی >>>
|
| 74 |
def poll_worker_service(job_id, render_job_id, worker_url):
|
| 75 |
POLLING_INTERVAL = 15
|
| 76 |
-
MAX_POLLING_ERRORS = 20
|
| 77 |
error_count = 0
|
| 78 |
|
| 79 |
-
while True:
|
| 80 |
try:
|
| 81 |
response = requests.post(f"{worker_url}/check_job_status", json={"job_id": render_job_id}, timeout=45)
|
| 82 |
response.raise_for_status()
|
| 83 |
-
error_count = 0
|
| 84 |
|
| 85 |
data = response.json()
|
| 86 |
with lock:
|
|
@@ -90,27 +76,26 @@ def poll_worker_service(job_id, render_job_id, worker_url):
|
|
| 90 |
|
| 91 |
if data.get("status") == "completed":
|
| 92 |
get_permanent_link(job_id, data.get("result"), worker_url)
|
| 93 |
-
return
|
| 94 |
elif data.get("status") == "error":
|
| 95 |
with lock:
|
| 96 |
current_job = jobs.get(job_id)
|
| 97 |
if current_job:
|
| 98 |
-
current_job["result"] = data.get("result", "
|
| 99 |
-
return
|
| 100 |
except requests.exceptions.RequestException as e:
|
| 101 |
error_count += 1
|
| 102 |
-
print(f"
|
| 103 |
if error_count >= MAX_POLLING_ERRORS:
|
| 104 |
with lock:
|
| 105 |
current_job = jobs.get(job_id)
|
| 106 |
if current_job:
|
| 107 |
current_job["status"] = "error"
|
| 108 |
-
current_job["result"] = f"
|
| 109 |
-
return
|
| 110 |
|
| 111 |
time.sleep(POLLING_INTERVAL)
|
| 112 |
|
| 113 |
-
# --- 3. API Endpoints ---
|
| 114 |
@app.route('/')
|
| 115 |
def index():
|
| 116 |
return render_template('index.html')
|
|
@@ -118,7 +103,7 @@ def index():
|
|
| 118 |
@app.route('/api/submit_job', methods=['POST'])
|
| 119 |
def submit_job():
|
| 120 |
if 'image_file' not in request.files or 'video_file' not in request.files:
|
| 121 |
-
return jsonify({"error": "
|
| 122 |
|
| 123 |
image_file = request.files['image_file']
|
| 124 |
video_file = request.files['video_file']
|
|
@@ -131,7 +116,7 @@ def submit_job():
|
|
| 131 |
MAX_RETRIES = 3
|
| 132 |
for attempt in range(MAX_RETRIES):
|
| 133 |
selected_worker_url = get_next_worker_url()
|
| 134 |
-
print(f"
|
| 135 |
try:
|
| 136 |
response = requests.post(
|
| 137 |
f"{selected_worker_url}/submit_new_job",
|
|
@@ -143,33 +128,33 @@ def submit_job():
|
|
| 143 |
response.raise_for_status()
|
| 144 |
render_data = response.json()
|
| 145 |
render_job_id = render_data.get("job_id")
|
| 146 |
-
if not render_job_id: raise Exception("
|
| 147 |
|
| 148 |
internal_job_id = str(uuid.uuid4())
|
| 149 |
-
with lock: jobs[internal_job_id] = {"status": "
|
| 150 |
|
| 151 |
thread = threading.Thread(target=poll_worker_service, args=(internal_job_id, render_job_id, selected_worker_url))
|
| 152 |
thread.start()
|
| 153 |
return jsonify({"job_id": internal_job_id})
|
| 154 |
except Exception as e:
|
| 155 |
-
print(f"
|
| 156 |
time.sleep(2)
|
| 157 |
|
| 158 |
-
final_error_message = ("<strong
|
| 159 |
-
"
|
| 160 |
return jsonify({"error": final_error_message}), 500
|
| 161 |
|
| 162 |
@app.route('/api/check_status', methods=['POST'])
|
| 163 |
def check_status():
|
| 164 |
data = request.get_json()
|
| 165 |
job_id = data.get('job_id')
|
| 166 |
-
if not job_id: return jsonify({"error": "
|
| 167 |
with lock: job = jobs.get(job_id, {"status": "not_found", "result": None})
|
| 168 |
return jsonify({"status": job["status"], "result": job["result"]})
|
| 169 |
|
| 170 |
@app.route('/proxy/<user>/<repo>/<path:file_path>')
|
| 171 |
def file_proxy(user, repo, file_path):
|
| 172 |
-
if not HF_TOKEN: return "
|
| 173 |
repo_id = f"{user}/{repo}"
|
| 174 |
file_url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{file_path}"
|
| 175 |
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
|
@@ -179,11 +164,11 @@ def file_proxy(user, repo, file_path):
|
|
| 179 |
return Response(hf_response.iter_content(chunk_size=8192),
|
| 180 |
content_type=hf_response.headers.get('Content-Type', 'application/octet-stream'))
|
| 181 |
except requests.exceptions.HTTPError as e:
|
| 182 |
-
if e.response.status_code == 404: return "
|
| 183 |
-
return f"
|
| 184 |
except Exception as e:
|
| 185 |
-
print(f"
|
| 186 |
-
return "
|
| 187 |
|
| 188 |
if __name__ == '__main__':
|
| 189 |
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))
|
|
|
|
| 8 |
from itertools import cycle
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
|
|
|
|
| 11 |
load_dotenv()
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
workers_env = os.getenv("WORKER_NODES", "")
|
|
|
|
| 14 |
if not workers_env:
|
| 15 |
+
raise ValueError("WORKER_NODES environment variable is not set")
|
|
|
|
| 16 |
|
|
|
|
| 17 |
WORKER_URLS = [url.strip() for url in workers_env.split(',') if url.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
worker_pool = cycle(WORKER_URLS)
|
| 19 |
|
| 20 |
UPLOADER_API_URL = "https://hamed744-uploadfile.hf.space/upload"
|
|
|
|
| 28 |
def get_next_worker_url():
|
| 29 |
return next(worker_pool)
|
| 30 |
|
|
|
|
| 31 |
def get_permanent_link(job_id, temp_render_url, worker_url):
|
| 32 |
try:
|
| 33 |
with lock: jobs[job_id]["status"] = "در حال دائمیسازی لینک..."
|
| 34 |
+
if not HF_TOKEN: raise Exception("HF_TOKEN not found")
|
| 35 |
|
| 36 |
video_full_url = f"{worker_url}{temp_render_url}"
|
| 37 |
|
|
|
|
| 42 |
data = response.json()
|
| 43 |
|
| 44 |
uploader_proxy_url = data.get("hf_url")
|
| 45 |
+
if not uploader_proxy_url: raise Exception("Invalid response from uploader")
|
| 46 |
|
| 47 |
path_part = uploader_proxy_url.split('/proxy/')[1]
|
| 48 |
final_proxy_url = f"/proxy/{path_part}"
|
|
|
|
| 52 |
jobs[job_id]["result"] = final_proxy_url
|
| 53 |
|
| 54 |
except Exception as e:
|
| 55 |
+
print(f"Error making link permanent for {job_id}: {e}")
|
| 56 |
with lock:
|
| 57 |
jobs[job_id]["status"] = "error"
|
| 58 |
+
jobs[job_id]["result"] = f"Error making link permanent. Temp link: {temp_render_url}"
|
| 59 |
|
|
|
|
| 60 |
def poll_worker_service(job_id, render_job_id, worker_url):
|
| 61 |
POLLING_INTERVAL = 15
|
| 62 |
+
MAX_POLLING_ERRORS = 20
|
| 63 |
error_count = 0
|
| 64 |
|
| 65 |
+
while True:
|
| 66 |
try:
|
| 67 |
response = requests.post(f"{worker_url}/check_job_status", json={"job_id": render_job_id}, timeout=45)
|
| 68 |
response.raise_for_status()
|
| 69 |
+
error_count = 0
|
| 70 |
|
| 71 |
data = response.json()
|
| 72 |
with lock:
|
|
|
|
| 76 |
|
| 77 |
if data.get("status") == "completed":
|
| 78 |
get_permanent_link(job_id, data.get("result"), worker_url)
|
| 79 |
+
return
|
| 80 |
elif data.get("status") == "error":
|
| 81 |
with lock:
|
| 82 |
current_job = jobs.get(job_id)
|
| 83 |
if current_job:
|
| 84 |
+
current_job["result"] = data.get("result", "Unknown worker error")
|
| 85 |
+
return
|
| 86 |
except requests.exceptions.RequestException as e:
|
| 87 |
error_count += 1
|
| 88 |
+
print(f"Connection error polling worker {worker_url} ({error_count}/{MAX_POLLING_ERRORS}): {e}")
|
| 89 |
if error_count >= MAX_POLLING_ERRORS:
|
| 90 |
with lock:
|
| 91 |
current_job = jobs.get(job_id)
|
| 92 |
if current_job:
|
| 93 |
current_job["status"] = "error"
|
| 94 |
+
current_job["result"] = f"Connection to worker ({worker_url}) lost."
|
| 95 |
+
return
|
| 96 |
|
| 97 |
time.sleep(POLLING_INTERVAL)
|
| 98 |
|
|
|
|
| 99 |
@app.route('/')
|
| 100 |
def index():
|
| 101 |
return render_template('index.html')
|
|
|
|
| 103 |
@app.route('/api/submit_job', methods=['POST'])
|
| 104 |
def submit_job():
|
| 105 |
if 'image_file' not in request.files or 'video_file' not in request.files:
|
| 106 |
+
return jsonify({"error": "Image and video files are required"}), 400
|
| 107 |
|
| 108 |
image_file = request.files['image_file']
|
| 109 |
video_file = request.files['video_file']
|
|
|
|
| 116 |
MAX_RETRIES = 3
|
| 117 |
for attempt in range(MAX_RETRIES):
|
| 118 |
selected_worker_url = get_next_worker_url()
|
| 119 |
+
print(f"Attempt {attempt + 1}/{MAX_RETRIES}: Sending to worker: {selected_worker_url}")
|
| 120 |
try:
|
| 121 |
response = requests.post(
|
| 122 |
f"{selected_worker_url}/submit_new_job",
|
|
|
|
| 128 |
response.raise_for_status()
|
| 129 |
render_data = response.json()
|
| 130 |
render_job_id = render_data.get("job_id")
|
| 131 |
+
if not render_job_id: raise Exception("Invalid response from worker")
|
| 132 |
|
| 133 |
internal_job_id = str(uuid.uuid4())
|
| 134 |
+
with lock: jobs[internal_job_id] = {"status": "Sending to worker...", "result": None}
|
| 135 |
|
| 136 |
thread = threading.Thread(target=poll_worker_service, args=(internal_job_id, render_job_id, selected_worker_url))
|
| 137 |
thread.start()
|
| 138 |
return jsonify({"job_id": internal_job_id})
|
| 139 |
except Exception as e:
|
| 140 |
+
print(f"Error on attempt {attempt + 1} for worker {selected_worker_url}: {e}")
|
| 141 |
time.sleep(2)
|
| 142 |
|
| 143 |
+
final_error_message = ("<strong>Servers are busy!</strong><br>"
|
| 144 |
+
"Please try again in a few minutes.")
|
| 145 |
return jsonify({"error": final_error_message}), 500
|
| 146 |
|
| 147 |
@app.route('/api/check_status', methods=['POST'])
|
| 148 |
def check_status():
|
| 149 |
data = request.get_json()
|
| 150 |
job_id = data.get('job_id')
|
| 151 |
+
if not job_id: return jsonify({"error": "Job ID is required"}), 400
|
| 152 |
with lock: job = jobs.get(job_id, {"status": "not_found", "result": None})
|
| 153 |
return jsonify({"status": job["status"], "result": job["result"]})
|
| 154 |
|
| 155 |
@app.route('/proxy/<user>/<repo>/<path:file_path>')
|
| 156 |
def file_proxy(user, repo, file_path):
|
| 157 |
+
if not HF_TOKEN: return "HF_TOKEN not set", 500
|
| 158 |
repo_id = f"{user}/{repo}"
|
| 159 |
file_url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{file_path}"
|
| 160 |
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
|
|
|
| 164 |
return Response(hf_response.iter_content(chunk_size=8192),
|
| 165 |
content_type=hf_response.headers.get('Content-Type', 'application/octet-stream'))
|
| 166 |
except requests.exceptions.HTTPError as e:
|
| 167 |
+
if e.response.status_code == 404: return "File not found", 404
|
| 168 |
+
return f"Hugging Face Server Error: {e.response.status_code}", e.response.status_code
|
| 169 |
except Exception as e:
|
| 170 |
+
print(f"Proxy error: {e}")
|
| 171 |
+
return "Error fetching file", 500
|
| 172 |
|
| 173 |
if __name__ == '__main__':
|
| 174 |
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))
|