oKen38461 commited on
Commit
3b85004
·
1 Parent(s): cb71803

OpenAIクライアントの初期化処理を簡素化し、プロキシ環境変数の一時的な無効化処理を削除しました。これにより、httpxのパッチでプロキシ問題が解決されるようになりました。また、エラーハンドリングを改善しました。

Browse files
Files changed (2) hide show
  1. app.py +97 -845
  2. utils/openai_api.py +3 -19
app.py CHANGED
@@ -32,12 +32,14 @@ def initialize_apis():
32
  except Exception as e:
33
  error_msg = f"Kling API: {str(e)}"
34
  if is_huggingface_spaces:
35
- error_msg += "\n\nHugging Face Spacesで環境変数を設定するには:\n"
36
- error_msg += "1. Space設定ページの「Settings」タブを開く\n"
37
- error_msg += "2. 「Repository secrets」セクション以下追加:\n"
38
- error_msg += " - USEAPI_NET_TOKEN\n"
39
- error_msg += " - USEAPI_NET_EMAIL\n"
40
- error_msg += "3. Spaceを再起動する"
 
 
41
  errors.append(error_msg)
42
  kling_api = None
43
 
@@ -57,23 +59,31 @@ def initialize_apis():
57
  return False, f"❌ APIの初期化に失敗しました: {'; '.join(errors)}"
58
 
59
  def calculate_credits_and_clips(duration_minutes: float) -> Tuple[int, int, int]:
60
- """必要なク数とクレジを計算"""
61
  total_seconds = int(duration_minutes * 60)
62
- num_clips = total_seconds // 10 # 10秒ごとのクリップ
63
 
64
- # クレジト計算(仮定値)
65
- standard_credits = (num_clips - 1) * 20 # 中間クリップ(Standard)
66
- professional_credits = 60 # 最終クリップ(Professional)
67
- total_credits = standard_credits + professional_credits
 
 
 
 
 
68
 
69
- return num_clips, total_credits, total_seconds
 
 
 
 
 
70
 
71
  async def generate_loop_video(
72
  start_image,
73
  end_image,
74
  prompt: str,
75
  duration_minutes: float,
76
- use_ai_split: bool,
77
  progress=gr.Progress()
78
  ) -> Optional[str]:
79
  """ループ動画を生成"""
