import gradio as gr import torch from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline import PyPDF2 import requests from bs4 import BeautifulSoup import re import warnings import os warnings.filterwarnings("ignore") # ZeroGPU環境の検出 IS_ZEROGPU = os.environ.get("SPACE_ID") is not None and "zero-gpu" in os.environ.get("SPACE_ID", "").lower() # spacesライブラリのインポート(ZeroGPU必須) try: import spaces print("✅ spacesライブラリが正常にインポートされました") GPU_DECORATOR = spaces.GPU except ImportError as e: print(f"⚠️ spacesライブラリのインポートに失敗: {e}") if IS_ZEROGPU: print("🚨 ZeroGPU環境ではspacesライブラリが必須です") raise ImportError("ZeroGPU環境でspacesライブラリが見つかりません。requirements.txtにspaces>=0.19.0を追加してください。") else: print("ℹ️ ローカル環境のため、spacesライブラリは不要です") # ローカル環境用のダミーデコレータ GPU_DECORATOR = lambda duration=None: lambda func: func class TextSummarizer: def __init__(self): # 環境の検出 self.is_zerogpu = IS_ZEROGPU print(f"実行環境: {'ZeroGPU' if self.is_zerogpu else 'Local/Standard'}") # デバイス設定 if self.is_zerogpu: self.device = "cuda" # ZeroGPUでは常にCUDA else: self.device = "cuda" if torch.cuda.is_available() else "cpu" print(f"使用デバイス: {self.device}") print(f"PyTorch バージョン: {torch.__version__}") # ZeroGPU以外でのバージョンチェック if not self.is_zerogpu: torch_version = torch.__version__.split('+')[0] try: major, minor = map(int, torch_version.split('.')[:2]) if major < 2 or (major == 2 and minor < 6): print("⚠️ 警告: PyTorch v2.6未満です。セキュリティ脆弱性(CVE-2025-32434)のため、アップグレードを推奨します。") except ValueError: print("⚠️ PyTorchバージョンの解析に失敗しました") # モデル読み込み(ZeroGPU対応) self._load_model() def _load_model(self): """モデルの読み込み(環境に応じて最適化)""" try: print("モデルを読み込み中...") # ZeroGPU環境では軽量モデルを優先 # if self.is_zerogpu: model_name = "tsmatz/mt5_summarize_japanese" # else: # model_name = "facebook/bart-large-cnn" # モデル読み込み設定(use_safetensorsを削除) pipeline_kwargs = { "task": "summarization", "model": model_name, "device": 0 if self.device == "cuda" else -1, "framework": "pt", "trust_remote_code": False # セキュリティ強化 } # 基本的なパイプライン作成 self.summarizer = pipeline(**pipeline_kwargs) print(f"✅ {model_name} の読み込み完了") except Exception as e: print(f"❌ メインモデル読み込みエラー: {e}") # 最軽量フォールバック try: print("最軽量フォールバックモデルを試行中...") self.summarizer = pipeline( "summarization", model="sshleifer/distilbart-cnn-6-6", device=0 if self.device == "cuda" else -1, trust_remote_code=False ) print("✅ 最軽量モデルで読み込み完了") except Exception as e2: print(f"❌ 全てのモデル読み込みに失敗: {e2}") raise Exception(f"モデルの読み込みに失敗しました: {e2}") def clean_text(self, text): """テキストの前処理""" # 不要な文字や改行を整理 text = re.sub(r'\n+', '\n', text) text = re.sub(r'\s+', ' ', text) text = text.strip() return text def chunk_text(self, text, max_length=1000): """長いテキストをチャンクに分割""" sentences = text.split('.') chunks = [] current_chunk = "" for sentence in sentences: if len(current_chunk + sentence) < max_length: current_chunk += sentence + "." else: if current_chunk: chunks.append(current_chunk.strip()) current_chunk = sentence + "." if current_chunk: chunks.append(current_chunk.strip()) return chunks def summarize_text(self, text, max_length=150, min_length=50): """テキストを要約(ZeroGPU対応)""" try: cleaned_text = self.clean_text(text) if len(cleaned_text) < 100: return "テキストが短すぎるため、要約できません。" # テキストが長い場合はチャンクに分割 if len(cleaned_text) > 1000: chunks = self.chunk_text(cleaned_text) summaries = [] for chunk in chunks: try: result = self.summarizer( chunk, max_length=max_length, min_length=min_length, do_sample=False ) summaries.append(result[0]['summary_text']) except Exception as e: print(f"チャンク要約エラー: {e}") continue # チャンクの要約を統合 combined_summary = " ".join(summaries) if len(combined_summary) > max_length * 2: # 再度要約 final_result = self.summarizer( combined_summary, max_length=max_length, min_length=min_length, do_sample=False ) return final_result[0]['summary_text'] else: return combined_summary else: result = self.summarizer( cleaned_text, max_length=max_length, min_length=min_length, do_sample=False ) return result[0]['summary_text'] except Exception as e: return f"要約処理でエラーが発生しました: {str(e)}" def structure_summary(self, summary_text): """要約を構造化""" # 簡単な構造化ロジック(実際のプロジェクトではより高度な処理が必要) sentences = summary_text.split('.') structured_output = "## 📋 要約結果\n\n" if len(sentences) >= 3: structured_output += "### 🎯 主要ポイント\n" structured_output += f"- {sentences[0].strip()}\n\n" structured_output += "### 📊 詳細内容\n" for i, sentence in enumerate(sentences[1:-1], 1): if sentence.strip(): structured_output += f"{i}. {sentence.strip()}\n" if sentences[-1].strip(): structured_output += f"\n### 💡 結論\n" structured_output += f"- {sentences[-1].strip()}\n" else: structured_output += f"### 📄 要約内容\n{summary_text}\n" return structured_output def extract_text_from_pdf(self, pdf_file): """PDFからテキストを抽出""" try: reader = PyPDF2.PdfReader(pdf_file) text = "" for page in reader.pages: text += page.extract_text() + "\n" return text except Exception as e: return f"PDFの読み込みでエラーが発生しました: {str(e)}" def extract_text_from_url(self, url): """Webサイトからテキストを抽出""" try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(url, headers=headers, timeout=10) response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, 'html.parser') # 不要なタグを削除 for tag in soup(['script', 'style', 'nav', 'header', 'footer']): tag.decompose() # テキストを抽出 text = soup.get_text() return self.clean_text(text) except Exception as e: return f"Webサイトの読み込みでエラーが発生しました: {str(e)}" # グローバルインスタンス(ZeroGPU環境では起動時に初期化が必要) summarizer = None def initialize_model(): """モデルの初期化(ZeroGPU対応)""" global summarizer if summarizer is None: summarizer = TextSummarizer() return summarizer @GPU_DECORATOR(duration=30) def process_text_input(text, max_length, min_length): """テキスト入力の処理""" try: print(f"テキスト処理開始: {len(text) if text else 0}文字") if not text or not text.strip(): return "## ⚠️ エラー\nテキストを入力してください。" # モデル初期化 model = initialize_model() # 要約実行 summary = model.summarize_text(text, int(max_length), int(min_length)) result = model.structure_summary(summary) print("テキスト処理完了") return result except Exception as e: error_msg = f"## ❌ エラーが発生しました\n```\n{str(e)}\n```" print(f"テキスト処理エラー: {e}") return error_msg @GPU_DECORATOR(duration=30) def process_pdf_input(pdf_file, max_length, min_length): """PDF入力の処理""" try: print(f"PDF処理開始: {pdf_file}") if pdf_file is None: return "## ⚠️ エラー\nPDFファイルを選択してください。" # モデル初期化 model = initialize_model() text = model.extract_text_from_pdf(pdf_file) if text.startswith("PDFの読み込みで"): return f"## ❌ エラー\n{text}" summary = model.summarize_text(text, int(max_length), int(min_length)) result = model.structure_summary(summary) print("PDF処理完了") return result except Exception as e: error_msg = f"## ❌ エラーが発生しました\n```\n{str(e)}\n```" print(f"PDF処理エラー: {e}") return error_msg @GPU_DECORATOR(duration=30) def process_url_input(url, max_length, min_length): """URL入力の処理""" try: print(f"URL処理開始: {url}") if not url or not url.strip(): return "## ⚠️ エラー\nURLを入力してください。" if not url.startswith(('http://', 'https://')): url = 'https://' + url # モデル初期化 model = initialize_model() text = model.extract_text_from_url(url) if text.startswith("Webサイトの読み込みで"): return f"## ❌ エラー\n{text}" summary = model.summarize_text(text, int(max_length), int(min_length)) result = model.structure_summary(summary) print("URL処理完了") return result except Exception as e: error_msg = f"## ❌ エラーが発生しました\n```\n{str(e)}\n```" print(f"URL処理エラー: {e}") return error_msg # Gradioインターフェース作成 def create_interface(): with gr.Blocks(title="🤖 ローカルLLM テキスト要約ツール", theme=gr.themes.Soft()) as app: gr.Markdown(""" # 🤖 ローカルLLM テキスト要約ツール (ZeroGPU対応) このツールは、ローカルで動作するLLMを使用してテキストを要約し、構造化された形式で出力します。 ## 🚀 環境対応 - **ZeroGPU**: Hugging Face Spaces自動最適化 - **ローカル環境**: 高性能モデル対応 - **セキュリティ**: trust_remote_code=False設定 ## 📝 対応入力形式 - **テキスト直接入力** - **PDFファイル** - **Webサイト URL** """) # 要約設定 with gr.Row(): max_length = gr.Slider( minimum=50, maximum=500, value=150, step=10, label="最大要約長", info="要約の最大文字数" ) min_length = gr.Slider( minimum=20, maximum=200, value=50, step=10, label="最小要約長", info="要約の最小文字数" ) # タブインターフェース with gr.Tabs(): # テキスト入力タブ with gr.TabItem("📝 テキスト入力"): with gr.Row(): with gr.Column(): text_input = gr.Textbox( lines=10, placeholder="要約したいテキストを入力してください...", label="入力テキスト" ) text_btn = gr.Button("🔍 要約実行", variant="primary") with gr.Column(): text_output = gr.Markdown(label="要約結果") text_btn.click( fn=process_text_input, inputs=[text_input, max_length, min_length], outputs=text_output, show_progress=True ) # PDF入力タブ with gr.TabItem("📄 PDF入力"): with gr.Row(): with gr.Column(): pdf_input = gr.File( file_types=[".pdf"], label="PDFファイルを選択" ) pdf_btn = gr.Button("🔍 PDF要約実行", variant="primary") with gr.Column(): pdf_output = gr.Markdown(label="要約結果") pdf_btn.click( fn=process_pdf_input, inputs=[pdf_input, max_length, min_length], outputs=pdf_output, show_progress=True ) # URL入力タブ with gr.TabItem("🌐 Website URL"): with gr.Row(): with gr.Column(): url_input = gr.Textbox( placeholder="https://example.com", label="ウェブサイトURL" ) url_btn = gr.Button("🔍 Web要約実行", variant="primary") with gr.Column(): url_output = gr.Markdown(label="要約結果") url_btn.click( fn=process_url_input, inputs=[url_input, max_length, min_length], outputs=url_output, show_progress=True ) # 使用方法 gr.Markdown(""" ## 🔧 使用方法 1. **要約設定**: 最大・最小要約長を調整 2. **入力方法選択**: テキスト直接入力、PDFアップロード、URL入力から選択 3. **実行**: 対応する実行ボタンをクリック 4. **結果確認**: 構造化された要約結果を確認 ## ⚙️ 技術仕様 (ZeroGPU対応) - **モデル**: DistilBART/BART (環境に応じて自動選択) - **ZeroGPU**: Hugging Face Spaces最適化 - **セキュリティ**: trust_remote_code=False設定 - **GPU加速**: 環境自動検出 - **出力形式**: 構造化Markdown ## 🔧 環境別最適化 - ZeroGPU: 軽量モデル自動選択 - ローカル: 高性能モデル利用可能 - 互換性: 幅広いTransformersバージョン対応 """) return app if __name__ == "__main__": # 環境情報表示 print(f""" 🚀 テキスト要約ツール起動 🚀 実行環境: {'ZeroGPU (Hugging Face Spaces)' if IS_ZEROGPU else 'ローカル環境'} PyTorchバージョン: {torch.__version__} spacesライブラリ: {'✅ 利用可能' if 'spaces' in globals() else '❌ 未インストール'} """) if IS_ZEROGPU: print("✅ ZeroGPU環境で最適化済み") print("🔧 @spaces.GPU デコレータが適用されています") else: # ローカル環境でのセキュリティチェック try: torch_version = torch.__version__.split('+')[0] major, minor = map(int, torch_version.split('.')[:2]) if major < 2 or (major == 2 and minor < 6): print("⚠️ セキュリティ警告: PyTorch v2.6未満") print(" 推奨: pip install torch>=2.6.0") else: print("✅ PyTorchセキュリティ: OK") except ValueError: print("⚠️ PyTorchバージョン確認不能") # アプリケーション起動 app = create_interface() if IS_ZEROGPU: # ZeroGPU環境用設定 app.launch() else: # ローカル環境用設定 app.launch( server_name="0.0.0.0", server_port=7860, share=True, debug=True )