Corin1998 commited on
Commit
da930cf
·
verified ·
1 Parent(s): f205853

Update ui/ui_app.py

Browse files
Files changed (1) hide show
  1. ui/ui_app.py +205 -273
ui/ui_app.py CHANGED
@@ -1,309 +1,241 @@
1
  # ui/ui_app.py
2
  from __future__ import annotations
3
- import json, traceback, base64, math
4
- from typing import Any, Dict, List
 
5
  import gradio as gr
6
  import pandas as pd
7
  import plotly.graph_objects as go
8
 
9
- from core.pdf_io import pdf_to_images, pdf_to_text
10
- from core.extract import extract_financials
11
- from core.scoring import score_company
12
- from core.external_score import (
13
- get_external_template_df, fill_missing_with_external, score_external_from_df, apply_llm_signals_to_df
14
- )
15
- from core.units import detect_unit, unit_factor, scale_financials_yen
16
- from core.openai_client import VISION_MODEL, TEXT_MODEL, get_client
17
- from core.llm_quant import extract_market_product_signals
18
-
19
- # ===== helpers =====
20
- def fin_to_df(fin: Dict[str, Any]) -> pd.DataFrame:
21
- rows = []
22
- def add(cat, d):
23
- for k, v in (d or {}).items():
24
- rows.append({"category": cat, "item": k, "value": v})
25
- add("balance_sheet", fin.get("balance_sheet"))
26
- add("income_statement", fin.get("income_statement"))
27
- add("cash_flows", fin.get("cash_flows"))
28
- return pd.DataFrame(rows, columns=["category", "item", "value"])
29
-
30
- def df_to_fin(df: pd.DataFrame) -> Dict[str, Any]:
31
- out = {"balance_sheet": {}, "income_statement": {}, "cash_flows": {}}
32
- for _, r in df.iterrows():
33
- cat, item, val = str(r["category"]), str(r["item"]), r["value"]
34
- try:
35
- parsed = None if val in (None, "", "null") else float(val)
36
- except Exception:
37
- parsed = None
38
- if cat in out:
39
- out[cat][item] = parsed
40
- return out
41
-
42
- def _fmt_yen(n: float) -> str:
43
- if n is None: return "—"
44
- try:
45
- n = float(n)
46
- except Exception:
47
- return "—"
48
- # 視認性のため 兆/億/万円 に自動スケール
49
- absn = abs(n)
50
- if absn >= 1e12:
51
- return f"{n/1e12:.2f} 兆円"
52
- if absn >= 1e8:
53
- return f"{n/1e8:.2f} 億円"
54
- if absn >= 1e4:
55
- return f"{n/1e4:.1f} 万円"
56
- return f"{int(n):,} 円"
57
 
58
- def _fmt_pct(r: float) -> str:
59
- if r is None: return "—"
 
 
60
  try:
61
- return f"{r*100:.2f}%"
62
  except Exception:
63
- return "—"
64
-
65
- def radar(score: Dict[str, Any]) -> go.Figure:
66
- labels = [d["metric"] for d in score["details"]]
67
- values = [d["score"] for d in score["details"]]
68
- fig = go.Figure()
69
- fig.add_trace(go.Scatterpolar(r=values + values[:1], theta=labels + labels[:1], fill="toself"))
70
- fig.update_layout(
71
- polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
72
- showlegend=False,
73
- margin=dict(l=20, r=20, t=30, b=20),
74
- height=380,
75
- title=f"総合スコア: {score['total_score']}(グレード: {score['grade']})"
76
- )
77
- return fig
78
-
79
- def health() -> str:
80
- msgs = []
81
- try:
82
- _ = get_client()
83
- msgs.append("✅ OpenAI: OK")
84
- except Exception as e:
85
- msgs.append(f"❌ OpenAI: {e}")
86
- msgs.append(f"ℹ️ Vision={VISION_MODEL} / Text={TEXT_MODEL}")
87
- return "<br>".join(msgs)
88
-
89
- def kpi_cards_html(fin: Dict[str, Any], score: Dict[str, Any]) -> str:
90
- bs = fin.get("balance_sheet", {}) or {}
91
- pl = fin.get("income_statement", {}) or {}
92
- assets = bs.get("total_assets")
93
- equity = bs.get("total_equity")
94
- curA = bs.get("current_assets"); curL = bs.get("current_liabilities")
95
- sales = pl.get("sales"); op = pl.get("operating_income"); net = pl.get("net_income")
96
-
97
- equity_ratio = (equity or 0) / (assets or 1) if assets else None
98
- current_ratio = (curA or 0) / (curL or 1) if curL else None
99
- opm = (op or 0) / (sales or 1) if sales else None
100
- npm = (net or 0) / (sales or 1) if sales else None
101
- roa = (net or 0) / (assets or 1) if assets else None
102
-
103
- def badge(val, kind="pct"):
104
- if val is None: return '<span class="px-2 py-1 rounded bg-gray-200 text-gray-700">—</span>'
105
- v = val*100 if kind=="pct" else val
106
- col = "#22c55e" if v >= (20 if kind=="pct" else 0) else "#f59e0b" if v >= (10 if kind=="pct" else 0) else "#ef4444"
107
- return f'<span class="px-2 py-1 rounded" style="background:{col}20;color:{col}">{v:.2f}{"%" if kind=="pct" else ""}</span>'
108
-
109
- html = f"""
110
- <div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px">
111
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
112
- <div>総資産</div><div style="font-size:1.2rem;font-weight:700">{_fmt_yen(assets)}</div>
113
- </div>
114
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
115
- <div>売上高</div><div style="font-size:1.2rem;font-weight:700">{_fmt_yen(sales)}</div>
116
- </div>
117
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
118
- <div>営業利益</div><div style="font-size:1.2rem;font-weight:700">{_fmt_yen(op)}</div>
119
- </div>
120
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
121
- <div>自己資本比率</div><div style="font-size:1.1rem;font-weight:700">{_fmt_pct(equity_ratio)} {badge(equity_ratio)}</div>
122
- </div>
123
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
124
- <div>流動比率</div><div style="font-size:1.1rem;font-weight:700">{_fmt_pct(current_ratio)} {badge(current_ratio)}</div>
125
- </div>
126
- <div class="card" style="padding:12px;border:1px solid #eee;border-radius:12px">
127
- <div>ROA</div><div style="font-size:1.1rem;font-weight:700">{_fmt_pct(roa)} {badge(roa)}</div>
128
- </div>
129
- </div>
130
- <div style="margin-top:8px;color:#64748b">社内スコア: <b>{score['total_score']}</b> (グレード {score['grade']})</div>
131
- """
132
  return html
133
 
134
- # ===== main actions =====
135
- def run_analyze(company: str, use_vision: bool, unit_sel: str, use_pdf_for_ext: bool, aux_text: str, files: list[str]):
136
- if not files:
137
- raise gr.Error("PDF をアップロードしてください。")
138
-
139
- # 1) 単位の自動推定
140
- first_text = pdf_to_text(files[0], pages=2)
141
- detected = detect_unit(first_text) or ""
142
- unit_label = unit_sel if unit_sel != "自動推定" else detected
143
- factor = unit_factor(unit_label)
144
-
145
- # 2) 抽出(Vision→失敗時Text)
146
- try:
147
- imgs: List[bytes] = []
148
- for p in files:
149
- imgs += pdf_to_images(p, dpi=200, max_pages=6)
150
- fin_raw = extract_financials(imgs if use_vision else None, None, company or "")
151
- except Exception:
152
- text_blob = ""
153
- for p in files:
154
- text_blob += pdf_to_text(p) + "\n\n"
155
- fin_raw = extract_financials(None, text_blob, company or "")
156
-
157
- # 3) 円換算
158
- fin_yen = scale_financials_yen(fin_raw, factor=factor)
159
-
160
- # 4) 社内スコア
161
- df = fin_to_df(fin_yen)
162
- score = score_company(fin_yen)
163
- fig = radar(score)
164
- kpi_html = kpi_cards_html(fin_yen, score)
165
-
166
- # 5) 外部評価に使う LLM 定量シグナル(市場/製品)
167
- signals = None
168
- try:
169
- ext_text = ""
170
- if use_pdf_for_ext:
171
- # 追加でテキストを多めに取得(セクション説明文など)
172
- for p in files:
173
- ext_text += pdf_to_text(p, pages=8) + "\n\n"
174
- ext_text = (aux_text or "") + "\n\n" + ext_text
175
- if len(ext_text.strip()) > 0:
176
- signals = extract_market_product_signals(ext_text[:18000], company_hint=company or "")
177
- except Exception:
178
- signals = None
179
-
180
- # 6) 外部評価テンプレを生成 → LLMで可能な項目を自動補完
181
- df_ext = get_external_template_df()
182
- df_ext = fill_missing_with_external(df_ext)
183
- if signals:
184
- df_ext = apply_llm_signals_to_df(df_ext, signals)
185
-
186
- # 7) AI所見(中立・根拠明示)
187
- try:
188
- client = get_client()
189
- prompt = f"""あなたは独立の財務アナリストです。主観や推測を避け、事実と“比率”を根拠に簡潔な箇条書きを作成。
190
- - 断定・煽り禁止、将来予測禁止
191
- - 各行の末尾に根拠(指標名=値)を括弧で併記
192
- - 外部評価(市場/製品)は参考情報として最後に別見出しで一言添える
193
-
194
- [財務データ(円換算)]
195
- {json.dumps(fin_yen, ensure_ascii=False)}
196
-
197
- [社内スコア(財務比率基準)]
198
- {json.dumps(score, ensure_ascii=False)}
199
-
200
- [外部シグナル(市場/製品;参考)]
201
- {json.dumps(signals or {}, ensure_ascii=False)}
202
- """
203
- resp = client.chat.completions.create(
204
- model=TEXT_MODEL,
205
- messages=[{"role":"system","content":"中立・簡潔・根拠明示。"},
206
- {"role":"user","content": prompt}],
207
- temperature=0.2,
208
- )
209
- insight = resp.choices[0].message.content
210
- except Exception as e:
211
- insight = f"AI所見の生成に失敗: {e}"
212
-
213
- unit_info = f"PDF表記の単位: <b>{detected}</b> / 適用単位: <b>{unit_label}</b>(円換算係数={factor:g})"
214
- signals_md = "—"
215
- if signals:
216
- ev = signals.get("market", {}).get("evidence") or []
217
- cagr = signals.get("market", {}).get("cagr_pct")
218
- pc = signals.get("products", {}).get("count")
219
- pg = signals.get("products", {}).get("growing_count")
220
- signals_md = f"- 市場CAGR: {cagr if cagr is not None else '—'}%\n- 主力商品数: {pc or '—'} / 成長中: {pg or '—'}\n- エビデンス: {(' / '.join(ev[:2])) if ev else '—'}"
221
-
222
- return (
223
- unit_info, # HTML
224
- kpi_html, # HTML
225
- json.dumps(fin_raw, ensure_ascii=False, indent=2), # 折りたたみ表示用
226
- json.dumps(fin_yen, ensure_ascii=False, indent=2), # 折りたたみ表示用
227
- df, # 編集表(円換算)
228
- json.dumps(score, ensure_ascii=False, indent=2),
229
- fig,
230
- insight,
231
- df_ext, # 外部テンプレ(LLM補完済み)
232
- signals_md # LLM抽出の要点
233
- )
234
-
235
- def run_recalc(df: pd.DataFrame):
236
- try:
237
- fin = df_to_fin(df) # ここは既に円換算された表が編集される想定
238
- score = score_company(fin)
239
- fig = radar(score)
240
- return (json.dumps(score, ensure_ascii=False, indent=2),
241
- fig,
242
- json.dumps(fin, ensure_ascii=False, indent=2))
243
- except Exception as e:
244
- tb = traceback.format_exc(limit=6)
245
- raise gr.Error(f"再計算に失敗しました: {e}\n\n<pre style='white-space:pre-wrap'>{tb}</pre>")
246
 
247
- def calc_external(df_ext: pd.DataFrame):
248
- res = score_external_from_df(df_ext)
249
- return json.dumps(res, ensure_ascii=False, indent=2)
 
 
 
 
 
 
250
 
