Rooni commited on
Commit
37785ec
·
verified ·
1 Parent(s): 08732a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -118
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" # URL для POST-запросов с файлами
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
- self.keys: List[str] = self._load_keys()
 
34
  self.current_index: int = 0
35
  self.usage_count: Dict[str, int] = {key: 0 for key in self.keys}
36
 
37
- def _load_keys(self) -> List[str]:
 
 
 
 
 
 
 
 
 
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 ключи для генерации изображений не загружены. Создайте `api_keys.txt`."
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=headers, timeout=120) as response:
89
- response_text = await response.text()
90
  if response.status == 200:
91
- try:
92
- prompts_str = json.loads(response_text).get('choices', [{}])[0].get('message', {}).get('content')
93
- prompts = json.loads(prompts_str).get("prompts", [])
94
- return prompts, f"✅ **Режиссёр (`openai-fast`)**: Сценарий на {len(prompts)} кадров получен."
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"❌ **Режиссёр**: Системная ошибка. {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,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
- headers["Authorization"] = f"Bearer {api_key}"
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'), filename=os.path.basename(previous_image_path), content_type='image/png')
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(session_temp_dir, exist_ok=True)
170
- for dir_path in [OUTPUT_DIR, ARCHIVE_DIR]: os.makedirs(dir_path, exist_ok=True)
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
- # 2. Получение сценария от openai-fast
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
- # 3. Генерация кадров
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
- params["model"] = "flux"
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 success:
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 = """ h1 { text-align: center; } footer { display: none !important; } """
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><p style="color: #f0f0f0; margin: 10px 0 0 0; font-size: 1.2em;">Создавайте анимацию из текста с помощью нейросетей</p></div>""")
238
  with gr.Row():
239
  with gr.Column(scale=2):
240
- main_idea=gr.Textbox(label="💡 1. Главная идея анимации",placeholder="Пример: 'Кот в очках читает книгу, и страницы оживают'",lines=3)
241
-
242
  with gr.Accordion("🤖 2. Выбор модели художника", open=True):
243
- artist_model=gr.Dropdown(IMAGE_MODELS_IMG2IMG, label="Модель-художник (генерирует кадры)", value="kontext")
244
-
245
  with gr.Accordion("⚙️ 3. Параметры анимации", open=True):
246
- with gr.Row():
247
- fps=gr.Slider(5, 30, value=10, step=1, label="Кадров/сек (FPS)")
248
- duration=gr.Slider(1, 10, value=2, step=1, label="Длительность (сек)")
249
- with gr.Row():
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
- delay_between_frames=gr.Slider(0, 30, value=2, step=1, label="Пауза между кадрами (сек)")
256
- proxy_string=gr.Textbox(label="SOCKS5 Прокси (опционально)", placeholder="socks5://user:pass@host:port")
257
-
258
- generate_btn=gr.Button("✨ Сгенерировать GIF!", variant="primary", size="lg")
259
-
260
  with gr.Column(scale=3):
261
- gr.Markdown("### 🎞️ Результат")
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="✨ Сгенерировать GIF!", interactive=True)}
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=[proxy_string, main_idea, fps, duration, width, height, artist_model, seed, delay_between_frames],
280
  outputs=[output_gif, status_box]
281
- ).then(
282
- on_generate_finish,
283
- outputs=[generate_btn]
284
- )
285
 
286
- def refresh_and_get_status():
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()