MakiAi commited on
Commit
9bf175f
·
1 Parent(s): 8a75a03

🎬 メインアプリケーションを動画結合アプリに完全変更

Browse files

- Wikipedia変換からFrame Bridge動画結合アプリに機能転換
- 単体処理: 2つの動画の最適フレーム検出・結合機能
- バッチ処理: 順次結合・ペア結合モードでの一括処理
- リアルタイム動画情報表示と類似度スコア可視化

Files changed (1) hide show
  1. app.py +210 -355
app.py CHANGED
@@ -1,418 +1,273 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- import html2text
4
- import re
5
  import gradio as gr
6
  from theme import create_zen_theme
7
- import tempfile
8
  import os
9
- import zipfile
10
- from urllib.parse import urlparse, unquote
11
 
12
- def scrape_wikipedia_to_markdown_final(url: str) -> str:
13
- """
14
- Wikipediaページをスクレイピングし、整形・不要部分削除を行い、
15
- タイトルを付けてMarkdownに変換します。
16
 
17
- 処理フロー:
18
- 1. ページのタイトルをH1見出しとして取得します。
19
- 2. 「登場人物」などの<dt>タグを見出しに変換します。
20
- 3. 生成されたMarkdown文字列から「## 脚注」以降を完全に削除します。
21
- 4. [編集]リンクを削除します。
22
- 5. 最終的にタイトルと本文を結合して返します。
23
-
24
- Args:
25
- url (str): スクレイピング対象のWikipediaページのURL。
26
-
27
- Returns:
28
- str: 整形・変換された最終的なMarkdownコンテンツ。失敗した場合は空の文字列。
29
- """
30
- try:
31
- # 1. HTMLの取得と解析
32
- headers = {
33
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
34
- }
35
- response = requests.get(url, headers=headers)
36
- response.raise_for_status() # HTTPエラーがあれば例外を発生させる
37
- response.encoding = response.apparent_encoding # 文字コードを自動検出
38
- soup = BeautifulSoup(response.text, 'html.parser')
39
-
40
- # --- ページのタイトルを取得 ---
41
- title_tag = soup.find('h1', id='firstHeading')
42
- page_title = title_tag.get_text(strip=True) if title_tag else "Wikipedia ページ"
43
-
44
- # 2. 主要コンテンツエリアの特定
45
- content_div = soup.find('div', class_='mw-parser-output')
46
- if not content_div:
47
- return "エラー: コンテンツエリアが見つかりませんでした。"
48
-
49
- # 3. HTMLの事前整形(登場人物などの見出し化)
50
- for dt_tag in content_div.find_all('dt'):
51
- h4_tag = soup.new_tag('h4')
52
- h4_tag.extend(dt_tag.contents)
53
- dt_tag.replace_with(h4_tag)
54
-
55
- # 4. HTMLからMarkdownへの一次変換
56
- h = html2text.HTML2Text()
57
- h.body_width = 0 # テキストの折り返しを無効にする
58
- full_markdown_text = h.handle(str(content_div))
59
-
60
- # 5. 生成されたMarkdownから「## 脚注」以降を削除
61
- footnote_marker = "\n## 脚注"
62
- footnote_index = full_markdown_text.find(footnote_marker)
63
- body_text = full_markdown_text[:footnote_index] if footnote_index != -1 else full_markdown_text
64
-
65
- # 6. [編集]リンクを正規表現で一括削除
66
- cleaned_body = re.sub(r'\[\[編集\]\(.+?\)]\n', '', body_text)
67
-
68
- # 7. タイトルと整形後の本文を結合
69
- final_markdown = f"# {page_title}\n\n{cleaned_body.strip()}"
70
-
71
- return final_markdown
72
-
73
- except requests.exceptions.RequestException as e:
74
- return f"HTTPリクエストエラー: {e}"
75
- except Exception as e:
76
- return f"予期せぬエラーが発生しました: {e}"
77
-
78
- def get_filename_from_url(url):
79
- """URLからファイル名を生成する関数"""
80
- try:
81
- # URLからページ名を抽出
82
- parsed_url = urlparse(url)
83
- page_name = parsed_url.path.split('/')[-1]
84
- # URLデコード
85
- page_name = unquote(page_name)
86
- # ファイル名として使用できない文字を置換
87
- safe_filename = re.sub(r'[<>:"/\\|?*]', '_', page_name)
88
- return f"{safe_filename}.md"
89
- except:
90
- return "wikipedia_page.md"
91
 
