kazutab commited on
Commit
100b9d3
·
verified ·
1 Parent(s): e981ed1

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +184 -190
app.py CHANGED
@@ -13,43 +13,25 @@ except ImportError:
13
  from transformers import AutoModelForCausalLM, AutoTokenizer
14
  print("llama-cpp-python is not installed. Falling back to transformers inference.")
15
 
16
- # ---------------------------------------------------------
17
- # AI Model Initialization
18
- # ---------------------------------------------------------
19
  if USE_LLAMA_CPP:
20
  GGUF_FILENAME = "production_mindmap_model.gguf"
21
  if os.path.exists(f"./{GGUF_FILENAME}"):
22
  GGUF_PATH = f"./{GGUF_FILENAME}"
23
- print(f"ローカルのモデルを使用します: {GGUF_PATH}")
24
  else:
25
  from huggingface_hub import hf_hub_download
26
  MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "kazutab/mindmap-studio-model")
27
  if not MODEL_REPO_ID:
28
- raise ValueError("ローカルにモデルが見つからず、MODEL_REPO_ID環境変数も設定されていません。")
29
- print(f"Hugging Face ({MODEL_REPO_ID}) からモデルをダウンロード中...")
30
  GGUF_PATH = hf_hub_download(repo_id=MODEL_REPO_ID, filename="backend/production_mindmap_model.gguf")
31
 
32
- print("Loading AI Model (GGUF) into memory...")
33
- model = Llama(
34
- model_path=GGUF_PATH,
35
- n_ctx=2048,
36
- n_gpu_layers=-1 # Use all GPU layers if available
37
- )
38
- print("AIモデル(GGUF)の起動完了!")
39
  else:
40
  MERGED_MODEL_PATH = "./production_mindmap_model_merged"
41
- print("Loading AI Model (Transformers FP16) into memory...")
42
  tokenizer = AutoTokenizer.from_pretrained(MERGED_MODEL_PATH)
43
  if tokenizer.pad_token is None:
44
  tokenizer.pad_token = tokenizer.eos_token
45
-
46
- model = AutoModelForCausalLM.from_pretrained(
47
- MERGED_MODEL_PATH,
48
- torch_dtype=torch.float16,
49
- device_map="auto"
50
- )
51
  model.eval()
52
- print("AIモデル(Transformers)の起動完了!")
53
 
54
  STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出アシスタントです。入力文章の論理構造を正確に読み取り、Markdown形式の目次(マインドマップ)を出力してください。
55
 
@@ -62,12 +44,9 @@ STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出ア
62
  def generate_mindmap(input_text: str):
63
  input_text = input_text.strip()
64
  if not input_text:
65
- return "<div style='color: red; padding: 20px;'>文章を入力してください。</div>"
66
-
67
- print(f"推論を開始します(文字数: {len(input_text)}文字)")
68
 
69
  USER_PROMPT = f"""以下の文章から論理構造を抽出し、Markdown形式の目次(マインドマップ)を出力してください。
70
-
71
  【出力時の厳守ルール(違反厳禁)】
72
  1. 否定表現の厳守:「〜しない」「過度に依存しない」などの否定表現を絶対に見落とさず、意味を逆転させないこと。
73
  2. 創作の禁止:記事に明記されていない具体的な行動や予定(例:「〜への参加」「〜の強化を目指す」など)を勝手に推測して付け足さないこと。
@@ -75,205 +54,220 @@ def generate_mindmap(input_text: str):
75
 
76
  入力文章:
77
  {input_text}"""
78
- messages = [
79
- {"role": "system", "content": STRICT_SYSTEM_PROMPT},
80
- {"role": "user", "content": USER_PROMPT}
81
- ]
82
 
83
  if USE_LLAMA_CPP:
84
  response = model.create_chat_completion(
85
- messages=messages,
86
- max_tokens=1024,
87
- temperature=0.0,
88
- repeat_penalty=1.1
89
  )
90
  generated_markdown = response['choices'][0]['message']['content'].strip()
91
  else:
92
- prompt = tokenizer.apply_chat_template(
93
- messages,
94
- tokenize=False,
95
- add_generation_prompt=True
96
- )
97
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
98
  with torch.no_grad():
99
- outputs = model.generate(
100
- **inputs,
101
- max_new_tokens=1024,
102
- do_sample=False,
103
- repetition_penalty=1.1,
104
- pad_token_id=tokenizer.pad_token_id,
105
- eos_token_id=tokenizer.eos_token_id
106
- )
107
- input_length = inputs["input_ids"].shape[1]
108
- generated_tokens = outputs[0][input_length:]
109
- generated_markdown = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
110
 
111
  generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip()
112
-
113
  if not generated_markdown.startswith('#'):
114
  if '##' in generated_markdown or '###' in generated_markdown:
115
  generated_markdown = "# マインドマップ\n" + generated_markdown
116
  else:
117
  generated_markdown = "# マインドマップ\n## 抽出結果\n- " + generated_markdown.replace('\n', '\n- ')
118
 
119
- # Markmap 用のHTMLを動的に構築して返す
120
- html_output = f"""
121
- <div class="markmap" style="width: 100%; height: 100%; min-height: 500px;">
122
- <script type="text/template">
123
- {generated_markdown}
124
- </script>
125
- </div>
126
- <script>
127
- // GradioのHTML更新後にMarkmapを強制再レンダリングする
128
- setTimeout(() => {{
129
- if (window.markmap && window.markmap.autoLoader) {{
130
- window.markmap.autoLoader.renderAll();
131
- }}
132
- }}, 100);
133
- </script>
134
- """
135
- return html_output
136
 
137
- # ---------------------------------------------------------
138
- # UI Construction (Native Gradio)
139
- # ---------------------------------------------------------
140
- # オリジナルのCSSを読み込み、Gradio用のオーバーライドを追記する
141
- with open("frontend/style.css", "r", encoding="utf-8") as f:
142
- base_css = f.read()
143
 
144
- gradio_overrides = """
145
- /* Gradio特有の不要な余白や枠線を完全に無効化 */
146
- .gradio-container {
147
- max-width: 100% !important;
148
- padding: 0 !important;
149
- margin: 0 !important;
150
- border: none !important;
151
- background: transparent !important;
152
- }
153
- footer { display: none !important; }
154
- #root { padding: 0 !important; }
155
 
156
- /* オリジナルのレイアウトをGradioのRow/Columnに強制適用 */
157
- .app-layout {
158
- gap: 0 !important;
159
- flex-wrap: nowrap !important;
160
- margin: 0 !important;
161
- }
162
- .sidebar {
163
- min-width: 360px !important;
164
- max-width: 360px !important;
165
- padding: 0 !important;
166
- border-radius: 0 !important;
167
- gap: 0 !important;
168
- border-right: 1px solid var(--border-color) !important;
169
- }
170
- .sidebar-content {
171
- padding: 24px !important;
172
- gap: 20px !important;
173
- }
174
- .canvas-area {
175
- border-radius: 0 !important;
176
- border: none !important;
177
- padding: 0 !important;
178
- margin: 0 !important;
179
  }
180
 
181
- /* Gradioのコンポーネントが勝手に作る枠線を消す */
182
- .form { border: none !important; background: transparent !important; box-shadow: none !important; }
183
- .block { padding: 0 !important; margin: 0 !important; border: none !important; box-shadow: none !important; background: transparent !important; }
184
 
185
- /* テキストエリアとボタンにオリジナルのスタイルを適用 */
186
- #text-input textarea {
187
- border-radius: 8px !important;
188
- height: 100% !important;
189
- min-height: 200px !important;
190
- border: 1px solid var(--border-color) !important;
191
- padding: 16px !important;
192
- font-size: 14px !important;
193
- box-shadow: 0 1px 2px rgba(0,0,0,0.02) !important;
194
- }
195
 
196
- button.btn-primary {
197
- background-color: #000 !important;
198
- color: #fff !important;
199
- border-radius: 6px !important;
200
- padding: 12px 16px !important;
201
- font-size: 14px !important;
202
- font-weight: 500 !important;
203
- display: flex !important;
204
- align-items: center !important;
205
- justify-content: center !important;
206
- gap: 8px !important;
207
- }
208
- button.btn-primary::before {
209
- 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>');
210
- display: inline-block;
211
- width: 16px;
212
- height: 16px;
213
- margin-right: 4px;
214
- }
215
- button.btn-primary:hover {
216
- background-color: #333 !important;
217
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  """
219
 