251
- # ===== UI =====
 
 
 
 
 
 
 
 
 
 
252
  def build_ui() -> gr.Blocks:
253
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"),
254
- fill_height=True, analytics_enabled=False) as demo:
255
- gr.Markdown("## 🧮 企業スコアリング(PDF解析 × OpenAI Vision)")
256
  with gr.Row():
257
  with gr.Column(scale=1):
258
  company = gr.Textbox(label="企業名(任意)", placeholder="例:株式会社OO")
259
- use_vision = gr.Checkbox(value=True, label="OpenAIでPDFをAI解析(Vision)")
260
- unit_sel = gr.Dropdown(choices=["自動推定","円","千円","百万円","千万円","億円"],
261
- value="自動推定", label="金額単位(PDF記載)")
262
- use_pdf_for_ext = gr.Checkbox(value=True, label="PDFから市場/製品情報も抽出(外部評価用)")
263
- aux_text = gr.Textbox(label="補助テキスト・Web抜粋(任意)", lines=4,
264
- placeholder="業界説明/IRの抜粋などを貼ると、市場CAGRや主力商品数を自動推定します。")
265
- files = gr.File(label="決算書PDF(複数可)", file_count="multiple", type="filepath")
266
  run_btn = gr.Button("📄 解析して反映", variant="primary")
267
- recalc_btn = gr.Button("🔁 表の値で再計算")
268
- health_btn = gr.Button("🩺 環境チェック")
269
- health_out = gr.HTML()
270
-
271
  with gr.Column(scale=1):
272
- unit_html = gr.HTML(label="単位情報")
273
- kpi_html = gr.HTML(label="主要KPI")
274
 
275
  with gr.Tabs():
276
  with gr.Tab("概要"):
277
- chart = gr.Plot(label="スコアレーダー(社内ルール)")
 
278
  with gr.Tab("抽出結果(編集可)"):
279
- with gr.Accordion("抽出JSON(換算前/円換算)", open=False):
280
- fin_json_raw = gr.Code(label="抽出JSON(換算前)", language="json", interactive=False)
281
- fin_json_yen = gr.Code(label="抽出JSON(円換算)", language="json")
282
- df_out = gr.Dataframe(headers=["category", "item", "value"], interactive=True, wrap=True)
283
  with gr.Tab("スコア(社内ルール)"):
284
- score_json = gr.Code(label="スコア(JSON)", language="json")
 
 
 
 
285
  with gr.Tab("AI所見(中立)"):
286
- insight_md = gr.Markdown()
 
287
  with gr.Tab("外部評価(定量化)"):
288
- signals_md = gr.Markdown(label="抽出シグナル(市場/製品)")
289
- df_ext = gr.Dataframe(headers=["カテゴリー","入力項目","値"], interactive=True, wrap=True)
290
- calc_t = gr.Button("🧮 外部スコア計算")
291
- ext_json = gr.Code(label="外部評価(JSON)", language="json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  run_btn.click(
294
- run_analyze,
295
- inputs=[company, use_vision, unit_sel, use_pdf_for_ext, aux_text, files],
296
- outputs=[unit_html, kpi_html, fin_json_raw, fin_json_yen, df_out, score_json, chart, insight_md, df_ext, signals_md],
297
  concurrency_limit=1
298
  )
299
 
300
  recalc_btn.click(
301
- run_recalc, inputs=[df_out], outputs=[score_json, chart, fin_json_yen],
 
 
302
  concurrency_limit=1
303
  )
304
 
305
- calc_t.click(calc_external, inputs=[df_ext], outputs=[ext_json], concurrency_limit=1)
306
-
307
- health_btn.click(health, outputs=health_out, concurrency_limit=1)
308
-
309
  return demo
 
1
  # ui/ui_app.py
2
  from __future__ import annotations
3
+ import os, io, json, base64, traceback
4
+ from typing import Any, Dict, List, Tuple
5
+
6
  import gradio as gr
7
  import pandas as pd
8
  import plotly.graph_objects as go
9
 
