YO-LOLO commited on
Commit
0ec1732
·
verified ·
1 Parent(s): e1171ed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +500 -60
app.py CHANGED
@@ -1,64 +1,504 @@
 
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
3
-
4
- """
5
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
- """
7
- client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
8
-
9
-
10
- def respond(
11
- message,
12
- history: list[tuple[str, str]],
13
- system_message,
14
- max_tokens,
15
- temperature,
16
- top_p,
17
- ):
18
- messages = [{"role": "system", "content": system_message}]
19
-
20
- for val in history:
21
- if val[0]:
22
- messages.append({"role": "user", "content": val[0]})
23
- if val[1]:
24
- messages.append({"role": "assistant", "content": val[1]})
25
-
26
- messages.append({"role": "user", "content": message})
27
-
28
- response = ""
29
-
30
- for message in client.chat_completion(
31
- messages,
32
- max_tokens=max_tokens,
33
- stream=True,
34
- temperature=temperature,
35
- top_p=top_p,
36
- ):
37
- token = message.choices[0].delta.content
38
-
39
- response += token
40
- yield response
41
-
42
-
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- demo = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
- )
61
 
 
 
 
 
 
 
 
 
 
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if __name__ == "__main__":
64
- demo.launch()
 
 
 
1
+ import os
2
  import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ from pathlib import Path
6
+ import time
7
+ import hashlib
8
+ from datetime import datetime
9
+ import torch
10
+ from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # 必要なライブラリをインポート
13
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
14
+ from langchain.embeddings import HuggingFaceEmbeddings
15
+ from langchain.vectorstores import Chroma
16
+ from langchain.chains import RetrievalQA
17
+ from langchain.prompts import PromptTemplate
18
+ from langchain.llms import HuggingFacePipeline
19
+ from langchain.schema import Document
20
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
21
+ from transformers import T5ForConditionalGeneration, T5Tokenizer
22
 