@@ -118,86 +128,47 @@ async def generate_loop_video(
118
  if not prompt or len(prompt.strip()) == 0:
119
  gr.Warning("プロンプトを入力してください。")
120
  return None
121
-
122
- if duration_minutes < 0.5 or duration_minutes > 10:
123
- gr.Warning("動画の長さは0.5分〜10分の範囲で指定してください。")
124
- return None
125
-
126
- # 画像のアスペクト比をチェック
127
- with Image.open(start_image) as img:
128
- start_width, start_height = img.size
129
- start_ratio = start_width / start_height
130
-
131
- with Image.open(end_image) as img:
132
- end_width, end_height = img.size
133
- end_ratio = end_width / end_height
134
-
135
- # 開始画像と終了画像のアスペクト比が大きく異なる場合は警告
136
- ratio_diff_percent = abs(start_ratio - end_ratio) / start_ratio * 100
137
- if ratio_diff_percent > 10:
138
- gr.Warning(
139
- f"⚠️ 開始画像({start_width}x{start_height})と終了画像({end_width}x{end_height})の"
140
- f"アスペクト比が異なります。\n"
141
- f"スムーズなループ動画を作成するには、同じアスペクト比の画像を使用することをお勧めします。"
142
- )
143
-
144
- # クリップ数とクレジットを計算
145
- num_clips, total_credits, total_seconds = calculate_credits_and_clips(duration_minutes)
146
-
147
- progress(0, f"動画生成を開始します。{num_clips}個のクリップを生成します...")
148
-
149
  temp_manager = TempFileManager()
150
-
151
  try:
152
- # 画像からプロンプト生成
 
 
 
 
 
 
153
  progress(0.05, "画像を分析中...")
154
 
155
  start_image_description = ""
156
  end_image_description = ""
157
 
158
- if openai_splitter:
159
  # 開始画像の分析
160
- start_image_description = await openai_splitter.generate_image_description(start_image)
161
  print(f"開始画像の説明: {start_image_description}")
162
 
163
  # 終了画像の分析
164
- end_image_description = await openai_splitter.generate_image_description(end_image)
165
  print(f"終了画像の説明: {end_image_description}")
166
-
167
- # プロンプトを分割
168
  progress(0.1, "プロンプトを分割中...")
169
 
170
- # OpenAI APIの状態を確認
171
- print(f"🔍 OpenAI API状態チェック:")
172
- print(f" - openai_splitter存在: {openai_splitter is not None}")
173
- if openai_splitter:
174
- print(f" - openai_splitter.client存在: {openai_splitter.client is not None}")
175
- if openai_splitter.client:
176
- print(f" - OpenAI APIキー設定済み: ✅")
177
- else:
178
- print(f" - OpenAI APIキー未設定: ❌")
179
- print(f" - use_ai_split: {use_ai_split}")
180
-
181
- if use_ai_split and openai_splitter:
182
  print("🤖 AI自動分割を使用します")
183
- # AI自動分割(画像の説明を使用)
184
  prompts = await openai_splitter.split_prompt(
185
- prompt,
186
- num_clips,
187
- start_image_description,
188
- end_image_description
189
  )
190
  print(f"✅ GPTが {len(prompts)} 個のプロンプトを生成しました")
191
-
192
- # 開始画像の説明を各プロンプトに追加
193
- for prompt_data in prompts:
194
- # すべてのクリップで画像説明をそのまま使用(既にカメラ固定情報が含まれている)
195
- prompt_data["prompt"] = f"{start_image_description} {prompt_data['prompt']}"
196
  else:
197
  print("📝 手動分割を使用します(OpenAI API未設定またはエラー)")
198
- # 手動分割(画像説明 + ユーザープロンプト)
199
  base_prompt = f"{start_image_description} {prompt}" if start_image_description else prompt
200
- prompts = [{"clip_number": i+1, "prompt": base_prompt} for i in range(num_clips)]
201
  print(f"📄 {len(prompts)} 個の同一ベースプロンプトを生成しました")
202
 
203
  # 動画クリップを生成
@@ -207,135 +178,54 @@ async def generate_loop_video(
207
  print(f"総クリップ数: {num_clips}")
208
  print(f"総時間: {total_seconds}秒")
209
 
210
- for i, prompt_data in enumerate(prompts):
211
- clip_prompt = prompt_data["prompt"]
212
- progress_value = 0.1 + (0.8 * i / num_clips)
 
 
213
 
214
  print(f"\n--- クリップ {i+1}/{num_clips} ---")
 
 
215
  print(f"📝 使用プロンプト: {clip_prompt}")
216
- progress(progress_value, f"クリップ {i+1}/{num_clips} を生成中...")
217
 
218
- # モデルとパラメータ決定
219
- if i == 0:
220
- # 最初のクリップ
221
- image_path = start_image
222
- model = "kling-v1-6"
223
- tail_image = None
224
- elif i == num_clips - 1:
225
- # 最後のクリップ(ループ用)
226
- # 前のクリップの最後のフレームを使用
227
- last_frame = VideoProcessor.extract_frames(video_paths[-1], 1)[0]
228
- temp_image = temp_manager.get_temp_path(".png")
229
- VideoProcessor.save_frame_as_image(last_frame, temp_image)
230
- image_path = temp_image
231
- model = "kling-v1-6" # Professional(tail_image対応)
232
- tail_image = end_image
233
- # 最後のクリップのプロンプトを調整(スムーズな収束を促す)
234
- clip_prompt = clip_prompt + ", smooth transition, gradual movement towards the final position"
235
- else:
236
- # 中間のクリップ
237
- # 前のクリップの最後のフレームを使用
238
- last_frame = VideoProcessor.extract_frames(video_paths[-1], 1)[0]
239
- temp_image = temp_manager.get_temp_path(".png")
240
- VideoProcessor.save_frame_as_image(last_frame, temp_image)
241
- image_path = temp_image
242
- model = "kling-v1-6"
243
- tail_image = None
244
 
245
- # タスクを作成
246
- # v1-6モデルを使用(proモードで高品質化)
 
247
  task_id = await kling_api.create_video_task(
248
- image_path=image_path,
249
  prompt=clip_prompt,
250
- model=model,
251
- duration=10,
252
- tail_image_path=tail_image,
253
- #mode="pro" # proモードで高品質化
254
  )
255
 
256
- if not task_id:
257
- raise Exception(f"クリップ {i+1} のタスク作成に失敗しました")
258
-
259
- # 結果を待機(最大20分待機)
260
- progress(progress_value, f"クリップ {i+1}/{num_clips} をポーリング中... (タスクID: {task_id})")
261
- video_url = await kling_api.poll_task_result(task_id, max_wait_minutes=20)
262
-
263
- if not video_url:
264
- raise Exception(f"クリップ {i+1} の生成に失敗しました")
265
 
266
  # 動画をダウンロード
267
- temp_video = temp_manager.get_temp_path(".mp4")
268
- print(f"動画をダウンロード中: {video_url} -> {temp_video}")
269
- await kling_api.download_video(video_url, temp_video)
270
- video_paths.append(temp_video)
271
- print(f"クリップ {i+1} 完了")
272
-
273
- # 動画を結合
274
- print(f"\n=== 動画結合開始 ===")
275
- print(f"生成されたクリップ数: {len(video_paths)}")
276
  progress(0.9, "動画を結合中...")
277
 
278
- # 出力ディレクトリを絶対パスで作成
279
- output_dir = os.path.abspath("output")
280
- os.makedirs(output_dir, exist_ok=True)
281
-
282
- output_filename = f"loop_video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
283
- output_path = os.path.join(output_dir, output_filename)
284
 
285
- # シームレスループを作成
286
- print(f"出力ファイル: {output_path}")
287
- success = VideoProcessor.create_seamless_loop(video_paths, output_path)
288
-
289
- if success and os.path.exists(output_path):
290
  progress(1.0, "完了!")
291
- print(f"✅ 動画結合成功: {output_path}")
292
-
293
- # ファイルサイズを確認
294
- file_size = os.path.getsize(output_path) / (1024 * 1024) # MB
295
- print(f"ファイルサイズ: {file_size:.2f} MB")
296
-
297
- # 生成情報を保存
298
- generation_info = {
299
- "timestamp": datetime.now().isoformat(),
300
- "duration_minutes": duration_minutes,
301
- "num_clips": num_clips,
302
- "total_credits": total_credits,
303
- "prompts": prompts,
304
- "output_path": output_path,
305
- "file_size_mb": file_size
306
- }
307
-
308
- info_path = output_path.replace(".mp4", "_info.json")
309
- with open(info_path, "w", encoding="utf-8") as f:
310
- json.dump(generation_info, f, ensure_ascii=False, indent=2)
311
-
312
- # APIクライアントは閉じない(グローバル変数のため)
313
- # 次回の実行で再利用される
314
-
315
- # ファイルの読み取り権限を確認
316
  try:
317
- # ファイルが存在し読み取り可能か確認
318
- if not os.path.exists(output_path):
319
- raise Exception(f"出力ファイルが見つかりません: {output_path}")
320
-
321
- if not os.access(output_path, os.R_OK):
322
- raise Exception(f"出力ファイルに読み取り権限がありません: {output_path}")
323
-
324
- # ファイルサイズが0でないことを確認
325
- if os.path.getsize(output_path) == 0:
326
- raise Exception(f"出力ファイルが空です: {output_path}")
327
-
328
- # ファイルのパーミッションを設定(読み取り可能に)
329
- try:
330
- os.chmod(output_path, 0o644)
331
- print(f"📝 ファイルパーミッションを設定: {output_path}")
332
- except Exception as perm_error:
333
- print(f"⚠️ パーミッション設定エラー(続行): {perm_error}")
334
-
335
- gr.Info(f"✅ ループ動画の生成が完了しました!使用クレジット: {total_credits}")
336
- print(f"📹 動画ファイルパスを返却: {output_path}")
337
-
338
- # Hugging Face Spacesの場合、相対パスを返す
339
  if os.getenv("SPACE_ID"):
340
  # 相対パスに変換
341
  relative_path = os.path.relpath(output_path)
@@ -357,40 +247,17 @@ async def generate_loop_video(
357
  import traceback
358
  traceback.print_exc()
359
 
360
- # APIクライアントは閉じない(グローバル変数のため)
361
- # エラー時でも次回の実行で再利用可能にする
362
-
363
- # クレジット不足エラーの特別な処理
364
  if "クレジット不足" in error_msg:
365
  gr.Error("⚠️ クレジット不足エラー")
366
- gr.Warning("""
367
- Kling AIのクレジットが不足しています。
368
- 動画生成を続けるには:
369
- 1. UseAPI.netダッシュボードにログイン
370
- 2. アカウント設定からクレジットを購入
371
- 3. 購入完了後、再度動画生成を実行
372
- """)
373
- return None
374
  elif "タスクが見つかりません" in error_msg or "タスク作成エラー" in error_msg:
375
  gr.Error("⚠️ タスク作成エラー")
376
- gr.Warning("""
377
- 動画生成タスクの作成に失敗しました。
378
- 考えられる原因:
379
- 1. クレジット不足 - UseAPI.netでクレジット残高を確認
380
- 2. 同時処理制限 - 他のタスクが処理中の場合は完了を待つ
381
- 3. APIの一時的な問題 - しばらく待ってから再試行
382
- """)
383
- return None
384
  else:
385
  gr.Error(f"エラーが発生しました: {error_msg}")
386
- return None
387
 
388
  finally:
389
- # クリーンアップ(一時ファイルなど)
390
  temp_manager.cleanup()
391
 
392
- # 画像生成関数を削除
393
-
394
  def format_duration(minutes: float) -> str:
395
  """分数を分秒形式の文字列に変換"""
396
  total_seconds = int(minutes * 60)
@@ -402,692 +269,77 @@ def format_duration(minutes: float) -> str:
402
  else:
403
  return f"{mins}分{secs}秒"
404
 
405
- def get_optimal_image_size(original_width: int, original_height: int) -> Tuple[int, int, str]:
406
- """元画像のアスペクト比に最も近い、Kling APIがサポートするサイズを返す"""
407
- aspect_ratio = original_width / original_height
408
-
409
- # Kling APIがサポートするアスペクト比と推奨サイズ
410
- supported_ratios = {
411
- "1:1": (1.0, 1024, 1024),
412
- "16:9": (16/9, 1920, 1080),
413
- "4:3": (4/3, 1440, 1080),
414
- "3:2": (3/2, 1536, 1024),
415
- "2:3": (2/3, 1024, 1536),
416
- "3:4": (3/4, 1080, 1440),
417
- "9:16": (9/16, 1080, 1920),
418
- "21:9": (21/9, 2520, 1080),
419
- }
420
-
421
- # 最も近いアスペクト比を見つける
422
- closest_ratio_name = None
423
- min_diff = float('inf')
424
-
425
- for ratio_name, (ratio_value, width, height) in supported_ratios.items():
426
- diff = abs(aspect_ratio - ratio_value)
427
- if diff < min_diff:
428
- min_diff = diff
429
- closest_ratio_name = ratio_name
430
-
431
- # 選択されたアスペクト比の推奨サイズを返す
432
- _, optimal_width, optimal_height = supported_ratios[closest_ratio_name]
433
-
434
- return optimal_width, optimal_height, closest_ratio_name
435
-
436
- def run_openai_test() -> str:
437
- """OpenAI API初期化テストを実行"""
438
- import traceback
439
-
440
- test_results = []
441
- test_results.append("=== OpenAI API 初期化テスト ===\n")
442
-
443
- try:
444
- # 1. 環境変数の確認
445
- api_key = os.getenv('OPENAI_API_KEY')
446
- if api_key:
447
- test_results.append("✅ OPENAI_API_KEY が設定されています")
448
- test_results.append(f" キーの長さ: {len(api_key)} 文字")
449
- test_results.append(f" キーのプレフィックス: {api_key[:7]}...")
450
- else:
451
- test_results.append("❌ OPENAI_API_KEY が設定されていません")
452
- return "\n".join(test_results)
453
-
454
- # 2. OpenAIライブラリのインポート
455
- test_results.append("\n--- OpenAIライブラリのインポート ---")
456
- try:
457
- from openai import OpenAI
458
- test_results.append("✅ openaiライブラリのインポートに成功")
459
- except Exception as e:
460
- test_results.append(f"❌ openaiライブラリのインポートに失敗: {e}")
461
- return "\n".join(test_results)
462
-
463
- # 3. OpenAIクライアントの初期化
464
- test_results.append("\n--- OpenAIクライアントの初期化 ---")
465
- try:
466
- # Hugging Face Spaces環境でのプロキシ問題を回避
467
- original_env = {}
468
- proxy_env_vars = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy']
469
-
470
- for var in proxy_env_vars:
471
- if var in os.environ:
472
- original_env[var] = os.environ[var]
473
- del os.environ[var]
474
-
475
- try:
476
- client = OpenAI()
477
- test_results.append("✅ OpenAIクライアントの初期化に成功")
478
- finally:
479
- # 元の環境変数を復元
480
- for var, value in original_env.items():
481
- os.environ[var] = value
482
-
483
- except Exception as e:
484
- test_results.append(f"❌ OpenAIクライアントの初期化に失敗: {e}")
485
- test_results.append(f" エラータイプ: {type(e).__name__}")
486
- test_results.append(" 詳細なトレースバック:")
487
- test_results.append(traceback.format_exc())
488
- return "\n".join(test_results)
489
-
490
- # 4. API接続テスト(簡単なリクエスト)
491
- test_results.append("\n--- API接続テスト ---")
492
- try:
493
- # モデルリストを取得(最小限のAPIコール)
494
- models = client.models.list()
495
- test_results.append("✅ API接続に成功")
496
- test_results.append(f" 利用可能なモデル数: {len(list(models))}")
497
- except Exception as e:
498
- test_results.append(f"❌ API接続に失敗: {e}")
499
- test_results.append(f" エラータイプ: {type(e).__name__}")
500
-
501
- except Exception as e:
502
- test_results.append(f"\n❌ 予期しないエラーが発生しました: {e}")
503
- test_results.append(traceback.format_exc())
504
-
505
- test_results.append("\n=== テスト完了 ===")
506
- return "\n".join(test_results)
507
-
508
- def run_kling_api_test() -> str:
509
- """Kling API接続テストを実行"""
510
- test_results = []
511
- test_results.append("=== Kling API 接続テスト ===\n")
512
-
513
- try:
514
- # 環境変数の確認
515
- token = os.getenv('USEAPI_NET_TOKEN')
516
- email = os.getenv('USEAPI_NET_EMAIL')
517
-
518
- if token:
519
- test_results.append("✅ USEAPI_NET_TOKEN が設定されています")
520
- test_results.append(f" トークンの長さ: {len(token)} 文字")
521
- test_results.append(f" トークンのプレフィックス: {token[:10]}...")
522
- else:
523
- test_results.append("❌ USEAPI_NET_TOKEN が設定されていません")
524
-
525
- if email:
526
- test_results.append(f"✅ USEAPI_NET_EMAIL が設定されています: {email}")
527
- else:
528
- test_results.append("⚠️ USEAPI_NET_EMAIL が設定されていません(オプション)")
529
-
530
- # Kling APIクライアントの初期化テスト
531
- test_results.append("\n--- Kling APIクライアントの初期化 ---")
532
- if not token:
533
- test_results.append("❌ トークンが必要です")
534
- return "\n".join(test_results)
535
-
536
- try:
537
- from utils.kling_api import KlingAPI
538
- api = KlingAPI()
539
- test_results.append("✅ Kling APIクライアントの初期化に成功")
540
- except Exception as e:
541
- test_results.append(f"❌ Kling APIクライアントの初期化に失敗: {e}")
542
- return "\n".join(test_results)
543
-
544
- except Exception as e:
545
- test_results.append(f"\n❌ 予期しないエラーが発生しました: {e}")
546
- import traceback
547
- test_results.append(traceback.format_exc())
548
-
549
- test_results.append("\n=== テスト完了 ===")
550
- return "\n".join(test_results)
551
-
552
- def run_system_info() -> str:
553
- """システム情報を取得"""
554
- info_results = []
555
- info_results.append("=== システム情報 ===\n")
556
-
557
- try:
558
- import platform
559
- import sys
560
-
561
- # 基本システム情報
562
- info_results.append("--- 基本情報 ---")
563
- info_results.append(f"Python版: {sys.version}")
564
- info_results.append(f"プラットフォーム: {platform.platform()}")
565
- info_results.append(f"アーキテクチャ: {platform.architecture()}")
566
-
567
- # 実行環境
568
- info_results.append("\n--- 実行環境 ---")
569
- is_huggingface_spaces = os.getenv("SPACE_ID") is not None
570
- if is_huggingface_spaces:
571
- info_results.append(f"✅ Hugging Face Spaces (Space ID: {os.getenv('SPACE_ID')})")
572
- else:
573
- info_results.append("✅ ローカル環境")
574
-
575
- # パッケージ情報
576
- info_results.append("\n--- 主要パッケージ版 ---")
577
- packages_to_check = ['openai', 'gradio', 'httpx', 'pillow', 'opencv-python']
578
-
579
- for package in packages_to_check:
580
- try:
581
- if package == 'opencv-python':
582
- import cv2
583
- version = cv2.__version__
584
- info_results.append(f"✅ {package}: {version}")
585
- elif package == 'pillow':
586
- from PIL import Image
587
- version = Image.__version__ if hasattr(Image, '__version__') else 'unknown'
588
- info_results.append(f"✅ {package}: {version}")
589
- else:
590
- module = __import__(package)
591
- version = getattr(module, '__version__', 'unknown')
592
- info_results.append(f"✅ {package}: {version}")
593
- except ImportError:
594
- info_results.append(f"❌ {package}: インストールされていません")
595
- except Exception as e:
596
- info_results.append(f"⚠️ {package}: エラー ({e})")
597
-
598
- # 環境変数の状態
599
- info_results.append("\n--- 環境変数の状態 ---")
600
- env_vars = [
601
- ('USEAPI_NET_TOKEN', '必須'),
602
- ('USEAPI_NET_EMAIL', 'オプション'),
603
- ('OPENAI_API_KEY', 'オプション'),
604
- ('SPACE_ID', 'HF Spaces'),
605
- ('GRADIO_SERVER_PORT', 'Gradio'),
606
- ]
607
-
608
- # プロキシ関連の環境変数
609
- proxy_vars = [
610
- ('HTTP_PROXY', 'プロキシ'),
611
- ('HTTPS_PROXY', 'プロキシ'),
612
- ('http_proxy', 'プロキシ'),
613
- ('https_proxy', 'プロキシ'),
614
- ('ALL_PROXY', 'プロキシ'),
615
- ('all_proxy', 'プロキシ'),
616
- ]
617
-
618
- for var_name, description in env_vars:
619
- value = os.getenv(var_name)
620
- if value:
621
- if 'TOKEN' in var_name or 'KEY' in var_name:
622
- # セキュリティのため部分的に表示
623
- display_value = f"{value[:7]}... (長さ: {len(value)})"
624
- else:
625
- display_value = value
626
- info_results.append(f"✅ {var_name}: {display_value} ({description})")
627
- else:
628
- info_results.append(f"❌ {var_name}: 未設定 ({description})")
629
-
630
- # プロキシ変数の状態(OpenAI API問題の診断用)
631
- info_results.append("\n--- プロキシ関連環境変数 ---")
632
- has_proxy = False
633
- for var_name, description in proxy_vars:
634
- value = os.getenv(var_name)
635
- if value:
636
- has_proxy = True
637
- info_results.append(f"⚠️ {var_name}: {value} ({description})")
638
- else:
639
- info_results.append(f"✅ {var_name}: 未設定 ({description})")
640
-
641
- if has_proxy:
642
- info_results.append("⚠️ プロキシ設定が検出されました。OpenAI API初期化時に一時的に無効化します。")
643
- else:
644
- info_results.append("✅ プロキシ設定は検出されませんでした。")
645
-
646
- except Exception as e:
647
- info_results.append(f"\n❌ システム情報の取得中にエラーが発生しました: {e}")
648
- import traceback
649
- info_results.append(traceback.format_exc())
650
-
651
- info_results.append("\n=== 情報取得完了 ===")
652
- return "\n".join(info_results)
653
-
654
- # 画像生成関連の関数を削除
655
-
656
  def create_ui():
657
  """Gradio UIを作成"""
658
- with gr.Blocks(title="動画生成 - Kling版", theme=gr.themes.Base(), css="""
659
- /* 全体的なダークテーマ */
660
- .gradio-container {
661
- background-color: #1a1a1a !important;
662
- color: #ffffff !important;
663
- }
664
-
665
- /* ラベルのスタイル(ボタンに見えないように) */
666
- label.block {
667
- font-weight: normal !important;
668
- background: transparent !important;
669
- border: none !important;
670
- color: #e0e0e0 !important;
671
- padding: 0.5rem 0 !important;
672
- margin-bottom: 0.5rem !important;
673
- }
674
-
675
- /* 入力要素のスタイル */
676
- .image-container {
677
- min-height: 200px;
678
- background-color: #2a2a2a !important;
679
- border: 1px solid #3a3a3a !important;
680
- border-radius: 8px !important;
681
- }
682
-
683
- /* 画像要素 */
684
- .image-frame {
685
- background-color: #2a2a2a !important;
686
- border: 1px solid #3a3a3a !important;
687
- border-radius: 8px !important;
688
- }
689
-
690
- /* テキストボックス */
691
- .gr-text-input, textarea {
692
- background-color: #2a2a2a !important;
693
- border: 1px solid #3a3a3a !important;
694
- color: #ffffff !important;
695
- border-radius: 8px !important;
696
- }
697
-
698
- .gr-text-input:focus, textarea:focus {
699
- border-color: #555555 !important;
700
- box-shadow: 0 0 0 1px #555555 !important;
701
- }
702
-
703
- /* スライダー */
704
- .gr-slider-container {
705
- background-color: transparent !important;
706
- }
707
-
708
- input[type="range"] {
709
- background-color: #3a3a3a !important;
710
- }
711
-
712
- /* スライダーの数値表示とリフレッシュボタンを非表示 - 正確なセレクタ */
713
- .tab-like-container {
714
- display: none !important;
715
- }
716
-
717
- /* aria-labelとdata-testid属性を使った確実な選択 */
718
- input[aria-label*="動画の長さ"][type="number"],
719
- input[data-testid="number-input"],
720
- button[aria-label="Reset to default value"],
721
- button[data-testid="reset-button"],
722
- button.reset-button {
723
- display: none !important;
724
- }
725
-
726
- /* Svelteクラスを含む要素(部分一致) */
727
- div[class*="tab-like-container"] {
728
- display: none !important;
729
- }
730
-
731
- /* ビデオプレイヤー */
732
- .video-container {
733
- background-color: #2a2a2a !important;
734
- border: 1px solid #3a3a3a !important;
735
- border-radius: 8px !important;
736
- }
737
-
738
- /* タブ */
739
- .tabs {
740
- background-color: transparent !important;
741
- }
742
-
743
- button.selected {
744
- background-color: #3a3a3a !important;
745
- color: #ffffff !important;
746
- }
747
-
748
- /* コンテンツの見た目を改善 */
749
- .contain { object-fit: contain !important; }
750
-
751
- /* レスポンシブ対応 */
752
- @media (max-width: 768px) {
753
- .main-container { flex-direction: column !important; }
754
- }
755
-
756
- /* パネルとブロック */
757
- .panel {
758
- background-color: #222222 !important;
759
- border: 1px solid #3a3a3a !important;
760
- }
761
-
762
- .block {
763
- background-color: transparent !important;
764
- border: none !important;
765
- }
766
-
767
- /* ボタン(本物のボタン)のスタイルを維持 */
768
- button.primary {
769
- background-color: #4a9eff !important;
770
- border: none !important;
771
- color: white !important;
772
- }
773
-
774
- button.primary:hover {
775
- background-color: #357abd !important;
776
- }
777
-
778
- /* セカンダリボタン */
779
- button.secondary {
780
- background-color: #3a3a3a !important;
781
- border: 1px solid #555555 !important;
782
- color: #e0e0e0 !important;
783
- }
784
-
785
- /* Markdownテキスト */
786
- .prose {
787
- color: #e0e0e0 !important;
788
- }
789
-
790
- .prose h1, .prose h2, .prose h3 {
791
- color: #ffffff !important;
792
- }
793
-
794
- /* 動画の長さ表示 */
795
- .duration-display input {
796
- background-color: transparent !important;
797
- border: none !important;
798
- text-align: center !important;
799
- font-size: 14px !important;
800
- color: #e0e0e0 !important;
801
- padding: 0 !important;
802
- cursor: default !important;
803
- }
804
-
805
- .duration-display {
806
- min-width: 80px !important;
807
- }
808
-
809
- /* クレジットボタンのコンテナとボタンのスタイル */
810
- .credit-link-container {
811
- display: flex !important;
812
- justify-content: center !important;
813
- width: 100% !important;
814
- }
815
-
816
- .credit-button {
817
- width: auto !important;
818
- min-width: auto !important;
819
- flex: 0 0 auto !important;
820
- }
821
-
822
- .credit-button button {
823
- width: auto !important;
824
- min-width: auto !important;
825
- padding: 0.4rem 1.2rem !important;
826
- white-space: nowrap !important;
827
- }
828
- """) as app:
829
- # JavaScriptで数値表示を削除
830
- gr.HTML("""
831
- <script>
832
- // ページ読み込み時とDOM変更時に数値表示とボタンを削除
833
- function hideSliderElements() {
834
- // グローバルスタイルを追加
835
- if (!document.getElementById('custom-slider-style')) {
836
- const style = document.createElement('style');
837
- style.id = 'custom-slider-style';
838
- style.textContent = `
839
- /* 正確なセレクタで数値表示とボタンを非表示 */
840
- .tab-like-container,
841
- div[class*="tab-like-container"],
842
- input[aria-label*="動画の長さ"][type="number"],
843
- input[data-testid="number-input"],
844
- button[aria-label="Reset to default value"],
845
- button[data-testid="reset-button"],
846
- button.reset-button,
847
- .reset-button {
848
- display: none !important;
849
- visibility: hidden !important;
850
- width: 0 !important;
851
- height: 0 !important;
852
- opacity: 0 !important;
853
- pointer-events: none !important;
854
- position: absolute !important;
855
- left: -9999px !important;
856
- }
857
- `;
858
- document.head.appendChild(style);
859
- }
860
- }
861
-
862
- // 初期実行
863
- hideSliderElements();
864
-
865
- // DOM監視
866
- const observer = new MutationObserver(hideSliderElements);
867
- observer.observe(document.body, { childList: true, subtree: true });
868
-
869
- // 念のため定期実行も維持
870
- setInterval(hideSliderElements, 500);
871
- </script>
872
- """, visible=False)
873
- gr.Markdown(
874
- """
875
- # 🎬 動画生成 - Kling版
876
- """
877
- )
878
 
879
  with gr.Tabs():
880
  with gr.Tab("🌀 ループ動画生成"):
881
- # メインコンテナを6:4に分割
882
- with gr.Row(elem_classes=["main-container"]):
883
- # 左側(6):入力要素
884
  with gr.Column(scale=6):
885
- # フレーム画像を横並び
886
  with gr.Row():
887
- start_image = gr.Image(
888
- label="開始フレーム",
889
- type="filepath",
890
- elem_classes=["contain", "image-container"]
891
- )
892
- end_image = gr.Image(
893
- label="終了フレーム",
894
- type="filepath",
895
- elem_classes=["contain", "image-container"]
896
- )
897
 
898
- # プロンプトと設定(フル幅)
899
- video_prompt = gr.Textbox(
900
- label="プロンプト",
901
- placeholder="例:美しい桜並木、花びらが舞い散る",
902
- lines=3,
903
- max_lines=6
904
- )
905
 
906
- # スライダーと表示ラベル
907
  with gr.Row():
908
- duration = gr.Slider(
909
- label="動画の長さ",
910
- minimum=0.5,
911
- maximum=10,
912
- value=1,
913
- step=0.5,
914
- show_label=True,
915
- info="", # 情報表示を空文字に
916
- interactive=True,
917
- scale=9
918
- )
919
- duration_display = gr.Textbox(
920
- value=format_duration(1),
921
- label="",
922
- interactive=False,
923
- scale=1,
924
- elem_classes=["duration-display"]
925
- )
926
 
927
- video_generate_btn = gr.Button("🎬 動画生成開始", variant="primary", size="lg")
928
 
929
- # クレジットリンクをセンター配置
930
- with gr.Row(elem_classes=["credit-link-container"]):
931
- auth_link_button = gr.Button("認証はこちら", link="https://useapi.net/docs/start-here/setup-kling", size="sm", elem_classes=["credit-button"])
932
- video_credit_link = gr.Button("クレジットの追加購��はコチラから", link="https://app.klingai.com/global/membership/spirit-unit", size="sm", elem_classes=["credit-button"])
933
-
934
- # 右側(4):生成結果のみ
935
  with gr.Column(scale=4):
936
- output_video = gr.Video(
937
- label="生成結果",
938
- elem_classes=["contain", "image-container"]
939
- )
940
-
941
- with gr.Tab("🔧 システムテスト"):
942
- gr.Markdown("### API接続とシステム状態の診断")
943
-
944
- with gr.Row():
945
- with gr.Column():
946
- gr.Markdown("#### テスト実行")
947
-
948
- with gr.Row():
949
- openai_test_btn = gr.Button("🤖 OpenAI API テスト", variant="secondary")
950
- kling_test_btn = gr.Button("🎬 Kling API テスト", variant="secondary")
951
- system_info_btn = gr.Button("ℹ️ システム情報", variant="secondary")
952
-
953
- test_output = gr.Textbox(
954
- label="テスト結果",
955
- lines=20,
956
- max_lines=30,
957
- interactive=False,
958
- show_copy_button=True
959
- )
960
-
961
- # 画像生成タブを削除
962
-
963
- # イベントハンドラー(参照モード関連は削除)
964
 
965
- async def generate_video_with_defaults(*args):
966
- """デフォルト値を含めて動画生成処理を実行"""
967
- # AI自動プロンプト分割は常にTrue
968
- return await generate_loop_video(*args, True)
969
 
970
- # スライダー値変更時に表示を更新
971
- duration.change(
972
- fn=format_duration,
973
- inputs=duration,
974
- outputs=duration_display
975
- )
976
-
977
- # 動画生成ボタンクリック時のイベント
978
  video_generate_btn.click(
979
- fn=generate_video_with_defaults,
980
- inputs=[
981
- start_image,
982
- end_image,
983
- video_prompt,
984
- duration
985
- ],
986
  outputs=output_video
987
  )
988
-
989
- # テストボタンのイベントハンドラー
990
- openai_test_btn.click(
991
- fn=run_openai_test,
992
- outputs=test_output
993
- )
994
-
995
- kling_test_btn.click(
996
- fn=run_kling_api_test,
997
- outputs=test_output
998
- )
999
-
1000
- system_info_btn.click(
1001
- fn=run_system_info,
1002
- outputs=test_output
1003
- )
1004
-
1005
- # 画像生成イベントを削除
1006
-
1007
- # アプリケーションロード時の初期化(バックグラウンドで実行)
1008
- app.load(
1009
- fn=lambda: None, # 警告を避けるため戻り値なしの関数に変更
1010
- outputs=None
1011
- )
1012
 
1013
  return app
1014
 
1015
  # アプリケーションを起動
1016
  if __name__ == "__main__":
1017
- # 必要なディレクトリを作成
1018
  os.makedirs("output", exist_ok=True)
1019
  os.makedirs("logs", exist_ok=True)
1020
  os.makedirs("temp", exist_ok=True)
1021
 
1022
- # デバッグ情報を表示
1023
  print("="*50)
1024
  print("🚀 動画生成 - Kling版 起動中...")
1025
  print("="*50)
1026
 
1027
- # 実行環境を検出
1028
  is_huggingface_spaces = os.getenv("SPACE_ID") is not None
1029
  if is_huggingface_spaces:
1030
  print(f"📍 実行環境: Hugging Face Spaces (Space ID: {os.getenv('SPACE_ID')})")
1031
  else:
1032
  print("📍 実行環境: ローカル")
1033
 
1034
- # 環境変数の状態を確認
1035
  env_status = []
1036
- if os.getenv('USEAPI_NET_TOKEN'):
1037
- env_status.append(" USEAPI_NET_TOKEN: 設定済み")
1038
- else:
1039
- env_status.append("❌ USEAPI_NET_TOKEN: 未設定")
1040
-
1041
- if os.getenv('USEAPI_NET_EMAIL'):
1042
- env_status.append("✅ USEAPI_NET_EMAIL: 設定済み")
1043
- else:
1044
- env_status.append("❌ USEAPI_NET_EMAIL: 未設定")
1045
-
1046
- if os.getenv('OPENAI_API_KEY'):
1047
- env_status.append("✅ OPENAI_API_KEY: 設定済み(オプション)")
1048
- else:
1049
- env_status.append("⚠️ OPENAI_API_KEY: 未設定(AI自動プロンプト分割は利用できません)")
1050
 
1051
  print("\n環境変数の状態:")
1052
  for status in env_status:
1053
  print(f" {status}")
1054
 
1055
- # APIを初期化
1056
  init_success, init_message = initialize_apis()
1057
  print(f"\n{init_message}")
1058
 
1059
- # APIの初期化に失敗してもアプリは起動する
1060
  if not init_success:
1061
  print("\n⚠️ 警告: APIの初期化に失敗しましたが、アプリケーションは起動します。")
1062
- print("環境変数を設定してからページをリロードしてください。")
1063
 
1064
- # UIを作成して起動
1065
  app = create_ui()
1066
 
1067
  print("\n🌐 アプリケーションを起動しています...")
1068
  print("="*50)
1069
 
1070
- # Hugging Face Spaces環境かどうかを検出
1071
- is_huggingface_spaces = os.getenv("SPACE_ID") is not None
1072
-
1073
- # 起動設定を調整
1074
- if is_huggingface_spaces:
1075
- # Hugging Face Spaces用の設定
1076
- app.launch(
1077
- server_port=7860,
1078
- share=False,
1079
- show_error=True,
1080
- quiet=False,
1081
- # ssr_mode=False # SSRモードを無効化
1082
- )
1083
- else:
1084
- # ローカル環境用の設定
1085
- app.launch(
1086
- server_name="127.0.0.1",
1087
- server_port=7860,
1088
- share=False,
1089
- show_error=True,
1090
- quiet=False,
1091
- inbrowser=True,
1092
- # ssr_mode=False # SSRモードを無効化
1093
- )
 
32
  except Exception as e:
33
  error_msg = f"Kling API: {str(e)}"
34
  if is_huggingface_spaces:
35
+ error_msg += """
36
+
37
+ Hugging Face Spaces環境変数設定するには:
38
+ 1. Space設定ページの「Settings」タブを開く
39
+ 2. 「Repository secrets」セクションで以下を追加:
40
+ - USEAPI_NET_TOKEN
41
+ - USEAPI_NET_EMAIL
42
+ 3. Spaceを再起動する"""
43
  errors.append(error_msg)
44
  kling_api = None
45
 
 
59
  return False, f"❌ APIの初期化に失敗しました: {'; '.join(errors)}"
60
 
61
  def calculate_credits_and_clips(duration_minutes: float) -> Tuple[int, int, int]:
62
+ """動画の長さに応じて、必要なクレジ数とクプ数を計算"""
63
  total_seconds = int(duration_minutes * 60)
 
64
 
65
+ # 10秒プを優先
66
+ num_10s_clips = total_seconds // 10
67
+ remaining_seconds = total_seconds % 10
68
+
69
+ num_5s_clips = 0
70
+ if remaining_seconds > 0:
71
+ num_5s_clips = 1
72
+
73
+ num_clips = num_10s_clips + num_5s_clips
74
 
75
+ # クレジット計算(概算)
76
+ # 5秒クリップ: 12クレジット
77
+ # 10秒クリップ: 20クレジット
78
+ credits = (num_10s_clips * 20) + (num_5s_clips * 12)
79
+
80
+ return credits, num_clips, total_seconds
81
 
82
  async def generate_loop_video(
83
  start_image,
84
  end_image,
85
  prompt: str,
86
  duration_minutes: float,
 
87
  progress=gr.Progress()
88
  ) -> Optional[str]:
89
  """ループ動画を生成"""
 
128
  if not prompt or len(prompt.strip()) == 0:
129
  gr.Warning("プロンプトを入力してください。")
130
  return None
131
+
132
+ # 一時ファイルマネージャ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  temp_manager = TempFileManager()
134
+
135
  try:
136
+ # 開始・終了画像を一時ファイルに保存
137
+ start_image_path = temp_manager.save_temp_file(start_image)
138
+ end_image_path = temp_manager.save_temp_file(end_image)
139
+
140
+ # クレジットとクリップ数を計算
141
+ _, num_clips, total_seconds = calculate_credits_and_clips(duration_minutes)
142
+
143
  progress(0.05, "画像を分析中...")
144
 
145
  start_image_description = ""
146
  end_image_description = ""
147
 
148
+ if openai_splitter and openai_splitter.client:
149
  # 開始画像の分析
150
+ start_image_description = await openai_splitter.generate_image_description(start_image_path)
151
  print(f"開始画像の説明: {start_image_description}")
152
 
153
  # 終了画像の分析
154
+ end_image_description = await openai_splitter.generate_image_description(end_image_path)
155
  print(f"終了画像の説明: {end_image_description}")
156
+
 
157
  progress(0.1, "プロンプトを分割中...")
158
 
159
+ # OpenAI APIが利用可能かチェック
160
+ use_ai_split = openai_splitter is not None and openai_splitter.client is not None
161
+
162
+ if use_ai_split:
 
 
 
 
 
 
 
 
163
  print("🤖 AI自動分割を使用します")
 
164
  prompts = await openai_splitter.split_prompt(
165
+ prompt, num_clips, start_image_description, end_image_description
 
 
 
166
  )
167
  print(f"✅ GPTが {len(prompts)} 個のプロンプトを生成しました")
 
 
 
 
 
168
  else:
169
  print("📝 手動分割を使用します(OpenAI API未設定またはエラー)")
 
170
  base_prompt = f"{start_image_description} {prompt}" if start_image_description else prompt
171
+ prompts = [{"clip_number": i + 1, "prompt": base_prompt} for i in range(num_clips)]
172
  print(f"📄 {len(prompts)} 個の同一ベースプロンプトを生成しました")
173
 
174
  # 動画クリップを生成
 
178
  print(f"総クリップ数: {num_clips}")
179
  print(f"総時間: {total_seconds}秒")
180
 
181
+ current_image_path = start_image_path
182
+
183
+ for i in range(num_clips):
184
+ progress_val = 0.2 + (0.7 * (i / num_clips))
185
+ progress(progress_val, f"クリップ {i+1}/{num_clips} を生成中...")
186
 
187
  print(f"\n--- クリップ {i+1}/{num_clips} ---")
188
+
189
+ clip_prompt = prompts[i]['prompt']
190
  print(f"📝 使用プロンプト: {clip_prompt}")
 
191
 
192
+ # 最後のクリップのみtail_image使用
193
+ tail_image_path = end_image_path if i == num_clips - 1 else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ clip_duration = 5 if total_seconds % 10 != 0 and i == num_clips - 1 else 10
196
+
197
+ # 動画生成タスクを作成
198
  task_id = await kling_api.create_video_task(
199
+ image_path=current_image_path,
200
  prompt=clip_prompt,
201
+ duration=clip_duration,
202
+ tail_image_path=tail_image_path
 
 
203
  )
204
 
205
+ # タスク完了を待機
206
+ video_url = await kling_api.poll_video_result(task_id, progress, progress_val, 0.7 / num_clips)
 
 
 
 
 
 
 
207
 
208
  # 動画をダウンロード
209
+ clip_path = await kling_api.download_video(video_url, f"clip_{i+1}")
210
+ video_paths.append(clip_path)
211
+ temp_manager.add_temp_file(clip_path)
212
+
213
+ # 次のクリップの開始フレームとして、生成された動画の最終フレームを使用
214
+ if i < num_clips - 1:
215
+ last_frame = VideoProcessor.extract_last_frame(clip_path)
216
+ current_image_path = temp_manager.save_pil_image(last_frame, f"frame_{i+1}")
217
+
218
  progress(0.9, "動画を結合中...")
219
 
220
+ output_path = VideoProcessor.combine_videos(video_paths)
221
+ temp_manager.add_temp_file(output_path) # 最終ファイルも一時ファイルとして管理
 
 
 
 
222
 
223
+ if output_path:
 
 
 
 
224
  progress(1.0, "完了!")
225
+ print(f"✅ 動画結合成功: {output_path}")
226
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  try:
228
+ # Hugging Face Spaces環境では相対パスを返す必要がある
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  if os.getenv("SPACE_ID"):
230
  # 相対パスに変換
231
  relative_path = os.path.relpath(output_path)
 
247
  import traceback
248
  traceback.print_exc()
249
 
 
 
 
 
250
  if "クレジット不足" in error_msg:
251
  gr.Error("⚠️ クレジット不足エラー")
 
 
 
 
 
 
 
 
252
  elif "タスクが見つかりません" in error_msg or "タスク作成エラー" in error_msg:
253
  gr.Error("⚠️ タスク作成エラー")
 
 
 
 
 
 
 
 
254
  else:
255
  gr.Error(f"エラーが発生しました: {error_msg}")
256
+ return None
257
 
258
  finally:
 
259
  temp_manager.cleanup()
260
 
 
 
261
  def format_duration(minutes: float) -> str:
262
  """分数を分秒形式の文字列に変換"""
263
  total_seconds = int(minutes * 60)
 
269
  else:
270
  return f"{mins}分{secs}秒"
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  def create_ui():
273
  """Gradio UIを作成"""
274
+ with gr.Blocks(title="動画生成 - Kling版", theme=gr.themes.Base()) as app:
275
+ gr.Markdown("# 🎬 動画生成 - Kling版")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  with gr.Tabs():
278
  with gr.Tab("🌀 ループ動画生成"):
279
+ with gr.Row():
 
 
280
  with gr.Column(scale=6):
 
281
  with gr.Row():
282
+ start_image = gr.Image(label="開始フレーム", type="filepath")
283
+ end_image = gr.Image(label="終了フレーム", type="filepath")
 
 
 
 
 
 
 
 
284
 
285
+ video_prompt = gr.Textbox(label="プロンプト", lines=3)
 
 
 
 
 
 
286
 
 
287
  with gr.Row():
288
+ duration = gr.Slider(label="動画の長さ", minimum=0.5, maximum=10, value=1, step=0.5, scale=9)
289
+ duration_display = gr.Textbox(value=format_duration(1), label="", interactive=False, scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ video_generate_btn = gr.Button("🎬 動画生成開始", variant="primary")
292
 
 
 
 
 
 
 
293
  with gr.Column(scale=4):
294
+ output_video = gr.Video(label="生成結果")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
+ duration.change(fn=format_duration, inputs=duration, outputs=duration_display)
 
 
 
297
 
 
 
 
 
 
 
 
 
298
  video_generate_btn.click(
299
+ fn=generate_loop_video,
300
+ inputs=[start_image, end_image, video_prompt, duration],
 
 
 
 
 
301
  outputs=output_video
302
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  return app
305
 
306
  # アプリケーションを起動
307
  if __name__ == "__main__":
 
308
  os.makedirs("output", exist_ok=True)
309
  os.makedirs("logs", exist_ok=True)
310
  os.makedirs("temp", exist_ok=True)
311
 
 
312
  print("="*50)
313
  print("🚀 動画生成 - Kling版 起動中...")
314
  print("="*50)
315
 
 
316
  is_huggingface_spaces = os.getenv("SPACE_ID") is not None
317
  if is_huggingface_spaces:
318
  print(f"📍 実行環境: Hugging Face Spaces (Space ID: {os.getenv('SPACE_ID')})")
319
  else:
320
  print("📍 実行環境: ローカル")
321
 
 
322
  env_status = []
323
+ if os.getenv('USEAPI_NET_TOKEN'): env_status.append("✅ USEAPI_NET_TOKEN: 設定済み")
324
+ else: env_status.append(" USEAPI_NET_TOKEN: 設定")
325
+ if os.getenv('USEAPI_NET_EMAIL'): env_status.append("✅ USEAPI_NET_EMAIL: 設定済み")
326
+ else: env_status.append("❌ USEAPI_NET_EMAIL: 未設定")
327
+ if os.getenv('OPENAI_API_KEY'): env_status.append("��� OPENAI_API_KEY: 設定済み(オプション)")
328
+ else: env_status.append("⚠️ OPENAI_API_KEY: 未設定")
 
 
 
 
 
 
 
 
329
 
330
  print("\n環境変数の状態:")
331
  for status in env_status:
332
  print(f" {status}")
333
 
 
334
  init_success, init_message = initialize_apis()
335
  print(f"\n{init_message}")
336
 
 
337
  if not init_success:
338
  print("\n⚠️ 警告: APIの初期化に失敗しましたが、アプリケーションは起動します。")
 
339
 
 
340
  app = create_ui()
341
 
342
  print("\n🌐 アプリケーションを起動しています...")
343
  print("="*50)
344
 
345
+ app.launch(server_name="0.0.0.0" if is_huggingface_spaces else "127.0.0.1")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/openai_api.py CHANGED
@@ -35,25 +35,9 @@ class OpenAIPromptSplitter:
35
  self.client = None
36
  else:
37
  try:
38
- # Hugging Face Spaces環境でのプロキシ問題を回避
39
- # 一時的に環境変数を保存し、プロキシ関連を削除
40
- original_env = {}
41
- proxy_env_vars = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy']
42
-
43
- for var in proxy_env_vars:
44
- if var in os.environ:
45
- original_env[var] = os.environ[var]
46
- del os.environ[var]
47
-
48
- try:
49
- # OpenAIクライアントを初期化(プロキシなし)
50
- self.client = OpenAI()
51
- print(f"✅ OpenAI APIクライアントの初期化に成功しました")
52
- finally:
53
- # 元の環境変数を復元
54
- for var, value in original_env.items():
55
- os.environ[var] = value
56
-
57
  except Exception as e:
58
  print(f"❌ OpenAI APIクライアントの初期化に失敗しました: {str(e)}")
59
  self.client = None
 
35
  self.client = None
36
  else:
37
  try:
38
+ # プロキシ問題はhttpxパッチで解決済み
39
+ self.client = OpenAI()
40
+ print(f"✅ OpenAI APIクライアントの初期化に成功しました")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  except Exception as e:
42
  print(f"❌ OpenAI APIクライアントの初期化に失敗しました: {str(e)}")
43
  self.client = None