File size: 14,884 Bytes
f6a570d 3c9e501 f6a570d d38182d 3648bc0 f6a570d d38182d f6a570d d38182d 2b94fac 3c9e501 f6a570d 3c9e501 2b94fac ad4ac6e 2b94fac ad4ac6e 2b94fac ad4ac6e 3c9e501 ad4ac6e f6a570d afb7b47 f6a570d ad4ac6e f6a570d 55e8938 f6a570d 2b94fac 3c9e501 dc83a2d 3c9e501 d38182d 2b94fac f6a570d afb7b47 f6a570d d38182d afb7b47 2b94fac afb7b47 d38182d afb7b47 55e8938 2b94fac d38182d f6a570d 09bfe29 ad4ac6e afb7b47 09bfe29 afb7b47 2b94fac afb7b47 2b94fac 55e8938 09bfe29 afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d d38182d 3c9e501 afb7b47 3c9e501 d38182d afb7b47 2b94fac afb7b47 3c9e501 afb7b47 dc83a2d 2b94fac 3c9e501 2b94fac d38182d 3c9e501 2b94fac 3c9e501 2b94fac 3c9e501 be3e773 2b94fac 3c9e501 2b94fac 3c9e501 2b94fac 3c9e501 2b94fac 3c9e501 2b94fac 3c9e501 2b94fac f6a570d e058d11 afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d afb7b47 d38182d afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d afb7b47 f6a570d e058d11 afb7b47 f6a570d d38182d f6a570d e058d11 afb7b47 f6a570d afb7b47 2b94fac 585c6fc 2b94fac f6a570d 09bfe29 afb7b47 3c9e501 afb7b47 f6a570d afb7b47 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | import os
import json
import time
import requests
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import logging
import threading
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
import tempfile
# --- تنظیمات اصلی ---
DATASET_REPO = "Ezmary/Karbaran-rayegan-tedad"
DATASET_FILENAME = "video_usage_data.json"
USAGE_LIMIT = 5
HF_TOKEN = os.environ.get("HF_TOKEN")
TEMP_DIR = "/app/tmp"
# ✅✅✅ آدرس ورودی اصلی (مسیریاب) که از متغیرهای راز خوانده میشود
ROUTER_WORKER_URL = os.environ.get("ROUTER_WORKER_URL")
# --- تنظیمات لاگینگ ---
# فقط پیام لاگ نمایش داده میشود
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(level=logging.INFO, format='%(message)s')
# ساکت کردن لاگهای کتابخانههای پرحرف
logging.getLogger('huggingface_hub').setLevel(logging.ERROR)
logging.getLogger('gunicorn.error').setLevel(logging.WARNING)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
logging.getLogger("urllib3").setLevel(logging.WARNING)
app = Flask(__name__)
CORS(app)
# --- مدیریت دادههای کاربران و قفلها ---
usage_data_cache = []
cache_lock = threading.Lock()
data_changed = threading.Event()
persistence_lock = threading.Lock()
api = None
if HF_TOKEN:
api = HfApi(token=HF_TOKEN)
# بررسی وجود آدرس مسیریاب در هنگام شروع به کار
if not ROUTER_WORKER_URL:
logging.error("❌ CRITICAL ERROR: The ROUTER_WORKER_URL environment variable is not set!")
else:
logging.info(f"✅ Main app is configured to use the router at: {ROUTER_WORKER_URL}")
# --- توابع مدیریت داده (بدون تغییر) ---
def load_initial_data():
global usage_data_cache
with cache_lock:
if not api: return
try:
with tempfile.TemporaryDirectory(dir=TEMP_DIR) as tmp_download_dir:
local_path = hf_hub_download(
repo_id=DATASET_REPO,
filename=DATASET_FILENAME,
repo_type="dataset",
token=HF_TOKEN,
force_download=True,
cache_dir=tmp_download_dir
)
with open(local_path, 'r', encoding='utf-8') as f:
content = f.read()
usage_data_cache = json.loads(content) if content else []
except Exception:
# در صورت بروز خطا، با لیست خالی شروع میکنیم
usage_data_cache = []
def persist_data_to_hub():
with persistence_lock:
if not data_changed.is_set() or not api: return
with cache_lock:
data_to_write = list(usage_data_cache)
data_changed.clear()
temp_filepath = None
try:
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=TEMP_DIR, delete=False, suffix='.json') as temp_f:
temp_filepath = temp_f.name
json.dump(data_to_write, temp_f, ensure_ascii=False, indent=2)
api.upload_file(
path_or_fileobj=temp_filepath,
path_in_repo=DATASET_FILENAME,
repo_id=DATASET_REPO,
repo_type="dataset",
commit_message="Update animation usage data"
)
except Exception:
# اگر آپلود ناموفق بود، دوباره پرچم را تنظیم میکنیم تا در تلاش بعدی ارسال شود
data_changed.set()
finally:
if temp_filepath and os.path.exists(temp_filepath):
os.remove(temp_filepath)
def background_persister():
while True:
time.sleep(30)
persist_data_to_hub()
# --- روتهای API ---
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/enhance-animation-prompt', methods=['POST'])
def enhance_animation_prompt():
if not ROUTER_WORKER_URL:
return jsonify({"error": "سرویس هوش مصنوعی به درستی پیکربندی نشده است."}), 503
user_prompt_text = request.form.get('prompt', '')
image_file = request.files.get('image')
if not image_file:
return jsonify({"error": "فایل تصویر الزامی است."}), 400
# 🧠🧠🧠 شاه پرامپت کامل در اینجا ساخته میشود 🧠🧠🧠
gemini_master_prompt = f"""
You are an expert AI Animation Planner. Your absolute highest priority is to faithfully and creatively execute the user's specific request. You are not just an artist; you are a technical problem solver.
**Input Analysis:**
1. **Image Content:** A still image.
2. **User's Idea (Persian):** "{user_prompt_text if user_prompt_text else 'این تصویر را به زیبایی و به صورت سینمایی متحرک کن'}"
**CRITICAL Decision-Making Framework (Follow these steps PRECISELY):**
**Step 1: Analyze the User's Intent.**
* Is the user's prompt empty or very generic (like "animate this")?
* If YES, proceed to **Mode A: Default Cinematic Enhancement**.
* Does the user's prompt describe a specific action or effect (e.g., "clouds moving," "playing guitar," "slow zoom out")?
* If YES, proceed to **Mode B: User-Directed Animation**.
---
**Mode A: Default Cinematic Enhancement (Only when the user gives no specific direction)**
* **Goal:** Add subtle, high-quality, believable motion to the image to bring it to life.
* **Action:** Analyze the image for potential motion.
* **Landscapes:** Animate clouds, water, fog, light rays, gentle parallax effect.
* **Portraits:** Add subtle breathing, eye blinks, gentle hair movement from a breeze.
* **Objects:** Add subtle light reflections or environmental effects.
* **Output:** Create an `animation_prompt` focused on these subtle, ambient movements. A simple camera move like a "very slow zoom in" is acceptable if it enhances the mood.
---
**Mode B: User-Directed Animation (The HIGHEST priority)**
* **Goal:** Execute the user's command, no matter what.
* **Action - Sub-Step 1: Feasibility Check.**
* Can the requested action be performed with elements already visible in the image? (e.g., User wants "blinking eyes" on a portrait. The eyes are visible.)
* Or, does the request require creating elements/actions NOT visible in the frame? (e.g., User wants "playing guitar" but the image is only a headshot.)
* **Action - Sub-Step 2: Generate the Plan based on Feasibility.**
* **If action is VISIBLE in-frame:** Your `animation_prompt` must focus **ENTIRELY** on executing that command perfectly. Do not add unnecessary camera moves unless the user requested it.
* *Example:* User says "make the water flow." Your prompt should be: "The water in the river flows realistically, with ripples and gentle waves. The rest of the scene is mostly static. Photorealistic, high detail."
* **If action is NOT VISIBLE in-frame (CRITICAL TASK):** You must **INVENT** the scene. Your primary tool is **revealing camera motion**. You must generate a prompt that starts with the existing frame and then expands it to include the user's request.
* **This is the core of your job.** Do not say it's impossible. *Make it possible.*
* **Example 1 (The Guitar Problem):** User uploads a headshot and writes "playing guitar."
* *Your Thought Process:* The guitar isn't visible. I must pull the camera back to reveal it.
* *Correct `animation_prompt`*: "**Cinematic slow pull back camera movement**, starting from the person's face, to **reveal they are skillfully playing an acoustic guitar**. Their fingers move realistically on the frets. The background is softly blurred. High detail, smooth motion, 8k."
* **Example 2:** User uploads a picture of a car on a road and writes "driving through a city at night."
* *Your Thought Process:* The city isn't there. I need to create it around the car.
* *Correct `animation_prompt`*: "The car drives forward smoothly through a vibrant, neon-lit city at night. Rain streaks across the windshield. Reflections of city lights glide across the car's wet surface. Cinematic, photorealistic, 8k."
---
**Final Output Generation (For BOTH modes):**
Based on your decision, generate the following two keys in English.
1. **`animation_prompt`:** Your detailed script for the animation engine, created according to the rules above. It must be descriptive, technical, and include quality keywords (`cinematic, photorealistic, high detail, smooth motion, 8k`).
2. **`negative_prompt`:** A comprehensive list of what to AVOID.
* **Always include these base negatives:** `ugly, deformed, noisy, blurry, distorted, grainy, shaking, jittery, flickering, unnatural movement, static image, watermark, text, signature, cartoon, anime, 3d render.`
* Add context-specific negatives. For a realistic scene, you might add `painting, illustration`.
**Provide the output ONLY in a clean JSON format, without any markdown or explanations:**
{{
"animation_prompt": "...",
"negative_prompt": "..."
}}
"""
# خواندن فایل تصویر در حافظه
image_file.seek(0)
image_bytes = image_file.read()
endpoint = f"{ROUTER_WORKER_URL}/v1/request"
logging.info(f"Relaying request to router at {endpoint}")
try:
files = {'image': (image_file.filename, image_bytes, image_file.mimetype)}
data = {'prompt': gemini_master_prompt} # شاه پرامپت کامل به مسیریاب ارسال میشود
# ارسال درخواست با تایماوت طولانیتر برای اطمینان
response = requests.post(endpoint, files=files, data=data, timeout=100)
# اگر پاسخ خطا بود (مثلاً 5xx)، استثنا ایجاد میکند
response.raise_for_status()
logging.info("✅ Successfully received response from router.")
return jsonify(response.json())
except requests.exceptions.Timeout:
logging.error("❌ Timeout error: The request to the router timed out.")
return jsonify({"error": "سرور پردازش با تاخیر مواجه است. لطفاً چند لحظه دیگر دوباره تلاش کنید."}), 504
except requests.exceptions.RequestException as e:
error_detail = "سرویس هوش مصنوعی با مشکل مواجه شده است. لطفاً بعداً تلاش کنید."
if e.response is not None:
try:
# تلاش برای استخراج پیام خطای اصلی از مسیریاب
router_error = e.response.json().get("detail", e.response.text)
error_detail = f"خطا از سرور پردازش: {router_error}"
except json.JSONDecodeError:
error_detail = f"سرور پردازش با خطای {e.response.status_code} پاسخ داد."
logging.error(f"❌ RequestException when contacting router: {error_detail}")
return jsonify({"error": error_detail}), 503
# --- روتهای مدیریت اعتبار (بدون تغییر) ---
def get_user_identifier(data):
fingerprint = data.get('fingerprint')
if fingerprint: return str(fingerprint)
if request.headers.getlist("X-Forwarded-For"):
return request.headers.getlist("X-Forwarded-For")[0].split(',')[0].strip()
return request.remote_addr
@app.route('/api/check-credit', methods=['POST'])
def check_credit():
data = request.get_json()
if not data: return jsonify({"error": "Invalid request"}), 400
user_id = get_user_identifier(data)
if not user_id: return jsonify({"error": "User identifier is required."}), 400
with cache_lock:
now = time.time()
one_week_seconds = 7 * 24 * 60 * 60
user_record = next((user for user in usage_data_cache if user.get('id') == user_id), None)
credits_remaining = USAGE_LIMIT
limit_reached = False
reset_timestamp = 0
if user_record:
if user_record.get('week_start', 0) < (now - one_week_seconds):
user_record['count'] = 0
user_record['week_start'] = now
data_changed.set()
credits_remaining = max(0, USAGE_LIMIT - user_record.get('count', 0))
if credits_remaining == 0:
limit_reached = True
reset_timestamp = user_record.get('week_start', now) + one_week_seconds
return jsonify({"credits_remaining": credits_remaining, "limit_reached": limit_reached, "reset_timestamp": reset_timestamp})
@app.route('/api/use-credit', methods=['POST'])
def use_credit():
data = request.get_json()
if not data: return jsonify({"error": "Invalid request"}), 400
user_id = get_user_identifier(data)
if not user_id: return jsonify({"error": "User identifier is required."}), 400
with cache_lock:
now = time.time()
one_week_seconds = 7 * 24 * 60 * 60
user_record = next((user for user in usage_data_cache if user.get('id') == user_id), None)
if user_record:
if user_record.get('week_start', 0) < (now - one_week_seconds):
user_record['count'] = 0
user_record['week_start'] = now
if user_record.get('count', 0) >= USAGE_LIMIT:
reset_timestamp = user_record.get('week_start', now) + one_week_seconds
return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": reset_timestamp}), 429
user_record['count'] += 1
else:
user_record = {"id": user_id, "count": 1, "week_start": now}
usage_data_cache.append(user_record)
credits_remaining = USAGE_LIMIT - user_record['count']
data_changed.set()
return jsonify({"status": "success", "credits_remaining": credits_remaining})
# --- روت بررسی سلامت (Health Check) ---
@app.route('/health', methods=['GET'])
def health_check():
return "OK", 200
# --- راهاندازی برنامه (بدون تغییر) ---
if __name__ != '__main__':
try:
load_initial_data()
persister_thread = threading.Thread(target=background_persister, daemon=True)
persister_thread.start()
except Exception as e:
logging.critical(f"Startup failed: {e}")
raise SystemExit("Startup failed due to an exception.")
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
app.run(host='0.0.0.0', port=port) |