23
+ # 条件付きインポート(ローカル環境とHugging Face Spacesの両方に対応)
24
+ try:
25
+ import fitz # PyMuPDF
26
+ PYMUPDF_AVAILABLE = True
27
+ except ImportError:
28
+ PYMUPDF_AVAILABLE = False
29
+ print("PyMuPDFが利用できません。PDFファイルはテキスト抽出のみで処理されます。")
30
+
31
+ try:
32
+ import easyocr
33
+ EASYOCR_AVAILABLE = True
34
+ except ImportError:
35
+ EASYOCR_AVAILABLE = False
36
+ print("EasyOCRが利用できません。OCR機能は無効化されます。")
37
+
38
+ try:
39
+ import cv2
40
+ CV2_AVAILABLE = True
41
+ except ImportError:
42
+ CV2_AVAILABLE = False
43
+ print("OpenCVが利用できません。画像処理機能は制限されます。")
44
+
45
+ class ManualChatbot:
46
+ def __init__(self, docs_dir="./manuals"):
47
+ """手順書チャットボットの初期化"""
48
+ self.docs_dir = docs_dir
49
+ self.vectorstore = None # ベクトルデータベースの初期化
50
+ self.file_hashes = {} # ファイルのハッシュ値を保持する辞書
51
+ self.last_update_check = None # 最後に更新をチェックした時間
52
+ self.processing_status = "未初期化"
53
+
54
+ # ディレクトリが存在しなければ作成
55
+ os.makedirs(docs_dir, exist_ok=True)
56
+ os.makedirs("./chroma_db", exist_ok=True)
57
+
58
+ # ファイルハッシュの記録ファイルパス
59
+ self.hash_file_path = os.path.join(os.path.dirname(docs_dir), "file_hashes.json")
60
+
61
+ # OCRの初期化(可能な場合)
62
+ if EASYOCR_AVAILABLE:
63
+ self.reader = easyocr.Reader(['ja', 'en']) # 日本語と英語に対応
64
+ print("EasyOCRを初期化しました")
65
+ else:
66
+ self.reader = None
67
+
68
+ # 要約用の T5 モデル準備(モデルサイズを小さくしてHF Spacesでの動作に最適化)
69
+ self.summarizer_model = None
70
+ self.summarizer_tokenizer = None
71
+
72
+ # ハッシュ読み込み
73
+ self._load_file_hashes()
74
+
75
+ def _load_file_hashes(self):
76
+ """保存されたファイルハッシュを読み込む"""
77
+ if os.path.exists(self.hash_file_path):
78
+ try:
79
+ import json
80
+ with open(self.hash_file_path, 'r') as f:
81
+ self.file_hashes = json.load(f)
82
+ print(f"{len(self.file_hashes)}件のファイルハッシュを読み込みました")
83
+ except Exception as e:
84
+ print(f"ファイルハッシュの読み込みに失敗しました: {str(e)}")
85
+ self.file_hashes = {}
86
+ else:
87
+ self.file_hashes = {}
88
+
89
+ def _save_file_hashes(self):
90
+ """ファイルハッシュを保存する"""
91
+ try:
92
+ import json
93
+ with open(self.hash_file_path, 'w') as f:
94
+ json.dump(self.file_hashes, f)
95
+ print(f"{len(self.file_hashes)}件のファイルハッシュを保存しました")
96
+ except Exception as e:
97
+ print(f"ファイルハッシュの保存に失敗しました: {str(e)}")
98
+
99
+ def _get_file_hash(self, file_path):
100
+ """ファイルのMD5ハッシュを計算する"""
101
+ hash_md5 = hashlib.md5()
102
+ with open(file_path, "rb") as f:
103
+ for chunk in iter(lambda: f.read(4096), b""):
104
+ hash_md5.update(chunk)
105
+ return hash_md5.hexdigest()
106
+
107
+ def process_uploaded_files(self, files):
108
+ """
109
+ Gradioからアップロードされたファイルを処理する
110
+ :param files: アップロードされたファイルのリスト
111
+ :return: 処理状況を示すメッセージ
112
+ """
113
+ if not files:
114
+ return "ファイルがアップロードされていません"
115
+
116
+ self.processing_status = "処理中..."
117
+
118
+ # 新しく追加されたファイルを一時的に保存し処理する
119
+ file_paths = []
120
+ for file in files:
121
+ if file is None:
122
+ continue
123
+
124
+ # ファイル拡張子を確認
125
+ filename = file.name
126
+ file_ext = os.path.splitext(filename)[1].lower()
127
+
128
+ if file_ext not in ['.pdf', '.xlsx', '.xls', '.png', '.jpg', '.jpeg']:
129
+ continue
130
+
131
+ # ファイルを保存する
132
+ save_path = os.path.join(self.docs_dir, os.path.basename(filename))
133
+ with open(save_path, 'wb') as f:
134
+ f.write(file.read())
135
+
136
+ file_paths.append(save_path)
137
+
138
+ if not file_paths:
139
+ self.processing_status = "サポートされているファイルがありませんでした"
140
+ return "サポートされているファイルがありませんでした(.pdf, .xlsx, .xls, .png, .jpg, .jpeg)"
141
+
142
+ # ファイルを処理して知識ベースを更新
143
+ self.update_knowledge_base(file_paths)
144
+
145
+ self.processing_status = "準備完了"
146
+ return f"{len(file_paths)}個のファイルが処理され、知識ベースに追加されました"
147
+
148
+ def update_knowledge_base(self, file_paths):
149
+ """
150
+ 指定したファイルから新しいデータを読み込み、インデックスを更新する
151
+ :param file_paths: 更新したファイルのパス一覧(リスト)
152
+ """
153
+ print(f"{len(file_paths)}件のファイルを処理します...")
154
+
155
+ new_documents = []
156
+ for file_path in file_paths:
157
+ if file_path.lower().endswith(".pdf"):
158
+ new_documents.extend(self._process_pdf(file_path))
159
+ elif file_path.lower().endswith((".xlsx", ".xls")):
160
+ new_documents.extend(self._process_excel(file_path))
161
+ elif file_path.lower().endswith((".png", ".jpg", ".jpeg")):
162
+ new_documents.extend(self._process_image(file_path))
163
+
164
+ if not new_documents:
165
+ print("処理対象のドキュメントがありませんでした")
166
+ return
167
+
168
+ print(f"{len(new_documents)}件のドキュメントを処理しました")
169
+
170
+ # テキスト分割
171
+ text_splitter = RecursiveCharacterTextSplitter(
172
+ chunk_size=1000,
173
+ chunk_overlap=200,
174
+ separators=["\n\n", "\n", "。", "、", " ", ""]
175
+ )
176
+
177
+ chunks = text_splitter.split_documents(new_documents)
178
+ print(f"{len(chunks)}個のテキストチャンクに分割しました")
179
+
180
+ # 埋め込みモデルの初期化
181
+ embeddings = HuggingFaceEmbeddings(
182
+ model_name="intfloat/multilingual-e5-base", # 軽量化のためbaseモデルを使用
183
+ model_kwargs={'device': 'cpu'} # Spacesでは常にCPUを使用
184
+ )
185
+
186
+ # 既存のベクトルストアが存在する場合は追加、なければ新規作成
187
+ if self.vectorstore is None:
188
+ self.vectorstore = Chroma.from_documents(
189
+ documents=chunks,
190
+ embedding=embeddings,
191
+ persist_directory="./chroma_db"
192
+ )
193
+ else:
194
+ # 既存のベクトルストアに新しいドキュメントを追加
195
+ self.vectorstore.add_documents(chunks)
196
+
197
+ # ベクトルストアを保存
198
+ self.vectorstore.persist()
199
+
200
+ # もしQAチェーンがなければ初期化
201
+ if not hasattr(self, 'qa_chain') or self.qa_chain is None:
202
+ self._initialize_qa_chain()
203
+ else:
204
+ # QAチェーンを更新���れた検索エンジンで更新
205
+ self.qa_chain.retriever = self.vectorstore.as_retriever(search_kwargs={"k": 3})
206
+
207
+ print("知識ベースを更新しました!")
208
+
209
+ def _process_pdf(self, file_path):
210
+ """PDFファイルを処理してドキュメントを返す"""
211
+ try:
212
+ # PyMuPDFが利用可能な場合
213
+ if PYMUPDF_AVAILABLE:
214
+ doc = fitz.open(file_path)
215
+ all_text = ""
216
+
217
+ for page_num, page in enumerate(doc):
218
+ text = page.get_text()
219
+ all_text += f"--- Page {page_num + 1} ---\n{text}\n\n"
220
+
221
+ # OCRが必要か確認(テキストが少ない場合)
222
+ if len(all_text.strip()) < 100 and EASYOCR_AVAILABLE and self.reader:
223
+ all_text = self.extract_text_from_pdf_with_ocr(file_path)
224
+
225
+ return [Document(page_content=all_text, metadata={"source": file_path})]
226
+ else:
227
+ # 簡易処理(PyMuPDFが利用できない場合)
228
+ # 注意: この場合はPDFの内容を適切に抽出できない可能性がある
229
+ return [Document(page_content=f"PDF file: {os.path.basename(file_path)}",
230
+ metadata={"source": file_path})]
231
+ except Exception as e:
232
+ print(f"PDFファイルの処理中にエラーが発生しました ({file_path}): {str(e)}")
233
+ return []
234
+
235
+ def _process_excel(self, file_path):
236
+ """Excelファイルを処理してドキュメントを返す"""
237
+ try:
238
+ # Pandas でExcelを読み込む
239
+ dfs = pd.read_excel(file_path, sheet_name=None)
240
+ documents = []
241
+
242
+ for sheet_name, df in dfs.items():
243
+ # NaN値を空文字列に変換
244
+ df = df.fillna('')
245
+
246
+ # 各行をテキストに変換
247
+ for idx, row in df.iterrows():
248
+ content = f"Sheet: {sheet_name}, Row: {idx}\n"
249
+ for col in df.columns:
250
+ content += f"{col}: {row[col]}\n"
251
+
252
+ doc = Document(
253
+ page_content=content,
254
+ metadata={"source": file_path, "sheet": sheet_name, "row": idx}
255
+ )
256
+ documents.append(doc)
257
+
258
+ return documents
259
+ except Exception as e:
260
+ print(f"Excelファイルの処理中にエラーが発生しました ({file_path}): {str(e)}")
261
+ return []
262
+
263
+ def _process_image(self, file_path):
264
+ """画像ファイルを処理してドキュメントを返す"""
265
+ try:
266
+ if not EASYOCR_AVAILABLE or not self.reader:
267
+ return [Document(
268
+ page_content=f"画像ファイル: {os.path.basename(file_path)} (OCR未対応)",
269
+ metadata={"source": file_path}
270
+ )]
271
+
272
+ img = Image.open(file_path)
273
+
274
+ # EasyOCRで画像からテキストを抽出
275
+ result = self.reader.readtext(np.array(img))
276
+
277
+ # 抽出されたテキストを結合
278
+ text = "\n".join([detection[1] for detection in result])
279
+
280
+ if not text.strip():
281
+ text = f"画像ファイル: {os.path.basename(file_path)} (テキスト検出なし)"
282
+
283
+ # ドキュメントとしてリストに追加
284
+ return [Document(page_content=text, metadata={"source": file_path})]
285
+ except Exception as e:
286
+ print(f"画像ファイルの処理中にエラーが発生しました ({file_path}): {str(e)}")
287
+ return []
288
+
289
+ def _initialize_qa_chain(self):
290
+ """QAチェーンを初期化する"""
291
+ try:
292
+ # LLMの初期化(小さいモデルを使用)
293
+ model_name = "cyberagent/open-calm-small" # 日本語対応の小さいモデル
294
+
295
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
296
+ model = AutoModelForCausalLM.from_pretrained(model_name)
297
+
298
+ pipe = pipeline(
299
+ "text-generation",
300
+ model=model,
301
+ tokenizer=tokenizer,
302
+ max_new_tokens=300,
303
+ temperature=0.7,
304
+ do_sample=True,
305
+ device="cpu" # Spaces環境ではCPU使用
306
+ )
307
+
308
+ local_llm = HuggingFacePipeline(pipeline=pipe)
309
+
310
+ # プロンプトテンプレート
311
+ template = """
312
+ 次の手順書データを使って質問に答えてください。
313
+
314
+ ### 手順書データ:
315
+ {context}
316
+
317
+ ### 質問:
318
+ {question}
319
+
320
+ ### 回答:
321
+ """
322
+
323
+ prompt = PromptTemplate(
324
+ template=template,
325
+ input_variables=["context", "question"]
326
+ )
327
+
328
+ # QAチェーンの作成
329
+ self.qa_chain = RetrievalQA.from_chain_type(
330
+ llm=local_llm,
331
+ chain_type="stuff",
332
+ retriever=self.vectorstore.as_retriever(search_kwargs={"k": 3}),
333
+ chain_type_kwargs={"prompt": prompt},
334
+ return_source_documents=True
335
+ )
336
+
337
+ print("QAチェーンを初期化しました")
338
+ except Exception as e:
339
+ print(f"QAチェーンの初期化中にエラーが発生しました: {str(e)}")
340
+ self.qa_chain = None
341
+
342
+ def extract_text_from_pdf_with_ocr(self, pdf_path):
343
+ """PDFファイルからテキストを抽出し、必要に応じてOCRを適用する"""
344
+ if not PYMUPDF_AVAILABLE or not EASYOCR_AVAILABLE or not self.reader:
345
+ return f"PDF: {os.path.basename(pdf_path)} (OCR未対応)"
346
+
347
+ doc = fitz.open(pdf_path)
348
+ full_text = ""
349
+
350
+ for page_num, page in enumerate(doc):
351
+ # テキストの抽出を試みる
352
+ text = page.get_text()
353
+
354
+ # テキストが少ない場合はOCRを適用する
355
+ if len(text.strip()) < 50: # 少ないテキストの閾値
356
+ # ページを画像として抽出
357
+ pix = page.get_pixmap()
358
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
359
+ img_np = np.array(img)
360
+
361
+ # EasyOCRを使用してテキスト抽出
362
+ result = self.reader.readtext(img_np)
363
+ ocr_text = "\n".join([detection[1] for detection in result])
364
+
365
+ # OCRテキストを使用
366
+ text = ocr_text if ocr_text.strip() else text
367
+
368
+ full_text += f"--- Page {page_num + 1} ---\n{text}\n\n"
369
+
370
+ return full_text
371
+
372
+ def ask(self, question):
373
+ """質問をボットに問いかけ、回答と参照ソースを取得する"""
374
+ if not hasattr(self, 'qa_chain') or self.qa_chain is None:
375
+ return "チャットボットがまだ初期化されていません。ファイルをアップロードしてください。", ""
376
+
377
+ try:
378
+ result = self.qa_chain.invoke({"query": question})
379
+
380
+ # 回答の取得
381
+ if "result" in result:
382
+ answer = result["result"]
383
+ else:
384
+ return "回答を生成できませんでした。", ""
385
+
386
+ # 参照ソースの取得
387
+ source_documents = result.get("source_documents", [])
388
+ sources_text = ""
389
+
390
+ if source_documents:
391
+ sources_text = "参照ソース:\n"
392
+ for i, doc in enumerate(source_documents, 1):
393
+ source = doc.metadata.get("source", "不明")
394
+ filename = os.path.basename(source)
395
+ sources_text += f"{i}. {filename}\n"
396
+
397
+ return answer, sources_text
398
+
399
+ except Exception as e:
400
+ return f"エラーが発生しました: {str(e)}", ""
401
+
402
+ def load(self):
403
+ """保存済みのベクトルストアを読み込む"""
404
+ if os.path.exists("./chroma_db"):
405
+ try:
406
+ embeddings = HuggingFaceEmbeddings(
407
+ model_name="intfloat/multilingual-e5-base",
408
+ model_kwargs={'device': 'cpu'}
409
+ )
410
+
411
+ self.vectorstore = Chroma(
412
+ persist_directory="./chroma_db",
413
+ embedding_function=embeddings
414
+ )
415
+
416
+ # QAチェーンを初期化
417
+ self._initialize_qa_chain()
418
+
419
+ self.processing_status = "準備完了"
420
+ return "保存済みの知識ベースを読み込みました"
421
+ except Exception as e:
422
+ self.processing_status = "エラー"
423
+ return f"知識ベースの読み込みに失敗しました: {str(e)}"
424
+ else:
425
+ self.processing_status = "初期化待ち"
426
+ return "知識ベースが見つかりません。ファイルをアップロードしてください。"
427
+
428
+ # Gradioインターフェースの作成
429
+ def create_interface():
430
+ # チャットボットのインスタンスを作成
431
+ bot = ManualChatbot(docs_dir="./manuals")
432
+
433
+ # 保存済みデータがあれば読み込む
434
+ load_status = bot.load()
435
+
436
+ # Gradioインターフェース
437
+ with gr.Blocks(title="手順書RAGチャットボット") as demo:
438
+ gr.Markdown("# 手順書RAGチャットボット")
439
+ gr.Markdown("PDFやExcel、画像ファイルをアップロードして、それらの内容に関する質問に答��ます。")
440
+
441
+ with gr.Tab("ファイルアップロード"):
442
+ upload_files = gr.File(file_count="multiple", label="PDFやExcel、画像ファイルをアップロード")
443
+ upload_button = gr.Button("処理開始")
444
+ status_output = gr.Textbox(label="ステータス", value=load_status)
445
+
446
+ upload_button.click(
447
+ fn=bot.process_uploaded_files,
448
+ inputs=[upload_files],
449
+ outputs=[status_output]
450
+ )
451
+
452
+ with gr.Tab("チャット"):
453
+ chatbot = gr.Chatbot(label="会話")
454
+ msg = gr.Textbox(label="質問を入力してください")
455
+ clear = gr.Button("クリア")
456
+
457
+ def respond(message, chat_history):
458
+ if not message.strip():
459
+ return chat_history
460
+
461
+ # ボットに質問する
462
+ bot_response, sources = bot.ask(message)
463
+
464
+ # 回答とソース情報を組み合わせる
465
+ full_response = bot_response
466
+ if sources:
467
+ full_response += f"\n\n{sources}"
468
+
469
+ # チャット履歴を更新する
470
+ chat_history.append((message, full_response))
471
+ return "", chat_history
472
+
473
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
474
+ clear.click(lambda: None, None, chatbot, queue=False)
475
+
476
+ with gr.Tab("使い方"):
477
+ gr.Markdown("""
478
+ ## 使い方
479
+
480
+ 1. **ファイルアップロード**タブで、PDFファイル、Excelファイル、または画像ファイルをアップロードします。
481
+ 2. **処理開始**ボタンをクリックして、ファイルを処理します。
482
+ 3. 処理が完了したら**チャット**タブに移動します。
483
+ 4. 質問を入力して、手順書の内容に基づいた回答を得ることができます。
484
+
485
+ ## サポートしているファイル形式
486
+
487
+ - PDF (.pdf)
488
+ - Excel (.xlsx, .xls)
489
+ - 画像ファイル (.png, .jpg, .jpeg)
490
+
491
+ ## 注意事項
492
+
493
+ - 大きなファイルの処理には時間がかかる場合があります。
494
+ - 画像からのテキスト抽出(OCR)は言語によって精度が異なります。
495
+ - 回答は参照元のドキュメントに基づいて生成されるため、データが不十分な場合は正確な回答ができない場合があります。
496
+ """)
497
+
498
+ return demo
499
+
500
+ # Hugging Face Spacesで実行する場合のエントリーポイント
501
  if __name__ == "__main__":
502
+ # Gradioインターフェースを作成して起動
503
+ demo = create_interface()
504
+ demo.launch()