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

Update ui/ui_app.py

Browse files
Files changed (1) hide show
  1. ui/ui_app.py +152 -40
ui/ui_app.py CHANGED
@@ -1,5 +1,6 @@
 
1
  from __future__ import annotations
2
- import json, traceback, base64
3
  from typing import Any, Dict, List
4
  import gradio as gr
5
  import pandas as pd
@@ -8,9 +9,12 @@ import plotly.graph_objects as go
8
  from core.pdf_io import pdf_to_images, pdf_to_text
9
  from core.extract import extract_financials
10
  from core.scoring import score_company
11
- from core.external_score import get_external_template_df, fill_missing_with_external, score_external_from_df
 
 
12
  from core.units import detect_unit, unit_factor, scale_financials_yen
13
  from core.openai_client import VISION_MODEL, TEXT_MODEL, get_client
 
14
 
15
  # ===== helpers =====
16
  def fin_to_df(fin: Dict[str, Any]) -> pd.DataFrame:
@@ -35,6 +39,29 @@ def df_to_fin(df: pd.DataFrame) -> Dict[str, Any]:
35
  out[cat][item] = parsed
36
  return out
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def radar(score: Dict[str, Any]) -> go.Figure:
39
  labels = [d["metric"] for d in score["details"]]
40
  values = [d["score"] for d in score["details"]]
@@ -59,18 +86,63 @@ def health() -> str:
59
  msgs.append(f"ℹ️ Vision={VISION_MODEL} / Text={TEXT_MODEL}")
60
  return "<br>".join(msgs)
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  # ===== main actions =====
63
- def run_analyze(company: str, use_vision: bool, unit_sel: str, files: list[str]):
64
  if not files:
65
  raise gr.Error("PDF をアップロードしてください。")
66
 
67
- # 1) 単位の自動推定(先頭数ページのテキスト)
68
  first_text = pdf_to_text(files[0], pages=2)
69
  detected = detect_unit(first_text) or "円"
70
  unit_label = unit_sel if unit_sel != "自動推定" else detected
71
  factor = unit_factor(unit_label)
72
 
73
- # 2) Vision(失敗時はテキストモデル)で抽出
74
  try:
75
  imgs: List[bytes] = []
76
  for p in files:
@@ -85,23 +157,48 @@ def run_analyze(company: str, use_vision: bool, unit_sel: str, files: list[str])
85
  # 3) 円換算
86
  fin_yen = scale_financials_yen(fin_raw, factor=factor)
87
 
88
- # 4) 内部スコア
89
  df = fin_to_df(fin_yen)
90
  score = score_company(fin_yen)
91
  fig = radar(score)
 
92
 
93
- # 5) AI所見(中立性を強化)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  try:
95
  client = get_client()
96
- prompt = f"""あなたは独立の財務アナリストです。主観や推測を避け、事実と比率を根拠に短くコメントしてください。
97
- - 書き方: 箇条書き、断定的表現・煽り表現は禁止。将来予測はしない。
98
- - 必ず根拠(指標名と値)を各行に併記する。
 
99
 
100
  [財務データ(円換算)]
101
  {json.dumps(fin_yen, ensure_ascii=False)}
102
 
103
- [社内スコア]
104
  {json.dumps(score, ensure_ascii=False)}
 
 
 
105
  """
106
  resp = client.chat.completions.create(
107
  model=TEXT_MODEL,
@@ -114,13 +211,26 @@ def run_analyze(company: str, use_vision: bool, unit_sel: str, files: list[str])
114
  insight = f"AI所見の生成に失敗: {e}"
115
 
116
  unit_info = f"PDF表記の単位: <b>{detected}</b> / 適用単位: <b>{unit_label}</b>(円換算係数={factor:g})"
117
- return (unit_info,
118
- json.dumps(fin_raw, ensure_ascii=False, indent=2), # 参照用(換算前)
119
- json.dumps(fin_yen, ensure_ascii=False, indent=2), # 実計算に使用
120
- df,
121
- json.dumps(score, ensure_ascii=False, indent=2),
122
- fig,
123
- insight)
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  def run_recalc(df: pd.DataFrame):
126
  try:
@@ -134,12 +244,6 @@ def run_recalc(df: pd.DataFrame):
134
  tb = traceback.format_exc(limit=6)
135
  raise gr.Error(f"再計算に失敗しました: {e}\n\n<pre style='white-space:pre-wrap'>{tb}</pre>")
136
 
137
- # ===== 外部評価 =====
138
- def open_external_template():
139
- df = get_external_template_df()
140
- df = fill_missing_with_external(df)
141
- return df
142
-
143
  def calc_external(df_ext: pd.DataFrame):
144
  res = score_external_from_df(df_ext)
145
  return json.dumps(res, ensure_ascii=False, indent=2)
@@ -155,6 +259,9 @@ def build_ui() -> gr.Blocks:
155
  use_vision = gr.Checkbox(value=True, label="OpenAIでPDFをAI解析(Vision)")
156
  unit_sel = gr.Dropdown(choices=["自動推定","円","千円","百万円","千万円","億円"],
157
  value="自動推定", label="金額単位(PDF記載)")
 
 
 
158
  files = gr.File(label="決算書PDF(複数可)", file_count="multiple", type="filepath")
159
  run_btn = gr.Button("📄 解析して反映", variant="primary")
160
  recalc_btn = gr.Button("🔁 表の値で再計算")
@@ -163,33 +270,38 @@ def build_ui() -> gr.Blocks:
163
 
164
  with gr.Column(scale=1):
165
  unit_html = gr.HTML(label="単位情報")
166
- fin_json_raw = gr.Code(label="抽出JSON(換算前)", language="json", interactive=False)
167
 
168
  with gr.Tabs():
169
- with gr.Tab("抽出結果(円換算・表で編集可)"):
170
- fin_json_yen = gr.Code(label="抽出JSON(円換算)", language="json")
 
 
 
 
171
  df_out = gr.Dataframe(headers=["category", "item", "value"], interactive=True, wrap=True)
172
- with gr.Tab("スコアリング(社内ルール)"):
173
  score_json = gr.Code(label="スコア(JSON)", language="json")
174
- chart = gr.Plot(label="スコアレーダー")
175
  with gr.Tab("AI所見(中立)"):
176
  insight_md = gr.Markdown()
177
- with gr.Tab("外部評価(テンプレ&採点)"):
178
- with gr.Row():
179
- open_t = gr.Button("📋 テンプレを開く")
180
- calc_t = gr.Button("🧮 外部スコア計算")
181
  df_ext = gr.Dataframe(headers=["カテゴリー","入力項目","値"], interactive=True, wrap=True)
 
182
  ext_json = gr.Code(label="外部評価(JSON)", language="json")
183
 
184
- run_btn.click(run_analyze,
185
- inputs=[company, use_vision, unit_sel, files],
186
- outputs=[unit_html, fin_json_raw, fin_json_yen, df_out, score_json, chart, insight_md],
187
- concurrency_limit=1)
 
 
188
 
189
- recalc_btn.click(run_recalc, inputs=[df_out], outputs=[score_json, chart, fin_json_yen],
190
- concurrency_limit=1)
 
 
191
 
192
- open_t.click(open_external_template, outputs=[df_ext], concurrency_limit=1)
193
  calc_t.click(calc_external, inputs=[df_ext], outputs=[ext_json], concurrency_limit=1)
194
 
195
  health_btn.click(health, outputs=health_out, concurrency_limit=1)
 
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
 
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:
 
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"]]
 
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:
 
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,
 
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:
 
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)
 
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("🔁 表の値で再計算")
 
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)