92
- def create_download_file(content, filename):
93
- """ダウンロード用の一時ファイルを作成する関数"""
 
 
 
 
 
 
94
  try:
95
- # 一時ディレクトリにファイルを作成
96
- temp_dir = tempfile.gettempdir()
97
- file_path = os.path.join(temp_dir, filename)
98
 
99
- with open(file_path, 'w', encoding='utf-8') as f:
100
- f.write(content)
 
 
 
 
 
101
 
102
- return file_path
 
 
 
 
 
 
 
 
 
103
  except Exception as e:
104
- print(f"ファイル作成エラー: {e}")
105
- return None
106
 
107
- def create_zip_file(file_paths, zip_filename="wikipedia_export.zip"):
108
- """複数のファイルをZIP形式でまとめる関数"""
 
 
 
109
  try:
110
- temp_dir = tempfile.gettempdir()
111
- zip_path = os.path.join(temp_dir, zip_filename)
112
 
113
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
114
- for file_path in file_paths:
115
- if os.path.exists(file_path):
116
- # ファイル名のみを取得してZIPに追加
117
- filename = os.path.basename(file_path)
118
- zipf.write(file_path, filename)
119
 
120
- return zip_path
121
- except Exception as e:
122
- print(f"ZIP作成エラー: {e}")
123
- return None
124
-
125
- def process_wikipedia_url(url):
126
- """Wikipedia URLを処理してMarkdownを生成するGradio用関数"""
127
- if not url:
128
- return "URLを入力してください。", None
129
-
130
- # URLが有効かチェック
131
- if not url.startswith('http'):
132
- return "有効なURLを入力してください(http://またはhttps://から始まるURL)。", None
133
-
134
- # Wikipedia URLかチェック
135
- if 'wikipedia.org' not in url:
136
- return "WikipediaのURLを入力してください。", None
137
-
138
- # スクレイピングを実行
139
- markdown_content = scrape_wikipedia_to_markdown_final(url)
140
-
141
- # ダウンロード用ファイルを作成
142
- if not markdown_content.startswith("エラー:") and not markdown_content.startswith("HTTP"):
143
- filename = get_filename_from_url(url)
144
- file_path = create_download_file(markdown_content, filename)
145
- return markdown_content, file_path
146
- else:
147
- return markdown_content, None
148
-
149
- def process_multiple_urls(urls_text, progress=gr.Progress()):
150
- """複数のWikipedia URLを一括処理してMarkdownを生成する関数"""
151
- if not urls_text.strip():
152
- return "URLリストを入力してください。", None, [], None
153
-
154
- # URLリストを行ごとに分割
155
- urls = [url.strip() for url in urls_text.strip().split('\n') if url.strip()]
156
-
157
- if not urls:
158
- return "有効なURLが見つかりませんでした。", None, [], None
159
-
160
- results = []
161
- all_content = []
162
- individual_files = []
163
- total_urls = len(urls)
164
- success_count = 0
165
-
166
- for i, url in enumerate(urls):
167
- progress((i + 1) / total_urls, f"処理中: {i + 1}/{total_urls}")
168
 
169
- # URLの検証
170
- if not url.startswith('http'):
171
- results.append(f"❌ 無効なURL: {url}")
172
- continue
173
-
174
- if 'wikipedia.org' not in url:
175
- results.append(f"❌ Wikipedia以外のURL: {url}")
176
- continue
177
 
