kazutab commited on
Commit
b91e85f
·
verified ·
1 Parent(s): d4da86c

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +176 -163
app.py CHANGED
@@ -1,163 +1,176 @@
1
- from fastapi import FastAPI, HTTPException
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from pydantic import BaseModel
4
- from fastapi.responses import FileResponse
5
- from fastapi.staticfiles import StaticFiles
6
- import os
7
- import re
8
-
9
- try:
10
- from llama_cpp import Llama
11
- USE_LLAMA_CPP = True
12
- print("llama-cpp-python is installed. Will use GGUF inference.")
13
- except ImportError:
14
- USE_LLAMA_CPP = False
15
- import torch
16
- from transformers import AutoModelForCausalLM, AutoTokenizer
17
- print("llama-cpp-python is not installed. Falling back to transformers inference.")
18
-
19
- app = FastAPI()
20
-
21
- # CORS configuration to allow the frontend to communicate with this API
22
- app.add_middleware(
23
- CORSMiddleware,
24
- allow_origins=["*"], # Allows all origins for local development
25
- allow_credentials=True,
26
- allow_methods=["*"], # Allows all methods
27
- allow_headers=["*"], # Allows all headers
28
- )
29
-
30
- class TextRequest(BaseModel):
31
- text: str
32
-
33
- class MapResponse(BaseModel):
34
- markdown: str
35
-
36
- # ---------------------------------------------------------
37
- # AI Model Initialization (Phase 5)
38
- # ---------------------------------------------------------
39
-
40
- if USE_LLAMA_CPP:
41
- GGUF_PATH = "./production_mindmap_model.gguf"
42
- print("Loading AI Model (GGUF) into memory...")
43
- model = Llama(
44
- model_path=GGUF_PATH,
45
- n_ctx=2048,
46
- n_gpu_layers=-1 # Use all GPU layers if available
47
- )
48
- print("AIモデル(GGUF)の起動完了!")
49
- else:
50
- MERGED_MODEL_PATH = "./production_mindmap_model_merged"
51
- print("Loading AI Model (Transformers FP16) into memory...")
52
- tokenizer = AutoTokenizer.from_pretrained(MERGED_MODEL_PATH)
53
- if tokenizer.pad_token is None:
54
- tokenizer.pad_token = tokenizer.eos_token
55
-
56
- model = AutoModelForCausalLM.from_pretrained(
57
- MERGED_MODEL_PATH,
58
- torch_dtype=torch.float16,
59
- device_map="auto"
60
- )
61
- model.eval()
62
- print("AIモデル(Transformers)の起動完了!")
63
-
64
- STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出アシスタントです。入力文章の論理構造を正確に読み取り、Markdown形式の目次(マインドマップ)を出力してください。
65
-
66
- 【厳格なルール】
67
- 1. 企業名、人物名、数値などの固有名詞や事実関係は、入力文章から「正確に」抽出すること。絶対に捏造したり、他の主語と混同したりしてはなりません。
68
- 2. ただし、情報を階層化して分かりやすく整理するための「一般的なカテゴリ名(例:背景、特徴、課題、概要など)」を親見出しとして補足することは許可します。
69
- 3. あなたの推測や外部知識を一切混ぜず、入力文章内の事実のみを構造化してください。"""
70
-
71
- @app.post("/generate", response_model=MapResponse)
72
- def generate_mindmap(request: TextRequest):
73
- input_text = request.text.strip()
74
- if not input_text:
75
- raise HTTPException(status_code=400, detail="Text is empty")
76
-
77
- print(f"APIリクエストを受信しました文字数: {len(input_text)}文字")
78
- print("AIが推論(Markdown構造)を生成中...")
79
-
80
- # Qwen用チャットプロンプト構築
81
- USER_PROMPT = f"""以下の文章から論理構造を抽出し、Markdown形式目次マインドマップ)を出してください
82
-
83
- 【出力時の厳守ルール(違反厳禁)】
84
- 1. 否定表現の厳守:「〜しない」「過度に依存しない」などの否定表現を絶対に見落とさず、意味を逆転させないこと。
85
- 2. 創作の禁止:記事に明記されていない具体的な行動や予定(例:「〜への参加」「〜の強化を目指す」など)を勝手に推測して付け足さないこと。
86
- 3. 事実の完全一致:抽出した内容が、元の文章の事実と完全に一致していることのみを出力すること。
87
-
88
- 入力文章:
89
- {input_text}"""
90
- messages = [
91
- {"role": "system", "content": STRICT_SYSTEM_PROMPT},
92
- {"role": "user", "content": USER_PROMPT}
93
- ]
94
-
95
- if USE_LLAMA_CPP:
96
- response = model.create_chat_completion(
97
- messages=messages,
98
- max_tokens=1024,
99
- temperature=0.0, # Greedy Decoding (推測・創造を完全に排除)
100
- repeat_penalty=1.1
101
- )
102
- generated_markdown = response['choices'][0]['message']['content'].strip()
103
- else:
104
- # トークナイザーでプロンプト化
105
- prompt = tokenizer.apply_chat_template(
106
- messages,
107
- tokenize=False,
108
- add_generation_prompt=True
109
- )
110
-
111
- inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
112
-
113
- with torch.no_grad():
114
- outputs = model.generate(
115
- **inputs,
116
- max_new_tokens=1024, # 新しく生成するトークン数の上限
117
- do_sample=False, # Greedy Decoding を有効
118
- repetition_penalty=1.1,
119
- pad_token_id=tokenizer.pad_token_id,
120
- eos_token_id=tokenizer.eos_token_id
121
- )
122
-
123
- # 4. 入力プロンプト部分を切り落として、新しく生成された部分だけを取り出す
124
- input_length = inputs["input_ids"].shape[1]
125
- generated_tokens = outputs[0][input_length:]
126
-
127
- # 5. 数値(トークン)を人間が読めるMarkdownテキストにデコード
128
- generated_markdown = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
129
-
130
- # 【超重要】T5トークナイザーが改行(\n)消してしまう問題の対策
131
- # 見出し記号(#)の前に強制的に改行を挿入して、Markmapが認識できるようにする
132
- generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip()
133
-
134
- # Markmapは必ず「#(大見出し)」から始まる必要があるため、存在しない場合は強制付与
135
- if not generated_markdown.startswith('#'):
136
- if '##' in generated_markdown or '###' in generated_markdown:
137
- generated_markdown = "# マインドマップ\n" + generated_markdown
138
- else:
139
- generated_markdown = "# マインドマップ\n## 抽出結果\n- " + generated_markdown.replace('\n', '\n- ')
140
-
141
- print("生成が完了しました! (コンソール出力時の文字化けエラーを防ぐため内容の表示を省略します)")
142
-
143
- return MapResponse(markdown=generated_markdown)
144
-
145
- # フロントエンドの静的ファイルをマウント
146
- app.mount("/assets", StaticFiles(directory="frontend"), name="assets")
147
-
148
- @app.get("/")
149
- async def root():
150
- # ルートURLにアクセスされたらフロトエンのindex.htmlを返す
151
- return FileResponse("frontend/index.html")
152
-
153
- @app.get("/{filename}")
154
- async def get_frontend_file(filename: str):
155
- # app.js や style.css などのファイルを返す
156
- file_path = os.path.join("frontend", filename)
157
- if os.path.exists(file_path):
158
- return FileResponse(file_path)
159
- raise HTTPException(status_code=404, detail="File not found")
160
-
161
- if __name__ == "__main__":
162
- import uvicorn
163
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from fastapi.responses import FileResponse
5
+ from fastapi.staticfiles import StaticFiles
6
+ import os
7
+ import re
8
+
9
+ try:
10
+ from llama_cpp import Llama
11
+ USE_LLAMA_CPP = True
12
+ print("llama-cpp-python is installed. Will use GGUF inference.")
13
+ except ImportError:
14
+ USE_LLAMA_CPP = False
15
+ import torch
16
+ from transformers import AutoModelForCausalLM, AutoTokenizer
17
+ print("llama-cpp-python is not installed. Falling back to transformers inference.")
18
+
19
+ app = FastAPI()
20
+
21
+ # CORS configuration to allow the frontend to communicate with this API
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"], # Allows all origins for local development
25
+ allow_credentials=True,
26
+ allow_methods=["*"], # Allows all methods
27
+ allow_headers=["*"], # Allows all headers
28
+ )
29
+
30
+ class TextRequest(BaseModel):
31
+ text: str
32
+
33
+ class MapResponse(BaseModel):
34
+ markdown: str
35
+
36
+ # ---------------------------------------------------------
37
+ # AI Model Initialization (Phase 5)
38
+ # ---------------------------------------------------------
39
+
40
+ if USE_LLAMA_CPP:
41
+ import os
42
+
43
+ GGUF_FILENAME = "production_mindmap_model.gguf"
44
+ if os.path.exists(f"./{GGUF_FILENAME}"):
45
+ GGUF_PATH = f"./{GGUF_FILENAME}"
46
+ print(f"ローカルのモデルを使用します: {GGUF_PATH}")
47
+ else:
48
+ from huggingface_hub import hf_hub_download
49
+ MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "kazutab/mindmap-studio-model")
50
+ if not MODEL_REPO_ID:
51
+ raise ValueError("ローカルにモデルが見つからず、MODEL_REPO_ID環境変数も設定されていません。")
52
+ print(f"Hugging Face ({MODEL_REPO_ID}) からモデルをダウンロード中...")
53
+ GGUF_PATH = hf_hub_download(repo_id=MODEL_REPO_ID, filename=GGUF_FILENAME)
54
+
55
+ print("Loading AI Model (GGUF) into memory...")
56
+ model = Llama(
57
+ model_path=GGUF_PATH,
58
+ n_ctx=2048,
59
+ n_gpu_layers=-1 # Use all GPU layers if available
60
+ )
61
+ print("AIモデル(GGUF)の起動完了!")
62
+ else:
63
+ MERGED_MODEL_PATH = "./production_mindmap_model_merged"
64
+ print("Loading AI Model (Transformers FP16) into memory...")
65
+ tokenizer = AutoTokenizer.from_pretrained(MERGED_MODEL_PATH)
66
+ if tokenizer.pad_token is None:
67
+ tokenizer.pad_token = tokenizer.eos_token
68
+
69
+ model = AutoModelForCausalLM.from_pretrained(
70
+ MERGED_MODEL_PATH,
71
+ torch_dtype=torch.float16,
72
+ device_map="auto"
73
+ )
74
+ model.eval()
75
+ print("AIモデル(Transformers)の起動完了!")
76
+
77
+ STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出アシタンです。入力文章の論理構造正確に読み取り、Markdown形式の目次マインドマップを出力してください。
78
+
79
+ 【厳格なルール】
80
+ 1. 企業名、人物名、数値など固有名詞や事実関係は、入力文章から「正確に」抽出すること。絶対に捏造したり、他の主語と混同したりしてはなりません。
81
+ 2. ただし、情報を階層化して分かりやすく整理するため「一般的なカテゴリ名例:背景、特徴、課題、概要など親見出しとし補足することは許可します
82
+ 3. あなたの推測や外部知識を一切混ぜず、入力文章内の事実のみを構造化してください。"""
83
+
84
+ @app.post("/generate", response_model=MapResponse)
85
+ def generate_mindmap(request: TextRequest):
86
+ input_text = request.text.strip()
87
+ if not input_text:
88
+ raise HTTPException(status_code=400, detail="Text is empty")
89
+
90
+ print(f"APIリクエストを受信しました(文字数: {len(input_text)}文字)")
91
+ print("AIが推論(Markdown構造)を生成中...")
92
+
93
+ # Qwen用のチャットプロンプト構築
94
+ USER_PROMPT = f"""以下の文章から論理構造を抽出し、Markdown形式の目次(マインドマップ)を出力してください。
95
+
96
+ 【出力時の厳守ルール(違反厳禁)】
97
+ 1. 否定表現の厳守:「〜しない」「過度に依存しない」などの否定表現を絶対に見落とさず、意味を逆転させないこと。
98
+ 2. 創作の禁止:記事に明記されていない具体的な行動や予定(例:「〜への参加」「〜の強化を目指す」など)を勝手に推測して付け足さないこと。
99
+ 3. 事実の完全一致:抽出した内容が、元の文章の事実と完全一致していることのみを出力すること。
100
+
101
+ 入力文章:
102
+ {input_text}"""
103
+ messages = [
104
+ {"role": "system", "content": STRICT_SYSTEM_PROMPT},
105
+ {"role": "user", "content": USER_PROMPT}
106
+ ]
107
+
108
+ if USE_LLAMA_CPP:
109
+ response = model.create_chat_completion(
110
+ messages=messages,
111
+ max_tokens=1024,
112
+ temperature=0.0, # Greedy Decoding (推測・創造を完全に排除)
113
+ repeat_penalty=1.1
114
+ )
115
+ generated_markdown = response['choices'][0]['message']['content'].strip()
116
+ else:
117
+ # トークナイザーでプロンプト
118
+ prompt = tokenizer.apply_chat_template(
119
+ messages,
120
+ tokenize=False,
121
+ add_generation_prompt=True
122
+ )
123
+
124
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
125
+
126
+ with torch.no_grad():
127
+ outputs = model.generate(
128
+ **inputs,
129
+ max_new_tokens=1024, # 新しく生成するトークン数の上限
130
+ do_sample=False, # Greedy Decoding 有効化
131
+ repetition_penalty=1.1,
132
+ pad_token_id=tokenizer.pad_token_id,
133
+ eos_token_id=tokenizer.eos_token_id
134
+ )
135
+
136
+ # 4. 入力プロンプト部分を切り落として、新しく生成された部分だけを取り出す
137
+ input_length = inputs["input_ids"].shape[1]
138
+ generated_tokens = outputs[0][input_length:]
139
+
140
+ # 5. 数値(トークン)を人間が読めるMarkdownテキストにデコード
141
+ generated_markdown = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
142
+
143
+ # 【超重要】T5トークナイザーが改行(\n)を消してしまう問題の対策
144
+ # 見出し記号(#)の前に強制的に改行を挿入して、Markmapが認識できるようにする
145
+ generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip()
146
+
147
+ # Markmapは必ず「#(大見出し)」から始まる必要があるため、存在しない場合は強制付与
148
+ if not generated_markdown.startswith('#'):
149
+ if '##' in generated_markdown or '###' in generated_markdown:
150
+ generated_markdown = "# マインドマップ\n" + generated_markdown
151
+ else:
152
+ generated_markdown = "# マインドマップ\n## 抽出結果\n- " + generated_markdown.replace('\n', '\n- ')
153
+
154
+ print("生成が完了しました! (コンソール出力時の文字化けエラーを防ぐため内容の表示を省略します)")
155
+
156
+ return MapResponse(markdown=generated_markdown)
157
+
158
+ # フロントエンドの静的ファイルをマウント
159
+ app.mount("/assets", StaticFiles(directory="frontend"), name="assets")
160
+
161
+ @app.get("/")
162
+ async def root():
163
+ # ルートURLにアクセスされたらフロントエンドのindex.htmlを返す
164
+ return FileResponse("frontend/index.html")
165
+
166
+ @app.get("/{filename}")
167
+ async def get_frontend_file(filename: str):
168
+ # app.js や style.css などのファイルを返す
169
+ file_path = os.path.join("frontend", filename)
170
+ if os.path.exists(file_path):
171
+ return FileResponse(file_path)
172
+ raise HTTPException(status_code=404, detail="File not found")
173
+
174
+ if __name__ == "__main__":
175
+ import uvicorn
176
+ uvicorn.run(app, host="0.0.0.0", port=7860)