220
- custom_css = base_css + "\n" + gradio_overrides
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  head_scripts = """
223
  <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
 
224
  <script src="https://cdn.jsdelivr.net/npm/markmap-view"></script>
225
- <script src="https://cdn.jsdelivr.net/npm/markmap-autoloader"></script>
226
- """
 
 
 
 
227
 
228
- with gr.Blocks(css=custom_css, head=head_scripts, title="MindMap Studio") as demo:
229
- with gr.Row(elem_classes="app-layout"):
230
- with gr.Column(elem_classes="sidebar"):
231
- # オリジナルのサイドバーヘッダー(SVGロゴ)を復元
232
- gr.HTML("""
233
- <div class="sidebar-header" style="padding: 24px;">
234
- <div class="logo" style="display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 18px;">
235
- <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>
236
- <span>MindMap Studio</span>
237
- </div>
238
- </div>
239
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
- with gr.Column(elem_classes="sidebar-content"):
242
- gr.HTML('<div class="input-group"><label style="font-size: 13px; font-weight: 500; color: #666666;">Source Text</label></div>')
243
-
244
- text_input = gr.Textbox(
245
- lines=10,
246
- placeholder="議事録や講義のテキストをペーストしてください...",
247
- show_label=False,
248
- container=False,
249
- elem_id="text-input"
250
- )
251
-
252
- submit_btn = gr.Button("マップを生成", elem_classes="btn-primary")
253
 
254
- # オリジナルのフッターを復元
255
- gr.HTML("""
256
- <div class="sidebar-footer" style="padding: 16px 24px; border-top: 1px solid #eaeaea; font-size: 12px; color: #a1a1aa; margin-top: auto;">
257
- <p>Powered by edha 1.0 3B</p>
258
- </div>
259
- """)
260
 
261
- with gr.Column(elem_classes="canvas-area"):
262
- map_output = gr.HTML(
263
- """
264
- <div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
265
- <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%;">
266
- MindMap Studioの回答は正しいとは限らないので、重要な情報は必ず見直してください。
267
- </div>
268
- </div>
269
- """
270
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- submit_btn.click(
273
- fn=generate_mindmap,
274
- inputs=text_input,
275
- outputs=map_output,
276
- api_name="generate"
277
- )
 
 
 
 
 
278
 
279
  demo.launch()
 
13
  from transformers import AutoModelForCausalLM, AutoTokenizer
14
  print("llama-cpp-python is not installed. Falling back to transformers inference.")
15
 
 
 
 
16
  if USE_LLAMA_CPP:
17
  GGUF_FILENAME = "production_mindmap_model.gguf"
18
  if os.path.exists(f"./{GGUF_FILENAME}"):
19
  GGUF_PATH = f"./{GGUF_FILENAME}"
 
20
  else:
21
  from huggingface_hub import hf_hub_download
22
  MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "kazutab/mindmap-studio-model")
23
  if not MODEL_REPO_ID:
24
+ raise ValueError("Local model not found and MODEL_REPO_ID is not set.")
 
25
  GGUF_PATH = hf_hub_download(repo_id=MODEL_REPO_ID, filename="backend/production_mindmap_model.gguf")
26
 
27
+ model = Llama(model_path=GGUF_PATH, n_ctx=2048, n_gpu_layers=-1)
 
 
 
 
 
 
28
  else:
29
  MERGED_MODEL_PATH = "./production_mindmap_model_merged"
 
30
  tokenizer = AutoTokenizer.from_pretrained(MERGED_MODEL_PATH)
31
  if tokenizer.pad_token is None:
32
  tokenizer.pad_token = tokenizer.eos_token
33
+ model = AutoModelForCausalLM.from_pretrained(MERGED_MODEL_PATH, torch_dtype=torch.float16, device_map="auto")
 
 
 
 
 
34
  model.eval()
 
35
 
36
  STRICT_SYSTEM_PROMPT = """あなたは極めて優秀で厳密な情報抽出アシスタントです。入力文章の論理構造を正確に読み取り、Markdown形式の目次(マインドマップ)を出力してください。
37
 
 
44
  def generate_mindmap(input_text: str):
45
  input_text = input_text.strip()
46
  if not input_text:
47
+ return ""
 
 
48
 
49
  USER_PROMPT = f"""以下の文章から論理構造を抽出し、Markdown形式の目次(マインドマップ)を出力してください。
 
50
  【出力時の厳守ルール(違反厳禁)】
51
  1. 否定表現の厳守:「〜しない」「過度に依存しない」などの否定表現を絶対に見落とさず、意味を逆転させないこと。
52
  2. 創作の禁止:記事に明記されていない具体的な行動や予定(例:「〜への参加」「〜の強化を目指す」など)を勝手に推測して付け足さないこと。
 
54
 
55
  入力文章:
56
  {input_text}"""
57
+ messages = [{"role": "system", "content": STRICT_SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT}]
 
 
 
58
 
59
  if USE_LLAMA_CPP:
60
  response = model.create_chat_completion(
61
+ messages=messages, max_tokens=1024, temperature=0.0, repeat_penalty=1.1
 
 
 
62
  )
63
  generated_markdown = response['choices'][0]['message']['content'].strip()
64
  else:
65
+ prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
 
 
 
 
66
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
67
  with torch.no_grad():
68
+ outputs = model.generate(**inputs, max_new_tokens=1024, do_sample=False, repetition_penalty=1.1)
69
+ generated_markdown = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
 
 
 
 
 
 
 
 
 
70
 
71
  generated_markdown = re.sub(r'\s*(#+ )', r'\n\1', generated_markdown).strip()
 
72
  if not generated_markdown.startswith('#'):
73
  if '##' in generated_markdown or '###' in generated_markdown:
74
  generated_markdown = "# マインドマップ\n" + generated_markdown
75
  else:
76
  generated_markdown = "# マインドマップ\n## 抽出結果\n- " + generated_markdown.replace('\n', '\n- ')
77
 
78
+ return generated_markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
 
 
 
 
 
 
80
 
81
+ # -----------------------------------------------------------------------------------------
82
+ # JS / CSS / HTML Injection for 100% Perfect Layout bypass
83
+ # -----------------------------------------------------------------------------------------
 
 
 
 
 
 
 
 
84
 
85
+ base_css = """
86
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
87
+
88
+ :root {
89
+ --bg-canvas: #fafafa;
90
+ --bg-sidebar: #ffffff;
91
+ --border-color: #eaeaea;
92
+ --text-primary: #171717;
93
+ --text-secondary: #666666;
94
+ --text-tertiary: #a1a1aa;
95
+ --focus-ring: rgba(0, 0, 0, 0.08);
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
+ * { box-sizing: border-box; margin: 0; padding: 0; }
99
+ body { font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", "Helvetica Neue", sans-serif; color: var(--text-primary); background-color: var(--bg-canvas); height: 100vh; overflow: hidden; -webkit-font-smoothing: antialiased; }
 
100
 
101
+ .app-layout { display: flex; height: 100vh; width: 100vw; }
 
 
 
 
 
 
 
 
 
102
 
103
+ .sidebar { width: 360px; min-width: 360px; max-width: 360px; background-color: var(--bg-sidebar); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; box-shadow: 1px 0 10px rgba(0,0,0,0.02); z-index: 10; }
104
+ .sidebar-header { padding: 24px; border-bottom: 1px solid var(--border-color); }
105
+ .logo { display: flex; align-items: center; gap: 12px; font-weight: 600; font-size: 16px; letter-spacing: -0.02em; color: #171717; }
106
+ .sidebar-content { flex-grow: 1; padding: 24px; display: flex; flex-direction: column; gap: 20px; }
107
+ .input-group { display: flex; flex-direction: column; gap: 8px; flex-grow: 1; }
108
+ .input-group label { font-size: 13px; font-weight: 500; color: var(--text-secondary); }
109
+
110
+ textarea.custom-textarea {
111
+ flex-grow: 1; width: 100%; height: 100%; resize: none; border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; font-family: inherit; font-size: 14px; line-height: 1.6; color: var(--text-primary); background-color: #fff; transition: all 0.2s ease; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
+ textarea.custom-textarea:focus { outline: none; border-color: #999; box-shadow: 0 0 0 4px var(--focus-ring); }
114
+ textarea.custom-textarea::placeholder { color: #a1a1aa; }
115
+
116
+ .btn-primary { background-color: #000; color: #fff; border: none; border-radius: 6px; padding: 12px 16px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; }
117
+ .btn-primary:hover { background-color: #333; }
118
+ .btn-primary:active { transform: scale(0.98); }
119
+ .btn-primary:disabled { background-color: #e5e5e5; color: #a3a3a3; cursor: not-allowed; }
120
+
121
+ #loading { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 13px; color: var(--text-secondary); padding: 12px; }
122
+ .spinner { width: 16px; height: 16px; border: 2px solid var(--border-color); border-top: 2px solid #000; border-radius: 50%; animation: spin 0.8s linear infinite; }
123
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
124
+ .hidden { display: none !important; }
125
+
126
+ .sidebar-footer { padding: 16px 24px; border-top: 1px solid var(--border-color); font-size: 12px; color: var(--text-tertiary); }
127
+
128
+ .canvas-area { flex-grow: 1; position: relative; background-color: var(--bg-canvas); background-image: radial-gradient(#e5e7eb 1px, transparent 1px); background-size: 20px 20px; }
129
+ #markmap { width: 100%; height: 100%; }
130
+
131
+ /* Gradio Overrides to remove padding and ensure full screen */
132
+ .gradio-container { padding: 0 !important; margin: 0 !important; max-width: 100vw !important; border: none !important; }
133
+ footer { display: none !important; }
134
+ #hidden-layer { display: none !important; }
135
  """
136
 
137
+ original_html = """
138
+ <div class="app-layout">
139
+ <aside class="sidebar">
140
+ <div class="sidebar-header">
141
+ <div class="logo">
142
+ <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>
143
+ <span>MindMap Studio</span>
144
+ </div>
145
+ </div>
146
+ <div class="sidebar-content">
147
+ <div class="input-group">
148
+ <label for="inputText">Source Text</label>
149
+ <textarea id="inputText" class="custom-textarea" placeholder="議事録や講義のテキストをペーストしてください..."></textarea>
150
+ </div>
151
+ <button id="generateBtn" class="btn-primary">
152
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
153
+ マップを生成
154
+ </button>
155
+ <div id="loading" class="hidden">
156
+ <div class="spinner"></div>
157
+ <span>Processing text...</span>
158
+ </div>
159
+ </div>
160
+ <div class="sidebar-footer">
161
+ <p>Powered by edha 1.0 3B</p>
162
+ </div>
163
+ </aside>
164
+ <main class="canvas-area">
165
+ <svg id="markmap"></svg>
166
+ <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%;">
167
+ MindMap Studioの回答は正しいとは限らないので、重要な情報は必ず見直してください。
168
+ </div>
169
+ </main>
170
+ </div>
171
+ """
172
 
173
  head_scripts = """
174
  <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
175
+ <script src="https://cdn.jsdelivr.net/npm/markmap-lib"></script>
176
  <script src="https://cdn.jsdelivr.net/npm/markmap-view"></script>
177
+ <script>
178
+ document.addEventListener('DOMContentLoaded', () => {
179
+ const { markmap } = window;
180
+ const { Markmap, loadCSS, loadJS, Transformer } = markmap;
181
+ const transformer = new Transformer();
182
+ let mm = null;
183
 
184
+ // Gradio injects elements dynamically, so we wait until our HTML and Gradio's hidden inputs are ready
185
+ const initInterval = setInterval(() => {
186
+ const svgEl = document.querySelector('#markmap');
187
+ const hiddenBtn = document.querySelector('#hidden-btn');
188
+ if (svgEl && hiddenBtn && !mm) {
189
+ clearInterval(initInterval);
190
+ mm = Markmap.create('#markmap');
191
+
192
+ const initialMarkdown = `
193
+ # マインドマップ自動生成
194
+ ## 使い方
195
+ - 左側に文章を入力します
196
+ - 「マップを生成」ボタンを押します
197
+ ## 特徴
198
+ - AIが文脈を理解して自動で構造化
199
+ - 専用カスタムAI(edha 1.0 3B)による情報抽出
200
+ `;
201
+ renderMindMap(initialMarkdown);
202
+ setupBridge();
203
+ }
204
+ }, 100);
205
+
206
+ function renderMindMap(markdownContent) {
207
+ const { root, features } = transformer.transform(markdownContent);
208
+ const { styles, scripts } = transformer.getUsedAssets(features);
209
+ if (styles) loadCSS(styles);
210
+ if (scripts) loadJS(scripts, { getMarkmap: () => markmap });
211
+ mm.setData(root);
212
+ mm.fit();
213
+ }
214
+
215
+ function setupBridge() {
216
+ const generateBtn = document.getElementById('generateBtn');
217
+ const inputText = document.getElementById('inputText');
218
+ const loadingDiv = document.getElementById('loading');
219
+
220
+ generateBtn.addEventListener('click', () => {
221
+ const text = inputText.value.trim();
222
+ if (!text) return;
223
 
224
+ generateBtn.disabled = true;
225
+ generateBtn.classList.add('hidden');
226
+ loadingDiv.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
227
 
228
+ // Bridge custom textarea to Gradio's hidden textarea
229
+ const hiddenInput = document.querySelector('#hidden-input textarea');
230
+ const hiddenBtn = document.querySelector('#hidden-btn');
 
 
 
231
 
232
+ if (hiddenInput && hiddenBtn) {
233
+ hiddenInput.value = text;
234
+ hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
235
+ setTimeout(() => hiddenBtn.click(), 50); // let Svelte process input
236
+ }
237
+ });
238
+
239
+ // Listen to changes on Gradio's hidden output
240
+ const hiddenOutput = document.querySelector('#hidden-output textarea');
241
+ if (hiddenOutput) {
242
+ let lastVal = hiddenOutput.value;
243
+ setInterval(() => {
244
+ if (hiddenOutput.value !== lastVal) {
245
+ lastVal = hiddenOutput.value;
246
+ if (lastVal && lastVal.trim().length > 0) {
247
+ renderMindMap(lastVal);
248
+
249
+ generateBtn.disabled = false;
250
+ generateBtn.classList.remove('hidden');
251
+ loadingDiv.classList.add('hidden');
252
+ }
253
+ }
254
+ }, 200);
255
+ }
256
+ }
257
+ });
258
+ </script>
259
+ """
260
 
261
+ with gr.Blocks(css=base_css, head=head_scripts, title="MindMap Studio") as demo:
262
+ # 完全にオリジナルのUIをインジェクト(Gradioのコンテナ制限を受けない純粋なHTML文字列)
263
+ gr.HTML(original_html)
264
+
265
+ # Python関数の実行に必要なGradioコンポーネント(目に見えないように隠蔽)
266
+ with gr.Row(elem_id="hidden-layer"):
267
+ hidden_input = gr.Textbox(elem_id="hidden-input")
268
+ hidden_output = gr.Textbox(elem_id="hidden-output")
269
+ hidden_btn = gr.Button(elem_id="hidden-btn")
270
+
271
+ hidden_btn.click(fn=generate_mindmap, inputs=hidden_input, outputs=hidden_output, api_name="generate")
272
 
273
  demo.launch()