Spaces:
Running on Zero
Running on Zero
File size: 12,195 Bytes
b91e85f 3b70b16 2c475e2 b91e85f 2c475e2 b91e85f b17c75f b91e85f 2d8e0aa 2c475e2 b91e85f 2c475e2 b91e85f 2c475e2 b91e85f 2c475e2 b91e85f 2c475e2 b91e85f 2c475e2 3b70b16 2c475e2 3b70b16 410a630 2c475e2 410a630 2c475e2 90ae8fd 410a630 90ae8fd 410a630 90ae8fd 410a630 2c475e2 410a630 90ae8fd 410a630 90ae8fd 410a630 90ae8fd 2c475e2 410a630 90ae8fd 410a630 90ae8fd 410a630 e981ed1 90ae8fd e981ed1 90ae8fd 2c475e2 410a630 2c475e2 90ae8fd e981ed1 2c475e2 e981ed1 2c475e2 e981ed1 2c475e2 e981ed1 2c475e2 3b70b16 2c475e2 3b70b16 2c475e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | import os
import re
import gradio as gr
import spaces
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.")
# ---------------------------------------------------------
# AI Model Initialization
# ---------------------------------------------------------
if USE_LLAMA_CPP:
GGUF_FILENAME = "production_mindmap_model.gguf"
if os.path.exists(f"./{GGUF_FILENAME}"):
GGUF_PATH = f"./{GGUF_FILENAME}"
print(f"ローカルのモデルを使用します: {GGUF_PATH}")
else:
from huggingface_hub import hf_hub_download
MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "kazutab/mindmap-studio-model")
if not MODEL_REPO_ID:
raise ValueError("ローカルにモデルが見つからず、MODEL_REPO_ID環境変数も設定されていません。")
print(f"Hugging Face ({MODEL_REPO_ID}) からモデルをダウンロード中...")
GGUF_PATH = hf_hub_download(repo_id=MODEL_REPO_ID, filename="backend/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. あなたの推測や外部知識を一切混ぜず、入力文章内の事実のみを構造化してください。"""
@spaces.GPU(duration=120)
def generate_mindmap(input_text: str):
input_text = input_text.strip()
if not input_text:
return "<div style='color: red; padding: 20px;'>文章を入力してください。</div>"
print(f"推論を開始します(文字数: {len(input_text)}文字)")
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,
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,
repetition_penalty=1.1,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id
)
input_length = inputs["input_ids"].shape[1]
generated_tokens = outputs[0][input_length:]
generated_markdown = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip()
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- ')
# Markmap 用のHTMLを動的に構築して返す
html_output = f"""
<div class="markmap" style="width: 100%; height: 100%; min-height: 500px;">
<script type="text/template">
{generated_markdown}
</script>
</div>
<script>
// GradioのHTML更新後にMarkmapを強制再レンダリングする
setTimeout(() => {{
if (window.markmap && window.markmap.autoLoader) {{
window.markmap.autoLoader.renderAll();
}}
}}, 100);
</script>
"""
return html_output
# ---------------------------------------------------------
# UI Construction (Native Gradio)
# ---------------------------------------------------------
# オリジナルのCSSを読み込み、Gradio用のオーバーライドを追記する
with open("frontend/style.css", "r", encoding="utf-8") as f:
base_css = f.read()
gradio_overrides = """
/* Gradio特有の不要な余白や枠線を完全に無効化 */
.gradio-container {
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
background: transparent !important;
}
footer { display: none !important; }
#root { padding: 0 !important; }
/* オリジナルのレイアウトをGradioのRow/Columnに強制適用 */
.app-layout {
gap: 0 !important;
flex-wrap: nowrap !important;
margin: 0 !important;
}
.sidebar {
min-width: 360px !important;
max-width: 360px !important;
padding: 0 !important;
border-radius: 0 !important;
gap: 0 !important;
border-right: 1px solid var(--border-color) !important;
}
.sidebar-content {
padding: 24px !important;
gap: 20px !important;
}
.canvas-area {
border-radius: 0 !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
/* Gradioのコンポーネントが勝手に作る枠線を消す */
.form { border: none !important; background: transparent !important; box-shadow: none !important; }
.block { padding: 0 !important; margin: 0 !important; border: none !important; box-shadow: none !important; background: transparent !important; }
/* テキストエリアとボタンにオリジナルのスタイルを適用 */
#text-input textarea {
border-radius: 8px !important;
height: 100% !important;
min-height: 200px !important;
border: 1px solid var(--border-color) !important;
padding: 16px !important;
font-size: 14px !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.02) !important;
}
button.btn-primary {
background-color: #000 !important;
color: #fff !important;
border-radius: 6px !important;
padding: 12px 16px !important;
font-size: 14px !important;
font-weight: 500 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
}
button.btn-primary::before {
content: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>');
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
}
button.btn-primary:hover {
background-color: #333 !important;
}
"""
custom_css = base_css + "\n" + gradio_overrides
head_scripts = """
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-autoloader"></script>
"""
with gr.Blocks(css=custom_css, head=head_scripts, title="MindMap Studio") as demo:
with gr.Row(elem_classes="app-layout"):
with gr.Column(elem_classes="sidebar"):
# オリジナルのサイドバーヘッダー(SVGロゴ)を復元
gr.HTML("""
<div class="sidebar-header" style="padding: 24px;">
<div class="logo" style="display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 18px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
<span>MindMap Studio</span>
</div>
</div>
""")
with gr.Column(elem_classes="sidebar-content"):
gr.HTML('<div class="input-group"><label style="font-size: 13px; font-weight: 500; color: #666666;">Source Text</label></div>')
text_input = gr.Textbox(
lines=10,
placeholder="議事録や講義のテキストをペーストしてください...",
show_label=False,
container=False,
elem_id="text-input"
)
submit_btn = gr.Button("マップを生成", elem_classes="btn-primary")
# オリジナルのフッターを復元
gr.HTML("""
<div class="sidebar-footer" style="padding: 16px 24px; border-top: 1px solid #eaeaea; font-size: 12px; color: #a1a1aa; margin-top: auto;">
<p>Powered by edha 1.0 3B</p>
</div>
""")
with gr.Column(elem_classes="canvas-area"):
map_output = gr.HTML(
"""
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
<div class="disclaimer" style="position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: #a1a1aa; text-align: center; pointer-events: none; z-index: 1000; width: 100%;">
MindMap Studioの回答は正しいとは限らないので、重要な情報は必ず見直してください。
</div>
</div>
"""
)
submit_btn.click(
fn=generate_mindmap,
inputs=text_input,
outputs=map_output,
api_name="generate"
)
demo.launch()
|