Opera8 commited on
Commit
c188f9b
·
verified ·
1 Parent(s): 484a96a

Create app.py

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