178
- # スクレイピング実行
179
- try:
180
- markdown_content = scrape_wikipedia_to_markdown_final(url)
181
- if markdown_content.startswith("エラー:") or markdown_content.startswith("HTTP"):
182
- results.append(f"❌ 処理失敗: {url}\n エラー: {markdown_content}")
183
- else:
184
- # ページタイトルを抽出
185
- title_match = re.match(r'^# (.+)', markdown_content)
186
- page_title = title_match.group(1) if title_match else "不明なページ"
187
-
188
- # 文字数とファイル情報を表示
189
- char_count = len(markdown_content)
190
- filename = get_filename_from_url(url)
191
-
192
- results.append(f"✅ 処理成功: {url}")
193
- results.append(f" 📄 ページタイトル: {page_title}")
194
- results.append(f" 📊 文字数: {char_count:,} 文字")
195
- results.append(f" 💾 ファイル名: {filename}")
196
-
197
- all_content.append(markdown_content)
198
- success_count += 1
199
-
200
- # 個別ファイルを作成
201
- file_path = create_download_file(markdown_content, filename)
202
- if file_path:
203
- individual_files.append(file_path)
204
- except Exception as e:
205
- results.append(f"❌ 処理エラー: {url}")
206
- results.append(f" エラー内容: {str(e)}")
207
-
208
- # サマリー情報を追加
209
- summary = [
210
- "=" * 60,
211
- "📊 処理結果サマリー",
212
- "=" * 60,
213
- f"🔗 処理対象URL数: {total_urls}",
214
- f"✅ 成功: {success_count}",
215
- f"❌ 失敗: {total_urls - success_count}",
216
- ""
217
- ]
218
-
219
- # 結果を結合
220
- final_result = "\n".join(summary + results)
221
-
222
- # 一括ダウンロード用ファイルを作成
223
- batch_file_path = None
224
- if all_content:
225
- combined_content = "\n\n" + "="*80 + "\n\n".join(all_content)
226
- batch_file_path = create_download_file(combined_content, "wikipedia_batch_export.md")
227
-
228
- # ZIPファイルを作成
229
- zip_file_path = None
230
- if individual_files:
231
- zip_file_path = create_zip_file(individual_files, "wikipedia_export.zip")
232
-
233
- return final_result, batch_file_path, individual_files, zip_file_path
234
 
235
  # Gradioインターフェースの作成
236
  def create_interface():
237
  """Gradioインターフェースを作成する関数"""
238
  theme = create_zen_theme()
239
 
240
- with gr.Blocks(theme=theme, title="Wikipedia to Markdown Converter") as demo:
241
  # ヘッダー
242
  gr.HTML("""
243
  <div style='text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, #d4a574 0%, #ffffff 50%, #f5f2ed 100%); color: #3d405b; border-radius: 12px;'>
244
- <h1 style='font-size: 3rem; margin-bottom: 0.5rem; text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>📚 Wikipedia to Markdown Converter</h1>
245
- <p style='font-size: 1.2rem; opacity: 0.8;'>WikipediaのURLを入力して、Markdown形式に変換します</p>
246
  </div>
247
  """)
248
 
249
  # タブの作成
250
  with gr.Tabs():
251
  # 単体処理タブ
252
- with gr.TabItem("🔗 単体処理"):
253
  with gr.Row():
254
  with gr.Column(scale=1):
255
- url_input = gr.Textbox(
256
- label="🔗 Wikipedia URL",
257
- placeholder="https://ja.wikipedia.org/wiki/...",
258
- value="https://ja.wikipedia.org/wiki/Python"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  )
260
- convert_btn = gr.Button("✨ 変換する", variant="primary")
 
 
261
 
262
  with gr.Column(scale=1):
263
- output_text = gr.Textbox(
264
- label="📝 変換されたMarkdown",
265
- lines=20,
266
- max_lines=50,
267
  show_copy_button=True
268
  )
269
- download_file = gr.File(
270
- label="📥 マークダウンファイルをダウンロード",
271
- visible=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  )
273
-
274
- # ボタンクリック時の処理
275
- def update_single_output(url):
276
- content, file_path = process_wikipedia_url(url)
277
- if file_path:
278
- return content, gr.update(value=file_path, visible=True)
279
- else:
280
- return content, gr.update(visible=False)
281
-
282
- convert_btn.click(
283
- fn=update_single_output,
284
- inputs=url_input,
285
- outputs=[output_text, download_file]
286
- )
287
-
288
- # 使用例
289
- def example_process(url):
290
- content, _ = process_wikipedia_url(url)
291
- return content
292
-
293
- gr.Examples(
294
- examples=[
295
- ["https://ja.wikipedia.org/wiki/Python"],
296
- ["https://ja.wikipedia.org/wiki/JavaScript"],
297
- ["https://ja.wikipedia.org/wiki/HTML"]
298
- ],
299
- inputs=url_input,
300
- outputs=output_text,
301
- fn=example_process,
302
- cache_examples=False
303
- )
304
 
305
- # 一括処理タブ
306
- with gr.TabItem("📋 一括処理"):
307
  with gr.Row():
308
  with gr.Column(scale=1):