10
+ # 既存の抽出/スコア関数を利用
11
+ from core.extract import parse_pdf # あなたの既存実装をインポート(PDF→fin(dict), df)
12
+ from core.scoring import score_company # 社内ルールのスコア(dict)
13
+ from core.external_scoring import score_external # 外部評価(定量化)(dict)
14
+ from core.ai_judgement import make_ai_memo # AI所見(中立)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # ============ 小さなUIヘルパ ============
17
+ def _safe_json_loads(s: str|Dict[str,Any]|None) -> Dict[str,Any]:
18
+ if s is None: return {}
19
+ if isinstance(s, dict): return s
20
  try:
21
+ return json.loads(s)
22
  except Exception:
23
+ return {}
24
+
25
+ def _kpi_cards_html(score: Dict[str,Any], ext: Dict[str,Any]) -> str:
26
+ total = score.get("total_score", None)
27
+ grade = score.get("grade", "-")
28
+ ext_total = ext.get("external_total", None)
29
+
30
+ gap = None
31
+ if isinstance(total, (int, float)) and isinstance(ext_total, (int, float)):
32
+ gap = round(total - ext_total, 1)
33
+
34
+ def kpi(label: str, value: str, sub: str = "") -> str:
35
+ return f"""
36
+ <div class="kpi">
37
+ <div class="kpi-label">{label}</div>
38
+ <div class="kpi-value">{value}</div>
39
+ <div class="kpi-sub">{sub}</div>
40
+ </div>"""
41
+
42
+ html = """
43
+ <style>
44
+ .kpi-wrap {display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:12px;margin-top:4px;}
45
+ .kpi{background:#fff;border:1px solid #e9eef5;border-radius:14px;padding:14px;box-shadow:0 2px 6px rgba(0,0,0,0.04);}
46
+ .kpi-label{color:#64748b;font-size:12px;margin-bottom:6px;}
47
+ .kpi-value{font-size:26px;font-weight:700;}
48
+ .kpi-sub{color:#94a3b8;font-size:11px;margin-top:2px;}
49
+ @media (max-width: 860px){.kpi-wrap{grid-template-columns:repeat(2,1fr);} }
50
+ </style>
51
+ <div class="kpi-wrap">
52
+ """
53
+
54
+ html += kpi("総合スコア(社内)", f"{total if total is not None else '—'}", f"グレード: {grade}")
55
+ html += kpi("外部評価(定量)", f"{ext_total if ext_total is not None else '—'}", "100点満点")
56
+ html += kpi("ギャップ(社内−外部)", f"{gap if gap is not None else '—'}", "+は社内>外部")
57
+ # 代表的な指標(上位1・下位1)をサマリに
58
+ det = score.get("details", []) or []
59
+ if det:
60
+ best = max(det, key=lambda x: x.get("score", 0))
61
+ worst = min(det, key=lambda x: x.get("score", 0))
62
+ html += kpi("強み指標", f"{best.get('metric','—')}", f"スコア {round(best.get('score',0),1)}")
63
+ # 4つ目で既に埋まっているので、下位はサブテキストで表示
64
+ html = html.replace("</div></div>", f"<div class='kpi-sub'>弱み指標: {worst.get('metric','—')} / {round(worst.get('score',0),1)}</div></div>", 1)
65
+ else:
66
+ html += kpi("指標ハイライト", "", "データ不足")
67
+
68
+ html += "</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  return html
70
 
71
+ def _score_bar(score: Dict[str,Any]) -> go.Figure:
72
+ det = score.get("details", []) or []
73
+ labels = [d.get("metric","") for d in det]
74
+ vals = [float(d.get("score",0)) for d in det]
75
+ # ソート(高→低)
76
+ pairs = sorted(zip(vals, labels), reverse=True)
77
+ vals, labels = zip(*pairs) if pairs else ([],[])
78
+ fig = go.Figure(go.Bar(x=list(vals), y=list(labels), orientation="h", text=[f"{v:.1f}" for v in vals], textposition="auto"))
79
+ fig.update_layout(height=460, margin=dict(l=10,r=10,t=30,b=10), title="指標別スコア(社内ルール)", xaxis=dict(range=[0,100]))
80
+ return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ def _external_bar(ext: Dict[str,Any]) -> go.Figure:
83
+ items = ext.get("items", []) or []
84
+ labels = [f"{i.get('category','')}: {i.get('name','')}" for i in items]
85
+ vals = [float(i.get("score",0)) for i in items]
86
+ pairs = sorted(zip(vals, labels))
87
+ vals, labels = zip(*pairs) if pairs else ([],[])
88
+ fig = go.Figure(go.Bar(x=list(vals), y=list(labels), orientation="h", text=[f"{v:.1f}" for v in vals], textposition="auto"))
89
+ fig.update_layout(height=520, margin=dict(l=10,r=10,t=30,b=10), title="外部評価(定量)項目スコア", xaxis=dict(range=[0,100]))
90
+ return fig
91
 
