Spaces:
Runtime error
Runtime error
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from fastapi.responses import FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| import os | |
| import re | |
| try: | |
| from llama_cpp import Llama | |
| USE_LLAMA_CPP = True | |
| print("llama-cpp-python is installed. Will use GGUF inference.") | |
| except ImportError: | |
| USE_LLAMA_CPP = False | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| print("llama-cpp-python is not installed. Falling back to transformers inference.") | |
| app = FastAPI() | |
| # CORS configuration to allow the frontend to communicate with this API | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Allows all origins for local development | |
| allow_credentials=True, | |
| allow_methods=["*"], # Allows all methods | |
| allow_headers=["*"], # Allows all headers | |
| ) | |
| class TextRequest(BaseModel): | |
| text: str | |
| class MapResponse(BaseModel): | |
| markdown: str | |
| # --------------------------------------------------------- | |
| # AI Model Initialization (Phase 5) | |
| # --------------------------------------------------------- | |
| if USE_LLAMA_CPP: | |
| GGUF_PATH = "./production_mindmap_model.gguf" | |
| print("Loading AI Model (GGUF) into memory...") | |
| model = Llama( | |
| model_path=GGUF_PATH, | |
| n_ctx=2048, | |
| n_gpu_layers=-1 # Use all GPU layers if available | |
| ) | |
| print("AIモデル(GGUF)の起動完了!") | |
| else: | |
| MERGED_MODEL_PATH = "./production_mindmap_model_merged" | |
| print("Loading AI Model (Transformers FP16) into memory...") | |
| tokenizer = AutoTokenizer.from_pretrained(MERGED_MODEL_PATH) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MERGED_MODEL_PATH, | |
| torch_dtype=torch.float16, | |
| device_map="auto" | |
| ) | |
| model.eval() | |
| print("AIモデル(Transformers)の起動完了!") | |
| STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出アシスタントです。入力文章の論理構造を正確に読み取り、Markdown形式の目次(マインドマップ)を出力してください。 | |
| 【厳格なルール】 | |
| 1. 企業名、人物名、数値などの固有名詞や事実関係は、入力文章から「正確に」抽出すること。絶対に捏造したり、他の主語と混同したりしてはなりません。 | |
| 2. ただし、情報を階層化して分かりやすく整理するための「一般的なカテゴリ名(例:背景、特徴、課題、概要など)」を親見出しとして補足することは許可します。 | |
| 3. あなたの推測や外部知識を一切混ぜず、入力文章内の事実のみを構造化してください。""" | |
| def generate_mindmap(request: TextRequest): | |
| input_text = request.text.strip() | |
| if not input_text: | |
| raise HTTPException(status_code=400, detail="Text is empty") | |
| print(f"APIリクエストを受信しました(文字数: {len(input_text)}文字)") | |
| print("AIが推論(Markdown構造)を生成中...") | |
| # Qwen用のチャットプロンプト構築 | |
| USER_PROMPT = f"""以下の文章から論理構造を抽出し、Markdown形式の目次(マインドマップ)を出力してください。 | |
| 【出力時の厳守ルール(違反厳禁)】 | |
| 1. 否定表現の厳守:「〜しない」「過度に依存しない」などの否定表現を絶対に見落とさず、意味を逆転させないこと。 | |
| 2. 創作の禁止:記事に明記されていない具体的な行動や予定(例:「〜への参加」「〜の強化を目指す」など)を勝手に推測して付け足さないこと。 | |
| 3. 事実の完全一致:抽出した内容が、元の文章の事実と完全に一致していることのみを出力すること。 | |
| 入力文章: | |
| {input_text}""" | |
| messages = [ | |
| {"role": "system", "content": STRICT_SYSTEM_PROMPT}, | |
| {"role": "user", "content": USER_PROMPT} | |
| ] | |
| if USE_LLAMA_CPP: | |
| response = model.create_chat_completion( | |
| messages=messages, | |
| max_tokens=1024, | |
| temperature=0.0, # Greedy Decoding (推測・創造を完全に排除) | |
| repeat_penalty=1.1 | |
| ) | |
| generated_markdown = response['choices'][0]['message']['content'].strip() | |
| else: | |
| # トークナイザーでプロンプト化 | |
| prompt = tokenizer.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True | |
| ) | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=1024, # 新しく生成するトークン数の上限 | |
| do_sample=False, # Greedy Decoding を有効化 | |
| repetition_penalty=1.1, | |
| pad_token_id=tokenizer.pad_token_id, | |
| eos_token_id=tokenizer.eos_token_id | |
| ) | |
| # 4. 入力プロンプト部分を切り落として、新しく生成された部分だけを取り出す | |
| input_length = inputs["input_ids"].shape[1] | |
| generated_tokens = outputs[0][input_length:] | |
| # 5. 数値(トークン)を人間が読めるMarkdownテキストにデコード | |
| generated_markdown = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip() | |
| # 【超重要】T5トークナイザーが改行(\n)を消してしまう問題の対策 | |
| # 見出し記号(#)の前に強制的に改行を挿入して、Markmapが認識できるようにする | |
| generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip() | |
| # Markmapは必ず「#(大見出し)」から始まる必要があるため、存在しない場合は強制付与 | |
| if not generated_markdown.startswith('#'): | |
| if '##' in generated_markdown or '###' in generated_markdown: | |
| generated_markdown = "# マインドマップ\n" + generated_markdown | |
| else: | |
| generated_markdown = "# マインドマップ\n## 抽出結果\n- " + generated_markdown.replace('\n', '\n- ') | |
| print("生成が完了しました! (コンソール出力時の文字化けエラーを防ぐため内容の表示を省略します)") | |
| return MapResponse(markdown=generated_markdown) | |
| # フロントエンドの静的ファイルをマウント | |
| app.mount("/assets", StaticFiles(directory="frontend"), name="assets") | |
| async def root(): | |
| # ルートURLにアクセスされたらフロントエンドのindex.htmlを返す | |
| return FileResponse("frontend/index.html") | |
| async def get_frontend_file(filename: str): | |
| # app.js や style.css などのファイルを返す | |
| file_path = os.path.join("frontend", filename) | |
| if os.path.exists(file_path): | |
| return FileResponse(file_path) | |
| raise HTTPException(status_code=404, detail="File not found") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |