Update app.py
Browse files
app.py
CHANGED
|
@@ -13,7 +13,7 @@ from typing import List, Dict, Optional, Tuple
|
|
| 13 |
# --- КОНФИГУРАЦИЯ ---
|
| 14 |
# URL адреса API
|
| 15 |
TEXT_API_URL = "https://text.pollinations.ai/openai"
|
| 16 |
-
IMAGE_API_URL = "https://image.pollinations.ai/prompt"
|
| 17 |
|
| 18 |
# Файлы и директории
|
| 19 |
API_KEYS_FILE = "api_keys.txt" # Ключи только для генерации ИЗОБРАЖЕНИЙ
|
|
@@ -30,11 +30,21 @@ class APIKeyRotator:
|
|
| 30 |
"""Управляет загрузкой, ротацией и отслеживанием использования API ключей."""
|
| 31 |
def __init__(self, filepath: str):
|
| 32 |
self.filepath = filepath
|
| 33 |
-
|
|
|
|
| 34 |
self.current_index: int = 0
|
| 35 |
self.usage_count: Dict[str, int] = {key: 0 for key in self.keys}
|
| 36 |
|
| 37 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
try:
|
| 39 |
with open(self.filepath, 'r', encoding='utf-8') as f:
|
| 40 |
return [line.strip() for line in f.readlines() if line.strip()]
|
|
@@ -50,7 +60,7 @@ class APIKeyRotator:
|
|
| 50 |
|
| 51 |
def get_status(self) -> str:
|
| 52 |
if not self.keys:
|
| 53 |
-
return "🔑 **Статус**: API ключи для генерации изображений не
|
| 54 |
status_lines = [f"🔑 **Загружено ключей (для изображений)**: {len(self.keys)}"]
|
| 55 |
for i, key in enumerate(self.keys):
|
| 56 |
masked_key = f"{key[:8]}...{key[-4:]}" if len(key) > 12 else key
|
|
@@ -71,32 +81,22 @@ key_rotator = APIKeyRotator(API_KEYS_FILE)
|
|
| 71 |
async def _call_director_api(session: aiohttp.ClientSession, prompt: str, seed: int) -> Tuple[Optional[List[str]], str]:
|
| 72 |
"""Запрашивает сценарий у модели 'openai-fast'. Ключ не требуется."""
|
| 73 |
payload = {
|
| 74 |
-
"model": "openai-fast",
|
| 75 |
-
"response_format": {"type": "json_object"},
|
| 76 |
"messages": [
|
| 77 |
-
{
|
| 78 |
-
"role": "system",
|
| 79 |
-
"content": "You are a scriptwriter for a short animated GIF. Generate a JSON object with a key 'prompts', containing a list of strings. The first prompt is a full description of the initial scene. Subsequent prompts describe CHANGES to the previous frame."
|
| 80 |
-
},
|
| 81 |
{"role": "user", "content": prompt}
|
| 82 |
-
],
|
| 83 |
-
"seed": seed
|
| 84 |
}
|
| 85 |
-
# Для openai-fast ключ не нужен, поэтому headers пустые
|
| 86 |
-
headers = {}
|
| 87 |
try:
|
| 88 |
-
async with session.post(TEXT_API_URL, json=payload, headers=
|
| 89 |
-
response_text = await response.text()
|
| 90 |
if response.status == 200:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
except Exception as e:
|
| 96 |
-
return None, f"❌ **Режиссёр**: Ошибка парсинга JSON. {e}\nОтвет: ```{response_text[:200]}```"
|
| 97 |
return None, f"❌ **Режиссёр**: Ошибка API (Статус: {response.status})."
|
| 98 |
except Exception as e:
|
| 99 |
-
return None, f"❌ **Режиссёр**:
|
| 100 |
|
| 101 |
|
| 102 |
async def _call_artist_api(session: aiohttp.ClientSession, params: Dict, output_path: str, previous_image_path: Optional[str] = None) -> bool:
|
|
@@ -105,32 +105,22 @@ async def _call_artist_api(session: aiohttp.ClientSession, params: Dict, output_
|
|
| 105 |
for attempt in range(max_retries):
|
| 106 |
headers = {}
|
| 107 |
api_key = key_rotator.get_next_key()
|
| 108 |
-
if api_key:
|
| 109 |
-
|
| 110 |
-
else:
|
| 111 |
-
# Если ключей нет, можно прервать попытки, но лучше дать шанс сработать без ключа
|
| 112 |
-
print("Warning: No API key found for image generation.")
|
| 113 |
-
|
| 114 |
form = aiohttp.FormData()
|
| 115 |
params["seed"] = params.get("seed", 0) + attempt * 10
|
| 116 |
-
for key, value in params.items():
|
| 117 |
-
form.add_field(key, str(value))
|
| 118 |
|
| 119 |
if previous_image_path and os.path.exists(previous_image_path):
|
| 120 |
-
form.add_field('image', open(previous_image_path, 'rb')
|
| 121 |
|
| 122 |
try:
|
| 123 |
async with session.post(IMAGE_API_URL, data=form, headers=headers, timeout=240) as response:
|
| 124 |
if response.status == 200:
|
| 125 |
-
with open(output_path, "wb") as f:
|
| 126 |
-
f.write(await response.read())
|
| 127 |
return True
|
| 128 |
-
if attempt == max_retries - 1:
|
| 129 |
-
print(f"Artist API Error: Status {response.status} on final attempt.")
|
| 130 |
except Exception as e:
|
| 131 |
-
if attempt == max_retries - 1:
|
| 132 |
-
print(f"Artist API System Error: {e} on final attempt.")
|
| 133 |
-
|
| 134 |
await asyncio.sleep(2)
|
| 135 |
return False
|
| 136 |
|
|
@@ -140,79 +130,55 @@ def _compile_gif_and_archive(image_files: List[str], fps: int, timestamp: str) -
|
|
| 140 |
images = [Image.open(f) for f in image_files]
|
| 141 |
temp_gif_path = os.path.join(OUTPUT_DIR, f"animation_{timestamp}.gif")
|
| 142 |
images[0].save(temp_gif_path, save_all=True, append_images=images[1:], duration=1000 // fps, loop=0)
|
| 143 |
-
|
| 144 |
archive_path = os.path.join(ARCHIVE_DIR, timestamp)
|
| 145 |
frames_archive_path = os.path.join(archive_path, "frames")
|
| 146 |
shutil.move(os.path.dirname(image_files[0]), frames_archive_path)
|
| 147 |
-
|
| 148 |
final_gif_path = os.path.join(archive_path, os.path.basename(temp_gif_path))
|
| 149 |
shutil.move(temp_gif_path, final_gif_path)
|
| 150 |
-
|
| 151 |
return final_gif_path
|
| 152 |
|
| 153 |
|
| 154 |
# --- ОСНОВНАЯ ЛОГИКА ГЕНЕРАЦИИ ---
|
| 155 |
async def generate_gif_flow(
|
| 156 |
proxy: str, main_idea: str, fps: int, duration: int, width: int, height: int,
|
| 157 |
-
artist_model: str, seed_val: int, delay: int,
|
| 158 |
-
progress: gr.Progress
|
| 159 |
) -> Tuple[Optional[str], str]:
|
| 160 |
-
"""Полный цикл генерации GIF."""
|
| 161 |
status_log = ["🚀 **Старт**: Инициализация..."]
|
| 162 |
yield "\n".join(status_log)
|
|
|
|
| 163 |
|
| 164 |
-
if not main_idea: raise gr.Error("Пожалуйста, введите основную идею для анимации.")
|
| 165 |
-
|
| 166 |
-
# 1. Настройка
|
| 167 |
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
| 168 |
session_temp_dir = os.path.join(TEMP_DIR, timestamp)
|
| 169 |
-
os.makedirs(
|
| 170 |
-
|
| 171 |
-
|
| 172 |
total_frames = int(fps * duration)
|
| 173 |
seed = int(seed_val) if seed_val != 0 else random.randint(1, 1000000)
|
| 174 |
status_log.append(f"🌱 **Параметры**: Seed: `{seed}`, Кадров: `{total_frames}`.")
|
| 175 |
yield "\n".join(status_log)
|
| 176 |
|
| 177 |
connector = ProxyConnector.from_url(proxy) if proxy else None
|
| 178 |
-
|
| 179 |
async with aiohttp.ClientSession(connector=connector) as session:
|
| 180 |
-
|
| 181 |
-
status_log.append("🧠 **Режиссёр**: Запрос сценария у `openai-fast`...")
|
| 182 |
-
yield "\n".join(status_log)
|
| 183 |
-
director_prompt = f"Based on the core idea '{main_idea}', generate a list of exactly {total_frames} prompts..."
|
| 184 |
prompts, msg = await _call_director_api(session, director_prompt, seed)
|
| 185 |
-
status_log.append(msg)
|
| 186 |
-
yield "\n".join(status_log)
|
| 187 |
if not prompts: return None, "\n".join(status_log)
|
| 188 |
|
| 189 |
-
|
| 190 |
-
generated_files: List[str] = []
|
| 191 |
-
previous_frame_path: Optional[str] = None
|
| 192 |
-
|
| 193 |
for i in progress.tqdm(range(total_frames), desc="🎨 Генерация кадров"):
|
| 194 |
header = f"🎬 **Кадр {i + 1}/{total_frames}**"
|
| 195 |
frame_path = os.path.join(session_temp_dir, f"frame_{i:04d}.png")
|
| 196 |
-
|
| 197 |
params = {"width": width, "height": height, "seed": seed + i, "nologo": "true", "prompt": prompts[i % len(prompts)]}
|
| 198 |
|
| 199 |
-
if i == 0
|
| 200 |
-
|
| 201 |
-
status_log.append(f"{header}: Режим `text2img` (модель `flux`).")
|
| 202 |
-
else:
|
| 203 |
-
params["model"] = artist_model
|
| 204 |
-
status_log.append(f"{header}: Режим `img2img` (модель `{artist_model}`).")
|
| 205 |
-
|
| 206 |
yield "\n".join(status_log)
|
| 207 |
-
success = await _call_artist_api(session, params, frame_path, previous_frame_path)
|
| 208 |
|
| 209 |
-
if
|
| 210 |
-
status_log.append(f"{header}: ✅
|
| 211 |
generated_files.append(frame_path)
|
| 212 |
previous_frame_path = frame_path
|
| 213 |
else:
|
| 214 |
-
status_log.append(f"❌ **СТОП**: {header}: Не удалось сгенерировать
|
| 215 |
-
yield "\n".join(status_log)
|
| 216 |
break
|
| 217 |
|
| 218 |
yield "\n".join(status_log)
|
|
@@ -222,76 +188,55 @@ async def generate_gif_flow(
|
|
| 222 |
status_log.append("😭 **Результат**: Не создано ни одного кадра.")
|
| 223 |
return None, "\n".join(status_log)
|
| 224 |
|
| 225 |
-
# 4. Сборка и архивация
|
| 226 |
status_log.append("🎞️ **Сборка**: Создание GIF и архивация...")
|
| 227 |
yield "\n".join(status_log)
|
| 228 |
final_gif_path = _compile_gif_and_archive(generated_files, fps, timestamp)
|
| 229 |
status_log.append(f"✅ **Готово**: GIF сохранена в `{final_gif_path}`.")
|
| 230 |
-
|
| 231 |
return final_gif_path, "\n".join(status_log)
|
| 232 |
|
|
|
|
| 233 |
# --- ИНТЕРФЕЙС GRADIO ---
|
| 234 |
def create_ui():
|
| 235 |
-
css = "
|
| 236 |
with gr.Blocks(css=css, title="🎬 GIF Animator Pro", theme=gr.themes.Soft()) as demo:
|
| 237 |
-
gr.HTML("""<div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%); border-radius: 10px; margin-bottom: 20px;"><h1 style="color: white; margin: 0; font-size: 2.8em; text-shadow: 2px 2px 4px #00000040;">🎬 GIF Animator Pro</h1><
|
| 238 |
with gr.Row():
|
| 239 |
with gr.Column(scale=2):
|
| 240 |
-
main_idea=gr.Textbox(label="💡 1. Главная идея анимации",
|
| 241 |
-
|
| 242 |
with gr.Accordion("🤖 2. Выбор модели художника", open=True):
|
| 243 |
-
artist_model=gr.Dropdown(IMAGE_MODELS_IMG2IMG, label="Модель-художник
|
| 244 |
-
|
| 245 |
with gr.Accordion("⚙️ 3. Параметры анимации", open=True):
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
width=gr.Slider(256, 1024, value=512, step=64, label="Ширина")
|
| 251 |
-
height=gr.Slider(256, 1024, value=512, step=64, label="Высота")
|
| 252 |
-
|
| 253 |
with gr.Accordion("🔧 4. Дополнительные настройки", open=False):
|
| 254 |
-
seed=gr.Number(label="Seed (0 = случайный)", value=0)
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
generate_btn=gr.Button("✨ Сгенерировать GIF!", variant="primary", size="lg")
|
| 259 |
-
|
| 260 |
with gr.Column(scale=3):
|
| 261 |
-
gr.
|
| 262 |
-
output_gif=gr.Image(label="Ваша анимация", type="filepath", interactive=False, height=400)
|
| 263 |
with gr.Tabs():
|
| 264 |
-
with gr.TabItem("📋 Журнал событий"):
|
| 265 |
-
status_box=gr.Markdown()
|
| 266 |
with gr.TabItem("🔑 Статус API ключей"):
|
| 267 |
-
keys_status=gr.Markdown(value=key_rotator.get_status)
|
| 268 |
-
refresh_btn=gr.Button("🔄 Обновить ключи")
|
| 269 |
|
| 270 |
-
# --- Обработчики событий ---
|
| 271 |
def on_generate_start(): return {generate_btn: gr.update(value="⏳ Генерация...", interactive=False), status_box: None, output_gif: None}
|
| 272 |
-
def on_generate_finish(): return {generate_btn: gr.update(value="✨ Сгенерировать
|
| 273 |
|
| 274 |
-
generate_btn.click(
|
| 275 |
-
on_generate_start,
|
| 276 |
-
outputs=[generate_btn, status_box, output_gif]
|
| 277 |
-
).then(
|
| 278 |
fn=generate_gif_flow,
|
| 279 |
-
inputs=[
|
| 280 |
outputs=[output_gif, status_box]
|
| 281 |
-
).then(
|
| 282 |
-
on_generate_finish,
|
| 283 |
-
outputs=[generate_btn]
|
| 284 |
-
)
|
| 285 |
|
| 286 |
-
|
| 287 |
-
key_rotator.refresh()
|
| 288 |
-
return key_rotator.get_status()
|
| 289 |
-
refresh_btn.click(refresh_and_get_status, outputs=[keys_status])
|
| 290 |
|
| 291 |
return demo
|
| 292 |
|
| 293 |
if __name__ == "__main__":
|
| 294 |
ui = create_ui()
|
| 295 |
# Для запуска на Hugging Face Spaces и локально используем launch() без аргументов.
|
| 296 |
-
# Это позволит Gradio и платформе HF правильно настроить сеть.
|
| 297 |
ui.launch()
|
|
|
|
| 13 |
# --- КОНФИГУРАЦИЯ ---
|
| 14 |
# URL адреса API
|
| 15 |
TEXT_API_URL = "https://text.pollinations.ai/openai"
|
| 16 |
+
IMAGE_API_URL = "https://image.pollinations.ai/prompt"
|
| 17 |
|
| 18 |
# Файлы и директории
|
| 19 |
API_KEYS_FILE = "api_keys.txt" # Ключи только для генерации ИЗОБРАЖЕНИЙ
|
|
|
|
| 30 |
"""Управляет загрузкой, ротацией и отслеживанием использования API ключей."""
|
| 31 |
def __init__(self, filepath: str):
|
| 32 |
self.filepath = filepath
|
| 33 |
+
# Пытаемся загрузить ключи из Секретов Hugging Face, если не получается - из файла
|
| 34 |
+
self.keys: List[str] = self._load_keys_from_env_or_file()
|
| 35 |
self.current_index: int = 0
|
| 36 |
self.usage_count: Dict[str, int] = {key: 0 for key in self.keys}
|
| 37 |
|
| 38 |
+
def _load_keys_from_env_or_file(self) -> List[str]:
|
| 39 |
+
"""Загружает ключи из переменных окружения (секреты HF) или из файла."""
|
| 40 |
+
# Рекомендованный способ: Секреты Hugging Face
|
| 41 |
+
hf_secrets = os.environ.get("POLLINATIONS_API_KEYS")
|
| 42 |
+
if hf_secrets:
|
| 43 |
+
print("Загрузка API ключей из Секретов Hugging Face.")
|
| 44 |
+
return [key.strip() for key in hf_secrets.split(',') if key.strip()]
|
| 45 |
+
|
| 46 |
+
# Запасной вариант: Файл api_keys.txt
|
| 47 |
+
print("Секреты HF не найдены. Попытка загрузки из api_keys.txt.")
|
| 48 |
try:
|
| 49 |
with open(self.filepath, 'r', encoding='utf-8') as f:
|
| 50 |
return [line.strip() for line in f.readlines() if line.strip()]
|
|
|
|
| 60 |
|
| 61 |
def get_status(self) -> str:
|
| 62 |
if not self.keys:
|
| 63 |
+
return "🔑 **Статус**: API ключи для генерации изображений не найдены. Добавьте их в Секреты Hugging Face или в файл `api_keys.txt`."
|
| 64 |
status_lines = [f"🔑 **Загружено ключей (для изображений)**: {len(self.keys)}"]
|
| 65 |
for i, key in enumerate(self.keys):
|
| 66 |
masked_key = f"{key[:8]}...{key[-4:]}" if len(key) > 12 else key
|
|
|
|
| 81 |
async def _call_director_api(session: aiohttp.ClientSession, prompt: str, seed: int) -> Tuple[Optional[List[str]], str]:
|
| 82 |
"""Запрашивает сценарий у модели 'openai-fast'. Ключ не требуется."""
|
| 83 |
payload = {
|
| 84 |
+
"model": "openai-fast", "response_format": {"type": "json_object"},
|
|
|
|
| 85 |
"messages": [
|
| 86 |
+
{"role": "system", "content": "You are a scriptwriter for a short animated GIF..."},
|
|
|
|
|
|
|
|
|
|
| 87 |
{"role": "user", "content": prompt}
|
| 88 |
+
], "seed": seed
|
|
|
|
| 89 |
}
|
|
|
|
|
|
|
| 90 |
try:
|
| 91 |
+
async with session.post(TEXT_API_URL, json=payload, headers={}, timeout=120) as response:
|
|
|
|
| 92 |
if response.status == 200:
|
| 93 |
+
response_text = await response.text()
|
| 94 |
+
prompts_str = json.loads(response_text).get('choices', [{}])[0].get('message', {}).get('content')
|
| 95 |
+
prompts = json.loads(prompts_str).get("prompts", [])
|
| 96 |
+
return prompts, f"✅ **Режиссёр (`openai-fast`)**: Сценар��й на {len(prompts)} кадров получен."
|
|
|
|
|
|
|
| 97 |
return None, f"❌ **Режиссёр**: Ошибка API (Статус: {response.status})."
|
| 98 |
except Exception as e:
|
| 99 |
+
return None, f"❌ **Режиссёр**: Ошибка. {e}"
|
| 100 |
|
| 101 |
|
| 102 |
async def _call_artist_api(session: aiohttp.ClientSession, params: Dict, output_path: str, previous_image_path: Optional[str] = None) -> bool:
|
|
|
|
| 105 |
for attempt in range(max_retries):
|
| 106 |
headers = {}
|
| 107 |
api_key = key_rotator.get_next_key()
|
| 108 |
+
if api_key: headers["Authorization"] = f"Bearer {api_key}"
|
| 109 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
form = aiohttp.FormData()
|
| 111 |
params["seed"] = params.get("seed", 0) + attempt * 10
|
| 112 |
+
for key, value in params.items(): form.add_field(key, str(value))
|
|
|
|
| 113 |
|
| 114 |
if previous_image_path and os.path.exists(previous_image_path):
|
| 115 |
+
form.add_field('image', open(previous_image_path, 'rb'))
|
| 116 |
|
| 117 |
try:
|
| 118 |
async with session.post(IMAGE_API_URL, data=form, headers=headers, timeout=240) as response:
|
| 119 |
if response.status == 200:
|
| 120 |
+
with open(output_path, "wb") as f: f.write(await response.read())
|
|
|
|
| 121 |
return True
|
|
|
|
|
|
|
| 122 |
except Exception as e:
|
| 123 |
+
if attempt == max_retries - 1: print(f"Artist API System Error: {e}")
|
|
|
|
|
|
|
| 124 |
await asyncio.sleep(2)
|
| 125 |
return False
|
| 126 |
|
|
|
|
| 130 |
images = [Image.open(f) for f in image_files]
|
| 131 |
temp_gif_path = os.path.join(OUTPUT_DIR, f"animation_{timestamp}.gif")
|
| 132 |
images[0].save(temp_gif_path, save_all=True, append_images=images[1:], duration=1000 // fps, loop=0)
|
|
|
|
| 133 |
archive_path = os.path.join(ARCHIVE_DIR, timestamp)
|
| 134 |
frames_archive_path = os.path.join(archive_path, "frames")
|
| 135 |
shutil.move(os.path.dirname(image_files[0]), frames_archive_path)
|
|
|
|
| 136 |
final_gif_path = os.path.join(archive_path, os.path.basename(temp_gif_path))
|
| 137 |
shutil.move(temp_gif_path, final_gif_path)
|
|
|
|
| 138 |
return final_gif_path
|
| 139 |
|
| 140 |
|
| 141 |
# --- ОСНОВНАЯ ЛОГИКА ГЕНЕРАЦИИ ---
|
| 142 |
async def generate_gif_flow(
|
| 143 |
proxy: str, main_idea: str, fps: int, duration: int, width: int, height: int,
|
| 144 |
+
artist_model: str, seed_val: int, delay: int, progress: gr.Progress
|
|
|
|
| 145 |
) -> Tuple[Optional[str], str]:
|
|
|
|
| 146 |
status_log = ["🚀 **Старт**: Инициализация..."]
|
| 147 |
yield "\n".join(status_log)
|
| 148 |
+
if not main_idea: raise gr.Error("Пожалуйста, введите основную идею.")
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
| 151 |
session_temp_dir = os.path.join(TEMP_DIR, timestamp)
|
| 152 |
+
for dir_path in [session_temp_dir, OUTPUT_DIR, ARCHIVE_DIR]: os.makedirs(dir_path, exist_ok=True)
|
| 153 |
+
|
|
|
|
| 154 |
total_frames = int(fps * duration)
|
| 155 |
seed = int(seed_val) if seed_val != 0 else random.randint(1, 1000000)
|
| 156 |
status_log.append(f"🌱 **Параметры**: Seed: `{seed}`, Кадров: `{total_frames}`.")
|
| 157 |
yield "\n".join(status_log)
|
| 158 |
|
| 159 |
connector = ProxyConnector.from_url(proxy) if proxy else None
|
|
|
|
| 160 |
async with aiohttp.ClientSession(connector=connector) as session:
|
| 161 |
+
director_prompt = f"Based on the core idea '{main_idea}', generate {total_frames} prompts..."
|
|
|
|
|
|
|
|
|
|
| 162 |
prompts, msg = await _call_director_api(session, director_prompt, seed)
|
| 163 |
+
status_log.append(msg); yield "\n".join(status_log)
|
|
|
|
| 164 |
if not prompts: return None, "\n".join(status_log)
|
| 165 |
|
| 166 |
+
generated_files, previous_frame_path = [], None
|
|
|
|
|
|
|
|
|
|
| 167 |
for i in progress.tqdm(range(total_frames), desc="🎨 Генерация кадров"):
|
| 168 |
header = f"🎬 **Кадр {i + 1}/{total_frames}**"
|
| 169 |
frame_path = os.path.join(session_temp_dir, f"frame_{i:04d}.png")
|
|
|
|
| 170 |
params = {"width": width, "height": height, "seed": seed + i, "nologo": "true", "prompt": prompts[i % len(prompts)]}
|
| 171 |
|
| 172 |
+
params["model"] = "flux" if i == 0 else artist_model
|
| 173 |
+
status_log.append(f"{header}: Режим `{'text2img' if i==0 else 'img2img'}` (модель `{params['model']}`).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
yield "\n".join(status_log)
|
|
|
|
| 175 |
|
| 176 |
+
if await _call_artist_api(session, params, frame_path, previous_frame_path):
|
| 177 |
+
status_log.append(f"{header}: ✅ Успешно.")
|
| 178 |
generated_files.append(frame_path)
|
| 179 |
previous_frame_path = frame_path
|
| 180 |
else:
|
| 181 |
+
status_log.append(f"❌ **СТОП**: {header}: Не удалось сгенерировать."); yield "\n".join(status_log)
|
|
|
|
| 182 |
break
|
| 183 |
|
| 184 |
yield "\n".join(status_log)
|
|
|
|
| 188 |
status_log.append("😭 **Результат**: Не создано ни одного кадра.")
|
| 189 |
return None, "\n".join(status_log)
|
| 190 |
|
|
|
|
| 191 |
status_log.append("🎞️ **Сборка**: Создание GIF и архивация...")
|
| 192 |
yield "\n".join(status_log)
|
| 193 |
final_gif_path = _compile_gif_and_archive(generated_files, fps, timestamp)
|
| 194 |
status_log.append(f"✅ **Готово**: GIF сохранена в `{final_gif_path}`.")
|
|
|
|
| 195 |
return final_gif_path, "\n".join(status_log)
|
| 196 |
|
| 197 |
+
|
| 198 |
# --- ИНТЕРФЕЙС GRADIO ---
|
| 199 |
def create_ui():
|
| 200 |
+
css = "h1 {text-align: center;} footer {display: none !important;}"
|
| 201 |
with gr.Blocks(css=css, title="🎬 GIF Animator Pro", theme=gr.themes.Soft()) as demo:
|
| 202 |
+
gr.HTML("""<div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%); border-radius: 10px; margin-bottom: 20px;"><h1 style="color: white; margin: 0; font-size: 2.8em; text-shadow: 2px 2px 4px #00000040;">🎬 GIF Animator Pro</h1></div>""")
|
| 203 |
with gr.Row():
|
| 204 |
with gr.Column(scale=2):
|
| 205 |
+
main_idea = gr.Textbox(label="💡 1. Главная идея анимации", lines=3)
|
|
|
|
| 206 |
with gr.Accordion("🤖 2. Выбор модели художника", open=True):
|
| 207 |
+
artist_model = gr.Dropdown(IMAGE_MODELS_IMG2IMG, label="Модель-художник", value="kontext")
|
|
|
|
| 208 |
with gr.Accordion("⚙️ 3. Параметры анимации", open=True):
|
| 209 |
+
fps = gr.Slider(5, 30, value=10, step=1, label="Кадров/сек")
|
| 210 |
+
duration = gr.Slider(1, 10, value=2, step=1, label="Длительность (сек)")
|
| 211 |
+
width = gr.Slider(256, 1024, value=512, step=64, label="Ширина")
|
| 212 |
+
height = gr.Slider(256, 1024, value=512, step=64, label="Высота")
|
|
|
|
|
|
|
|
|
|
| 213 |
with gr.Accordion("🔧 4. Дополнительные настройки", open=False):
|
| 214 |
+
seed = gr.Number(label="Seed (0 = случайный)", value=0)
|
| 215 |
+
delay = gr.Slider(0, 30, value=2, step=1, label="Пауза между кадрами")
|
| 216 |
+
proxy = gr.Textbox(label="SOCKS5 Прокси (опционально)")
|
| 217 |
+
generate_btn = gr.Button("✨ Сгенерировать GIF!", variant="primary", size="lg")
|
|
|
|
|
|
|
| 218 |
with gr.Column(scale=3):
|
| 219 |
+
output_gif = gr.Image(label="🎞️ Результат", interactive=False, height=400)
|
|
|
|
| 220 |
with gr.Tabs():
|
| 221 |
+
with gr.TabItem("📋 Журнал событий"): status_box = gr.Markdown()
|
|
|
|
| 222 |
with gr.TabItem("🔑 Статус API ключей"):
|
| 223 |
+
keys_status = gr.Markdown(value=key_rotator.get_status)
|
| 224 |
+
refresh_btn = gr.Button("🔄 Обновить ключи")
|
| 225 |
|
|
|
|
| 226 |
def on_generate_start(): return {generate_btn: gr.update(value="⏳ Генерация...", interactive=False), status_box: None, output_gif: None}
|
| 227 |
+
def on_generate_finish(): return {generate_btn: gr.update(value="✨ Сгенерировать!", interactive=True)}
|
| 228 |
|
| 229 |
+
generate_btn.click(on_generate_start, outputs=[generate_btn, status_box, output_gif]).then(
|
|
|
|
|
|
|
|
|
|
| 230 |
fn=generate_gif_flow,
|
| 231 |
+
inputs=[proxy, main_idea, fps, duration, width, height, artist_model, seed, delay],
|
| 232 |
outputs=[output_gif, status_box]
|
| 233 |
+
).then(on_generate_finish, outputs=[generate_btn])
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
+
refresh_btn.click(lambda: (key_rotator.refresh(), key_rotator.get_status())[1], outputs=[keys_status])
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
return demo
|
| 238 |
|
| 239 |
if __name__ == "__main__":
|
| 240 |
ui = create_ui()
|
| 241 |
# Для запуска на Hugging Face Spaces и локально используем launch() без аргументов.
|
|
|
|
| 242 |
ui.launch()
|