92
+ def _strengths_md(score: Dict[str,Any]) -> str:
93
+ det = score.get("details", []) or []
94
+ if not det: return "強み・弱みの抽出に必要な明細が見つかりませんでした。"
95
+ det_sorted = sorted(det, key=lambda x: x.get("score",0), reverse=True)
96
+ top3 = det_sorted[:3]
97
+ bottom3 = det_sorted[-3:][::-1]
98
+ def fmt(items):
99
+ return "\n".join([f"- **{i.get('metric','')}**:{i.get('score',0):.1f} 点(値: {i.get('value','—')}{i.get('unit','')})" for i in items])
100
+ return f"### 強み(上位3)\n{fmt(top3)}\n\n### 改善余地(下位3)\n{fmt(bottom3)}"
101
+
102
+ # ============ アプリ本体 ============
103
  def build_ui() -> gr.Blocks:
104
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"), fill_height=True, analytics_enabled=False) as demo:
105
+ gr.Markdown("## 🧮 企業スコアリング(PDF解析 × 定量評価 × AI所見)")
106
+
107
  with gr.Row():
108
  with gr.Column(scale=1):
109
  company = gr.Textbox(label="企業名(任意)", placeholder="例:株式会社OO")
110
+ files = gr.File(label="決算書PDF(複数可)", file_count="multiple", type="filepath")
111
+ force_ocr = gr.Checkbox(False, label="OCRを強制(スキャンPDF向け / やや遅い)")
 
 
 
 
 
112
  run_btn = gr.Button("📄 解析して反映", variant="primary")
113
+ recalc_btn = gr.Button("🔁 この表の値で再計算")
 
 
 
114
  with gr.Column(scale=1):
115
+ kpi_html = gr.HTML(label="ハイライトKPI")
 
116
 
117
  with gr.Tabs():
118
  with gr.Tab("概要"):
119
+ strengths_md = gr.Markdown()
120
+
121
  with gr.Tab("抽出結果(編集可)"):
122
+ df_out = gr.Dataframe(headers=["category","item","value"], interactive=True, wrap=True)
123
+
 
 
124
  with gr.Tab("スコア(社内ルール)"):
125
+ score_bar = gr.Plot()
126
+ radar_plot = gr.Plot()
127
+ with gr.Accordion("🔎 原データ(JSON)", open=False):
128
+ score_json_view = gr.JSON(value={})
129
+
130
  with gr.Tab("AI所見(中立)"):
131
+ ai_md = gr.Markdown()
132
+
133
  with gr.Tab("外部評価(定量化)"):
