Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,8 @@ import time
|
|
| 6 |
import sqlite3
|
| 7 |
import tempfile
|
| 8 |
import ffmpeg
|
| 9 |
-
|
|
|
|
| 10 |
from deep_translator import GoogleTranslator
|
| 11 |
|
| 12 |
app = Flask(__name__)
|
|
@@ -17,10 +18,8 @@ GITHUB_REPO = os.environ.get("GITHUB_REPO", "Action")
|
|
| 17 |
SPACE_HOST = os.environ.get("SPACE_HOST", "")
|
| 18 |
SPACE_URL = f"https://{SPACE_HOST}"
|
| 19 |
|
| 20 |
-
# --- کلمات ممنوعه ---
|
| 21 |
FORBIDDEN_KEYWORDS = {"sex", "sexy", "porn", "nude", "erotic", "سکس", "سکسی", "پورن", "شهوانی", "برهنه", "لخت"}
|
| 22 |
|
| 23 |
-
# --- تنظیمات دیتابیس SQLite ---
|
| 24 |
DB_DIR = "/data" if os.path.exists("/data") else "."
|
| 25 |
DB_PATH = os.path.join(DB_DIR, "users.db")
|
| 26 |
TEMP_DIR = "tmp"
|
|
@@ -28,44 +27,49 @@ USAGE_LIMIT = 5
|
|
| 28 |
ONE_WEEK_SECONDS = 7 * 24 * 60 * 60
|
| 29 |
ONE_DAY_SECONDS = 24 * 60 * 60
|
| 30 |
|
| 31 |
-
# متغیرهای مربوط به دیتابیس جدید تصاویر
|
| 32 |
DB_IMAGE_PATH = os.path.join(DB_DIR, "users_image.db")
|
| 33 |
IMAGE_USAGE_LIMIT = 5
|
| 34 |
|
| 35 |
-
# متغیرهای مربوط به دیتابیس جدید ویرایش تصاویر
|
| 36 |
DB_IMAGE_EDIT_PATH = os.path.join(DB_DIR, "users_image_edit.db")
|
| 37 |
EDIT_USAGE_LIMIT = 5
|
| 38 |
|
|
|
|
|
|
|
| 39 |
os.makedirs(TEMP_DIR, exist_ok=True)
|
| 40 |
os.makedirs("static/images", exist_ok=True)
|
| 41 |
|
| 42 |
def init_db():
|
| 43 |
conn = sqlite3.connect(DB_PATH)
|
| 44 |
c = conn.cursor()
|
| 45 |
-
c.execute('''CREATE TABLE IF NOT EXISTS users
|
| 46 |
-
(id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
|
| 47 |
conn.commit()
|
| 48 |
conn.close()
|
| 49 |
|
| 50 |
def init_image_db():
|
| 51 |
conn = sqlite3.connect(DB_IMAGE_PATH)
|
| 52 |
c = conn.cursor()
|
| 53 |
-
c.execute('''CREATE TABLE IF NOT EXISTS users
|
| 54 |
-
(id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
|
| 55 |
conn.commit()
|
| 56 |
conn.close()
|
| 57 |
|
| 58 |
def init_image_edit_db():
|
| 59 |
conn = sqlite3.connect(DB_IMAGE_EDIT_PATH)
|
| 60 |
c = conn.cursor()
|
| 61 |
-
c.execute('''CREATE TABLE IF NOT EXISTS users
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
conn.commit()
|
| 64 |
conn.close()
|
| 65 |
|
| 66 |
init_db()
|
| 67 |
init_image_db()
|
| 68 |
init_image_edit_db()
|
|
|
|
| 69 |
|
| 70 |
def get_db_connection():
|
| 71 |
conn = sqlite3.connect(DB_PATH)
|
|
@@ -82,7 +86,11 @@ def get_image_edit_db_connection():
|
|
| 82 |
conn.row_factory = sqlite3.Row
|
| 83 |
return conn
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def delete_github_run(gh_run_id):
|
| 87 |
time.sleep(20)
|
| 88 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/actions/runs/{gh_run_id}"
|
|
@@ -100,36 +108,25 @@ def has_forbidden_words(text):
|
|
| 100 |
text_lower = text.lower()
|
| 101 |
return any(keyword in text_lower for keyword in FORBIDDEN_KEYWORDS)
|
| 102 |
|
| 103 |
-
# --- مسیرهای صفحات HTML ---
|
| 104 |
@app.route('/')
|
| 105 |
def index():
|
| 106 |
return render_template('fluxpro.html')
|
| 107 |
|
| 108 |
@app.route('/<page_name>')
|
| 109 |
def serve_html(page_name):
|
| 110 |
-
if not page_name.endswith('.html'):
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
return render_template(page_name)
|
| 114 |
-
except:
|
| 115 |
-
return "Page not found", 404
|
| 116 |
|
| 117 |
-
# --- API برای ساخت عکس (Flux و Cartoon) ---
|
| 118 |
@app.route('/api/generate', methods=['POST'])
|
| 119 |
def generate():
|
| 120 |
-
if not GITHUB_TOKEN:
|
| 121 |
-
return jsonify({"status": "error", "message": "Secret GITHUB_TOKEN is missing!"}), 500
|
| 122 |
-
|
| 123 |
data = request.json
|
| 124 |
persian_prompt = data.get('prompt', '')
|
| 125 |
|
| 126 |
-
if has_forbidden_words(persian_prompt):
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
try:
|
| 130 |
-
english_prompt = GoogleTranslator(source='fa', target='en').translate(persian_prompt)
|
| 131 |
-
except:
|
| 132 |
-
english_prompt = persian_prompt
|
| 133 |
|
| 134 |
run_id = str(uuid.uuid4())
|
| 135 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
|
@@ -137,66 +134,37 @@ def generate():
|
|
| 137 |
|
| 138 |
payload = {
|
| 139 |
"event_type": data.get('action_name', 'generate-flux'),
|
| 140 |
-
"client_payload": {
|
| 141 |
-
"prompt": english_prompt,
|
| 142 |
-
"width": data.get('width', 1024),
|
| 143 |
-
"height": data.get('height', 1024),
|
| 144 |
-
"run_id": run_id,
|
| 145 |
-
"space_url": SPACE_URL
|
| 146 |
-
}
|
| 147 |
}
|
| 148 |
r = requests.post(url, headers=headers, json=payload)
|
| 149 |
-
|
| 150 |
if r.status_code == 204:
|
| 151 |
-
return jsonify({
|
| 152 |
-
|
| 153 |
-
"run_id": run_id,
|
| 154 |
-
"translated_prompt": english_prompt,
|
| 155 |
-
"translated": english_prompt
|
| 156 |
-
})
|
| 157 |
-
else:
|
| 158 |
-
return jsonify({"status": "error", "message": r.text}), 400
|
| 159 |
|
| 160 |
-
# --- API مخصوص ویرایش عکس (AI Photoshop) ---
|
| 161 |
@app.route('/api/edit', methods=['POST'])
|
| 162 |
def edit_image():
|
| 163 |
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "Token missing"}), 500
|
| 164 |
if 'image' not in request.files: return jsonify({"status": "error", "message": "Image required"}), 400
|
| 165 |
-
|
| 166 |
file = request.files['image']
|
| 167 |
persian_prompt = request.form.get('prompt', '')
|
| 168 |
|
| 169 |
-
if has_forbidden_words(persian_prompt):
|
| 170 |
-
return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
|
| 171 |
-
|
| 172 |
try: english_prompt = GoogleTranslator(source='fa', target='en').translate(persian_prompt)
|
| 173 |
except: english_prompt = persian_prompt
|
| 174 |
|
| 175 |
run_id = str(uuid.uuid4())
|
| 176 |
-
|
| 177 |
input_filename = f"{run_id}_input.png"
|
| 178 |
file.save(f"static/images/{input_filename}")
|
| 179 |
image_public_url = f"{SPACE_URL}/static/images/{input_filename}"
|
| 180 |
|
| 181 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
| 182 |
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
| 183 |
-
payload = {
|
| 184 |
-
"event_type": "generate-editor",
|
| 185 |
-
"client_payload": {
|
| 186 |
-
"prompt": english_prompt,
|
| 187 |
-
"image_url": image_public_url,
|
| 188 |
-
"run_id": run_id,
|
| 189 |
-
"space_url": SPACE_URL
|
| 190 |
-
}
|
| 191 |
-
}
|
| 192 |
r = requests.post(url, headers=headers, json=payload)
|
| 193 |
|
| 194 |
-
if r.status_code == 204:
|
| 195 |
-
|
| 196 |
-
else:
|
| 197 |
-
return jsonify({"status": "error", "message": r.text}), 400
|
| 198 |
|
| 199 |
-
# --- API مخصوص تبدیل عکس به ویدیو ---
|
| 200 |
@app.route('/api/generate-video', methods=['POST'])
|
| 201 |
def generate_video():
|
| 202 |
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "Token missing"}), 500
|
|
@@ -206,116 +174,62 @@ def generate_video():
|
|
| 206 |
user_prompt = request.form.get('prompt', '').strip()
|
| 207 |
duration = request.form.get('duration', 5.0)
|
| 208 |
|
| 209 |
-
if has_forbidden_words(user_prompt):
|
| 210 |
-
return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
|
| 211 |
|
| 212 |
run_id = str(uuid.uuid4())
|
| 213 |
-
|
| 214 |
input_filename = f"{run_id}_video_input.png"
|
| 215 |
-
|
| 216 |
-
file.save(local_image_path)
|
| 217 |
image_public_url = f"{SPACE_URL}/static/images/{input_filename}"
|
| 218 |
|
| 219 |
-
master_prompt = "
|
| 220 |
-
|
| 221 |
-
1. If the user prompt is empty or generic (like "animate this"), add subtle, high-quality, believable cinematic motion (e.g., slow zoom, water flowing, wind in hair).
|
| 222 |
-
2. If the user gives specific directions, focus ENTIRELY on executing that command perfectly. If the action is not visible in-frame, use cinematic camera movements to reveal it.
|
| 223 |
-
3. You must output ONLY a highly detailed, descriptive animation prompt in ENGLISH. Do not translate literally; ENHANCE the prompt for a text-to-video AI model.
|
| 224 |
-
4. MUST Include keywords at the end: cinematic, photorealistic, high detail, smooth motion, 8k.
|
| 225 |
-
|
| 226 |
-
CRITICAL RULE: DO NOT say "Here is the prompt" or give any conversational explanations. DO NOT output JSON. Output ONLY the raw English animation prompt text and NOTHING ELSE."""
|
| 227 |
-
|
| 228 |
-
english_prompt = ""
|
| 229 |
-
gemma_success = False
|
| 230 |
-
|
| 231 |
-
prompt_for_gemma = user_prompt if user_prompt else "لطفاً این تصویر را به یک ویدیوی سینمایی بسیار جذاب و واقعگرایانه متحرک کن."
|
| 232 |
-
combined_prompt = f"{master_prompt}\n\nUser request: {prompt_for_gemma}"
|
| 233 |
|
| 234 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
| 235 |
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
|
|
|
|
|
|
| 236 |
|
| 237 |
for attempt in range(2):
|
| 238 |
try:
|
| 239 |
gemma_run_id = f"gemma_{run_id}_{attempt}"
|
| 240 |
gemma_txt_path = os.path.join("static/images", f"{gemma_run_id}.txt")
|
|
|
|
| 241 |
|
| 242 |
-
payload_gemma =
|
| 243 |
-
"event_type": "chat-gemma",
|
| 244 |
-
"client_payload": {
|
| 245 |
-
"prompt": combined_prompt,
|
| 246 |
-
"file_url": image_public_url,
|
| 247 |
-
"file_mime": "image/png",
|
| 248 |
-
"run_id": gemma_run_id,
|
| 249 |
-
"space_url": SPACE_URL
|
| 250 |
-
}
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
r_gemma = requests.post(url, headers=headers, json=payload_gemma)
|
| 254 |
-
|
| 255 |
-
if r_gemma.status_code == 204:
|
| 256 |
waited = 0
|
| 257 |
while waited < 90:
|
| 258 |
if os.path.exists(gemma_txt_path):
|
| 259 |
-
with open(gemma_txt_path, "r", encoding="utf-8") as f:
|
| 260 |
-
english_prompt = f.read().strip()
|
| 261 |
gemma_success = True
|
| 262 |
break
|
| 263 |
-
time.sleep(3)
|
| 264 |
-
waited += 3
|
| 265 |
|
| 266 |
if gemma_success:
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
gemma_success = False
|
| 273 |
-
else:
|
| 274 |
-
break
|
| 275 |
-
|
| 276 |
-
except Exception as e:
|
| 277 |
-
print(f"Gemma Action Error on attempt {attempt + 1}: {e}")
|
| 278 |
-
|
| 279 |
-
if not gemma_success:
|
| 280 |
-
time.sleep(4)
|
| 281 |
|
| 282 |
if not gemma_success:
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
english_prompt += ", cinematic motion, photorealistic, high detail, smooth animation, 8k"
|
| 288 |
-
else:
|
| 289 |
-
english_prompt = "cinematic motion, photorealistic, high detail, smooth animation, 8k"
|
| 290 |
-
except:
|
| 291 |
-
english_prompt = "cinematic motion, photorealistic, high detail, smooth animation, 8k"
|
| 292 |
-
|
| 293 |
-
payload_video = {
|
| 294 |
-
"event_type": "generate-video",
|
| 295 |
-
"client_payload": {
|
| 296 |
-
"prompt": english_prompt,
|
| 297 |
-
"duration": duration,
|
| 298 |
-
"image_url": image_public_url,
|
| 299 |
-
"run_id": run_id,
|
| 300 |
-
"space_url": SPACE_URL
|
| 301 |
-
}
|
| 302 |
-
}
|
| 303 |
r_vid = requests.post(url, headers=headers, json=payload_video)
|
| 304 |
|
| 305 |
-
if r_vid.status_code == 204:
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
| 309 |
|
| 310 |
-
# --- API مخصوص چت با Gemma-4 ---
|
| 311 |
@app.route('/api/chat', methods=['POST'])
|
| 312 |
def chat_api():
|
| 313 |
-
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "
|
| 314 |
-
|
| 315 |
text_prompt = request.form.get('prompt', '')
|
| 316 |
-
|
| 317 |
-
if has_forbidden_words(text_prompt):
|
| 318 |
-
return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
|
| 319 |
|
| 320 |
file_obj = request.files.get('file')
|
| 321 |
run_id = str(uuid.uuid4())
|
|
@@ -329,27 +243,131 @@ def chat_api():
|
|
| 329 |
file_obj.save(f"static/images/{filename}")
|
| 330 |
file_url = f"{SPACE_URL}/static/images/{filename}"
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
}
|
| 345 |
-
r = requests.post(url, headers=headers, json=payload)
|
| 346 |
-
|
| 347 |
-
if r.status_code == 204:
|
| 348 |
return jsonify({"status": "success", "run_id": run_id})
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
-
# --- API میکس ویدیوها سمت سرور ---
|
| 353 |
@app.route('/api/merge-videos', methods=['POST'])
|
| 354 |
def merge_videos():
|
| 355 |
if 'base_video' not in request.files or 'new_clip' not in request.files:
|
|
@@ -357,7 +375,6 @@ def merge_videos():
|
|
| 357 |
|
| 358 |
base_video_file = request.files['base_video']
|
| 359 |
new_clip_file = request.files['new_clip']
|
| 360 |
-
|
| 361 |
temp_files_to_clean = []
|
| 362 |
|
| 363 |
try:
|
|
@@ -366,96 +383,66 @@ def merge_videos():
|
|
| 366 |
output_path = os.path.join(TEMP_DIR, f"{uuid.uuid4().hex}.mp4")
|
| 367 |
|
| 368 |
temp_files_to_clean.extend([base_video_path, new_clip_path, output_path])
|
| 369 |
-
|
| 370 |
base_video_file.save(base_video_path)
|
| 371 |
new_clip_file.save(new_clip_path)
|
| 372 |
|
| 373 |
probe = ffmpeg.probe(base_video_path)
|
| 374 |
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
|
| 375 |
-
width = int(video_stream['width'])
|
| 376 |
-
height = int(video_stream['height'])
|
| 377 |
|
| 378 |
input1 = ffmpeg.input(base_video_path)
|
| 379 |
input2 = ffmpeg.input(new_clip_path).filter('scale', width, height).filter('setsar', '1')
|
| 380 |
-
|
| 381 |
-
merged_video = ffmpeg.concat(input1, input2, v=1, a=0).output(
|
| 382 |
-
output_path,
|
| 383 |
-
crf=18,
|
| 384 |
-
preset='slow',
|
| 385 |
-
pix_fmt='yuv420p'
|
| 386 |
-
)
|
| 387 |
-
|
| 388 |
merged_video.run(overwrite_output=True, quiet=True)
|
| 389 |
|
| 390 |
@after_this_request
|
| 391 |
def cleanup(response):
|
| 392 |
try:
|
| 393 |
for f_path in temp_files_to_clean:
|
| 394 |
-
if os.path.exists(f_path):
|
| 395 |
-
|
| 396 |
-
except:
|
| 397 |
-
pass
|
| 398 |
return response
|
| 399 |
-
|
| 400 |
return send_file(output_path, mimetype='video/mp4', as_attachment=True, download_name='merged_video.mp4')
|
| 401 |
|
| 402 |
-
except
|
| 403 |
for f_path in temp_files_to_clean:
|
| 404 |
-
if os.path.exists(f_path):
|
| 405 |
-
|
| 406 |
-
return jsonify({"error": "خطا در پردازش و میکس ویدیو در سرور."}), 500
|
| 407 |
-
except Exception as e:
|
| 408 |
-
for f_path in temp_files_to_clean:
|
| 409 |
-
if os.path.exists(f_path):
|
| 410 |
-
os.remove(f_path)
|
| 411 |
-
return jsonify({"error": "خطای پیشبینی نشده در سرور."}), 500
|
| 412 |
|
| 413 |
-
# --- مدیریت اعتبار برای ویدیو ---
|
| 414 |
@app.route('/api/check-credit', methods=['POST'])
|
| 415 |
def check_credit():
|
| 416 |
data = request.get_json()
|
| 417 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 418 |
user_id = get_user_identifier(data)
|
| 419 |
-
|
| 420 |
conn = get_db_connection()
|
| 421 |
c = conn.cursor()
|
| 422 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 423 |
user_record = c.fetchone()
|
| 424 |
-
|
| 425 |
now = time.time()
|
| 426 |
credits_remaining = USAGE_LIMIT
|
| 427 |
limit_reached = False
|
| 428 |
reset_timestamp = 0
|
| 429 |
-
|
| 430 |
if user_record:
|
| 431 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
| 432 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 433 |
conn.commit()
|
| 434 |
-
credits_remaining = USAGE_LIMIT
|
| 435 |
else:
|
| 436 |
credits_remaining = max(0, USAGE_LIMIT - user_record['count'])
|
| 437 |
if credits_remaining == 0:
|
| 438 |
limit_reached = True
|
| 439 |
reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
|
| 440 |
-
|
| 441 |
conn.close()
|
| 442 |
-
return jsonify({
|
| 443 |
-
"credits_remaining": credits_remaining,
|
| 444 |
-
"limit_reached": limit_reached,
|
| 445 |
-
"reset_timestamp": reset_timestamp
|
| 446 |
-
})
|
| 447 |
|
| 448 |
@app.route('/api/use-credit', methods=['POST'])
|
| 449 |
def use_credit():
|
| 450 |
data = request.get_json()
|
| 451 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 452 |
user_id = get_user_identifier(data)
|
| 453 |
-
|
| 454 |
conn = get_db_connection()
|
| 455 |
c = conn.cursor()
|
| 456 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 457 |
user_record = c.fetchone()
|
| 458 |
-
|
| 459 |
now = time.time()
|
| 460 |
if user_record:
|
| 461 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
|
@@ -463,67 +450,50 @@ def use_credit():
|
|
| 463 |
else:
|
| 464 |
if user_record['count'] >= USAGE_LIMIT:
|
| 465 |
conn.close()
|
| 466 |
-
|
| 467 |
-
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": reset_timestamp}), 429
|
| 468 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 469 |
else:
|
| 470 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
| 471 |
-
|
| 472 |
conn.commit()
|
| 473 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 474 |
-
|
| 475 |
-
credits_remaining = USAGE_LIMIT - new_count
|
| 476 |
-
|
| 477 |
conn.close()
|
| 478 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 479 |
|
| 480 |
-
|
| 481 |
-
# --- مدیریت اعتبار اختصاصی روزانه برای صفحه جدید ساخت تصویر ---
|
| 482 |
@app.route('/api/check-image-credit', methods=['POST'])
|
| 483 |
def check_image_credit():
|
| 484 |
data = request.get_json()
|
| 485 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 486 |
user_id = get_user_identifier(data)
|
| 487 |
-
|
| 488 |
conn = get_image_db_connection()
|
| 489 |
c = conn.cursor()
|
| 490 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 491 |
user_record = c.fetchone()
|
| 492 |
-
|
| 493 |
now = time.time()
|
| 494 |
credits_remaining = IMAGE_USAGE_LIMIT
|
| 495 |
limit_reached = False
|
| 496 |
reset_timestamp = 0
|
| 497 |
-
|
| 498 |
if user_record:
|
| 499 |
if user_record['week_start'] < (now - ONE_DAY_SECONDS):
|
| 500 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 501 |
conn.commit()
|
| 502 |
-
credits_remaining = IMAGE_USAGE_LIMIT
|
| 503 |
else:
|
| 504 |
credits_remaining = max(0, IMAGE_USAGE_LIMIT - user_record['count'])
|
| 505 |
if credits_remaining == 0:
|
| 506 |
limit_reached = True
|
| 507 |
reset_timestamp = user_record['week_start'] + ONE_DAY_SECONDS
|
| 508 |
-
|
| 509 |
conn.close()
|
| 510 |
-
return jsonify({
|
| 511 |
-
"credits_remaining": credits_remaining,
|
| 512 |
-
"limit_reached": limit_reached,
|
| 513 |
-
"reset_timestamp": reset_timestamp
|
| 514 |
-
})
|
| 515 |
|
| 516 |
@app.route('/api/use-image-credit', methods=['POST'])
|
| 517 |
def use_image_credit():
|
| 518 |
data = request.get_json()
|
| 519 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 520 |
user_id = get_user_identifier(data)
|
| 521 |
-
|
| 522 |
conn = get_image_db_connection()
|
| 523 |
c = conn.cursor()
|
| 524 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 525 |
user_record = c.fetchone()
|
| 526 |
-
|
| 527 |
now = time.time()
|
| 528 |
if user_record:
|
| 529 |
if user_record['week_start'] < (now - ONE_DAY_SECONDS):
|
|
@@ -531,67 +501,50 @@ def use_image_credit():
|
|
| 531 |
else:
|
| 532 |
if user_record['count'] >= IMAGE_USAGE_LIMIT:
|
| 533 |
conn.close()
|
| 534 |
-
|
| 535 |
-
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": reset_timestamp}), 429
|
| 536 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 537 |
else:
|
| 538 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
| 539 |
-
|
| 540 |
conn.commit()
|
| 541 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 542 |
-
|
| 543 |
-
credits_remaining = IMAGE_USAGE_LIMIT - new_count
|
| 544 |
-
|
| 545 |
conn.close()
|
| 546 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 547 |
|
| 548 |
-
|
| 549 |
-
# --- مدیریت اعتبار اختصاصی هفتگی برای ویرایش تصویر ---
|
| 550 |
@app.route('/api/check-edit-credit', methods=['POST'])
|
| 551 |
def check_edit_credit():
|
| 552 |
data = request.get_json()
|
| 553 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 554 |
user_id = get_user_identifier(data)
|
| 555 |
-
|
| 556 |
conn = get_image_edit_db_connection()
|
| 557 |
c = conn.cursor()
|
| 558 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 559 |
user_record = c.fetchone()
|
| 560 |
-
|
| 561 |
now = time.time()
|
| 562 |
credits_remaining = EDIT_USAGE_LIMIT
|
| 563 |
limit_reached = False
|
| 564 |
reset_timestamp = 0
|
| 565 |
-
|
| 566 |
if user_record:
|
| 567 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
| 568 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 569 |
conn.commit()
|
| 570 |
-
credits_remaining = EDIT_USAGE_LIMIT
|
| 571 |
else:
|
| 572 |
credits_remaining = max(0, EDIT_USAGE_LIMIT - user_record['count'])
|
| 573 |
if credits_remaining == 0:
|
| 574 |
limit_reached = True
|
| 575 |
reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
|
| 576 |
-
|
| 577 |
conn.close()
|
| 578 |
-
return jsonify({
|
| 579 |
-
"credits_remaining": credits_remaining,
|
| 580 |
-
"limit_reached": limit_reached,
|
| 581 |
-
"reset_timestamp": reset_timestamp
|
| 582 |
-
})
|
| 583 |
|
| 584 |
@app.route('/api/use-edit-credit', methods=['POST'])
|
| 585 |
def use_edit_credit():
|
| 586 |
data = request.get_json()
|
| 587 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 588 |
user_id = get_user_identifier(data)
|
| 589 |
-
|
| 590 |
conn = get_image_edit_db_connection()
|
| 591 |
c = conn.cursor()
|
| 592 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 593 |
user_record = c.fetchone()
|
| 594 |
-
|
| 595 |
now = time.time()
|
| 596 |
if user_record:
|
| 597 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
|
@@ -599,57 +552,36 @@ def use_edit_credit():
|
|
| 599 |
else:
|
| 600 |
if user_record['count'] >= EDIT_USAGE_LIMIT:
|
| 601 |
conn.close()
|
| 602 |
-
|
| 603 |
-
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": reset_timestamp}), 429
|
| 604 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 605 |
else:
|
| 606 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
| 607 |
-
|
| 608 |
conn.commit()
|
| 609 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 610 |
-
|
| 611 |
-
credits_remaining = EDIT_USAGE_LIMIT - new_count
|
| 612 |
-
|
| 613 |
conn.close()
|
| 614 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 615 |
|
| 616 |
-
|
| 617 |
-
# --- دریافت و ارسال فایل ---
|
| 618 |
@app.route('/api/webhook/upload', methods=['POST'])
|
| 619 |
def webhook_upload():
|
| 620 |
run_id = request.form.get('run_id')
|
| 621 |
gh_run_id = request.form.get('github_run_id')
|
| 622 |
ext = request.form.get('ext', 'webp')
|
| 623 |
-
|
| 624 |
-
if 'file' not in request.files or not run_id:
|
| 625 |
-
return "Invalid request", 400
|
| 626 |
-
|
| 627 |
file = request.files['file']
|
| 628 |
file.save(f"static/images/{run_id}.{ext}")
|
| 629 |
-
|
| 630 |
if gh_run_id: threading.Thread(target=delete_github_run, args=(gh_run_id,)).start()
|
| 631 |
return "OK", 200
|
| 632 |
|
| 633 |
@app.route('/api/status/<run_id>')
|
| 634 |
def check_status(run_id):
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
'png', 'jpg', 'jpeg', 'webp', 'gif', # فرمتهای عکس
|
| 638 |
-
'mp4', 'webm', 'mov', 'mkv', 'avi', # فرمتهای ویدیو
|
| 639 |
-
'mp3', 'wav', 'ogg', 'm4a', # فرمتهای صدا
|
| 640 |
-
'txt', 'json' # فرمتهای متن
|
| 641 |
-
]
|
| 642 |
-
|
| 643 |
-
# بررسی میکند که آیا فایلی با این run_id و هر کدام از پسوندهای بالا وجود دارد یا خیر
|
| 644 |
-
for ext in valid_extensions:
|
| 645 |
-
if os.path.exists(f"static/images/{run_id}.{ext}"):
|
| 646 |
-
return jsonify({"status": "ready", "url": f"/static/images/{run_id}.{ext}"})
|
| 647 |
-
|
| 648 |
return jsonify({"status": "processing"})
|
| 649 |
|
| 650 |
@app.route('/static/images/<filename>')
|
| 651 |
-
def serve_file(filename):
|
| 652 |
-
return send_from_directory('static/images', filename)
|
| 653 |
|
| 654 |
if __name__ == '__main__':
|
| 655 |
-
|
|
|
|
|
|
| 6 |
import sqlite3
|
| 7 |
import tempfile
|
| 8 |
import ffmpeg
|
| 9 |
+
import json
|
| 10 |
+
from flask import Flask, request, jsonify, render_template, send_from_directory, send_file, after_this_request, Response
|
| 11 |
from deep_translator import GoogleTranslator
|
| 12 |
|
| 13 |
app = Flask(__name__)
|
|
|
|
| 18 |
SPACE_HOST = os.environ.get("SPACE_HOST", "")
|
| 19 |
SPACE_URL = f"https://{SPACE_HOST}"
|
| 20 |
|
|
|
|
| 21 |
FORBIDDEN_KEYWORDS = {"sex", "sexy", "porn", "nude", "erotic", "سکس", "سکسی", "پورن", "شهوانی", "برهنه", "لخت"}
|
| 22 |
|
|
|
|
| 23 |
DB_DIR = "/data" if os.path.exists("/data") else "."
|
| 24 |
DB_PATH = os.path.join(DB_DIR, "users.db")
|
| 25 |
TEMP_DIR = "tmp"
|
|
|
|
| 27 |
ONE_WEEK_SECONDS = 7 * 24 * 60 * 60
|
| 28 |
ONE_DAY_SECONDS = 24 * 60 * 60
|
| 29 |
|
|
|
|
| 30 |
DB_IMAGE_PATH = os.path.join(DB_DIR, "users_image.db")
|
| 31 |
IMAGE_USAGE_LIMIT = 5
|
| 32 |
|
|
|
|
| 33 |
DB_IMAGE_EDIT_PATH = os.path.join(DB_DIR, "users_image_edit.db")
|
| 34 |
EDIT_USAGE_LIMIT = 5
|
| 35 |
|
| 36 |
+
DB_CHAT_QUEUE_PATH = os.path.join(DB_DIR, "chat_queue.db")
|
| 37 |
+
|
| 38 |
os.makedirs(TEMP_DIR, exist_ok=True)
|
| 39 |
os.makedirs("static/images", exist_ok=True)
|
| 40 |
|
| 41 |
def init_db():
|
| 42 |
conn = sqlite3.connect(DB_PATH)
|
| 43 |
c = conn.cursor()
|
| 44 |
+
c.execute('''CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
|
|
|
|
| 45 |
conn.commit()
|
| 46 |
conn.close()
|
| 47 |
|
| 48 |
def init_image_db():
|
| 49 |
conn = sqlite3.connect(DB_IMAGE_PATH)
|
| 50 |
c = conn.cursor()
|
| 51 |
+
c.execute('''CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
|
|
|
|
| 52 |
conn.commit()
|
| 53 |
conn.close()
|
| 54 |
|
| 55 |
def init_image_edit_db():
|
| 56 |
conn = sqlite3.connect(DB_IMAGE_EDIT_PATH)
|
| 57 |
c = conn.cursor()
|
| 58 |
+
c.execute('''CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
|
| 59 |
+
conn.commit()
|
| 60 |
+
conn.close()
|
| 61 |
+
|
| 62 |
+
def init_chat_queue_db():
|
| 63 |
+
conn = sqlite3.connect(DB_CHAT_QUEUE_PATH)
|
| 64 |
+
c = conn.cursor()
|
| 65 |
+
c.execute('''CREATE TABLE IF NOT EXISTS queue (run_id TEXT PRIMARY KEY, prompt TEXT, file_url TEXT, file_mime TEXT, status TEXT, timestamp REAL)''')
|
| 66 |
conn.commit()
|
| 67 |
conn.close()
|
| 68 |
|
| 69 |
init_db()
|
| 70 |
init_image_db()
|
| 71 |
init_image_edit_db()
|
| 72 |
+
init_chat_queue_db()
|
| 73 |
|
| 74 |
def get_db_connection():
|
| 75 |
conn = sqlite3.connect(DB_PATH)
|
|
|
|
| 86 |
conn.row_factory = sqlite3.Row
|
| 87 |
return conn
|
| 88 |
|
| 89 |
+
def get_chat_queue_db_connection():
|
| 90 |
+
conn = sqlite3.connect(DB_CHAT_QUEUE_PATH)
|
| 91 |
+
conn.row_factory = sqlite3.Row
|
| 92 |
+
return conn
|
| 93 |
+
|
| 94 |
def delete_github_run(gh_run_id):
|
| 95 |
time.sleep(20)
|
| 96 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/actions/runs/{gh_run_id}"
|
|
|
|
| 108 |
text_lower = text.lower()
|
| 109 |
return any(keyword in text_lower for keyword in FORBIDDEN_KEYWORDS)
|
| 110 |
|
|
|
|
| 111 |
@app.route('/')
|
| 112 |
def index():
|
| 113 |
return render_template('fluxpro.html')
|
| 114 |
|
| 115 |
@app.route('/<page_name>')
|
| 116 |
def serve_html(page_name):
|
| 117 |
+
if not page_name.endswith('.html'): page_name += '.html'
|
| 118 |
+
try: return render_template(page_name)
|
| 119 |
+
except: return "Page not found", 404
|
|
|
|
|
|
|
|
|
|
| 120 |
|
|
|
|
| 121 |
@app.route('/api/generate', methods=['POST'])
|
| 122 |
def generate():
|
| 123 |
+
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "Secret GITHUB_TOKEN is missing!"}), 500
|
|
|
|
|
|
|
| 124 |
data = request.json
|
| 125 |
persian_prompt = data.get('prompt', '')
|
| 126 |
|
| 127 |
+
if has_forbidden_words(persian_prompt): return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
|
| 128 |
+
try: english_prompt = GoogleTranslator(source='fa', target='en').translate(persian_prompt)
|
| 129 |
+
except: english_prompt = persian_prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
run_id = str(uuid.uuid4())
|
| 132 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
|
|
|
| 134 |
|
| 135 |
payload = {
|
| 136 |
"event_type": data.get('action_name', 'generate-flux'),
|
| 137 |
+
"client_payload": {"prompt": english_prompt, "width": data.get('width', 1024), "height": data.get('height', 1024), "run_id": run_id, "space_url": SPACE_URL}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
r = requests.post(url, headers=headers, json=payload)
|
|
|
|
| 140 |
if r.status_code == 204:
|
| 141 |
+
return jsonify({"status": "success", "run_id": run_id, "translated_prompt": english_prompt, "translated": english_prompt})
|
| 142 |
+
else: return jsonify({"status": "error", "message": r.text}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
|
|
|
| 144 |
@app.route('/api/edit', methods=['POST'])
|
| 145 |
def edit_image():
|
| 146 |
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "Token missing"}), 500
|
| 147 |
if 'image' not in request.files: return jsonify({"status": "error", "message": "Image required"}), 400
|
|
|
|
| 148 |
file = request.files['image']
|
| 149 |
persian_prompt = request.form.get('prompt', '')
|
| 150 |
|
| 151 |
+
if has_forbidden_words(persian_prompt): return jsonify({"status": "error", "message": "متن نامناسب"}), 400
|
|
|
|
|
|
|
| 152 |
try: english_prompt = GoogleTranslator(source='fa', target='en').translate(persian_prompt)
|
| 153 |
except: english_prompt = persian_prompt
|
| 154 |
|
| 155 |
run_id = str(uuid.uuid4())
|
|
|
|
| 156 |
input_filename = f"{run_id}_input.png"
|
| 157 |
file.save(f"static/images/{input_filename}")
|
| 158 |
image_public_url = f"{SPACE_URL}/static/images/{input_filename}"
|
| 159 |
|
| 160 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
| 161 |
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
| 162 |
+
payload = {"event_type": "generate-editor", "client_payload": {"prompt": english_prompt, "image_url": image_public_url, "run_id": run_id, "space_url": SPACE_URL}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
r = requests.post(url, headers=headers, json=payload)
|
| 164 |
|
| 165 |
+
if r.status_code == 204: return jsonify({"status": "success", "run_id": run_id, "translated": english_prompt})
|
| 166 |
+
else: return jsonify({"status": "error", "message": r.text}), 400
|
|
|
|
|
|
|
| 167 |
|
|
|
|
| 168 |
@app.route('/api/generate-video', methods=['POST'])
|
| 169 |
def generate_video():
|
| 170 |
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "Token missing"}), 500
|
|
|
|
| 174 |
user_prompt = request.form.get('prompt', '').strip()
|
| 175 |
duration = request.form.get('duration', 5.0)
|
| 176 |
|
| 177 |
+
if has_forbidden_words(user_prompt): return jsonify({"status": "error", "message": "کلمات نامناسب"}), 400
|
|
|
|
| 178 |
|
| 179 |
run_id = str(uuid.uuid4())
|
|
|
|
| 180 |
input_filename = f"{run_id}_video_input.png"
|
| 181 |
+
file.save(os.path.join("static/images", input_filename))
|
|
|
|
| 182 |
image_public_url = f"{SPACE_URL}/static/images/{input_filename}"
|
| 183 |
|
| 184 |
+
master_prompt = "You are an expert AI Animation Planner. Your absolute highest priority is to faithfully and creatively execute the user's specific request based on the provided image. MUST Include keywords at the end: cinematic, photorealistic, high detail, smooth motion, 8k. Output ONLY the raw English animation prompt text."
|
| 185 |
+
combined_prompt = f"{master_prompt}\n\nUser request: {user_prompt if user_prompt else 'animate this image cinematic'}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
| 188 |
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
| 189 |
+
gemma_success = False
|
| 190 |
+
english_prompt = ""
|
| 191 |
|
| 192 |
for attempt in range(2):
|
| 193 |
try:
|
| 194 |
gemma_run_id = f"gemma_{run_id}_{attempt}"
|
| 195 |
gemma_txt_path = os.path.join("static/images", f"{gemma_run_id}.txt")
|
| 196 |
+
payload_gemma = {"event_type": "chat-gemma", "client_payload": {"prompt": combined_prompt, "file_url": image_public_url, "file_mime": "image/png", "run_id": gemma_run_id, "space_url": SPACE_URL}}
|
| 197 |
|
| 198 |
+
if requests.post(url, headers=headers, json=payload_gemma).status_code == 204:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
waited = 0
|
| 200 |
while waited < 90:
|
| 201 |
if os.path.exists(gemma_txt_path):
|
| 202 |
+
with open(gemma_txt_path, "r", encoding="utf-8") as f: english_prompt = f.read().strip()
|
|
|
|
| 203 |
gemma_success = True
|
| 204 |
break
|
| 205 |
+
time.sleep(3); waited += 3
|
|
|
|
| 206 |
|
| 207 |
if gemma_success:
|
| 208 |
+
english_prompt = english_prompt.replace("```text", "").replace("```", "").replace("```json", "").strip()
|
| 209 |
+
if "پردازش متوقف شد" in english_prompt: gemma_success = False
|
| 210 |
+
else: break
|
| 211 |
+
except: pass
|
| 212 |
+
if not gemma_success: time.sleep(4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
if not gemma_success:
|
| 215 |
+
try: english_prompt = GoogleTranslator(source='auto', target='en').translate(user_prompt) + ", cinematic motion, 8k" if user_prompt else "cinematic motion, photorealistic, high detail, smooth animation, 8k"
|
| 216 |
+
except: english_prompt = "cinematic motion, photorealistic, high detail, smooth animation, 8k"
|
| 217 |
+
|
| 218 |
+
payload_video = {"event_type": "generate-video", "client_payload": {"prompt": english_prompt, "duration": duration, "image_url": image_public_url, "run_id": run_id, "space_url": SPACE_URL}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
r_vid = requests.post(url, headers=headers, json=payload_video)
|
| 220 |
|
| 221 |
+
if r_vid.status_code == 204: return jsonify({"status": "success", "run_id": run_id, "translated": english_prompt})
|
| 222 |
+
else: return jsonify({"status": "error", "message": r_vid.text}), 400
|
| 223 |
+
|
| 224 |
+
# ==========================================
|
| 225 |
+
# سیستم چت بات همراه با استریم و دیباگر هوشمند
|
| 226 |
+
# ==========================================
|
| 227 |
|
|
|
|
| 228 |
@app.route('/api/chat', methods=['POST'])
|
| 229 |
def chat_api():
|
| 230 |
+
if not GITHUB_TOKEN: return jsonify({"status": "error", "message": "⚠️ توکن گیتهاب در اسپیس ست نشده است!"}), 500
|
|
|
|
| 231 |
text_prompt = request.form.get('prompt', '')
|
| 232 |
+
if has_forbidden_words(text_prompt): return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
|
|
|
|
|
|
|
| 233 |
|
| 234 |
file_obj = request.files.get('file')
|
| 235 |
run_id = str(uuid.uuid4())
|
|
|
|
| 243 |
file_obj.save(f"static/images/{filename}")
|
| 244 |
file_url = f"{SPACE_URL}/static/images/{filename}"
|
| 245 |
|
| 246 |
+
try:
|
| 247 |
+
conn = get_chat_queue_db_connection()
|
| 248 |
+
c = conn.cursor()
|
| 249 |
+
c.execute("INSERT INTO queue (run_id, prompt, file_url, file_mime, status, timestamp) VALUES (?, ?, ?, ?, 'pending', ?)",
|
| 250 |
+
(run_id, text_prompt, file_url, mime_type, time.time()))
|
| 251 |
+
conn.commit()
|
| 252 |
+
conn.close()
|
| 253 |
+
|
| 254 |
+
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
| 255 |
+
payload = {"event_type": "chat-worker-pool", "client_payload": {"space_url": SPACE_URL}}
|
| 256 |
+
requests.post(f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches", headers=headers, json=payload)
|
| 257 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
return jsonify({"status": "success", "run_id": run_id})
|
| 259 |
+
except Exception as e:
|
| 260 |
+
print(f"Chat DB Error: {e}")
|
| 261 |
+
return jsonify({"status": "error", "message": f"⚠️ خطای دیتابیس در ثبت پیام: {str(e)}"}), 500
|
| 262 |
+
|
| 263 |
+
@app.route('/api/worker/poll', methods=['POST'])
|
| 264 |
+
def worker_poll():
|
| 265 |
+
start_time = time.time()
|
| 266 |
+
while time.time() - start_time < 25:
|
| 267 |
+
try:
|
| 268 |
+
conn = get_chat_queue_db_connection()
|
| 269 |
+
conn.row_factory = sqlite3.Row
|
| 270 |
+
c = conn.cursor()
|
| 271 |
+
|
| 272 |
+
c.execute("SELECT run_id FROM queue WHERE status='pending' ORDER BY timestamp ASC LIMIT 1")
|
| 273 |
+
row = c.fetchone()
|
| 274 |
+
|
| 275 |
+
if row:
|
| 276 |
+
r_id = row['run_id']
|
| 277 |
+
c.execute("UPDATE queue SET status='processing' WHERE run_id=? AND status='pending'", (r_id,))
|
| 278 |
+
if c.rowcount > 0:
|
| 279 |
+
c.execute("SELECT * FROM queue WHERE run_id=?", (r_id,))
|
| 280 |
+
job = c.fetchone()
|
| 281 |
+
conn.commit()
|
| 282 |
+
conn.close()
|
| 283 |
+
return jsonify({"status": "job", "job": dict(job)})
|
| 284 |
+
conn.commit()
|
| 285 |
+
conn.close()
|
| 286 |
+
except Exception as e:
|
| 287 |
+
print(f"Poll DB error: {e}") # پرینت در لاگ اسپیس برای دیباگ
|
| 288 |
+
|
| 289 |
+
time.sleep(2)
|
| 290 |
+
return jsonify({"status": "empty"})
|
| 291 |
+
|
| 292 |
+
@app.route('/api/webhook/stream', methods=['POST'])
|
| 293 |
+
def webhook_stream():
|
| 294 |
+
run_id = request.form.get('run_id')
|
| 295 |
+
text = request.form.get('text', '')
|
| 296 |
+
if run_id:
|
| 297 |
+
try:
|
| 298 |
+
with open(f"static/images/{run_id}.stream.txt", "w", encoding="utf-8") as f:
|
| 299 |
+
f.write(text)
|
| 300 |
+
except Exception as e:
|
| 301 |
+
print(f"Webhook write error: {e}")
|
| 302 |
+
return "OK", 200
|
| 303 |
+
|
| 304 |
+
@app.route('/api/stream_chat/<run_id>')
|
| 305 |
+
def stream_chat(run_id):
|
| 306 |
+
def generate():
|
| 307 |
+
stream_file = f"static/images/{run_id}.stream.txt"
|
| 308 |
+
final_file = f"static/images/{run_id}.txt"
|
| 309 |
+
last_text = ""
|
| 310 |
+
|
| 311 |
+
# 60 ثانیه انتظار - اگر اکشن گیت هاب کار نکند این ارور در صفحه چت نوشته میشود
|
| 312 |
+
for _ in range(300):
|
| 313 |
+
if os.path.exists(stream_file) or os.path.exists(final_file):
|
| 314 |
+
break
|
| 315 |
+
time.sleep(0.2)
|
| 316 |
+
else:
|
| 317 |
+
error_msg = "⚠️ **خطای سیستم:** اکشن گیتهاب روشن نشد یا نتوانست در ۶۰ ثانیه پیام را پردازش کند. لطفاً بخش لاگهای Actions در گیتهاب را بررسی کنید."
|
| 318 |
+
yield f"data: {json.dumps({'text': error_msg})}\n\n"
|
| 319 |
+
yield f"data: DONE\n\n"
|
| 320 |
+
return
|
| 321 |
+
|
| 322 |
+
while True:
|
| 323 |
+
if os.path.exists(stream_file):
|
| 324 |
+
try:
|
| 325 |
+
with open(stream_file, "r", encoding="utf-8") as f: current_text = f.read()
|
| 326 |
+
if current_text and current_text != last_text:
|
| 327 |
+
yield f"data: {json.dumps({'text': current_text})}\n\n"
|
| 328 |
+
last_text = current_text
|
| 329 |
+
except: pass
|
| 330 |
+
|
| 331 |
+
if os.path.exists(final_file):
|
| 332 |
+
try:
|
| 333 |
+
with open(final_file, "r", encoding="utf-8") as f: final_text = f.read()
|
| 334 |
+
if final_text and final_text != last_text:
|
| 335 |
+
yield f"data: {json.dumps({'text': final_text})}\n\n"
|
| 336 |
+
except: pass
|
| 337 |
+
yield f"data: DONE\n\n"
|
| 338 |
+
break
|
| 339 |
+
time.sleep(0.15)
|
| 340 |
+
|
| 341 |
+
return Response(generate(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', 'Connection': 'keep-alive'})
|
| 342 |
+
|
| 343 |
+
def maintain_worker_pool():
|
| 344 |
+
while True:
|
| 345 |
+
time.sleep(40)
|
| 346 |
+
if not GITHUB_TOKEN or not SPACE_URL: continue
|
| 347 |
+
try:
|
| 348 |
+
url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/actions/runs?event=repository_dispatch"
|
| 349 |
+
headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
|
| 350 |
+
r = requests.get(url, headers=headers)
|
| 351 |
+
|
| 352 |
+
if r.status_code != 200:
|
| 353 |
+
payload = {"event_type": "chat-worker-pool", "client_payload": {"space_url": SPACE_URL}}
|
| 354 |
+
requests.post(f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches", headers=headers, json=payload)
|
| 355 |
+
time.sleep(120)
|
| 356 |
+
continue
|
| 357 |
+
|
| 358 |
+
runs = r.json().get('workflow_runs', [])
|
| 359 |
+
active_count = sum(1 for run in runs if run.get('name') == 'Gemma 4 Chatbot Worker' and run.get('status') in ['in_progress', 'queued'])
|
| 360 |
+
|
| 361 |
+
if active_count < 3:
|
| 362 |
+
dispatch_url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
|
| 363 |
+
payload = {"event_type": "chat-worker-pool", "client_payload": {"space_url": SPACE_URL}}
|
| 364 |
+
for _ in range(3 - active_count):
|
| 365 |
+
requests.post(dispatch_url, headers=headers, json=payload)
|
| 366 |
+
time.sleep(1)
|
| 367 |
+
except: pass
|
| 368 |
+
|
| 369 |
+
# ==========================================
|
| 370 |
|
|
|
|
| 371 |
@app.route('/api/merge-videos', methods=['POST'])
|
| 372 |
def merge_videos():
|
| 373 |
if 'base_video' not in request.files or 'new_clip' not in request.files:
|
|
|
|
| 375 |
|
| 376 |
base_video_file = request.files['base_video']
|
| 377 |
new_clip_file = request.files['new_clip']
|
|
|
|
| 378 |
temp_files_to_clean = []
|
| 379 |
|
| 380 |
try:
|
|
|
|
| 383 |
output_path = os.path.join(TEMP_DIR, f"{uuid.uuid4().hex}.mp4")
|
| 384 |
|
| 385 |
temp_files_to_clean.extend([base_video_path, new_clip_path, output_path])
|
|
|
|
| 386 |
base_video_file.save(base_video_path)
|
| 387 |
new_clip_file.save(new_clip_path)
|
| 388 |
|
| 389 |
probe = ffmpeg.probe(base_video_path)
|
| 390 |
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
|
| 391 |
+
width, height = int(video_stream['width']), int(video_stream['height'])
|
|
|
|
| 392 |
|
| 393 |
input1 = ffmpeg.input(base_video_path)
|
| 394 |
input2 = ffmpeg.input(new_clip_path).filter('scale', width, height).filter('setsar', '1')
|
| 395 |
+
merged_video = ffmpeg.concat(input1, input2, v=1, a=0).output(output_path, crf=18, preset='slow', pix_fmt='yuv420p')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
merged_video.run(overwrite_output=True, quiet=True)
|
| 397 |
|
| 398 |
@after_this_request
|
| 399 |
def cleanup(response):
|
| 400 |
try:
|
| 401 |
for f_path in temp_files_to_clean:
|
| 402 |
+
if os.path.exists(f_path): os.remove(f_path)
|
| 403 |
+
except: pass
|
|
|
|
|
|
|
| 404 |
return response
|
|
|
|
| 405 |
return send_file(output_path, mimetype='video/mp4', as_attachment=True, download_name='merged_video.mp4')
|
| 406 |
|
| 407 |
+
except Exception:
|
| 408 |
for f_path in temp_files_to_clean:
|
| 409 |
+
if os.path.exists(f_path): os.remove(f_path)
|
| 410 |
+
return jsonify({"error": "خطا در پردازش ویدیو"}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
|
|
|
| 412 |
@app.route('/api/check-credit', methods=['POST'])
|
| 413 |
def check_credit():
|
| 414 |
data = request.get_json()
|
| 415 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 416 |
user_id = get_user_identifier(data)
|
|
|
|
| 417 |
conn = get_db_connection()
|
| 418 |
c = conn.cursor()
|
| 419 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 420 |
user_record = c.fetchone()
|
|
|
|
| 421 |
now = time.time()
|
| 422 |
credits_remaining = USAGE_LIMIT
|
| 423 |
limit_reached = False
|
| 424 |
reset_timestamp = 0
|
|
|
|
| 425 |
if user_record:
|
| 426 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
| 427 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 428 |
conn.commit()
|
|
|
|
| 429 |
else:
|
| 430 |
credits_remaining = max(0, USAGE_LIMIT - user_record['count'])
|
| 431 |
if credits_remaining == 0:
|
| 432 |
limit_reached = True
|
| 433 |
reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
|
|
|
|
| 434 |
conn.close()
|
| 435 |
+
return jsonify({"credits_remaining": credits_remaining, "limit_reached": limit_reached, "reset_timestamp": reset_timestamp})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
@app.route('/api/use-credit', methods=['POST'])
|
| 438 |
def use_credit():
|
| 439 |
data = request.get_json()
|
| 440 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 441 |
user_id = get_user_identifier(data)
|
|
|
|
| 442 |
conn = get_db_connection()
|
| 443 |
c = conn.cursor()
|
| 444 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 445 |
user_record = c.fetchone()
|
|
|
|
| 446 |
now = time.time()
|
| 447 |
if user_record:
|
| 448 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
|
|
|
| 450 |
else:
|
| 451 |
if user_record['count'] >= USAGE_LIMIT:
|
| 452 |
conn.close()
|
| 453 |
+
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": user_record['week_start'] + ONE_WEEK_SECONDS}), 429
|
|
|
|
| 454 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 455 |
else:
|
| 456 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
|
|
|
| 457 |
conn.commit()
|
| 458 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 459 |
+
credits_remaining = USAGE_LIMIT - c.fetchone()['count']
|
|
|
|
|
|
|
| 460 |
conn.close()
|
| 461 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 462 |
|
|
|
|
|
|
|
| 463 |
@app.route('/api/check-image-credit', methods=['POST'])
|
| 464 |
def check_image_credit():
|
| 465 |
data = request.get_json()
|
| 466 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 467 |
user_id = get_user_identifier(data)
|
|
|
|
| 468 |
conn = get_image_db_connection()
|
| 469 |
c = conn.cursor()
|
| 470 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 471 |
user_record = c.fetchone()
|
|
|
|
| 472 |
now = time.time()
|
| 473 |
credits_remaining = IMAGE_USAGE_LIMIT
|
| 474 |
limit_reached = False
|
| 475 |
reset_timestamp = 0
|
|
|
|
| 476 |
if user_record:
|
| 477 |
if user_record['week_start'] < (now - ONE_DAY_SECONDS):
|
| 478 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 479 |
conn.commit()
|
|
|
|
| 480 |
else:
|
| 481 |
credits_remaining = max(0, IMAGE_USAGE_LIMIT - user_record['count'])
|
| 482 |
if credits_remaining == 0:
|
| 483 |
limit_reached = True
|
| 484 |
reset_timestamp = user_record['week_start'] + ONE_DAY_SECONDS
|
|
|
|
| 485 |
conn.close()
|
| 486 |
+
return jsonify({"credits_remaining": credits_remaining, "limit_reached": limit_reached, "reset_timestamp": reset_timestamp})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
@app.route('/api/use-image-credit', methods=['POST'])
|
| 489 |
def use_image_credit():
|
| 490 |
data = request.get_json()
|
| 491 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 492 |
user_id = get_user_identifier(data)
|
|
|
|
| 493 |
conn = get_image_db_connection()
|
| 494 |
c = conn.cursor()
|
| 495 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 496 |
user_record = c.fetchone()
|
|
|
|
| 497 |
now = time.time()
|
| 498 |
if user_record:
|
| 499 |
if user_record['week_start'] < (now - ONE_DAY_SECONDS):
|
|
|
|
| 501 |
else:
|
| 502 |
if user_record['count'] >= IMAGE_USAGE_LIMIT:
|
| 503 |
conn.close()
|
| 504 |
+
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": user_record['week_start'] + ONE_DAY_SECONDS}), 429
|
|
|
|
| 505 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 506 |
else:
|
| 507 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
|
|
|
| 508 |
conn.commit()
|
| 509 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 510 |
+
credits_remaining = IMAGE_USAGE_LIMIT - c.fetchone()['count']
|
|
|
|
|
|
|
| 511 |
conn.close()
|
| 512 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 513 |
|
|
|
|
|
|
|
| 514 |
@app.route('/api/check-edit-credit', methods=['POST'])
|
| 515 |
def check_edit_credit():
|
| 516 |
data = request.get_json()
|
| 517 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 518 |
user_id = get_user_identifier(data)
|
|
|
|
| 519 |
conn = get_image_edit_db_connection()
|
| 520 |
c = conn.cursor()
|
| 521 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 522 |
user_record = c.fetchone()
|
|
|
|
| 523 |
now = time.time()
|
| 524 |
credits_remaining = EDIT_USAGE_LIMIT
|
| 525 |
limit_reached = False
|
| 526 |
reset_timestamp = 0
|
|
|
|
| 527 |
if user_record:
|
| 528 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
| 529 |
c.execute("UPDATE users SET count = 0, week_start = ? WHERE id = ?", (now, user_id))
|
| 530 |
conn.commit()
|
|
|
|
| 531 |
else:
|
| 532 |
credits_remaining = max(0, EDIT_USAGE_LIMIT - user_record['count'])
|
| 533 |
if credits_remaining == 0:
|
| 534 |
limit_reached = True
|
| 535 |
reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
|
|
|
|
| 536 |
conn.close()
|
| 537 |
+
return jsonify({"credits_remaining": credits_remaining, "limit_reached": limit_reached, "reset_timestamp": reset_timestamp})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
@app.route('/api/use-edit-credit', methods=['POST'])
|
| 540 |
def use_edit_credit():
|
| 541 |
data = request.get_json()
|
| 542 |
if not data: return jsonify({"error": "Invalid request"}), 400
|
| 543 |
user_id = get_user_identifier(data)
|
|
|
|
| 544 |
conn = get_image_edit_db_connection()
|
| 545 |
c = conn.cursor()
|
| 546 |
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 547 |
user_record = c.fetchone()
|
|
|
|
| 548 |
now = time.time()
|
| 549 |
if user_record:
|
| 550 |
if user_record['week_start'] < (now - ONE_WEEK_SECONDS):
|
|
|
|
| 552 |
else:
|
| 553 |
if user_record['count'] >= EDIT_USAGE_LIMIT:
|
| 554 |
conn.close()
|
| 555 |
+
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": user_record['week_start'] + ONE_WEEK_SECONDS}), 429
|
|
|
|
| 556 |
c.execute("UPDATE users SET count = count + 1 WHERE id = ?", (user_id,))
|
| 557 |
else:
|
| 558 |
c.execute("INSERT INTO users (id, count, week_start) VALUES (?, 1, ?)", (user_id, now))
|
|
|
|
| 559 |
conn.commit()
|
| 560 |
c.execute("SELECT count FROM users WHERE id = ?", (user_id,))
|
| 561 |
+
credits_remaining = EDIT_USAGE_LIMIT - c.fetchone()['count']
|
|
|
|
|
|
|
| 562 |
conn.close()
|
| 563 |
return jsonify({"status": "success", "credits_remaining": credits_remaining})
|
| 564 |
|
|
|
|
|
|
|
| 565 |
@app.route('/api/webhook/upload', methods=['POST'])
|
| 566 |
def webhook_upload():
|
| 567 |
run_id = request.form.get('run_id')
|
| 568 |
gh_run_id = request.form.get('github_run_id')
|
| 569 |
ext = request.form.get('ext', 'webp')
|
| 570 |
+
if 'file' not in request.files or not run_id: return "Invalid request", 400
|
|
|
|
|
|
|
|
|
|
| 571 |
file = request.files['file']
|
| 572 |
file.save(f"static/images/{run_id}.{ext}")
|
|
|
|
| 573 |
if gh_run_id: threading.Thread(target=delete_github_run, args=(gh_run_id,)).start()
|
| 574 |
return "OK", 200
|
| 575 |
|
| 576 |
@app.route('/api/status/<run_id>')
|
| 577 |
def check_status(run_id):
|
| 578 |
+
for ext in ['png', 'jpg', 'jpeg', 'webp', 'gif', 'mp4', 'webm', 'mov', 'mkv', 'avi', 'mp3', 'wav', 'ogg', 'm4a', 'txt', 'json']:
|
| 579 |
+
if os.path.exists(f"static/images/{run_id}.{ext}"): return jsonify({"status": "ready", "url": f"/static/images/{run_id}.{ext}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
return jsonify({"status": "processing"})
|
| 581 |
|
| 582 |
@app.route('/static/images/<filename>')
|
| 583 |
+
def serve_file(filename): return send_from_directory('static/images', filename)
|
|
|
|
| 584 |
|
| 585 |
if __name__ == '__main__':
|
| 586 |
+
threading.Thread(target=maintain_worker_pool, daemon=True).start()
|
| 587 |
+
app.run(host='0.0.0.0', port=7860)
|