mindmap-studio / app.py
kazutab's picture
Upload folder using huggingface_hub
22510af verified
Raw
History Blame Contribute Delete
7.25 kB
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. あなたの推測や外部知識を一切混ぜず、入力文章内の事実のみを構造化してください。"""
@app.post("/generate", response_model=MapResponse)
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")
@app.get("/")
async def root():
# ルートURLにアクセスされたらフロントエンドのindex.htmlを返す
return FileResponse("frontend/index.html")
@app.get("/{filename}")
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)