134
+ ext_bar = gr.Plot()
135
+ with gr.Accordion("🔎 原データ(JSON)", open=False):
136
+ ext_json_view = gr.JSON(value={})
137
+
138
+ # ---------- ハンドラ ----------
139
+ def _run(files: List[str], company: str, force_ocr: bool):
140
+ """
141
+ 1) PDF→抽出 2) 社内スコア 3) 外部評価(定量) 4) AI所見 5) 可視化
142
+ """
143
+ if not files:
144
+ raise gr.Error("PDF をアップロードしてください。")
145
+
146
+ # --- 1) 抽出 ---
147
+ fin_dict, fin_df = parse_pdf(files, company=company or "", force_ocr=force_ocr)
148
+
149
+ # --- 2) 社内スコア ---
150
+ score = score_company(fin_dict)
151
+
152
+ # --- 3) 外部評価(定量) ---
153
+ ext = score_external(company=company or "", fin=fin_dict, df=fin_df)
154
+
155
+ # --- 4) AI所見(中立・簡潔) ---
156
+ ai = make_ai_memo(company=company or "", fin=fin_dict, score=score, ext=ext)
157
+
158
+ # --- 5) 可視化 ---
159
+ kpi = _kpi_cards_html(score, ext)
160
+ sbar = _score_bar(score)
161
+ ebar = _external_bar(ext)
162
+ strengths = _strengths_md(score)
163
+
164
+ return (
165
+ kpi,
166
+ strengths,
167
+ fin_df,
168
+ sbar,
169
+ # 既存のレーダー(社内ルール可視化)。score側で作っているなら差し替えてOK
170
+ go.Figure(
171
+ data=[go.Scatterpolar(
172
+ r=[d["score"] for d in score.get("details", [])] + ([score.get("details", [])[0]["score"]] if score.get("details") else []),
173
+ theta=[d["metric"] for d in score.get("details", [])] + ([score.get("details", [])[0]["metric"]] if score.get("details") else []),
174
+ fill="toself"
175
+ )],
176
+ layout=go.Layout(
177
+ title="スコア・レーダー(社内)",
178
+ polar=dict(radialaxis=dict(visible=True, range=[0,100])),
179
+ showlegend=False, height=380, margin=dict(l=20,r=20,t=30,b=20)
180
+ )
181
+ ),
182
+ score, # gr.JSON 用
183
+ ai,
184
+ ebar,
185
+ ext # gr.JSON 用
186
+ )
187
+
188
+ def _recalc(df: pd.DataFrame, last_company: str):
189
+ """
190
+ テーブル編集後の再計算。抽出は行わず、DF→fin辞書へ戻して再スコア。
191
+ """
192
+ # df -> fin 辞書(あなたの既存関数に合わせて調整)
193
+ fin = {"balance_sheet":{}, "income_statement":{}, "cash_flows":{}}
194
+ for _, r in df.iterrows():
195
+ cat = str(r.get("category",""))
196
+ item= str(r.get("item",""))
197
+ val = r.get("value", None)
198
+ try:
199
+ parsed = None if val in (None,"","null") else float(val)
200
+ except Exception:
201
+ parsed = None
202
+ if cat in fin: fin[cat][item] = parsed
203
+
204
+ score = score_company(fin)
205
+ ext = score_external(company=last_company or "", fin=fin, df=df)
206
+ kpi = _kpi_cards_html(score, ext)
207
+ sbar = _score_bar(score)
208
+ ebar = _external_bar(ext)
209
+ strengths = _strengths_md(score)
210
+ return (
211
+ kpi, strengths, sbar,
212
+ go.Figure(
213
+ data=[go.Scatterpolar(
214
+ r=[d["score"] for d in score.get("details", [])] + ([score.get("details", [])[0]["score"]] if score.get("details") else []),
215
+ theta=[d["metric"] for d in score.get("details", [])] + ([score.get("details", [])[0]["metric"]] if score.get("details") else []),
216
+ fill="toself"
217
+ )],
218
+ layout=go.Layout(
219
+ title="スコア・レーダー(社内)",
220
+ polar=dict(radialaxis=dict(visible=True, range=[0,100])),
221
+ showlegend=False, height=380, margin=dict(l=20,r=20,t=30,b=20)
222
+ )
223
+ ),
224
+ score, ebar, ext
225
+ )
226
 
227
  run_btn.click(
228
+ _run,
229
+ inputs=[files, company, force_ocr],
230
+ outputs=[kpi_html, strengths_md, df_out, score_bar, radar_plot, score_json_view, ai_md, ext_bar, ext_json_view],
231
  concurrency_limit=1
232
  )
233
 
234
  recalc_btn.click(
235
+ _recalc,
236
+ inputs=[df_out, company],
237
+ outputs=[kpi_html, strengths_md, score_bar, radar_plot, score_json_view, ext_bar, ext_json_view],
238
  concurrency_limit=1
239
  )
240
 
 
 
 
 
241
  return demo