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