Opera8 commited on
Commit
dd0de60
·
verified ·
1 Parent(s): a85923e

Upload app.py

Browse files
Files changed (1) hide show
  1. templates/app.py +861 -0
templates/app.py ADDED
@@ -0,0 +1,861 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import requests
4
+ import httpx
5
+ import json
6
+ import uuid
7
+ import time
8
+ from datetime import date
9
+ from fastapi import FastAPI, Request, BackgroundTasks
10
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse, StreamingResponse
11
+
12
+ # 🟢 ایمپورت و فراخوانی ماژول مستقل روبیکا بات
13
+ from rubikabot import router as rubikabot_router
14
+ from soti import router as soti_router
15
+
16
+ app = FastAPI()
17
+ app.include_router(rubikabot_router) # 🟢 ثبت تمام آدرس‌های آپلود و دانلودد روبیکا به صورت یکپارچه
18
+ app.include_router(soti_router)
19
+
20
+ # ایجاد پوشه‌ههای مشترک برای وضعیت‌ها، فایل‌های صوتی و دیتای مصرف کاربران
21
+ JOBS_DIR = "./tts_jobs"
22
+ USAGE_DIR = "./tts_usage"
23
+ os.makedirs(JOBS_DIR, exist_ok=True)
24
+ os.makedirs(USAGE_DIR, exist_ok=True)
25
+
26
+ # 🔒 متغیرها و دیتابیس حافظه موقت رانفلر برای قفل دوگانه (IP + Fingerprint)
27
+ CLONE_USAGE_LIMIT = 1
28
+ EDIT_USAGE_LIMIT = 5
29
+
30
+ def get_client_ip(request: Request) -> str:
31
+ """استخراج دقیق آی‌پی واقعی کاربر از پشت پروکسی‌های Cloudflare و هاست رانفلر"""
32
+ cf_ip = request.headers.get("CF-Connecting-IP")
33
+ if cf_ip:
34
+ return cf_ip.strip()
35
+ x_forwarded_for = request.headers.get("X-Forwarded-For")
36
+ if x_forwarded_for:
37
+ return x_forwarded_for.split(",")[0].strip()
38
+ if request.client and request.client.host:
39
+ return request.client.host
40
+ return "0.0.0.0"
41
+
42
+ # --- بخش مدیریت دائمی اعتبار کلون صدا (ذخیره روی دیسک رانفلر) ---
43
+ def get_local_clone_usage(key: str) -> dict:
44
+ today_str = date.today().isoformat()
45
+ # تولید آدرس فایل ذخیره بر اساس اثر انگشت یا آی‌پی کاربر
46
+ safe_key = "".join([c for c in key if c.isalnum() or c in ["-", "_", "."]])
47
+ file_path = os.path.join(USAGE_DIR, f"clone_{safe_key}.json")
48
+ if os.path.exists(file_path):
49
+ try:
50
+ with open(file_path, "r", encoding="utf-8") as f:
51
+ data = json.load(f)
52
+ if data.get("date") == today_str:
53
+ return data
54
+ except: pass
55
+ return {"count": 0, "date": today_str}
56
+
57
+ def save_local_clone_usage(key: str, data: dict):
58
+ safe_key = "".join([c for c in key if c.isalnum() or c in ["-", "_", "."]])
59
+ file_path = os.path.join(USAGE_DIR, f"clone_{safe_key}.json")
60
+ try:
61
+ with open(file_path, "w", encoding="utf-8") as f:
62
+ json.dump(data, f)
63
+ except: pass
64
+
65
+ def check_local_clone_limit(fingerprint: str, ip: str) -> bool:
66
+ """بررسی محدودیت کلون صدا: اگر اثر انگشت یا آی‌پی سهمیه‌اش پر باشد، بلاک می‌کند"""
67
+ for key in [fingerprint, ip]:
68
+ if not key: continue
69
+ data = get_local_clone_usage(key)
70
+ if data["count"] >= CLONE_USAGE_LIMIT:
71
+ return True
72
+ return False
73
+
74
+ def use_local_clone(fingerprint: str, ip: str):
75
+ """ثبت مصرف سهمیه کلون صدا روی هارد دیسک رانفلر"""
76
+ for key in [fingerprint, ip]:
77
+ if not key: continue
78
+ data = get_local_clone_usage(key)
79
+ data["count"] += 1
80
+ save_local_clone_usage(key, data)
81
+
82
+ # --- بخش مدیریت دائمی اعتبار ویرایش تصاویر (ذخیره روی دیسک رانفلر) ---
83
+ def get_local_edit_usage(key: str) -> dict:
84
+ current_week = date.today().isocalendar()[1]
85
+ safe_key = "".join([c for c in key if c.isalnum() or c in ["-", "_", "."]])
86
+ file_path = os.path.join(USAGE_DIR, f"edit_{safe_key}.json")
87
+ if os.path.exists(file_path):
88
+ try:
89
+ with open(file_path, "r", encoding="utf-8") as f:
90
+ data = json.load(f)
91
+ if data.get("week") == current_week:
92
+ return data
93
+ except: pass
94
+ return {"count": 0, "week": current_week}
95
+
96
+ def save_local_edit_usage(key: str, data: dict):
97
+ safe_key = "".join([c for c in key if c.isalnum() or c in ["-", "_", "."]])
98
+ file_path = os.path.join(USAGE_DIR, f"edit_{safe_key}.json")
99
+ try:
100
+ with open(file_path, "w", encoding="utf-8") as f:
101
+ json.dump(data, f)
102
+ except: pass
103
+
104
+ def check_local_edit_limit(fingerprint: str, ip: str) -> bool:
105
+ """بررسی محدودیت ادیت عکس: اگر اثر انگشت یا آی‌پی سهمیه هفتگی‌اش پر باشد، بلاک می‌کند"""
106
+ for key in [fingerprint, ip]:
107
+ if not key: continue
108
+ data = get_local_edit_usage(key)
109
+ if data["count"] >= EDIT_USAGE_LIMIT:
110
+ return True
111
+ return False
112
+
113
+ def use_local_edit(fingerprint: str, ip: str):
114
+ """ثبت مصرف سهمیه ویرایش عکس روی هارد دیسک رانفلر"""
115
+ for key in [fingerprint, ip]:
116
+ if not key: continue
117
+ data = get_local_edit_usage(key)
118
+ data["count"] += 1
119
+ save_local_edit_usage(key, data)
120
+
121
+
122
+ def clean_old_files():
123
+ """پاکسازی خودکار فایل‌های قدیمی برای جلوگیری از پر شدن حافظه سرور"""
124
+ now = time.time()
125
+
126
+ # فایل‌های صوتی و جاب‌ها بعد از ۳۰ دقیقه (۱۸۰۰ ثانیه) حذف می‌شوند
127
+ try:
128
+ for f in os.listdir(JOBS_DIR):
129
+ fpath = os.path.join(JOBS_DIR, f)
130
+ if os.path.isfile(fpath) and os.stat(fpath).st_mtime < now - 1800:
131
+ os.remove(fpath)
132
+ except: pass
133
+
134
+ # لاگ‌های اعتبار روزانه کاربران بعد از ۴۸ ساعت (۱۷۲۸۰۰ ثانیه) حذف می‌شوند
135
+ try:
136
+ for f in os.listdir(USAGE_DIR):
137
+ fpath = os.path.join(USAGE_DIR, f)
138
+ if os.path.isfile(fpath) and os.stat(fpath).st_mtime < now - 172800:
139
+ os.remove(fpath)
140
+ except: pass
141
+
142
+ def save_job_state(job_id: str, status: str, audio_data: bytes = None, error_message: str = None):
143
+ """ذخیره وضعیت کار در فایل متنی مشترک دیسک"""
144
+ clean_old_files()
145
+
146
+ state = {
147
+ "status": status,
148
+ "error_message": error_message,
149
+ "has_audio": audio_data is not None
150
+ }
151
+ state_file = os.path.join(JOBS_DIR, f"{job_id}.json")
152
+ with open(state_file, "w", encoding="utf-8") as f:
153
+ json.dump(state, f, ensure_ascii=False)
154
+
155
+ if audio_data:
156
+ audio_file = os.path.join(JOBS_DIR, f"{job_id}.mp3")
157
+ with open(audio_file, "wb") as f:
158
+ f.write(audio_data)
159
+
160
+ def get_job_state(job_id: str):
161
+ """خواندن وضعیت کار از فایل متنی مشترک دیسک"""
162
+ state_file = os.path.join(JOBS_DIR, f"{job_id}.json")
163
+ if not os.path.exists(state_file):
164
+ return None
165
+ try:
166
+ with open(state_file, "r", encoding="utf-8") as f:
167
+ return json.load(f)
168
+ except:
169
+ return None
170
+
171
+ def get_job_audio(job_id: str):
172
+ """خواندن دیتای باینری صوتی ذخیره شده روی دیسک"""
173
+ audio_file = os.path.join(JOBS_DIR, f"{job_id}.mp3")
174
+ if os.path.exists(audio_file):
175
+ try:
176
+ with open(audio_file, "rb") as f:
177
+ return f.read()
178
+ except:
179
+ return None
180
+ return None
181
+
182
+ async def run_tts_background(job_id: str, payload: dict, headers: dict):
183
+ try:
184
+ # برای ارتباط با sada8888-tts2 فقط متن، گوینده و خلاقیت فرستاده می‌شود (مشابه پادکست)
185
+ target_payload = {
186
+ "text": payload.get("text"),
187
+ "speaker": payload.get("speaker"),
188
+ "temperature": payload.get("temperature", 1.5)
189
+ }
190
+
191
+ async with httpx.AsyncClient(timeout=180.0) as client:
192
+ resp = await client.post(
193
+ "https://sada8888-tts2.hf.space/api/generate",
194
+ json=target_payload,
195
+ headers={"Content-Type": "application/json"}
196
+ )
197
+ if resp.status_code == 200:
198
+ save_job_state(job_id, "completed", audio_data=resp.content)
199
+ else:
200
+ save_job_state(job_id, "error", error_message=f"خطا از سرور تولید صدا: {resp.text}")
201
+ except Exception as e:
202
+ save_job_state(job_id, "error", error_message=str(e))
203
+
204
+ # =================================================================
205
+ # 1. مسیرهای اصلی و بخش متن به صدا (TTS PRO)
206
+ # =================================================================
207
+ @app.get("/")
208
+ async def root_path():
209
+ # صفحه اصلی چیزی نشان نمی‌دهد و دایرکت هم نمی‌شود
210
+ return HTMLResponse("Alpha API Server is running.", status_code=200)
211
+
212
+ @app.get("/tts")
213
+ @app.get("/tts/")
214
+ async def old_tts_path():
215
+ # لینک قدیمی غیرفعال شد
216
+ return HTMLResponse("<h1>این مسیر تغییر یافته است. لطفا از ttspro استفاده کنید.</h1>", status_code=404)
217
+
218
+ @app.get("/ttspro", response_class=HTMLResponse)
219
+ @app.get("/ttspro/", response_class=HTMLResponse)
220
+ async def serve_ttspro():
221
+ # فقط این آدرس فایل ttspro.html را باز میکند
222
+ if os.path.exists("ttspro.html"):
223
+ return FileResponse("ttspro.html")
224
+ return HTMLResponse("<h1>خطا: فایل ttspro.html پیدا نشد! لطفا آن را آپلود کنید.</h1>", status_code=404)
225
+
226
+
227
+ # =================================================================
228
+ # 2. آینه چت بات (پروکسی خام و بدون بافر برای سرعت حداکثری)
229
+ # =================================================================
230
+ CHAT_SPACE_URL = "https://sada8888-ttslive-chat.hf.space"
231
+
232
+ @app.get("/chat", response_class=HTMLResponse)
233
+ async def serve_chat():
234
+ if os.path.exists("chat.html"):
235
+ return FileResponse("chat.html")
236
+ return HTMLResponse("<h1>خطا: فایل chat.html ��یدا نشد!</h1>", status_code=404)
237
+
238
+ @app.get("/audio/{filename}")
239
+ async def serve_chat_audio(filename: str):
240
+ # اگر فایل صوتی مربوط به درخواست‌های متن به صدای استاندارد ما بود
241
+ if filename.startswith("standard_"):
242
+ job_id = filename.replace("standard_", "").replace(".mp3", "").replace(".wav", "")
243
+ audio_data = get_job_audio(job_id)
244
+ if audio_data:
245
+ return StreamingResponse(
246
+ iter([audio_data]),
247
+ media_type="audio/mpeg"
248
+ )
249
+ return HTMLResponse("<h1>فایل صوتی یافت نشد</h1>", status_code=404)
250
+
251
+ # روال عادی برای چت‌پروکسی
252
+ async def iterfile():
253
+ try:
254
+ async with httpx.AsyncClient(timeout=10.0) as client:
255
+ async with client.stream("GET", f"{CHAT_SPACE_URL}/audio/{filename}") as r:
256
+ r.raise_for_status()
257
+ async for chunk in r.aiter_bytes(chunk_size=8192):
258
+ if chunk: yield chunk
259
+ except Exception:
260
+ yield b""
261
+ return StreamingResponse(iterfile(), media_type="audio/wav")
262
+
263
+ @app.post("/api/chat_proxy")
264
+ async def chat_proxy_bridge(request: Request):
265
+ try:
266
+ body_bytes = await request.body()
267
+
268
+ async def stream_generator():
269
+ try:
270
+ # اتصال مستقیم به هاگینگ فیس با httpx برای جلوگیری از مسدود شدن ورکرها
271
+ async with httpx.AsyncClient(timeout=60.0) as client:
272
+ async with client.stream(
273
+ "POST",
274
+ f"{CHAT_SPACE_URL}/api/chat_proxy",
275
+ content=body_bytes,
276
+ headers={"Content-Type": "application/json"}
277
+ ) as resp:
278
+ async for line in resp.aiter_lines():
279
+ if line:
280
+ yield (line + "\n").encode('utf-8')
281
+ except Exception as e:
282
+ err = {"status": "error", "message": f"خطا در ارتباط رانفلر با سرور اصلی: {str(e)}"}
283
+ yield (json.dumps(err) + "\n").encode('utf-8')
284
+
285
+ headers = {
286
+ "Cache-Control": "no-cache, no-transform",
287
+ "X-Accel-Buffering": "no",
288
+ "Connection": "keep-alive",
289
+ "Transfer-Encoding": "chunked"
290
+ }
291
+ return StreamingResponse(stream_generator(), media_type="application/json", headers=headers)
292
+ except Exception as e:
293
+ return JSONResponse({"status": "error", "message": "سرور موقتا در دسترس نیست."}, status_code=500)
294
+
295
+
296
+ # =================================================================
297
+ # 3. دور زدن فیلترینگ برای API های متن به صدا (TTS Proxy) با کش یکپارچه
298
+ # =================================================================
299
+ HF_SPACE_BASE_URL_TTS = "https://ezmarynoori-tts.hf.space"
300
+ USAGE_LIMIT_GENERATE = 10
301
+
302
+ def get_user_usage(fingerprint: str):
303
+ """خواندن آمار مصرف کاربر از دیسک مشترک سرور"""
304
+ today_str = date.today().isoformat()
305
+ usage_file = os.path.join(USAGE_DIR, f"usage_{fingerprint}.json")
306
+ if os.path.exists(usage_file):
307
+ try:
308
+ with open(usage_file, "r", encoding="utf-8") as f:
309
+ data = json.load(f)
310
+ if data.get("last_reset") == today_str:
311
+ return data
312
+ except: pass
313
+ return {"count": 0, "last_reset": today_str}
314
+
315
+ def increment_user_usage(fingerprint: str):
316
+ """افزایش آمار مصرف کاربر و ذخیره روی دیسک مشترک"""
317
+ data = get_user_usage(fingerprint)
318
+ data["count"] += 1
319
+ usage_file = os.path.join(USAGE_DIR, f"usage_{fingerprint}.json")
320
+ try:
321
+ with open(usage_file, "w", encoding="utf-8") as f:
322
+ json.dump(data, f)
323
+ except: pass
324
+
325
+ def check_limit_tts(payload):
326
+ if payload.get('subscriptionStatus') != 'paid':
327
+ fingerprint = payload.get('fingerprint')
328
+ if not fingerprint:
329
+ return False
330
+
331
+ record = get_user_usage(fingerprint)
332
+ if record["count"] >= USAGE_LIMIT_GENERATE:
333
+ return False
334
+
335
+ increment_user_usage(fingerprint)
336
+ return True
337
+
338
+ @app.post("/api/check-credit-tts")
339
+ async def check_credit_tts(request: Request):
340
+ try:
341
+ data = await request.json()
342
+ fingerprint = data.get('fingerprint')
343
+ subscription_status = data.get('subscriptionStatus')
344
+
345
+ if not fingerprint:
346
+ return JSONResponse({"message": "Fingerprint required."}, status_code=400)
347
+
348
+ if subscription_status == 'paid':
349
+ return JSONResponse({"credits_remaining": "unlimited", "limit_reached": False})
350
+
351
+ record = get_user_usage(fingerprint)
352
+ credits = max(0, USAGE_LIMIT_GENERATE - record["count"])
353
+ return JSONResponse({"credits_remaining": credits, "limit_reached": credits <= 0})
354
+ except:
355
+ return JSONResponse({"message": "Server Error"}, status_code=500)
356
+
357
+ @app.post("/api/check-clone-credit")
358
+ async def check_clone_credit(request: Request):
359
+ """بررسی و شارژ خودکار اعتبار شبیه‌سازی صدا بر اساس قفل دوگانه دائمی (IP + Fingerprint)"""
360
+ try:
361
+ data = await request.json()
362
+ fingerprint = data.get('fingerprint')
363
+ subscription_status = data.get('subscriptionStatus')
364
+ client_ip = get_client_ip(request)
365
+
366
+ if not fingerprint:
367
+ return JSONResponse({"message": "Fingerprint required."}, status_code=400)
368
+
369
+ if subscription_status == 'paid':
370
+ return JSONResponse({"credits_remaining": "unlimited", "limit_reached": False})
371
+
372
+ # بررسی قفل دوگانه دیسکی
373
+ limit_reached = check_local_clone_limit(fingerprint, client_ip)
374
+
375
+ today_str = date.today().isoformat()
376
+
377
+ # محاسبه اعتبار باقیمانده بر اساس بدترین سناریو دیسک رانفلر
378
+ rec = get_local_clone_usage(fingerprint)
379
+ rem_fp = max(0, CLONE_USAGE_LIMIT - rec["count"])
380
+
381
+ rec = get_local_clone_usage(client_ip)
382
+ rem_ip = max(0, CLONE_USAGE_LIMIT - rec["count"])
383
+
384
+ credits_remaining = min(rem_fp, rem_ip)
385
+
386
+ return JSONResponse({
387
+ "credits_remaining": credits_remaining,
388
+ "limit_reached": limit_reached or (credits_remaining <= 0)
389
+ })
390
+ except Exception as e:
391
+ return JSONResponse({"message": f"Server Error: {str(e)}"}, status_code=500)
392
+
393
+ @app.post("/api/use-clone-credit")
394
+ async def use_clone_credit(request: Request):
395
+ """ثبت و کسر یک واحد اعتبار شبیه‌سازی صدا در سیستم قفل دوگانه دیسک رانفلر"""
396
+ try:
397
+ data = await request.json()
398
+ fingerprint = data.get('fingerprint')
399
+ subscription_status = data.get('subscriptionStatus')
400
+ client_ip = get_client_ip(request)
401
+
402
+ if not fingerprint or subscription_status == 'paid':
403
+ return JSONResponse({"status": "success"})
404
+
405
+ use_local_clone(fingerprint, client_ip)
406
+ return JSONResponse({"status": "success"})
407
+ except Exception as e:
408
+ return JSONResponse({"message": f"Server Error: {str(e)}"}, status_code=500)
409
+
410
+ def get_hf_headers(request: Request):
411
+ headers = {
412
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
413
+ "Accept": "application/json"
414
+ }
415
+ client_ip = request.headers.get("X-Forwarded-For")
416
+ if client_ip:
417
+ headers["X-Forwarded-For"] = client_ip
418
+ elif request.client and request.client.host:
419
+ headers["X-Forwarded-For"] = request.client.host
420
+ return headers
421
+
422
+ HF_VIDEO_SPACE_URL = "https://sada8888-tts3.hf.space"
423
+
424
+ @app.post("/api/generate")
425
+ async def submit_job(request: Request, background_tasks: BackgroundTasks):
426
+ payload = await request.json()
427
+ headers = get_hf_headers(request)
428
+
429
+ # --- تشخیص هوشمند درخواست تولید عکس (Flux Pro و غیره) ---
430
+ if "action_name" in payload and "prompt" in payload:
431
+ try:
432
+ resp = await asyncio.to_thread(requests.post, f"{HF_VIDEO_SPACE_URL}/api/generate", json=payload, headers=headers, timeout=120)
433
+ return JSONResponse(resp.json(), status_code=resp.status_code)
434
+ except Exception as e:
435
+ return JSONResponse({"status": "error", "message": f"خطای سرور تصویر: {str(e)}"}, status_code=500)
436
+
437
+ # --- تشخیص هوشمند درخواست‌های پادکست (متصل به اسپیس جدید) ---
438
+ if "fingerprint" not in payload and "subscriptionStatus" not in payload:
439
+ try:
440
+ # ارسال امن به اسپیس جدید پادکست
441
+ resp = await asyncio.to_thread(
442
+ requests.post,
443
+ "https://sada8888-tts2.hf.space/api/generate",
444
+ json=payload,
445
+ headers=headers,
446
+ timeout=180
447
+ )
448
+ # اگر خطای سرور ابری دریافت کردیم با فرمت درست به کلاینت ارور می‌دهیم
449
+ if resp.status_code != 200:
450
+ return JSONResponse({"error": f"خطا از سرور تولید پادکست: {resp.text}"}, status_code=resp.status_code)
451
+
452
+ # بازگرداندن فایل صوتی تولید شده به کلاینت
453
+ return StreamingResponse(
454
+ iter([resp.content]),
455
+ media_type="audio/mpeg"
456
+ )
457
+ except Exception as e:
458
+ return JSONResponse({"error": f"قطعی ارتباط با سرور ابری: {str(e)}"}, status_code=500)
459
+
460
+ # --- درخواست‌های عادی TTS از صفحه اصلی (متصل به همان اسپیس پادکست جدید) ---
461
+ if not check_limit_tts(payload): return JSONResponse({"message": "سقف تکمیل شده"}, status_code=429)
462
+ try:
463
+ job_id = f"job_{uuid.uuid4().hex}"
464
+ save_job_state(job_id, "processing")
465
+ background_tasks.add_task(run_tts_background, job_id, payload, headers)
466
+ return JSONResponse({"job_id": job_id}, status_code=200)
467
+ except Exception as e:
468
+ return JSONResponse({"message": f"خطا در شروع فرآیند: {str(e)}"}, status_code=500)
469
+
470
+ @app.post("/api/check_status")
471
+ async def check_status(request: Request):
472
+ try:
473
+ payload = await request.json()
474
+ job_id = payload.get("job_id")
475
+
476
+ # بررسی وضعیت وظیفه از دیسک مشترک (سازگار با چند ورکر همزمان)
477
+ job_state = get_job_state(job_id) if job_id else None
478
+
479
+ if job_state:
480
+ if job_state["status"] == "completed":
481
+ return JSONResponse({
482
+ "status": "completed",
483
+ "proxy_url": f"/audio/standard_{job_id}.mp3"
484
+ })
485
+ elif job_state["status"] == "error":
486
+ return JSONResponse({
487
+ "status": "error",
488
+ "result": job_state.get("error_message", "خطا در تولید صدا")
489
+ })
490
+ else:
491
+ return JSONResponse({"status": "processing"})
492
+
493
+ # در غیر این صورت (سایر کارهای مربوط به فضاهای دیگر)
494
+ resp = await asyncio.to_thread(requests.post, f"{HF_SPACE_BASE_URL_TTS}/api/check_status", json=payload, timeout=30)
495
+ return JSONResponse(resp.json(), status_code=resp.status_code)
496
+ except: return JSONResponse({"status": "error"}, status_code=500)
497
+
498
+ @app.post("/api/generate_podcast")
499
+ async def generate_podcast_proxy(request: Request):
500
+ payload = await request.json()
501
+ if not check_limit_tts(payload): return JSONResponse({"message": "سقف تکمیل شده"}, status_code=429)
502
+ try:
503
+ resp = await asyncio.to_thread(requests.post, f"{HF_SPACE_BASE_URL_TTS}/api/generate_podcast", json=payload, timeout=30)
504
+ return JSONResponse(resp.json(), status_code=resp.status_code)
505
+ except: return JSONResponse({"message": "خطا"}, status_code=500)
506
+
507
+ @app.post("/api/podcast_status")
508
+ async def podcast_status_proxy(request: Request):
509
+ try:
510
+ payload = await request.json()
511
+ resp = await asyncio.to_thread(requests.post, f"{HF_SPACE_BASE_URL_TTS}/api/podcast_status", json=payload, timeout=30)
512
+ return JSONResponse(resp.json(), status_code=resp.status_code)
513
+ except: return JSONResponse({"status": "error"}, status_code=500)
514
+
515
+
516
+ # =================================================================
517
+ # 4. سرویس استودیوی پادکست و مسیریابی هوشمند دیتابیس‌ها
518
+ # =================================================================
519
+ # لینک اسپیس جدید پادکست
520
+ HF_PODCAST_SPACE_URL = "https://sada8888-tts2.hf.space"
521
+
522
+ @app.get("/podcast", response_class=HTMLResponse)
523
+ @app.get("/podcast/", response_class=HTMLResponse)
524
+ async def serve_podcast():
525
+ if os.path.exists("podcast.html"):
526
+ return FileResponse("podcast.html")
527
+ return HTMLResponse("<h1>خطا: فایل podcast.html پیدا نشد!</h1>", status_code=404)
528
+
529
+ @app.post("/api/check-credit")
530
+ async def smart_check_credit(request: Request):
531
+ try:
532
+ payload = await request.json()
533
+ headers = get_hf_headers(request)
534
+ referer = request.headers.get("referer", "").lower()
535
+
536
+ if "podcast" in referer:
537
+ url = f"{HF_PODCAST_SPACE_URL}/api/check-credit"
538
+ else:
539
+ url = f"{HF_VIDEO_SPACE_URL}/api/check-credit"
540
+
541
+ resp = await asyncio.to_thread(requests.post, url, json=payload, headers=headers, timeout=30)
542
+ return JSONResponse(resp.json(), status_code=resp.status_code)
543
+ except Exception as e:
544
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط با سرور: {str(e)}"}, status_code=500)
545
+
546
+ @app.post("/api/use-credit")
547
+ async def smart_use_credit(request: Request):
548
+ try:
549
+ payload = await request.json()
550
+ headers = get_hf_headers(request)
551
+ referer = request.headers.get("referer", "").lower()
552
+
553
+ if "podcast" in referer:
554
+ url = f"{HF_PODCAST_SPACE_URL}/api/use-credit"
555
+ else:
556
+ url = f"{HF_VIDEO_SPACE_URL}/api/use-credit"
557
+
558
+ resp = await asyncio.to_thread(requests.post, url, json=payload, headers=headers, timeout=30)
559
+ return JSONResponse(resp.json(), status_code=resp.status_code)
560
+ except: return JSONResponse({"status": "error"}, status_code=500)
561
+
562
+ @app.post("/api/create-full-podcast")
563
+ async def podgen_create_full(request: Request):
564
+ try:
565
+ payload = await request.json()
566
+ headers = get_hf_headers(request)
567
+ resp = await asyncio.to_thread(requests.post, f"{HF_PODCAST_SPACE_URL}/api/create-full-podcast", json=payload, headers=headers, timeout=60)
568
+ return JSONResponse(resp.json(), status_code=resp.status_code)
569
+ except: return JSONResponse({"error": "ارتباط با سرور قطع شد"}, status_code=500)
570
+
571
+ @app.get("/api/podcast-status/{task_id}")
572
+ async def podgen_status(request: Request, task_id: str):
573
+ try:
574
+ headers = get_hf_headers(request)
575
+ resp = await asyncio.to_thread(requests.get, f"{HF_PODCAST_SPACE_URL}/api/podcast-status/{task_id}", headers=headers, timeout=30)
576
+ return JSONResponse(resp.json(), status_code=resp.status_code)
577
+ except: return JSONResponse({"status": "error"}, status_code=500)
578
+
579
+ @app.post("/api/auto-podcast")
580
+ async def podgen_auto(request: Request):
581
+ try:
582
+ payload = await request.json()
583
+ headers = get_hf_headers(request)
584
+ resp = await asyncio.to_thread(requests.post, f"{HF_PODCAST_SPACE_URL}/api/auto-podcast", json=payload, headers=headers, timeout=60)
585
+ return JSONResponse(resp.json(), status_code=resp.status_code)
586
+ except: return JSONResponse({"error": "ارتباط قطع شد"}, status_code=500)
587
+
588
+ @app.get("/api/auto-podcast-status/{task_id}")
589
+ async def podgen_auto_status(request: Request, task_id: str):
590
+ try:
591
+ headers = get_hf_headers(request)
592
+ resp = await asyncio.to_thread(requests.get, f"{HF_PODCAST_SPACE_URL}/api/auto-podcast-status/{task_id}", headers=headers, timeout=30)
593
+ return JSONResponse(resp.json(), status_code=resp.status_code)
594
+ except: return JSONResponse({"status": "error"}, status_code=500)
595
+
596
+ @app.get("/api/download-podcast/{filename}")
597
+ async def podgen_download(request: Request, filename: str):
598
+ async def iterfile():
599
+ try:
600
+ headers = get_hf_headers(request)
601
+ async with httpx.AsyncClient(timeout=120.0) as client:
602
+ async with client.stream("GET", f"{HF_PODCAST_SPACE_URL}/api/download-podcast/{filename}", headers=headers) as r:
603
+ r.raise_for_status()
604
+ async for chunk in r.aiter_bytes(chunk_size=65536):
605
+ if chunk: yield chunk
606
+ except Exception:
607
+ yield b""
608
+ return StreamingResponse(iterfile(), media_type="audio/mpeg")
609
+
610
+
611
+ # =================================================================
612
+ # 5. سرویس‌های ساخت ویدیو، تصویر و ویرایش تصویر (اتصال مستقیم خام)
613
+ # =================================================================
614
+ @app.get("/video", response_class=HTMLResponse)
615
+ @app.get("/video/", response_class=HTMLResponse)
616
+ async def serve_video_studio():
617
+ if os.path.exists("video.html"):
618
+ return FileResponse("video.html")
619
+ return HTMLResponse("<h1>خطا: فایل video.html پیدا نشد!</h1>", status_code=404)
620
+
621
+ @app.get("/flux", response_class=HTMLResponse)
622
+ @app.get("/flux/", response_class=HTMLResponse)
623
+ async def serve_flux_studio():
624
+ if os.path.exists("flux.html"):
625
+ return FileResponse("flux.html")
626
+ return HTMLResponse("<h1>خطا: فایل flux.html پیدا نشد!</h1>", status_code=404)
627
+
628
+ @app.get("/image", response_class=HTMLResponse)
629
+ @app.get("/image/", response_class=HTMLResponse)
630
+ async def serve_image_studio():
631
+ if os.path.exists("image.html"):
632
+ return FileResponse("image.html")
633
+ elif os.path.exists("flux.html"):
634
+ return FileResponse("flux.html")
635
+ return HTMLResponse("<h1>خطا: فایل image.html پیدا نشد!</h1>", status_code=404)
636
+
637
+ @app.get("/edit", response_class=HTMLResponse)
638
+ @app.get("/edit/", response_class=HTMLResponse)
639
+ async def serve_edit_studio():
640
+ if os.path.exists("edit.html"):
641
+ return FileResponse("edit.html")
642
+ return HTMLResponse("<h1>خطا: فایل edit.html پیدا نشد!</h1>", status_code=404)
643
+
644
+ # --- مسیرهای اعتبار اختصاصی تصاویر ---
645
+ @app.post("/api/check-image-credit")
646
+ async def proxy_check_image_credit(request: Request):
647
+ try:
648
+ body = await request.body()
649
+ headers = get_hf_headers(request)
650
+ if "content-type" in request.headers:
651
+ headers["Content-Type"] = request.headers["content-type"]
652
+ resp = await asyncio.to_thread(requests.post, f"{HF_VIDEO_SPACE_URL}/api/check-image-credit", data=body, headers=headers, timeout=30)
653
+ return JSONResponse(resp.json(), status_code=resp.status_code)
654
+ except Exception as e:
655
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
656
+
657
+ @app.post("/api/use-image-credit")
658
+ async def proxy_use_image_credit(request: Request):
659
+ try:
660
+ body = await request.body()
661
+ headers = get_hf_headers(request)
662
+ if "content-type" in request.headers:
663
+ headers["Content-Type"] = request.headers["content-type"]
664
+ resp = await asyncio.to_thread(requests.post, f"{HF_VIDEO_SPACE_URL}/api/use-image-credit", data=body, headers=headers, timeout=30)
665
+ return JSONResponse(resp.json(), status_code=resp.status_code)
666
+ except Exception as e:
667
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
668
+
669
+ # --- مسیرهای اعتبار اختصاصی ویرایش تصویر (فتوشاپ هوش مصنوعی) با قفل دوگانه دائمی رانفلر ---
670
+ @app.post("/api/check-edit-credit")
671
+ async def proxy_check_edit_credit(request: Request):
672
+ try:
673
+ data = await request.json()
674
+ fingerprint = data.get('fingerprint')
675
+ subscription_status = data.get('subscriptionStatus')
676
+ client_ip = get_client_ip(request)
677
+
678
+ if subscription_status == 'paid':
679
+ return JSONResponse({"credits_remaining": "unlimited", "limit_reached": False})
680
+
681
+ # بررسی دائمی قفل دوگانه (آی‌پی + اثر انگشت) از دیسک رانفلر
682
+ limit_reached = check_local_edit_limit(fingerprint, client_ip)
683
+
684
+ current_week = date.today().isocalendar()[1]
685
+
686
+ # بررسی بدترین سناریوی سهمیه هفتگی دیسکی
687
+ rem_fp = 0
688
+ if fingerprint:
689
+ rec = get_local_edit_usage(fingerprint)
690
+ if rec["week"] == current_week:
691
+ rem_fp = max(0, EDIT_USAGE_LIMIT - rec["count"])
692
+ else:
693
+ rem_fp = EDIT_USAGE_LIMIT
694
+ else:
695
+ rem_fp = EDIT_USAGE_LIMIT
696
+
697
+ rec = get_local_edit_usage(client_ip)
698
+ if rec["week"] == current_week:
699
+ rem_ip = max(0, EDIT_USAGE_LIMIT - rec["count"])
700
+ else:
701
+ rem_ip = EDIT_USAGE_LIMIT
702
+
703
+ credits_remaining = min(rem_fp, rem_ip)
704
+
705
+ return JSONResponse({
706
+ "credits_remaining": credits_remaining,
707
+ "limit_reached": limit_reached or (credits_remaining <= 0)
708
+ })
709
+ except Exception as e:
710
+ return JSONResponse({"status": "error", "message": f"error: {str(e)}"}, status_code=500)
711
+
712
+ @app.post("/api/use-edit-credit")
713
+ async def proxy_use_edit_credit(request: Request):
714
+ try:
715
+ data = await request.json()
716
+ fingerprint = data.get('fingerprint')
717
+ subscription_status = data.get('subscriptionStatus')
718
+ client_ip = get_client_ip(request)
719
+
720
+ if subscription_status != 'paid':
721
+ use_local_edit(fingerprint, client_ip)
722
+ return JSONResponse({"status": "success"})
723
+ except Exception as e:
724
+ return JSONResponse({"status": "error", "message": f"error: {str(e)}"}, status_code=500)
725
+
726
+ # --- پروکسی درخواست‌های تولید و ویرایش ---
727
+ @app.post("/api/generate-video")
728
+ async def proxy_generate_video(request: Request):
729
+ try:
730
+ body = await request.body()
731
+ headers = get_hf_headers(request)
732
+
733
+ if "content-type" in request.headers:
734
+ headers["Content-Type"] = request.headers["content-type"]
735
+
736
+ resp = await asyncio.to_thread(
737
+ requests.post,
738
+ f"{HF_VIDEO_SPACE_URL}/api/generate-video",
739
+ data=body,
740
+ headers=headers,
741
+ timeout=300
742
+ )
743
+ try:
744
+ return JSONResponse(resp.json(), status_code=resp.status_code)
745
+ except:
746
+ return JSONResponse({"status": "error", "message": "خطا در سرور ابری", "details": resp.text}, status_code=502)
747
+ except Exception as e:
748
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
749
+
750
+ @app.post("/api/merge-videos")
751
+ async def proxy_merge_videos(request: Request):
752
+ try:
753
+ body = await request.body()
754
+ headers = get_hf_headers(request)
755
+ if "content-type" in request.headers:
756
+ headers["Content-Type"] = request.headers["content-type"]
757
+
758
+ resp = await asyncio.to_thread(
759
+ requests.post,
760
+ f"{HF_VIDEO_SPACE_URL}/api/merge-videos",
761
+ data=body,
762
+ headers=headers,
763
+ timeout=400
764
+ )
765
+
766
+ if resp.status_code == 200:
767
+ return StreamingResponse(
768
+ iter([resp.content]),
769
+ media_type=resp.headers.get("Content-Type", "video/mp4"),
770
+ headers={"Content-Disposition": resp.headers.get("Content-Disposition", "attachment; filename=merged_video.mp4")}
771
+ )
772
+ else:
773
+ try:
774
+ err_json = resp.json()
775
+ return JSONResponse({"status": "error", "message": err_json.get("error", "خطا در پردازش ویدیو")}, status_code=resp.status_code)
776
+ except:
777
+ return JSONResponse({"status": "error", "message": "خطا در پردازش و میکس ویدیو"}, status_code=resp.status_code)
778
+ except Exception as e:
779
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
780
+
781
+ @app.get("/api/status/{run_id}")
782
+ async def proxy_video_status(request: Request, run_id: str):
783
+ try:
784
+ headers = get_hf_headers(request)
785
+ resp = await asyncio.to_thread(requests.get, f"{HF_VIDEO_SPACE_URL}/api/status/{run_id}", headers=headers, timeout=30)
786
+ return JSONResponse(resp.json(), status_code=resp.status_code)
787
+ except Exception as e:
788
+ return JSONResponse({"status": "processing"}, status_code=500)
789
+
790
+ @app.get("/static/images/{filename}")
791
+ async def proxy_static_images(request: Request, filename: str):
792
+ headers = get_hf_headers(request)
793
+ url = f"{HF_VIDEO_SPACE_URL}/static/images/{filename}"
794
+
795
+ async def async_iterfile():
796
+ try:
797
+ async with httpx.AsyncClient(timeout=120.0) as client:
798
+ async with client.stream("GET", url, headers=headers) as response:
799
+ response.raise_for_status()
800
+ async for chunk in response.aiter_bytes(chunk_size=65536):
801
+ if chunk:
802
+ yield chunk
803
+ except Exception:
804
+ yield b""
805
+
806
+ media_type = "application/octet-stream"
807
+ if filename.endswith(".mp4"): media_type = "video/mp4"
808
+ elif filename.endswith(".png"): media_type = "image/png"
809
+ elif filename.endswith(".webp"): media_type = "image/webp"
810
+ elif filename.endswith(".jpg") or filename.endswith(".jpeg"): media_type = "image/jpeg"
811
+ elif filename.endswith(".txt"): media_type = "text/plain"
812
+
813
+ response_headers = {
814
+ "Cache-Control": "public, max-age=86400"
815
+ }
816
+
817
+ return StreamingResponse(async_iterfile(), media_type=media_type, headers=response_headers)
818
+
819
+ @app.post("/api/edit")
820
+ async def proxy_edit_image(request: Request):
821
+ try:
822
+ body = await request.body()
823
+ headers = get_hf_headers(request)
824
+ if "content-type" in request.headers:
825
+ headers["Content-Type"] = request.headers["content-type"]
826
+
827
+ resp = await asyncio.to_thread(requests.post, f"{HF_VIDEO_SPACE_URL}/api/edit", data=body, headers=headers, timeout=120)
828
+ try:
829
+ return JSONResponse(resp.json(), status_code=resp.status_code)
830
+ except:
831
+ return JSONResponse({"status": "error", "message": "خطا در سرور ابری", "details": resp.text}, status_code=502)
832
+ except Exception as e:
833
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
834
+
835
+ @app.post("/api/chat")
836
+ async def proxy_gemma_chat(request: Request):
837
+ try:
838
+ body = await request.body()
839
+ headers = get_hf_headers(request)
840
+ if "content-type" in request.headers:
841
+ headers["Content-Type"] = request.headers["content-type"]
842
+
843
+ resp = await asyncio.to_thread(requests.post, f"{HF_VIDEO_SPACE_URL}/api/chat", data=body, headers=headers, timeout=120)
844
+ try:
845
+ return JSONResponse(resp.json(), status_code=resp.status_code)
846
+ except:
847
+ return JSONResponse({"status": "error", "message": "خطا در سرور ابری", "details": resp.text}, status_code=502)
848
+ except Exception as e:
849
+ return JSONResponse({"status": "error", "message": f"خطا در ارتباط رانفلر: {str(e)}"}, status_code=500)
850
+
851
+ # alphattspro-main/app.py -> انتهای فایل
852
+
853
+ # =================================================================
854
+ # 6. مسیر استاتیک (باید همیشه در انتهای کد باشد)
855
+ # =================================================================
856
+ @app.get("/{path:path}")
857
+ async def serve_static(path: str):
858
+ # کلمات "soti" و "sotifree" به استثناها اضافه شدند تا رانفلر آن‌ها را فایل فرض نکند
859
+ if os.path.exists(path) and path not in ["chat", "tts", "ws", "podcast", "video", "flux", "image", "edit", "ttspro", "soti", "sotifree"]:
860
+ return FileResponse(path)
861
+ return HTMLResponse("Not Found", status_code=404)