309
- urls_input = gr.Textbox(
310
- label="📋 Wikipedia URLリスト(1行に1つずつ)",
311
- placeholder="https://ja.wikipedia.org/wiki/Python\nhttps://ja.wikipedia.org/wiki/JavaScript\nhttps://ja.wikipedia.org/wiki/HTML",
312
- lines=10,
313
- value="https://ja.wikipedia.org/wiki/Python\nhttps://ja.wikipedia.org/wiki/JavaScript"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  )
315
- batch_convert_btn = gr.Button("🚀 一括変換する", variant="primary")
 
316
 
317
  with gr.Column(scale=1):
318
- batch_output_text = gr.Textbox(
319
- label="📝 一括変換結果",
 
320
  lines=15,
321
- max_lines=30,
322
  show_copy_button=True
323
  )
324
- batch_download_file = gr.File(
325
- label="📥 全体をまとめてダウンロード",
326
- visible=False
327
- )
328
- zip_download_file = gr.File(
329
- label="🗜️ ZIPファイルでダウンロード",
330
- visible=False
331
- )
332
 
333
- # 個別ダウンロードエリア
334
- individual_downloads = gr.Column(visible=False)
335
- with individual_downloads:
336
- gr.Markdown("### 📥 個別ダウンロード")
337
- individual_file_1 = gr.File(label="", visible=False)
338
- individual_file_2 = gr.File(label="", visible=False)
339
- individual_file_3 = gr.File(label="", visible=False)
340
- individual_file_4 = gr.File(label="", visible=False)
341
- individual_file_5 = gr.File(label="", visible=False)
 
 
342
 
343
- # 一括処理ボタンクリック時の処理
344
- def update_batch_output(urls_text):
345
- content, batch_file_path, individual_files, zip_file_path = process_multiple_urls(urls_text)
346
-
347
- # 戻り値のリストを準備
348
- outputs = [content]
349
-
350
- # 一括ダウンロードファイル
351
- if batch_file_path:
352
- outputs.append(gr.update(value=batch_file_path, visible=True))
353
- else:
354
- outputs.append(gr.update(visible=False))
355
-
356
- # ZIPダウンロードファイル
357
- if zip_file_path:
358
- outputs.append(gr.update(value=zip_file_path, visible=True))
359
- else:
360
- outputs.append(gr.update(visible=False))
361
-
362
- # 個別ダウンロードエリアの表示/非表示
363
- if individual_files:
364
- outputs.append(gr.update(visible=True))
365
- else:
366
- outputs.append(gr.update(visible=False))
367
-
368
- # 個別ファイル(最大5つまで表示)
369
- for i in range(5):
370
- if i < len(individual_files):
371
- filename = os.path.basename(individual_files[i])
372
- outputs.append(gr.update(value=individual_files[i], visible=True, label=f"📄 {filename}"))
373
- else:
374
- outputs.append(gr.update(visible=False))
375
-
376
- return outputs
377
 
378
- batch_convert_btn.click(
379
- fn=update_batch_output,
380
- inputs=urls_input,
381
- outputs=[
382
- batch_output_text,
383
- batch_download_file,
384
- zip_download_file,
385
- individual_downloads,
386
- individual_file_1,
387
- individual_file_2,
388
- individual_file_3,
389
- individual_file_4,
390
- individual_file_5
391
- ]
392
  )
393
 
394
- gr.Markdown("### 💡 一括処理の使い方")
395
- gr.Markdown("1. テキストエリアに変換したいWikipediaのURLを1行に1つずつ入力します")
396
- gr.Markdown("2. 「🚀 一括変換する」ボタンをクリックします")
397
- gr.Markdown("3. 処理の進行状況が表示され、完了後に結果が表示されます")
398
- gr.Markdown("4. 各URLの処理結果(成功/失敗)が明確に表示されます")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
 
400
  gr.Markdown("---")
