Spaces:
Sleeping
Sleeping
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 | |
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 | |
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 | |
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 | |
) |