Sada8888 commited on
Commit
4778604
·
verified ·
1 Parent(s): 4fb4c45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -274
app.py CHANGED
@@ -6,7 +6,8 @@ import time
6
  import sqlite3
7
  import tempfile
8
  import ffmpeg
9
- from flask import Flask, request, jsonify, render_template, send_from_directory, send_file, after_this_request
 
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
- (id TEXT PRIMARY KEY, count INTEGER, week_start REAL)''')
 
 
 
 
 
 
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
- page_name += '.html'
112
- try:
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
- return jsonify({"status": "error", "message": "متن ورودی شامل کلمات نامناسب است."}), 400
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
- "status": "success",
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
- return jsonify({"status": "success", "run_id": run_id, "translated": english_prompt})
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
- local_image_path = os.path.join("static/images", input_filename)
216
- file.save(local_image_path)
217
  image_public_url = f"{SPACE_URL}/static/images/{input_filename}"
218
 
219
- 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.
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
- if "```" in english_prompt:
268
- english_prompt = english_prompt.replace("```text", "").replace("```", "").replace("```json", "").strip()
269
-
270
- if "پردازش متوقف شد" in english_prompt or "سهمیه سرور موقتاً پر شده" in english_prompt:
271
- print(f"Gemma returned quota error on attempt {attempt + 1}. Retrying...")
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
- print("Gemma action failed or returned errors. Using fallback translation.")
284
- try:
285
- if user_prompt:
286
- english_prompt = GoogleTranslator(source='auto', target='en').translate(user_prompt)
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
- return jsonify({"status": "success", "run_id": run_id, "translated": english_prompt})
307
- else:
308
- return jsonify({"status": "error", "message": r_vid.text}), 400
 
 
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": "Token missing"}), 500
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
- url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/dispatches"
333
- headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {GITHUB_TOKEN}"}
334
-
335
- payload = {
336
- "event_type": "chat-gemma",
337
- "client_payload": {
338
- "prompt": text_prompt,
339
- "file_url": file_url,
340
- "file_mime": mime_type,
341
- "run_id": run_id,
342
- "space_url": SPACE_URL
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
- else:
350
- return jsonify({"status": "error", "message": r.text}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- os.remove(f_path)
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 ffmpeg.Error as e:
403
  for f_path in temp_files_to_clean:
404
- if os.path.exists(f_path):
405
- os.remove(f_path)
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
- reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
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
- new_count = c.fetchone()['count']
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
- reset_timestamp = user_record['week_start'] + ONE_DAY_SECONDS
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
- new_count = c.fetchone()['count']
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
- reset_timestamp = user_record['week_start'] + ONE_WEEK_SECONDS
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
- new_count = c.fetchone()['count']
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
- valid_extensions = [
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
- app.run(host='0.0.0.0', port=7860)
 
 
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)