fudii0921 commited on
Commit
2cf2f23
·
verified ·
1 Parent(s): 51b4d63

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -0
app.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pip3 install cohere google-genai python-dotenv tqdm psycopg2-binary gradio
2
+ import os
3
+ import cohere
4
+ import base64
5
+ from dotenv import load_dotenv
6
+ from pathlib import Path
7
+ import fitz # PyMuPDF
8
+ from google import genai
9
+ from google.genai import types
10
+ import numpy as np
11
+ import psycopg2
12
+ import gradio as gr
13
+ import shutil # To clean up images directory
14
+
15
+ # 環境変数をロードします
16
+ load_dotenv()
17
+ COHERE_API_KEY = os.getenv("COHERE_API_KEY")
18
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
19
+
20
+ # Cohereクライアントを初期化します
21
+ # APIキーが設定されていない場合は、エラーを処理できるようにNoneに設定します
22
+ try:
23
+ co = cohere.ClientV2(api_key=COHERE_API_KEY)
24
+ except Exception as e:
25
+ print(f"Cohereクライアントの初期化に失敗しました: {e}。COHERE_API_KEYが正しく設定されているか確認してください。")
26
+ co = None # エラーを後で処理するためにNoneに設定
27
+
28
+ # データベース接続情報 (ユーザーの指示に従いハードコード)
29
+ '''DB_CONFIG = {
30
+ "dbname": "smair",
31
+ "user": "smairuser",
32
+ "password": "smairuser",
33
+ "host": "www.ryhintl.com",
34
+ "port": 10629
35
+ }'''
36
+
37
+ # 処理済みファイルを保存するディレクトリ
38
+ # アプリケーション起動時に存在確認し、クリーンアップします
39
+ IMAGES_DIR = Path("./pdfimages")
40
+ if IMAGES_DIR.exists():
41
+ print(IMAGES_DIR)
42
+ #shutil.rmtree(IMAGES_DIR) # 以前の実行からの画像をクリア
43
+ IMAGES_DIR.mkdir(exist_ok=True)
44
+
45
+ def convert_pdf_to_images(pdf_path, output_dir_str, zoom=2.0):
46
+ """
47
+ 指定されたPDFファイルをスライド単位で画像(PNG)に変換する関数です。
48
+
49
+ Parameters:
50
+ ----------
51
+ pdf_path : str or Path
52
+ 入力PDFファイルのパス。
53
+
54
+ output_dir_str : str
55
+ 変換後のPNG画像を保存するディレクトリのパス(文字列)。
56
+
57
+ zoom : float, optional (default=2.0)
58
+ 出力画像の拡大率。
59
+
60
+ Returns:
61
+ -------
62
+ image_paths : list of str
63
+ 変換された各スライド画像(PNG)のファイルパス一覧。
64
+ """
65
+ output_dir = Path(output_dir_str)
66
+ output_dir.mkdir(parents=True, exist_ok=True) # ディレクトリが存在しない場合は作成
67
+ pdf_name = Path(pdf_path).stem # PDFファイル名から拡張子を除去
68
+ doc = fitz.open(pdf_path) # PyMuPDFでPDFを開く
69
+ image_paths = []
70
+
71
+ for i, page in enumerate(doc):
72
+ mat = fitz.Matrix(zoom, zoom) # 拡大行列を作成
73
+ pix = page.get_pixmap(matrix=mat) # ピクセルマップを取得
74
+ image_path = output_dir / f"{pdf_name}_{i+1}.png" # 出力画像パスを生成
75
+ pix.save(str(image_path)) # 画像を保存
76
+ image_paths.append(str(image_path)) # パスリストに追加
77
+
78
+ doc.close() # PDFドキュメントを閉じる
79
+ return image_paths
80
+
81
+ def get_image_embedding(image_path):
82
+ """
83
+ 指定した画像ファイル(PNGなど)を Cohere Embed 4 を使ってベクトル化する関数です。
84
+
85
+ Parameters:
86
+ ----------
87
+ image_path : str or Path
88
+ ベクトル化したい画像ファイルのパス。
89
+
90
+ Returns:
91
+ -------
92
+ response : cohere.EmbedByTypeResponse
93
+ Cohere API から返されるレスポンスオブジェクト。
94
+ """
95
+ if co is None:
96
+ raise ValueError("Cohereクライアントが初期化されていません。COHERE_API_KEYを確認してください。")
97
+
98
+ with open(image_path, "rb") as f:
99
+ image_data = f.read() # 画像ファイルをバイナリで読み込み
100
+ stringified_buffer = base64.b64encode(image_data).decode("utf-8") # base64エンコード
101
+ content_type = "image/png" # MIMEタイプ
102
+ image_base64 = f"data:{content_type};base64,{stringified_buffer}" # データURI形式
103
+
104
+ response = co.embed(
105
+ model="embed-v4.0", # 使用するCohereモデル
106
+ input_type="image", # 入力タイプは画像
107
+ embedding_types=["float"], # 浮動小数点ベクトルとして埋め込みを要求
108
+ images=[image_base64], # エンコードされた画像リスト
109
+ )
110
+ return response
111
+
112
+ def generate_answer_with_gemini(image_paths, text_prompt):
113
+ """
114
+ 複数の画像とテキストプロンプトを使って、Google Gemini API によるマルチモーダル応答を生成する関数です。
115
+
116
+ Parameters:
117
+ ----------
118
+ image_paths : list of str or Path
119
+ 入力画像ファイルのパスリスト。
120
+
121
+ text_prompt : str
122
+ Geminiモデルに渡す自然言語のプロンプト(質問・指示文)。
123
+
124
+ Returns:
125
+ -------
126
+ response_text : str
127
+ Gemini APIから返された自然言語応答。API呼び出しに失敗した場合はエラーメッセージを返します。
128
+ """
129
+ if not GEMINI_API_KEY:
130
+ return "[Gemini APIエラー] GEMINI_API_KEYが環境変数に設定されていません。"
131
+
132
+ client = genai.Client(api_key=GEMINI_API_KEY) # Geminiクライアントを初期化
133
+ contents = [text_prompt] # プロンプトをコンテンツリストに追加
134
+
135
+ # 画像パスを検証し、存在するファイルのみを処理します
136
+ valid_image_paths = []
137
+ for path_str in image_paths:
138
+ path = Path(path_str)
139
+ if path.exists() and path.is_file():
140
+ valid_image_paths.append(path)
141
+ else:
142
+ print(f"警告: 画像ファイルが見つかりません: {path_str}")
143
+
144
+ if not valid_image_paths:
145
+ return "[Geminiエラー] 有効な画像がGeminiに渡されませんでした。検索結果を確認してください。"
146
+
147
+ for path in valid_image_paths:
148
+ mime_type = "image/png" if path.suffix.lower() == ".png" else "image/jpeg"
149
+ try:
150
+ with open(path, "rb") as f:
151
+ image_bytes = f.read() # 画像をバイトデータとして読み込み
152
+ part = types.Part.from_bytes(data=image_bytes, mime_type=mime_type) # Gemini Partオブジェクトに変換
153
+ contents.append(part) # コンテンツリストに画像を追加
154
+ except Exception as e:
155
+ print(f"警告: Geminiへの画像読み込み中にエラーが発生しました: {path}, エラー: {e}")
156
+ continue # エラーが発生しても続行
157
+
158
+ try:
159
+ response = client.models.generate_content(
160
+ model="gemini-2.5-flash", # 使用するGeminiモデル
161
+ contents=contents, # プロンプトと画像を含むコンテンツ
162
+ config=types.GenerateContentConfig(
163
+ system_instruction="あなたはスライド画像に関する質問に答えるAIアシスタントです。スライド画像の内容を理解し、質問に答えてください。回答にはスライド画像の内容のみを使用してください。",
164
+ ),
165
+ )
166
+ return response.text # Geminiの回答テキストを返す
167
+ except Exception as e:
168
+ return f"[Gemini APIエラー] {str(e)}" # エラーメッセージを返す
169
+
170
+ def build_pgvector_index_gradio(pdf_file_path, progress=gr.Progress()):
171
+ """
172
+ PDFを画像に変換し、画像から埋め込みを生成し、pgvectorに保存する関数。
173
+ Gradioからの呼び出し用にラップし、プログレスバーに対応します。
174
+
175
+ Parameters:
176
+ ----------
177
+ pdf_file_path : str
178
+ アップロードされたPDFファイルのパス。
179
+ progress : gr.Progress
180
+ Gradioのプログレスバーオブジェクト。
181
+
182
+ Returns:
183
+ -------
184
+ tuple: (str, list)
185
+ 処理ステータスメッセージと、処理された画像ファイルのパスリスト。
186
+ """
187
+ if not COHERE_API_KEY:
188
+ yield "エラー: COHERE_API_KEYが設定されていません。", []
189
+ return
190
+
191
+ if not pdf_file_path:
192
+ yield "エラー: PDFファイルが提供されていません。", []
193
+ return
194
+
195
+ # 新しいPDFアップロードのために、以前の画像とパスをクリアします
196
+ if IMAGES_DIR.exists():
197
+ shutil.rmtree(IMAGES_DIR)
198
+ IMAGES_DIR.mkdir(exist_ok=True)
199
+
200
+ try:
201
+ # 1. PDFを画像に変換
202
+ processing_message = "PDFから画像を抽出中..."
203
+ progress(0.1, desc=processing_message) # 進捗を更新
204
+ yield processing_message, [] # UIを更新
205
+
206
+ image_paths = convert_pdf_to_images(pdf_file_path, str(IMAGES_DIR))
207
+
208
+ if not image_paths:
209
+ yield "警告: PDFから画像が抽出されませんでした。ファイルを確認してください。", []
210
+ return
211
+
212
+ conn = None
213
+ try:
214
+ #conn = psycopg2.connect(**DB_CONFIG) # データベースに接続
215
+ conn = psycopg2.connect(
216
+ dbname="smair",
217
+ user="smairuser",
218
+ password="smairuser",
219
+ host="www.ryhintl.com",
220
+ port=10629
221
+ )
222
+ conn.autocommit = True # 個別のトランザクション処理のために自動コミットを有効化
223
+ cursor = conn.cursor()
224
+
225
+ # imgpdf_embeddings テーブルが存在するかどうかを確認
226
+ cursor.execute("""
227
+ SELECT EXISTS (
228
+ SELECT FROM
229
+ pg_tables
230
+ WHERE
231
+ schemaname = 'public' AND
232
+ tablename = 'imgpdf_embeddings'
233
+ );
234
+ """)
235
+ table_exists = cursor.fetchone()[0]
236
+
237
+ if not table_exists:
238
+ table_create_message = "テーブル 'imgpdf_embeddings' が存在しません。新しく作成します。"
239
+ progress(0.2, desc=table_create_message)
240
+ yield table_create_message, []
241
+ cursor.execute("""
242
+ CREATE TABLE imgpdf_embeddings (
243
+ id SERIAL PRIMARY KEY,
244
+ image_path VARCHAR(255) UNIQUE NOT NULL,
245
+ embedding VECTOR(1536) -- Cohere Embed v4.0 (Image)の次元に合わせる
246
+ );
247
+ """)
248
+ else:
249
+ table_exists_message = "テーブル 'imgpdf_embeddings' は既に存在します。"
250
+ progress(0.2, desc=table_exists_message)
251
+ yield table_exists_message, []
252
+
253
+ processing_message = f"{len(image_paths)} 枚のスライド画像を生成しました。画像を埋め込み、pgvectorデータベースに保存中..."
254
+ yield processing_message, image_paths # 処理された画像パスをすぐに表示
255
+
256
+ # 埋め込みを繰り返し挿入し、進捗を更新
257
+ for i, path in enumerate(image_paths):
258
+ try:
259
+ res = get_image_embedding(path)
260
+ embedding = res.embeddings.float_[0]
261
+
262
+ cursor.execute(
263
+ "INSERT INTO imgpdf_embeddings (image_path, embedding) VALUES (%s, %s) ON CONFLICT (image_path) DO NOTHING;",
264
+ (str(path), str(embedding)) # リストをそのままstr()で文字列化
265
+ )
266
+
267
+ # プログレスバーを更新
268
+ progress((i + 1) / len(image_paths), desc=f"{Path(path).name} の埋め込みを保存しました。")
269
+
270
+ except cohere.errors.TooManyRequestsError as e:
271
+ final_message = f"エラー: Cohere APIのレート制限に達しました。{e.message}。残りの処理を中断します。"
272
+ yield final_message, image_paths
273
+ return # レート制限に達した場合は処理を停止
274
+ except Exception as e:
275
+ # 個別の挿入が失敗しても続行
276
+ warning_message = f"警告: {Path(path).name} の処理中にエラーが発生しました。スキップします。エラー: {e}"
277
+ print(warning_message) # デバッグのためにコンソールに出力
278
+ yield warning_message, image_paths # UIを警告で更新
279
+ continue
280
+
281
+ final_message = "pgvectorへの保存が完了しました。"
282
+ yield final_message, image_paths
283
+
284
+ except psycopg2.Error as e:
285
+ yield f"データベース接続または操作エラー: {e}", []
286
+ except Exception as e:
287
+ yield f"予期せぬエラーが発生しました: {e}", []
288
+ finally:
289
+ if conn:
290
+ conn.close() # データベース接続を閉じる
291
+
292
+ except Exception as e:
293
+ yield f"PDF処理中にエラーが発生しました: {e}", []
294
+
295
+
296
+ def search_pgvector_with_text_gradio(text_query_str):
297
+ """
298
+ テキストクエリに基づいて、pgvectorで類似画像を検索し、Geminiで回答を生成する関数。
299
+ Gradioからの呼び出し用にラップします。
300
+
301
+ Parameters:
302
+ ----------
303
+ text_query_str : str
304
+ 検索するテキストクエリ。
305
+
306
+ Returns:
307
+ -------
308
+ tuple: (str, list, str)
309
+ 検索ステータスメッセージ、検索された画像ファイルのパスリスト、Geminiによる回答。
310
+ """
311
+ if not text_query_str:
312
+ return "検索クエリを入力してください。", [], ""
313
+
314
+ if co is None:
315
+ return "エラー: Cohereクライアントが初期化されていません。COHERE_API_KEYを確認してください。", [], ""
316
+
317
+ conn = None
318
+ try:
319
+ #conn = psycopg2.connect(**DB_CONFIG) # データベースに接続
320
+ conn = psycopg2.connect(
321
+ dbname="smair",
322
+ user="smairuser",
323
+ password="smairuser",
324
+ host="www.ryhintl.com",
325
+ port=10629
326
+ )
327
+ cursor = conn.cursor()
328
+
329
+ response = co.embed(
330
+ model="embed-v4.0", # テキスト埋め込みに使用するCohereモデル
331
+ input_type="text", # 入力タイプはテキスト
332
+ embedding_types=["float"],
333
+ texts=[text_query_str],
334
+ )
335
+ query_vec = response.embeddings.float_[0] # ここで query_vec は既にリストです
336
+
337
+ # pgvectorのL2距離演算子 `<->` を使用して類似検索
338
+ # LIMITはk=3に固定
339
+ cursor.execute(
340
+ "SELECT image_path FROM imgpdf_embeddings ORDER BY embedding <-> %s LIMIT %s",
341
+ (str(query_vec), 3) # query_vec は既にリストなので、そのままstr()で文字列化
342
+ )
343
+
344
+ results = cursor.fetchall() # 結果を取得
345
+
346
+ cursor.close()
347
+ conn.close() # データベース接続を閉じる
348
+
349
+ retrieved_image_paths = [row[0] for row in results] # 検索された画像パスを抽出
350
+
351
+ if not retrieved_image_paths:
352
+ return "テキス���クエリに一致する画像が見つかりませんでした。", [], "一致する画像が見つかりませんでした。"
353
+
354
+ # 検索された画像パスを表示し、Geminiで回答を生成
355
+ gemini_answer = generate_answer_with_gemini(retrieved_image_paths, text_query_str)
356
+
357
+ return "類似画像の検索とGeminiによる回答が完了しました。", retrieved_image_paths, gemini_answer
358
+
359
+ except psycopg2.Error as e:
360
+ return f"データベース検索エラー: {e}", [], ""
361
+ except cohere.errors.TooManyRequestsError as e:
362
+ return f"エラー: Cohere APIのレート制限に達しました。{e.message}", [], ""
363
+ except Exception as e:
364
+ return f"予期せぬエラーが発生しました: {e}", [], ""
365
+ finally:
366
+ if conn:
367
+ conn.close()
368
+
369
+ # Gradio UI Blocks
370
+ with gr.Blocks(title="PDF to RAG with Cohere, PGVector, and Gemini") as demo:
371
+ gr.Markdown(
372
+ """
373
+ <div style="font-size: 12px;">
374
+ <h1>PDFからのRAG(Retrieval-Augmented Generation)アプリ</h1>
375
+ <p>PDFファイルをアップロードし、その内容をCohere EmbeddingsとPGVectorを使用して検索可能な形式でデータベースに保存します。その後、テキストクエリに基づいて関連するスライド画像を検索し、Google Geminiを使って回答を生成します。</p>
376
+ <hr>
377
+ </div>
378
+ """
379
+ )
380
+
381
+ with gr.Tab("PDF処理"):
382
+ pdf_upload_input = gr.File(label="PDFファイルをアップロード", type="filepath", file_types=[".pdf"])
383
+ process_button = gr.Button("PDFを処理してデータベースに保存")
384
+ processing_status = gr.Markdown(label="処理ステータス", value="PDFをアップロードして「処理」ボタンを押してください。")
385
+ # 処理された画像を表示するためのギャラリー
386
+ processed_image_gallery = gr.Gallery(label="処理された画像 (データベースに保存済)", preview=True, height="auto", object_fit="contain")
387
+
388
+ with gr.Tab("RAGによる質問"):
389
+ query_text_input = gr.Textbox(label="質問を入力してください", placeholder="PDFの内容について質問を入力...", info="Mainstream領域でつなげている領域は何ですか?")
390
+ search_button = gr.Button("質問を検索してGeminiで回答")
391
+ search_status = gr.Markdown(label="検索ステータス")
392
+ retrieved_images_gallery = gr.Gallery(label="検索された関連画像", preview=True, height="auto", object_fit="contain")
393
+ gemini_answer_output = gr.Markdown(label="Geminiによる回答")
394
+
395
+ # イベントハンドラー
396
+ # `process_button`がクリックされたときに`build_pgvector_index_gradio`を実行
397
+ process_button.click(
398
+ fn=build_pgvector_index_gradio,
399
+ inputs=[pdf_upload_input],
400
+ outputs=[processing_status, processed_image_gallery]
401
+ )
402
+
403
+ # `search_button`がクリックされたときに`search_pgvector_with_text_gradio`を実行
404
+ search_button.click(
405
+ fn=search_pgvector_with_text_gradio,
406
+ inputs=[query_text_input],
407
+ outputs=[search_status, retrieved_images_gallery, gemini_answer_output]
408
+ )
409
+
410
+ # Gradioアプリを起動
411
+ if __name__ == "__main__":
412
+ demo.launch(share=False) # share=Trueで外部共有可能に
413
+