401
- gr.Markdown("### 🎯 基本的な使用方法")
402
- gr.Markdown("- **単体処理**: 1つのWikipediaページを変換したい場合")
403
- gr.Markdown("- **一括処理**: 複数のWikipediaページを一度に変換したい場合")
404
- gr.Markdown("- 生成されたMarkdownは右側のテキストエリアからコピーできます")
405
- gr.Markdown("- **📥 ダウンロード機能**: 変換が成功すると、マークダウンファイルとして直接ダウンロードできます")
406
- gr.Markdown(" - 単体処理: ページ名に基づいたファイル名で個別ダウンロード")
407
- gr.Markdown(" - 一括処理: 各URLごとの個別ダウンロード + 全体をまとめた一括ダウンロード + **🗜️ ZIPファイル**")
408
- gr.Markdown(" - 個別ダウンロード: 成功した各ページを個別のファイルとしてダウンロード可能(最大5つまで表示)")
409
- gr.Markdown(" - **ZIPダウンロード**: 複数のMarkdownファイルを1つのZIPファイルにまとめてダウンロード")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  # ZENテーマの説明
412
  gr.HTML("""
413
  <div style='text-align: center; margin-top: 2rem; padding: 1.5rem; background: #ffffff; border-radius: 12px;'>
414
  <h3 style='color: #3d405b; margin-top: 0;'>🧘‍♀️ ZENテーマ</h3>
415
- <p style='color: #8b7355;'>和モダンなデザインで、使いやすさと美しさを追求しました</p>
 
416
  </div>
417
  """)
418
 
 
 
 
 
 
1
  import gradio as gr
2
  from theme import create_zen_theme
3
+ from src.frame_bridge import FrameBridge, BatchProcessor
4
  import os
 
 
5
 
6
+ # Frame Bridge インスタンスを作成
7
+ frame_bridge = FrameBridge(exclude_edge_frames=True)
8
+ batch_processor = BatchProcessor(exclude_edge_frames=True)
 
9
 
