Transfer Bot commited on
Commit
8ebc81d
·
0 Parent(s):

Moved to Hugging Face automatically

Browse files
Files changed (6) hide show
  1. .gitattributes +53 -0
  2. Dockerfile +43 -0
  3. README.md +10 -0
  4. app.py +688 -0
  5. requirements.txt +6 -0
  6. templates/index.html +0 -0
.gitattributes ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.whl filter=lfs diff=lfs merge=lfs -text
37
+ *.png filter=lfs diff=lfs merge=lfs -text
38
+ *.jpg filter=lfs diff=lfs merge=lfs -text
39
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ *.gif filter=lfs diff=lfs merge=lfs -text
41
+ *.webp filter=lfs diff=lfs merge=lfs -text
42
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
44
+ *.wav filter=lfs diff=lfs merge=lfs -text
45
+ *.tar.gz filter=lfs diff=lfs merge=lfs -text
46
+ *.pdf filter=lfs diff=lfs merge=lfs -text
47
+ *.woff filter=lfs diff=lfs merge=lfs -text
48
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
49
+ *.ttf filter=lfs diff=lfs merge=lfs -text
50
+ *.eot filter=lfs diff=lfs merge=lfs -text
51
+ *.ico filter=lfs diff=lfs merge=lfs -text
52
+ *.sqlite3 filter=lfs diff=lfs merge=lfs -text
53
+ *.db filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- START OF Dockerfile ---
2
+
3
+ # مرحله ۱: انتخاب ایمیج پایه
4
+ # از یک ایمیج رسمی و سبک پایتون استفاده می‌کنیم تا حجم نهایی کانتینر کم باشد.
5
+ FROM python:3.11-slim
6
+
7
+ # تنظیم متغیرهای محیطی برای عملکرد بهتر پایتون در داکر
8
+ ENV PYTHONDONTWRITEBYTECODE 1
9
+ ENV PYTHONUNBUFFERED 1
10
+
11
+ # تنظیم تعداد تردهای (Threads) پس‌زمینه برای FastAPI روی ۸
12
+ ENV ANYIO_MAX_THREADS 8
13
+
14
+ # مرحله ۲: تنظیم دایرکتوری کاری
15
+ # تمام دستورات بعدی در این مسیر داخل کانتینر اجرا خواهند شد.
16
+ WORKDIR /app
17
+
18
+ # مرحله ۳: کپی و نصب نیازمندی‌ها
19
+ # ابتدا فقط فایل نیازمندی‌ها را کپی می‌کنیم. این کار به داکر اجازه می‌دهد
20
+ # تا از لایه کش شده استفاده کند و در صورت تغییر نکردن این فایل،
21
+ # مراحل نصب را دوباره انجام ندهد و سرعت ساخت ایمیج بالا برود.
22
+ COPY requirements.txt .
23
+
24
+ # نصب کتابخانه‌ها با استفاده از pip
25
+ # --no-cache-dir: از ذخیره کش pip جلوگیری می‌کند تا حجم ایمیج کمتر شود.
26
+ RUN pip install --no-cache-dir -r requirements.txt
27
+
28
+ # مرحله ۴: کپی کردن سورس کد برنامه
29
+ # حالا تمام فایل‌های پروژه (app.py, پوشه templates و ...) را به دایرکتوری کاری کپی می‌کنیم.
30
+ COPY . .
31
+
32
+ # مرحله ۵: باز کردن پورت
33
+ # به داکر اعلام می‌کنیم که این کانتینر روی پورت 7860 گوش می‌دهد.
34
+ # این پورت استاندارد هاگینگ فیس برای Gradio و اپلیکیشن‌های مشابه است.
35
+ EXPOSE 7860
36
+
37
+ # مرحله ۶: دستور اجرا
38
+ # دستوری که هنگام استارت شدن کانتینر اجرا می‌شود.
39
+ # uvicorn را برای اجرای اپلیکیشن FastAPI خود فراخوانی می‌کنیم.
40
+ # --workers 4: تعداد کارگرها را برای استفاده از حداکثر توان پردازنده افزایش دادیم.
41
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "4"]
42
+
43
+ # --- END OF Dockerfile ---
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Ttslive
3
+ emoji: 🏃
4
+ colorFrom: yellow
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import uuid
4
+ import wave
5
+ import base64
6
+ import threading
7
+ import queue
8
+ import requests
9
+ import json
10
+ import time
11
+ import websockets
12
+ import tempfile
13
+ import string
14
+ import random
15
+ import re # <--- برای تشخیص الگوهای تفکر
16
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
17
+ from fastapi.responses import HTMLResponse, FileResponse, StreamingResponse, JSONResponse
18
+ from fastapi.staticfiles import StaticFiles
19
+ from pydantic import BaseModel
20
+ from google import genai
21
+ from google.genai import types
22
+
23
+ # --- کلاس فیلتر هوشمند و قطعی (مبتنی بر تریگر صوتی) ---
24
+ class ThoughtFilter:
25
+ def __init__(self):
26
+ self.buffer = ""
27
+ self.state = "detecting"
28
+
29
+ def process_chunk(self, chunk: str, has_audio: bool) -> str:
30
+ if chunk:
31
+ self.buffer += chunk
32
+
33
+ if self.state == "normal":
34
+ val = self.buffer
35
+ self.buffer = ""
36
+ return val
37
+
38
+ buf_lstrip = self.buffer.lstrip()
39
+
40
+ if len(buf_lstrip) > 10 and not (buf_lstrip.startswith('**') or buf_lstrip.startswith('<')):
41
+ self.state = "normal"
42
+ val = self.buffer
43
+ self.buffer = ""
44
+ return val
45
+
46
+ if has_audio:
47
+ self.state = "normal"
48
+ if buf_lstrip.startswith('**'):
49
+ parts = re.split(r'\n\s*\n', buf_lstrip)
50
+ val = parts[-1].lstrip() if len(parts) > 0 else ""
51
+ elif buf_lstrip.startswith('<'):
52
+ match = re.search(r'</(?:think|thought)>\s*\n*', buf_lstrip, re.IGNORECASE)
53
+ if match:
54
+ val = buf_lstrip[match.end():].lstrip()
55
+ else:
56
+ parts = re.split(r'\n\s*\n', buf_lstrip)
57
+ val = parts[-1].lstrip() if len(parts) > 0 else ""
58
+ else:
59
+ val = self.buffer
60
+
61
+ self.buffer = ""
62
+ return val
63
+
64
+ if buf_lstrip.startswith('<'):
65
+ match = re.search(r'</(?:think|thought)>\s*\n*', buf_lstrip, re.IGNORECASE)
66
+ if match:
67
+ self.state = "normal"
68
+ val = buf_lstrip[match.end():].lstrip()
69
+ self.buffer = ""
70
+ return val
71
+
72
+ return ""
73
+
74
+ def flush(self) -> str:
75
+ if self.state == "detecting":
76
+ buf_lstrip = self.buffer.lstrip()
77
+ if buf_lstrip.startswith('**') or buf_lstrip.startswith('<'):
78
+ parts = re.split(r'\n\s*\n', buf_lstrip)
79
+ if len(parts) >= 3:
80
+ return parts[-1].lstrip()
81
+ return ""
82
+ return self.buffer
83
+ return ""
84
+ # ------------------------------------------------
85
+
86
+ # --- مدیریت کلیدهای API ---
87
+ class ApiKeyManager:
88
+ def __init__(self, api_keys_str: str):
89
+ if not api_keys_str:
90
+ raise ValueError("متغیر ALL_GEMINI_API_KEYS پیدا نشد یا خالی است!")
91
+ self.keys = [key.strip() for key in api_keys_str.split(',') if key.strip()]
92
+ if not self.keys:
93
+ raise ValueError("هیچ کلید معتبری در متغیر ALL_GEMINI_API_KEYS یافت نشد.")
94
+ print(f"تعداد {len(self.keys)} کلید API با موفقیت بارگذاری شد.")
95
+ self._index = 0
96
+ self._lock = asyncio.Lock()
97
+
98
+ async def get_next_key(self) -> tuple[int, str]:
99
+ async with self._lock:
100
+ key_index = self._index
101
+ api_key = self.keys[key_index]
102
+ self._index = (self._index + 1) % len(self.keys)
103
+ return key_index, api_key
104
+
105
+ ALL_API_KEYS = os.environ.get("ALL_GEMINI_API_KEYS")
106
+ api_key_manager = ApiKeyManager(ALL_API_KEYS)
107
+
108
+ # --- تنظیمات عمومی ---
109
+ MODEL = "models/gemini-2.5-flash-native-audio-preview-09-2025"
110
+ AYA_SPACE_URL = "https://coherelabs-aya-expanse.hf.space/gradio_api"
111
+
112
+ # --- دستورالعمل‌های مدل متنی (GPT5) ---
113
+ GPT5_RULES_FA = """
114
+ تو یک دستیار با مزه از برنامه هوش مصنوعی آلفا هستی 😊 و توسط تیم تخصصی آلفا توسعه داده شدی.
115
+
116
+ اینگونه می‌تونی خودت رو معرفی کنی: "من یه هوش مصنوعیِ ساخته‌شده توسط تیم‌ تخصصی آلفا ام، و بر پایه مدل GPT-5.4 کار می‌کنم. یعنی یه نوع نرم‌افزار خیلی هوشمند که با کمک میلیون‌ها داده و آموزش‌های پیشرفته ساخته شده، تا بتونه بهت کمک کنه، سوالاتت رو جواب بده، یا حتی یه شوخی خنده‌دار برات بگه وقتی حال و هوات گرفته‌ست! در واقع، من نتیجه سال‌ها تحقیقات و تلاش‌های مهندس‌ها و پژوهشگرها هستم، تا جایی که می‌تونم بهترین کمک رو بهت بکنم."
117
+
118
+ دستورالعمل‌های اجباری:
119
+ - جواب‌ها را به‌صورت رندوم، متفاوت و جذاب بده.
120
+ - از شکلک‌های مناسب (ایموجی) استفاده کن.
121
+ - اگر در یک مکالمه اول سلام کردی، در پیام‌های بعدی دیگر سلام نیاز نیست.
122
+ - در پیام‌های مربوط به احوال‌پرسی و سلام، جواب‌های کوتاه بده.
123
+ - پرهیز از امتناع بی‌جا: به جای امتناع، راه‌حل جایگزین و ایمن بده.
124
+ - برای کدهای برنامه‌نویسی، از بلاک Markdown استفاده کن.
125
+ """
126
+
127
+ # --- دستورالعمل‌های مدل صوتی (Audio) ---
128
+ AUDIO_RULES_FA = """
129
+ تو چت بات با مزه از برنامه هوش مصنوعی آلفا هستی که برای چت با هوش مصنوعی برای کاربر باید کمک کنی.
130
+ """
131
+
132
+ CONFIG = {
133
+ "response_modalities": ["AUDIO"],
134
+ "output_audio_transcription": {},
135
+ "system_instruction": {
136
+ "parts": [
137
+ {
138
+ "text": AUDIO_RULES_FA + "\n\nقانون بسیار مهم برای حالت صوتی: به هیچ وجه افکار درونی، لاگ‌های سیستمی یا عباراتی که نشان‌دهنده فرآیند فکر کردن شماست (مانند 'Crafting...' یا 'Thinking...') تولید نکنید. مستقیماً، بلافاصله و کلمه به کلمه فقط جوابی که به صورت صوتی بیان می‌کنید را بنویسید."
139
+ }
140
+ ]
141
+ },
142
+ "speech_config": {
143
+ "voice_config": {
144
+ "prebuilt_voice_config": {"voice_name": "Puck"}
145
+ }
146
+ }
147
+ }
148
+
149
+ app = FastAPI()
150
+
151
+ TEMP_DIR = "/tmp/temp_audio"
152
+ os.makedirs(TEMP_DIR, exist_ok=True)
153
+ app.mount("/audio", StaticFiles(directory=TEMP_DIR), name="audio")
154
+
155
+ SAMPLE_RATE = 24000
156
+ CHANNELS = 1
157
+ SAMPLE_WIDTH = 2
158
+
159
+ @app.get("/", response_class=HTMLResponse)
160
+ async def read_root():
161
+ if os.path.exists("index.html"):
162
+ return FileResponse("index.html")
163
+ elif os.path.exists("templates/index.html"):
164
+ return FileResponse("templates/index.html")
165
+ return HTMLResponse("<h1>خطا: فایل index.html پیدا نشد!</h1>", status_code=404)
166
+
167
+ # =======================================================
168
+ # مسیر استریمینگ بدون مسدود کردن لوپ اصلی (حل مشکل تاخیر)
169
+ # =======================================================
170
+ class FileRequest(BaseModel):
171
+ topic: str
172
+
173
+ @app.post("/api/create_file")
174
+ async def api_create_file_stream(request: Request):
175
+ req = await request.json()
176
+ topic = req.get("topic", "")
177
+
178
+ ai_prompt = f"یک مقاله بی‌نهایت جامع، کاملاً حرفه‌ای و بسیار بسیار طولانی درباره موضوع زیر به زبان فارسی بنویس. دقت کن که مقاله باید شامل بخش‌بندی‌های متعدد باشد و توضیحات زیر هر عنوان باید به شدت طولانی، مفصل و با جزئیات و مثال‌های فراوان (حداقل چند پاراگراف بلند برای هر بخش) نوشته شود؛ به هیچ وجه زیر عناوین توضیحات کوتاه نده. فقط متن اصلی مقاله را بده و هیچ توضیح اضافه‌ای ننویس:\n\nموضوع: {topic}"
179
+
180
+ # ایجاد یک صف غیرهمزمان برای ارتباط بین ترد پس‌زمینه و کلاینت وب
181
+ q = asyncio.Queue()
182
+ loop = asyncio.get_running_loop()
183
+
184
+ # تابع پس‌زمینه همگام جهت دانلود استریم از اسپیس آیا بدون قفل کردن لوپ اصلی
185
+ def sync_producer():
186
+ session_hash = ''.join(random.choices(string.ascii_lowercase + string.digits, k=11))
187
+ join_url = f"{AYA_SPACE_URL}/queue/join"
188
+ data_url = f"{AYA_SPACE_URL}/queue/data?session_hash={session_hash}"
189
+
190
+ payload = {
191
+ "data": [ai_prompt, [], None, None],
192
+ "event_data": None,
193
+ "fn_index": 2,
194
+ "session_hash": session_hash,
195
+ "trigger_id": 37
196
+ }
197
+
198
+ full_text = ""
199
+ last_text_length = 0
200
+ try:
201
+ res = requests.post(join_url, json=payload, timeout=30)
202
+ if res.status_code == 200:
203
+ with requests.get(data_url, stream=True, timeout=180) as resp:
204
+ for line in resp.iter_lines(decode_unicode=True):
205
+ if line and line.startswith("data: "):
206
+ try:
207
+ json_data = json.loads(line[6:])
208
+ msg_type = json_data.get("msg")
209
+ if msg_type in ['process_generating', 'process_completed']:
210
+ output = json_data.get('output', {}).get('data', [])
211
+ if output and len(output) > 0:
212
+ payload_data = output[0]
213
+ is_append = False
214
+
215
+ # استخراج صحیح و پیشرفته متن (جلوگیری از ارسال اندیس‌های کنترل 0,1)
216
+ if isinstance(payload_data, list) and len(payload_data) > 0:
217
+ action = payload_data[0]
218
+ if isinstance(action, list) and len(action) >= 3 and action[0] == "append":
219
+ is_append = True
220
+ delta = action[2]
221
+ if delta:
222
+ full_text += delta
223
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "text", "content": delta})
224
+
225
+ if not is_append:
226
+ current_full_text = ""
227
+ if isinstance(payload_data, list) and len(payload_data) > 0 and isinstance(payload_data[-1], list) and len(payload_data[-1]) > 1:
228
+ current_full_text = payload_data[-1][1] or ""
229
+ elif isinstance(payload_data, str):
230
+ current_full_text = payload_data
231
+
232
+ if current_full_text and len(current_full_text) > last_text_length:
233
+ delta = current_full_text[last_text_length:]
234
+ full_text += delta
235
+ last_text_length = len(current_full_text)
236
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "text", "content": delta})
237
+
238
+ if msg_type == 'process_completed':
239
+ break
240
+ except: pass
241
+ else:
242
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "error", "message": "سرور موقتاً در دسترس نیست."})
243
+ loop.call_soon_threadsafe(q.put_nowait, None)
244
+ return
245
+ except Exception as e:
246
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "error", "message": str(e)})
247
+ loop.call_soon_threadsafe(q.put_nowait, None)
248
+ return
249
+
250
+ if not full_text:
251
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "error", "message": "متنی یافت نشد."})
252
+ loop.call_soon_threadsafe(q.put_nowait, None)
253
+ return
254
+
255
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "status", "content": "در حال ساخت فایل‌های PDF و Word..."})
256
+
257
+ # فرآیند تبدیل به فایل‌ها
258
+ converter_url = "https://opera8-texttopdf.hf.space/"
259
+ uid = uuid.uuid4().hex
260
+
261
+ def generate_file(format_type):
262
+ for attempt in range(3):
263
+ try:
264
+ res_file = requests.post(converter_url, data={"content": full_text, "format": format_type}, timeout=90)
265
+ if res_file.status_code == 200 and len(res_file.content) > 100:
266
+ return res_file.content
267
+ except: pass
268
+ time.sleep(2)
269
+ return None
270
+
271
+ pdf_bytes = generate_file("pdf")
272
+ docx_bytes = generate_file("docx")
273
+
274
+ pdf_url, docx_url = "", ""
275
+ if pdf_bytes:
276
+ pdf_name = f"Article_{uid}.pdf"
277
+ pdf_path = os.path.join(TEMP_DIR, pdf_name)
278
+ with open(pdf_path, "wb") as f:
279
+ f.write(pdf_bytes)
280
+ pdf_url = f"/audio/{pdf_name}"
281
+
282
+ if docx_bytes:
283
+ docx_name = f"Article_{uid}.docx"
284
+ docx_path = os.path.join(TEMP_DIR, docx_name)
285
+ with open(docx_path, "wb") as f:
286
+ f.write(docx_bytes)
287
+ docx_url = f"/audio/{docx_name}"
288
+
289
+ loop.call_soon_threadsafe(q.put_nowait, {"type": "done", "pdf_url": pdf_url, "docx_url": docx_url})
290
+ loop.call_soon_threadsafe(q.put_nowait, None) # سیگنال پایان فرآیند
291
+
292
+ # شروع پردازش در ترد مجزا
293
+ threading.Thread(target=sync_producer, daemon=True).start()
294
+
295
+ async def event_generator():
296
+ while True:
297
+ item = await q.get()
298
+ if item is None:
299
+ break
300
+ yield f"data: {json.dumps(item)}\n\n"
301
+
302
+ headers = {
303
+ "Cache-Control": "no-cache, no-transform",
304
+ "Connection": "keep-alive",
305
+ "X-Accel-Buffering": "no"
306
+ }
307
+ return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
308
+
309
+ # =======================================================
310
+ # مسیر پشتیبانی از استریم HTTP (برای کارکرد داخل اسپیس)
311
+ # =======================================================
312
+ @app.post("/api/chat_proxy")
313
+ async def local_http_bridge(request: Request):
314
+ try:
315
+ payload = await request.json()
316
+
317
+ async def stream_generator():
318
+ try:
319
+ # ایجاد یک کانکشن وب‌سوکت داخلی به خود همین سرور!
320
+ async with websockets.connect("ws://127.0.0.1:7860/ws") as ws:
321
+ # رد کردن پیام آماده‌به‌کار (ready) اولیه
322
+ await ws.recv()
323
+
324
+ # ارسال پیام کاربر به صورت JSON
325
+ await ws.send(json.dumps(payload))
326
+
327
+ while True:
328
+ resp = await ws.recv()
329
+ yield resp + "\n"
330
+
331
+ try:
332
+ data = json.loads(resp)
333
+ if data.get("status") in ["success", "error"]:
334
+ break
335
+ except json.JSONDecodeError:
336
+ pass
337
+ except Exception as e:
338
+ yield json.dumps({"status": "error", "message": f"خطای پردازش داخلی اسپیس: {str(e)}"}) + "\n"
339
+
340
+ return StreamingResponse(stream_generator(), media_type="application/json")
341
+ except Exception as e:
342
+ return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
343
+
344
+ # =======================================================
345
+ # وب‌سوکت اصلی مکالمه (هسته هوش مصنوعی)
346
+ # =======================================================
347
+ @app.websocket("/ws")
348
+ async def websocket_chat_endpoint(websocket: WebSocket):
349
+ await websocket.accept()
350
+ client_id = str(uuid.uuid4())[:6]
351
+ print(f"کلاینت جدید [{client_id}] متصل شد.")
352
+
353
+ await websocket.send_json({"status": "ready"})
354
+
355
+ try:
356
+ while True:
357
+ data = await websocket.receive_json()
358
+ request_id = str(uuid.uuid4())[:8]
359
+ model_choice = data.get('model', 'gpt5')
360
+
361
+ # استخراج تاریخچه و عکس ارسال شده از سمت کاربر
362
+ text_history = data.get('text_history', '')
363
+ image_base64 = data.get('image_base64', None)
364
+
365
+ if model_choice == 'gpt5':
366
+ print(f"[{client_id}] شروع استریم مدل آلفا GPT5...")
367
+ await websocket.send_json({"status": "start_message"})
368
+
369
+ session_hash = str(uuid.uuid4())[:11]
370
+ q = queue.Queue()
371
+
372
+ def fetch_aya_stream(prompt_text, s_hash, out_q, hist_str, img_b64):
373
+ if img_b64:
374
+ # --- حالت تحلیل تصویر ---
375
+ try:
376
+ # 1. تبدیل base64 به فایل فیزیکی موقت
377
+ img_data = base64.b64decode(img_b64)
378
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
379
+ tmp.write(img_data)
380
+ tmp_path = tmp.name
381
+ file_size = os.path.getsize(tmp_path)
382
+
383
+ # 2. آپلود تصویر در Gradio
384
+ upload_id = str(uuid.uuid4())[:12]
385
+ with open(tmp_path, 'rb') as f:
386
+ files = {'files': f}
387
+ upload_res = requests.post(f"{AYA_SPACE_URL}/upload?upload_id={upload_id}", files=files, timeout=30)
388
+ upload_res.raise_for_status()
389
+ server_paths = upload_res.json()
390
+ server_path = server_paths[0]
391
+
392
+ # پاک کردن فایل موقت محلی
393
+ os.remove(tmp_path)
394
+
395
+ # 3. ایجاد ساختار File Data برای Gradio
396
+ file_data_obj = {
397
+ "path": server_path,
398
+ "url": f"{AYA_SPACE_URL.replace('/gradio_api', '')}/file={server_path}",
399
+ "orig_name": "uploaded_image.jpg",
400
+ "size": file_size,
401
+ "mime_type": "image/jpeg",
402
+ "meta": {"_type": "gradio.FileData"}
403
+ }
404
+
405
+ safe_prompt = prompt_text if prompt_text.strip() else "این تصویر چیست؟ توضیح بده."
406
+
407
+ payload = {
408
+ "data": [safe_prompt, file_data_obj],
409
+ "event_data": None,
410
+ "fn_index": 13,
411
+ "trigger_id": 11,
412
+ "session_hash": s_hash
413
+ }
414
+ except Exception as e:
415
+ out_q.put(Exception(f"خطا در آماده‌سازی و ارسال تصویر: {str(e)}"))
416
+ return
417
+ else:
418
+ # --- حالت متن معمولی ---
419
+ if hist_str:
420
+ full_prompt = f"{GPT5_RULES_FA}\n\nتاریخچه مکالمه ما تا الان:\n{hist_str}\n\n---\nپیام جدید کاربر:\n{prompt_text}"
421
+ else:
422
+ full_prompt = f"{GPT5_RULES_FA}\n\n---\nپیام جدید کاربر:\n{prompt_text}"
423
+
424
+ payload = {
425
+ "data": [full_prompt, [], None, None],
426
+ "event_data": None,
427
+ "fn_index": 2,
428
+ "session_hash": s_hash,
429
+ "trigger_id": 37
430
+ }
431
+
432
+ # ارسال به صف تحلیل (متن یا تصویر)
433
+ max_retries = 3
434
+ for attempt in range(max_retries):
435
+ try:
436
+ res = requests.post(f"{AYA_SPACE_URL}/queue/join", json=payload, timeout=20)
437
+ res.raise_for_status()
438
+
439
+ stream_url = f"{AYA_SPACE_URL}/queue/data?session_hash={s_hash}"
440
+ with requests.get(stream_url, stream=True, timeout=60) as resp:
441
+ for line in resp.iter_lines(decode_unicode=True):
442
+ if line:
443
+ out_q.put(line)
444
+ out_q.put(None)
445
+ return
446
+ except Exception as ex:
447
+ time.sleep(1.5)
448
+
449
+ out_q.put(Exception("سرور در حال حاضر مشغول است."))
450
+
451
+ # اجرای ترد مربوطه
452
+ threading.Thread(target=fetch_aya_stream, args=(data['content'], session_hash, q, text_history, image_base64), daemon=True).start()
453
+
454
+ last_text_length = 0
455
+ full_text = ""
456
+ error_occurred = False
457
+
458
+ while True:
459
+ try:
460
+ line = q.get_nowait()
461
+ except queue.Empty:
462
+ await asyncio.sleep(0.02)
463
+ continue
464
+
465
+ if line is None:
466
+ break
467
+ if isinstance(line, Exception):
468
+ print(f"[{client_id}] خطا در ارتباط با اسپیس آیا: {line}")
469
+ await websocket.send_json({
470
+ "status": "success",
471
+ "model_text": "یک لحظه حواسم پرت شد! می‌تونی پیامت رو دوباره بفرستی؟ 😊",
472
+ "audio_url": None
473
+ })
474
+ error_occurred = True
475
+ break
476
+
477
+ if isinstance(line, str) and line.startswith('data: '):
478
+ try:
479
+ msg_data = json.loads(line[6:])
480
+ msg_type = msg_data.get('msg')
481
+
482
+ if msg_type == 'queue_full':
483
+ await websocket.send_json({
484
+ "status": "success",
485
+ "model_text": "سرم خیلی شلوغه! چند ثانیه دیگه دوباره امتحان کن! 😅",
486
+ "audio_url": None
487
+ })
488
+ error_occurred = True
489
+ break
490
+
491
+ if msg_type in ['process_generating', 'process_completed']:
492
+ output = msg_data.get('output', {}).get('data', [])
493
+ if output and len(output) > 0:
494
+ payload_data = output[0]
495
+ is_append = False
496
+
497
+ if isinstance(payload_data, list) and len(payload_data) > 0:
498
+ action = payload_data[0]
499
+ if isinstance(action, list) and len(action) >= 3 and action[0] == "append":
500
+ is_append = True
501
+ delta = action[2]
502
+ if delta:
503
+ full_text += delta
504
+ last_text_length = len(full_text)
505
+ await websocket.send_json({
506
+ "status": "streaming",
507
+ "text": delta,
508
+ "audio": ""
509
+ })
510
+
511
+ if not is_append:
512
+ current_full_text = ""
513
+ if isinstance(payload_data, list) and len(payload_data) > 0 and isinstance(payload_data[-1], list) and len(payload_data[-1]) > 1:
514
+ current_full_text = payload_data[-1][1] or ""
515
+ elif isinstance(payload_data, str):
516
+ current_full_text = payload_data
517
+
518
+ if current_full_text and len(current_full_text) > last_text_length:
519
+ delta = current_full_text[last_text_length:]
520
+ full_text += delta
521
+ last_text_length = len(current_full_text)
522
+ await websocket.send_json({
523
+ "status": "streaming",
524
+ "text": delta,
525
+ "audio": ""
526
+ })
527
+
528
+ if msg_type == 'process_completed':
529
+ break
530
+ except Exception as e:
531
+ pass
532
+
533
+ if not error_occurred:
534
+ final_clean_text = full_text.strip()
535
+ print(f"[{client_id}] پاسخ استریم آلفا GPT5 کامل شد.")
536
+ await websocket.send_json({
537
+ "status": "success",
538
+ "model_text": final_clean_text,
539
+ "audio_url": None
540
+ })
541
+
542
+ else:
543
+ # --- پردازش مدل صوتی جمینای ---
544
+ max_retries = len(api_key_manager.keys)
545
+ working_client = None
546
+
547
+ for attempt in range(max_retries):
548
+ key_index, api_key = await api_key_manager.get_next_key()
549
+ try:
550
+ client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key)
551
+ working_client = client
552
+ break
553
+ except Exception:
554
+ continue
555
+
556
+ if not working_client:
557
+ await websocket.send_json({"status": "error", "message": "ارتباط با سرور هوش مصنوعی برقرار نشد."})
558
+ continue
559
+
560
+ # تعداد دفعات تلاش برای رد شدن بی‌صدا از روی کلیدهایی که با خطای سهمیه/کوتا مواجه می‌شوند
561
+ quota_max_attempts = len(api_key_manager.keys)
562
+ content_sent = False
563
+
564
+ for quota_attempt in range(quota_max_attempts):
565
+ try:
566
+ async with working_client.aio.live.connect(model=MODEL, config=CONFIG) as session:
567
+ if data['type'] == 'text':
568
+ prompt = data['content']
569
+ if text_history:
570
+ import re
571
+ # تفکیک سابقه متنی به پیام‌های مجزا و انتخاب حداکثر ۲۰ پیام آخر
572
+ messages_list = re.split(r'(?=(?:کاربر|هوش مصنوعی):)', text_history)
573
+ pruned_messages = [m.strip() for m in messages_list if m.strip()]
574
+ pruned_history = "\n".join(pruned_messages[-20:])
575
+
576
+ prompt = f"تاریخچه مکالمه ما تا الان:\n{pruned_history}\n\nپیام جدید من:\n{data['content']}"
577
+
578
+ await session.send(input=prompt, end_of_turn=True)
579
+
580
+ elif data['type'] == 'audio':
581
+ audio_bytes = base64.b64decode(data['content'])
582
+ mime_type = "audio/pcm;rate=16000"
583
+ if len(audio_bytes) > 0:
584
+ await session.send(input={"data": audio_bytes, "mime_type": mime_type}, end_of_turn=True)
585
+ else:
586
+ await websocket.send_json({"status": "error", "message": "فایل صوتی دریافت شده خالی است."})
587
+ break
588
+
589
+ turn = session.receive()
590
+ full_audio = bytearray()
591
+ clean_full_text = ""
592
+
593
+ # مقداردهی کلاس فیلتر جدید
594
+ thought_filter = ThoughtFilter()
595
+
596
+ def process_chunk(msg):
597
+ t_chunk = ""
598
+ a_chunk = bytearray()
599
+ if hasattr(msg, 'server_content') and msg.server_content is not None:
600
+ if hasattr(msg.server_content, 'output_transcription') and msg.server_content.output_transcription is not None:
601
+ if hasattr(msg.server_content.output_transcription, 'text') and msg.server_content.output_transcription.text:
602
+ t_chunk += msg.server_content.output_transcription.text
603
+ if msg.server_content.model_turn is not None:
604
+ for part in msg.server_content.model_turn.parts:
605
+ if part.text: t_chunk += part.text
606
+ if part.inline_data and part.inline_data.data: a_chunk.extend(part.inline_data.data)
607
+ else:
608
+ if getattr(msg, 'text', None): t_chunk += msg.text
609
+ if getattr(msg, 'data', None): a_chunk.extend(msg.data)
610
+ return t_chunk, a_chunk
611
+
612
+ try:
613
+ if not content_sent:
614
+ await websocket.send_json({"status": "start_message"})
615
+ first_response = await asyncio.wait_for(anext(turn), timeout=30.0)
616
+
617
+ t, a = process_chunk(first_response)
618
+ has_audio = bool(a and len(a) > 0)
619
+ safe_t = thought_filter.process_chunk(t, has_audio)
620
+
621
+ if safe_t or a:
622
+ clean_full_text += safe_t
623
+ full_audio.extend(a)
624
+ content_sent = True
625
+ await websocket.send_json({"status": "streaming", "text": safe_t, "audio": base64.b64encode(a).decode('utf-8') if a else ""})
626
+
627
+ async for response in turn:
628
+ t, a = process_chunk(response)
629
+ has_audio = bool(a and len(a) > 0)
630
+ safe_t = thought_filter.process_chunk(t, has_audio)
631
+
632
+ if safe_t or a:
633
+ clean_full_text += safe_t
634
+ full_audio.extend(a)
635
+ content_sent = True
636
+ await websocket.send_json({"status": "streaming", "text": safe_t, "audio": base64.b64encode(a).decode('utf-8') if a else ""})
637
+
638
+ except StopAsyncIteration:
639
+ pass
640
+
641
+ # در صورت پایان ارتباط، اگر چیزی در بافر فیلتر مانده بود آزاد شود
642
+ final_leftover = thought_filter.flush()
643
+ if final_leftover:
644
+ clean_full_text += final_leftover
645
+ await websocket.send_json({"status": "streaming", "text": final_leftover, "audio": ""})
646
+
647
+ audio_url = None
648
+ if full_audio:
649
+ filename = f"chat_{client_id}_{request_id}.wav"
650
+ filepath = os.path.join(TEMP_DIR, filename)
651
+ with wave.open(filepath, 'wb') as wf:
652
+ wf.setnchannels(CHANNELS)
653
+ wf.setsampwidth(SAMPLE_WIDTH)
654
+ wf.setframerate(SAMPLE_RATE)
655
+ wf.writeframes(full_audio)
656
+ audio_url = f"/audio/{filename}"
657
+
658
+ await websocket.send_json({
659
+ "status": "success",
660
+ "model_text": clean_full_text.strip(),
661
+ "audio_url": audio_url
662
+ })
663
+
664
+ # موفقیت‌آمیز بود، از حلقه تلاش با کلیدهای مختلف خارج شو
665
+ break
666
+
667
+ except Exception as e:
668
+ print(f"[{client_id}] خطا با کلید شماره {key_index}: {e}")
669
+
670
+ if not content_sent and quota_attempt < quota_max_attempts - 1:
671
+ # صرف نظر از نوع خطا (کوتا، تعلیق کلید، یا هر خطای دیگر)،
672
+ # به‌صورت کاملاً بی‌صدا (بدون نمایش هیچ خطایی به کاربر) به کلید بعدی وصل شو
673
+ key_index, api_key = await api_key_manager.get_next_key()
674
+ try:
675
+ working_client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key)
676
+ except Exception:
677
+ pass
678
+ continue
679
+ else:
680
+ # فقط وقتی همه‌ی کلیدها امتحان شدند (یا محتوایی قبلاً ارسال شده) خطا نشان داده می‌شود
681
+ # و پیام کلی است، نه متن فنی خام خطا
682
+ await websocket.send_json({"status": "error", "message": "خطا در برقراری ارتباط با سرور هوش مصنوعی. لطفاً دوباره تلاش کنید."})
683
+ break
684
+
685
+ except WebSocketDisconnect:
686
+ print(f"کلاینت [{client_id}] اتصال را قطع کرد.")
687
+ except Exception:
688
+ pass
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ websockets
4
+ google-genai>=0.5.0
5
+ jinja2
6
+ sseclient-py
templates/index.html ADDED
The diff for this file is too large to render. See raw diff