10
+ def process_sample_videos():
11
+ """サンプル動画を処理する関数"""
12
+ video1_path = "examples/assets/example/REI/input/REI-001.mp4"
13
+ video2_path = "examples/assets/example/REI/input/REI-002.mp4"
14
+
15
+ if not os.path.exists(video1_path) or not os.path.exists(video2_path):
16
+ return "サンプル動画ファイルが見つかりません。", None, None, None, 0.0
17
+
18
+ return frame_bridge.process_video_bridge(video1_path, video2_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ def process_batch_videos(input_folder, output_folder, mode, filename):
21
+ """バッチ動画処理関数"""
22
+ if not input_folder or not os.path.exists(input_folder):
23
+ return "入力フォルダが指定されていないか、存在しません。", None
24
+
25
+ if not output_folder:
26
+ output_folder = "output"
27
+
28
  try:
29
+ # バッチプロセッサを初期化
30
+ processor = BatchProcessor(output_dir=output_folder, exclude_edge_frames=True)
 
31
 
32
+ if mode == "順次結合":
33
+ success, final_output, results = processor.process_sequential_merge(input_folder, filename or "merged_sequence.mp4")
34
+ if success:
35
+ report = processor.generate_report(results)
36
+ return f"✅ 順次結合完了!\n📁 出力: {final_output}\n\n{report}", final_output
37
+ else:
38
+ return "❌ 順次結合に失敗しました", None
39
 
40
+ elif mode == "ペア結合":
41
+ success, output_files, results = processor.process_pairwise_merge(input_folder)
42
+ if success:
43
+ report = processor.generate_report(results)
44
+ # 最初の出力ファイルを返す(複数ある場合)
45
+ first_output = output_files[0] if output_files else None
46
+ return f"✅ ペア結合完了!\n📁 出力ファイル数: {len(output_files)}\n\n{report}", first_output
47
+ else:
48
+ return "❌ ペア結合に失敗しました", None
49
+
50
  except Exception as e:
51
+ return f"処理エラー: {str(e)}", None
 
52
 
53
+ def process_video_bridge(video1, video2, progress=gr.Progress()):
54
+ """2つの動画を分析して最適な結合点を見つけ、結合する関数"""
55
+ if video1 is None or video2 is None:
56
+ return "2つの動画ファイルをアップロードしてください。", None, None, None, None
57
+
58
  try:
59
+ progress(0.1, "動画を分析中...")
 
60
 
61
+ result_text, output_path, frame1_path, frame2_path, similarity = frame_bridge.process_video_bridge(video1, video2)
 
 
 
 
 
62
 
63
+ progress(1.0, "完了!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ return result_text, output_path, frame1_path, frame2_path, similarity
 
 
 
 
 
 
 
66
 
67
+ except Exception as e:
68
+ return f"処理エラー: {str(e)}", None, None, None, None
69
+
70
+ def analyze_video_details(video_path):
71
+ """動画の詳細情報を分析する関数"""
72
+ if video_path is None:
73
+ return ""
74
+ return frame_bridge.processor.analyze_video_details(video_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  # Gradioインターフェースの作成
77
  def create_interface():
78
  """Gradioインターフェースを作成する関数"""
79
  theme = create_zen_theme()
80
 
81
+ with gr.Blocks(theme=theme, title="Frame Bridge - 動画フレーム結合アプリ") as demo:
82
  # ヘッダー
83
  gr.HTML("""
84
  <div style='text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, #d4a574 0%, #ffffff 50%, #f5f2ed 100%); color: #3d405b; border-radius: 12px;'>
85
+ <h1 style='font-size: 3rem; margin-bottom: 0.5rem; text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>🎬 Frame Bridge</h1>
86
+ <p style='font-size: 1.2rem; opacity: 0.8;'>2つの動画を最適なフレームで自動結合するAIアプリ</p>
87
  </div>
88
  """)
89
 
90
  # タブの作成
91
  with gr.Tabs():
92
  # 単体処理タブ
93
+ with gr.TabItem("🎥 単体処理"):
94
  with gr.Row():
95
  with gr.Column(scale=1):
96
+ gr.Markdown("### 📹 動画アップロード")
97
+ video1_input = gr.Video(
98
+ label="🎥 動画1(前半)",
99
+ height=300
100
+ )
101
+ video1_info = gr.Textbox(
102
+ label="📊 動画1の情報",
103
+ lines=6,
104
+ interactive=False
105
+ )
106
+
107
+ video2_input = gr.Video(
108
+ label="🎥 動画2(後半)",
109
+ height=300
110
+ )
111
+ video2_info = gr.Textbox(
112
+ label="📊 動画2の情報",
113
+ lines=6,
114
+ interactive=False
115
  )
116
+
117
+ bridge_btn = gr.Button("🌉 フレームブリッジ実行", variant="primary", size="lg")
118
+ sample_btn = gr.Button("🎬 サンプル動画で試す", variant="secondary", size="lg")
119
 
120
  with gr.Column(scale=1):
121
+ gr.Markdown("### 🎯 結合結果")
122
+ result_text = gr.Textbox(
123
+ label="📝 分析結果",
124
+ lines=10,
125
  show_copy_button=True
126
  )
127
+
128
+ merged_video = gr.Video(
129
+ label="🎬 結合された動画",
130
+ height=300
131
+ )
132
+
133
+ # 接続フレーム表示
134
+ with gr.Row():
135
+ connection_frame1 = gr.Image(
136
+ label="🔗 動画1の接続フレーム",
137
+ height=200
138
+ )
139
+ connection_frame2 = gr.Image(
140
+ label="🔗 動画2の接続フレーム",
141
+ height=200
142
+ )
143
+
144
+ similarity_score = gr.Number(
145
+ label="📈 フレーム類似度スコア",
146
+ precision=3
147
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ # バッチ処理タブ
150
+ with gr.TabItem("📁 バッチ処理"):
151
  with gr.Row():
152
  with gr.Column(scale=1):
153
+ gr.Markdown("### 📂 フォルダ指定")
154
+ input_folder = gr.Textbox(
155
+ label="📥 入力フォルダパス",
156
+ placeholder="例: examples/assets/example/REI/input",
157
+ value="examples/assets/example/REI/input"
158
+ )
159
+ output_folder = gr.Textbox(
160
+ label="📤 出力フォルダパス",
161
+ placeholder="例: examples/assets/example/REI/output",
162
+ value="examples/assets/example/REI/output"
163
+ )
164
+
165
+ processing_mode = gr.Radio(
166
+ label="🔄 処理モード",
167
+ choices=["順次結合", "ペア結合"],
168
+ value="順次結合",
169
+ info="順次結合: 全動画を1つに結合 / ペア結合: 2つずつペアで結合"
170
+ )
171
+
172
+ output_filename = gr.Textbox(
173
+ label="📄 出力ファイル名 (順次結合のみ)",
174
+ placeholder="REI_merged_sequence.mp4",
175
+ value="REI_merged_sequence.mp4"
176
  )
177
+
178
+ batch_btn = gr.Button("🚀 バッチ処理実行", variant="primary", size="lg")
179
 
180
  with gr.Column(scale=1):
181
+ gr.Markdown("### 📊 処理結果")
182
+ batch_result = gr.Textbox(
183
+ label="📝 バッチ処理結果",
184
  lines=15,
 
185
  show_copy_button=True
186
  )
 
 
 
 
 
 
 
 
187
 
188
+ batch_output = gr.Video(
189
+ label="🎬 出力動画(プレビュー)",
190
+ height=300
191
+ )
192
+
193
+
194
+ # 動画情報の自動更新
195
+ def update_video1_info(video):
196
+ if video is None:
197
+ return ""
198
+ return analyze_video_details(video)
199
 
200
+ def update_video2_info(video):
201
+ if video is None:
202
+ return ""
203
+ return analyze_video_details(video)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
+ video1_input.change(
206
+ fn=update_video1_info,
207
+ inputs=video1_input,
208
+ outputs=video1_info
 
 
 
 
 
 
 
 
 
 
209
  )
210
 
211
+ video2_input.change(
212
+ fn=update_video2_info,
213
+ inputs=video2_input,
214
+ outputs=video2_info
215
+ )
216
+
217
+ # メイン処理
218
+ bridge_btn.click(
219
+ fn=process_video_bridge,
220
+ inputs=[video1_input, video2_input],
221
+ outputs=[result_text, merged_video, connection_frame1, connection_frame2, similarity_score]
222
+ )
223
+
224
+ # サンプル動画処理
225
+ sample_btn.click(
226
+ fn=process_sample_videos,
227
+ inputs=[],
228
+ outputs=[result_text, merged_video, connection_frame1, connection_frame2, similarity_score]
229
+ )
230
+
231
+ # バッチ処理
232
+ batch_btn.click(
233
+ fn=process_batch_videos,
234
+ inputs=[input_folder, output_folder, processing_mode, output_filename],
235
+ outputs=[batch_result, batch_output]
236
+ )
237
 
238
+ # 使用方法の説明
239
  gr.Markdown("---")
240
+ gr.Markdown("### 🎯 使用方法")
241
+
242
+ with gr.Tabs():
243
+ with gr.TabItem("🎥 単体処理"):
244
+ gr.Markdown("1. **動画1(前半)**: 結合したい最初の動画をアップロード")
245
+ gr.Markdown("2. **動画2(後半)**: 結合したい2番目の動画をアップロード")
246
+ gr.Markdown("3. **フレームブリッジ実行**: AIが最適な接続点を自動検出して結合")
247
+ gr.Markdown("4. **サンプル動画で試す**: assetsフォルダのサンプル動画で機能をテスト")
248
+
249
+ with gr.TabItem("📁 バッチ処理"):
250
+ gr.Markdown("1. **入力フォルダ**: 動画ファイルが格納されたフォルダパスを指定")
251
+ gr.Markdown("2. **出力フォルダ**: 結合結果を保存するフォルダパスを指定")
252
+ gr.Markdown("3. **処理モード選択**:")
253
+ gr.Markdown(" - **順次結合**: フォルダ内の全動画を名前順に1つの動画に結合")
254
+ gr.Markdown(" - **ペア結合**: 動画を2つずつペアにして結合(複数の出力ファイル)")
255
+ gr.Markdown("4. **出力ファイル名**: 順次結合の場合の最終ファイル名を指定")
256
+ gr.Markdown("5. **バッチ処理実行**: 指定した設定で一括処理を開始")
257
+
258
+ gr.Markdown("### 🔬 技術的特徴")
259
+ gr.Markdown("- **SSIM(構造的類似性指標)**: フレーム間の視覚的類似度を高精度で計算")
260
+ gr.Markdown("- **自動最適化**: 動画1の終了部分と動画2の開始部分から最適な接続点を検出")
261
+ gr.Markdown("- **スムーズな結合**: 視覚的に自然な動画結合を実現")
262
+ gr.Markdown("- **バッチ処理**: 複数動画の自動処理とレポート生成")
263
+ gr.Markdown("- **ファイル名ソート**: 自然順序でのファイル名ソートによる正確な順序処理")
264
 
265
  # ZENテーマの説明
266
  gr.HTML("""
267
  <div style='text-align: center; margin-top: 2rem; padding: 1.5rem; background: #ffffff; border-radius: 12px;'>
268
  <h3 style='color: #3d405b; margin-top: 0;'>🧘‍♀️ ZENテーマ</h3>
269
+ <p style='color: #8b7355;'>和モダンなデザインで、直感的な動画編集体験を提供</p>
270
+ <p style='color: #8b7355; font-size: 0.9rem;'>単体処理とバッチ処理の両方に対応した高機能動画結合アプリ</p>
271
  </div>
272
  """)
273