Corin1998 commited on
Commit
876e0a2
·
verified ·
1 Parent(s): 5c82b31

Update ui/ui_app.py

Browse files
Files changed (1) hide show
  1. ui/ui_app.py +171 -222
ui/ui_app.py CHANGED
@@ -1,251 +1,200 @@
1
  # ui/ui_app.py
2
  from __future__ import annotations
3
- import os, json, io
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
- try:
12
- from core.scoring import score_company # 既存の社内スコア(レーダー)
13
- except Exception:
14
- def score_company(fin: Dict[str,Any]) -> Dict[str,Any]:
15
- bs = fin.get("balance_sheet",{}) or {}
16
- is_ = fin.get("income_statement",{}) or {}
17
- safe = lambda x: float(x) if x not in (None,"") else 0.0
18
- kpis = {
19
- "総資産": safe(bs.get("total_assets")),
20
- "売上高": safe(is_.get("sales")),
21
- "営業利益": safe(is_.get("operating_income")),
22
- "純利益": safe(is_.get("net_income")),
23
- }
24
- details = []
25
- for k,v in kpis.items():
26
- val = max(0.0, min(100.0, (v/(kpis["売上高"]+1e-9))*30 if k!="売上高" else 50))
27
- details.append({"metric": k, "score": round(val,1)})
28
- total = round(sum(d["score"] for d in details)/len(details),1) if details else 0.0
29
- grade = "A" if total>=85 else "B" if total>=70 else "C" if total>=55 else "D"
30
- return {"total_score": total, "grade": grade, "details": details}
31
-
32
- try:
33
- from core.external_scoring import score_external # あなたの外部評価(定量化)
34
- except Exception:
35
- def score_external(df: pd.DataFrame) -> Dict[str,Any]:
36
- return {"name":"外部評価(簡易)","external_total": 60.0, "items":[],"notes":"モジュール未検出のため簡易"}
37
-
38
- try:
39
- from core.ai_judgement import make_ai_memo # AI所見(中立)
40
- except Exception:
41
- def make_ai_memo(fin: Dict[str,Any], score: Dict[str,Any], ext: Dict[str,Any]) -> str:
42
- return "(AI所見モジュール未検出のため簡易)\n- 財務の整合性と収益性を総合的に確認してください。"
43
-
44
- from core.extract import parse_pdf # PDF→(fin, df, meta, log)
45
-
46
- UNITS = [("自動",""), ("円","円"), ("千円","千円"), ("万円","万円"),
47
- ("百万円","百万円"), ("千万円","千万円"), ("億円","億円")]
48
-
49
- def _fmt_yen(x: Any) -> str:
50
  try:
51
- f = float(x)
52
- sign = "-" if f < 0 else ""
53
- f = abs(f)
54
- return f"{sign}{f:,.0f} 円"
55
  except Exception:
56
- return "—"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- def _radar(score: Dict[str, Any]) -> go.Figure:
59
- labels = [d["metric"] for d in score.get("details",[])]
60
- values = [d["score"] for d in score.get("details",[])]
61
- if not labels:
62
- labels, values = ["データ不足"], [0]
63
- fig = go.Figure()
64
- fig.add_trace(go.Scatterpolar(r=values + values[:1], theta=labels + labels[:1], fill="toself"))
65
- fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
66
- showlegend=False, margin=dict(l=20, r=20, t=30, b=20), height=380,
67
- title=f"総合スコア: {score.get('total_score',0)}(グレード: {score.get('grade','-')})")
68
- return fig
69
-
70
- def _summary_cards(fin: Dict[str,Any]) -> str:
71
- bs = fin.get("balance_sheet",{}) or {}
72
- is_ = fin.get("income_statement",{}) or {}
73
  html = f"""
74
  <style>
75
- .cards {{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}}
76
- .card {{border:1px solid #eee;border-radius:12px;padding:12px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}}
77
- .k {{font-size:.8rem;color:#666}}
78
- .v {{font-size:1.1rem;font-weight:700;margin-top:6px}}
79
- @media (max-width: 980px) {{ .cards {{grid-template-columns:repeat(2,minmax(0,1fr));}}}}
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </style>
 
 
 
 
 
 
 
81
  <div class="cards">
82
- <div class="card"><div class="k">総資産</div><div class="v">{_fmt_yen(bs.get('total_assets'))}</div></div>
83
- <div class="card"><div class="k">売上高</div><div class="v">{_fmt_yen(is_.get('sales'))}</div></div>
84
- <div class="card"><div class="k">営業利益</div><div class="v">{_fmt_yen(is_.get('operating_income'))}</div></div>
85
- <div class="card"><div class="k">当期純利益</div><div class="v">{_fmt_yen(is_.get('net_income'))}</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
  """
88
  return html
89
 
90
- def build_ui():
91
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"), fill_height=True, analytics_enabled=False) as demo:
92
- gr.Markdown("## 🧮 企業スコアリング(PDF解析 × 単位自動判定 × UI改善)")
93
 
94
- state_fin = gr.State({})
95
- state_df = gr.State(pd.DataFrame(columns=["category","item","value"]))
96
- state_unit_detected = gr.State({"label":"円","scale":1.0})
97
- state_unit_current = gr.State({"label":"円","scale":1.0})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  with gr.Row():
100
- with gr.Column(scale=1, min_width=320):
101
- company = gr.Textbox(label="企業名(任意)", placeholder="例:株式会社OO")
102
- use_vision = gr.Checkbox(value=True, label="OpenAI Visionで解析")
103
- files = gr.File(label="決算書PDF(複数可)", file_count="multiple", type="filepath")
104
-
105
- with gr.Group():
106
- unit_manual = gr.Dropdown(choices=[u for u,_ in UNITS], value="自動", label="単位の上書き(任意)")
107
- apply_unit_btn = gr.Button("↻ 単位を反映(換算)")
 
 
 
108
 
109
- run_btn = gr.Button("📄 PDFを解析", variant="primary")
110
- dl_csv = gr.File(label="ダウンロード(CSV)", interactive=False)
111
- dl_json = gr.File(label="ダウンロード(JSON)", interactive=False)
112
 
113
- unit_info = gr.HTML()
114
- debug_out = gr.Textbox(label="ログ", lines=10, interactive=False)
115
 
116
  with gr.Column(scale=2):
117
- summary = gr.HTML()
118
  with gr.Tabs():
119
- with gr.Tab("編集可能テーブル"):
120
- df_out = gr.Dataframe(headers=["category","item","value"], interactive=True, wrap=True)
121
- recalc_btn = gr.Button("🔁 表の値で再計算")
122
- with gr.Tab("スコア"):
123
- score_json = gr.JSON(label="スコア(内部JSON)", visible=False)
124
- chart = gr.Plot(label="スコアレーダー")
125
- with gr.Tab("外部評価"):
126
- ext_json = gr.JSON(label="外部評価(内部JSON)", visible=False)
127
- ext_md = gr.Markdown()
128
- with gr.Tab("AI所見"):
129
- insight_md = gr.Markdown()
130
-
131
- # -------- ハンドラ --------
132
- def on_analyze(company: str, use_vision: bool, files: List[str]):
133
- try:
134
- fin, df, meta, log = parse_pdf(files, company, use_vision)
135
- # 状態に反映
136
- state_fin_v = fin
137
- state_df_v = df
138
- det = {"label": meta["unit_label"], "scale": meta["unit_scale"]}
139
- cur = det.copy()
140
-
141
- # スコア
142
- sc = score_company(fin)
143
- fig = _radar(sc)
144
-
145
- # 外部評価(数値中心)
146
- ext = score_external(df)
147
- ext_md_text = f"**外部評価合計:** {ext.get('external_total','—')}\n\n" \
148
- f"{ext.get('notes','')}"
149
-
150
- # 所見
151
- memo = make_ai_memo(fin, sc, ext)
152
-
153
- # ダウンロード用ファイル作成
154
- csv_path = "/tmp/result.csv"
155
- json_path = "/tmp/result.json"
156
- df.to_csv(csv_path, index=False)
157
- with open(json_path, "w", encoding="utf-8") as f:
158
- json.dump(fin, f, ensure_ascii=False, indent=2)
159
-
160
- unit_badge = f"<b>単位(自動):</b> {det['label']} ×{det['scale']:,.0f}"
161
- if meta.get("warnings"):
162
- unit_badge += "<br>" + " / ".join(f"⚠️ {w}" for w in meta["warnings"])
163
-
164
- return (
165
- fin, # state_fin
166
- df, # state_df
167
- det, # detected
168
- cur, # current
169
- _summary_cards(fin),
170
- df, fig, sc,
171
- ext, ext_md_text,
172
- memo,
173
- unit_badge,
174
- log,
175
- csv_path,
176
- json_path
177
- )
178
- except Exception as e:
179
- import traceback
180
- tb = traceback.format_exc(limit=4)
181
- raise gr.Error(f"解析に失敗しました: {e}\n\n{tb}")
182
 
183
  run_btn.click(
184
  on_analyze,
185
  inputs=[company, use_vision, files],
186
- outputs=[
187
- state_fin, state_df, state_unit_detected, state_unit_current,
188
- summary,
189
- df_out, chart, score_json,
190
- ext_json, ext_md,
191
- insight_md,
192
- unit_info,
193
- debug_out,
194
- dl_csv, dl_json
195
- ],
196
- )
197
-
198
- def on_apply_unit(unit_label: str, df_cur: pd.DataFrame,
199
- det: Dict[str,Any], cur: Dict[str,Any], fin_cur: Dict[str,Any]):
200
- from core.unit_utils import UNIT_SCALE, apply_unit_scale
201
- # 新しい係数
202
- if unit_label in ("", "自動"):
203
- new = det
204
- else:
205
- new = {"label": unit_label, "scale": UNIT_SCALE.get(unit_label, 1.0)}
206
- # 係数比で再換算(現在→新)
207
- ratio = (new["scale"] / max(cur.get("scale",1.0), 1e-12))
208
-
209
- def _scale_df(df: pd.DataFrame) -> pd.DataFrame:
210
- df2 = df.copy()
211
- for i in df2.index:
212
- try:
213
- v = df2.at[i,"value"]
214
- df2.at[i,"value"] = float(v) * ratio if v not in (None,"") else v
215
- except Exception:
216
- pass
217
- return df2
218
-
219
- df2 = _scale_df(df_cur)
220
- fin2 = apply_unit_scale(fin_cur, ratio) # 比率で再拡大/縮小
221
-
222
- sc2 = score_company(fin2)
223
- fig2 = _radar(sc2)
224
- unit_badge = f"<b>単位(現在):</b> {new['label']} ×{new['scale']:,.0f}"
225
- return fin2, df2, new, _summary_cards(fin2), df2, fig2, sc2, unit_badge
226
-
227
- apply_unit_btn.click(
228
- on_apply_unit,
229
- inputs=[unit_manual, df_out, state_unit_detected, state_unit_current, state_fin],
230
- outputs=[state_fin, state_df, state_unit_current, summary, df_out, chart, score_json, unit_info]
231
- )
232
-
233
- def on_recalc(df_cur: pd.DataFrame, fin_cur: Dict[str,Any]):
234
- # ユーザー編集を反映して再計算
235
- fin2 = {"balance_sheet":{}, "income_statement":{}, "cash_flows":{}}
236
- for _, r in df_cur.iterrows():
237
- cat, item, val = str(r["category"]), str(r["item"]), r["value"]
238
- try: v = None if val in (None,"","null") else float(val)
239
- except Exception: v = None
240
- if cat in fin2: fin2[cat][item] = v
241
- sc2 = score_company(fin2)
242
- fig2 = _radar(sc2)
243
- return fin2, sc2, fig2, _summary_cards(fin2)
244
-
245
- recalc_btn.click(
246
- on_recalc,
247
- inputs=[df_out, state_fin],
248
- outputs=[state_fin, score_json, chart, summary]
249
  )
 
250
 
251
  return demo
 
1
  # ui/ui_app.py
2
  from __future__ import annotations
3
+ import json
4
+ import shutil
 
5
  import gradio as gr
6
+
7
+ from core.extract import parse_pdf # (fin, df, meta, log) を返す
8
+
9
+ APP_TITLE = "📊 企業スコアリング/PDF解析(Vision対応・単位自動推定)"
10
+
11
+
12
+ # --------- ユーティリティ(UI用) ---------
13
+ def _fmt_num(v):
14
+ if v is None or v == "":
15
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
+ x = float(v)
 
 
 
18
  except Exception:
19
+ return str(v)
20
+ # 大きな数は3桁区切り
21
+ return f"{x:,.0f}"
22
+
23
+ def _get(dct, path, default=None):
24
+ cur = dct or {}
25
+ for k in path:
26
+ if not isinstance(cur, dict) or k not in cur:
27
+ return default
28
+ cur = cur[k]
29
+ return cur
30
+
31
+ def render_summary(fin: dict, meta: dict) -> str:
32
+ """JSONをそのまま出さず、要点だけのカードをHTMLで描画"""
33
+ unit_label = meta.get("unit_label", "(不明)")
34
+ company = _get(fin, ["company", "name"], "—")
35
+ period = f"{_get(fin, ['period','start_date'],'—')} 〜 {_get(fin, ['period','end_date'],'—')}"
36
+
37
+ bs = fin.get("balance_sheet", {}) or {}
38
+ is_ = fin.get("income_statement", {}) or {}
39
+ cf = fin.get("cash_flows", {}) or {}
40
+
41
+ def row(label, key, src):
42
+ return f"""
43
+ <div class="row">
44
+ <div class="k">{label}</div>
45
+ <div class="v">{_fmt_num(src.get(key))}</div>
46
+ </div>"""
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  html = f"""
49
  <style>
50
+ .cards {{ display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:12px; }}
51
+ @media (max-width: 1024px) {{ .cards {{ grid-template-columns: 1fr; }} }}
52
+ .card {{
53
+ background:#fff; border-radius:14px; box-shadow:0 4px 14px rgba(0,0,0,0.06);
54
+ padding:16px 16px 10px; border:1px solid #eee;
55
+ }}
56
+ .hd {{ font-weight:700; font-size:14px; margin-bottom:6px; color:#334; letter-spacing: .02em; }}
57
+ .sub {{ color:#667; font-size:12px; margin-bottom:10px; }}
58
+ .row {{ display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px dashed #eef; }}
59
+ .row:last-child {{ border-bottom:none; }}
60
+ .k {{ color:#566; }}
61
+ .v {{ font-weight:600; color:#111; }}
62
+ .meta {{
63
+ margin: 8px 0 14px; color:#556; font-size:12px; display:flex; gap:18px; flex-wrap:wrap;
64
+ }}
65
+ .badge {{
66
+ background:#f6f7fb; border:1px solid #e7e9f2; padding:4px 8px; border-radius:999px; font-size:12px;
67
+ }}
68
  </style>
69
+
70
+ <div class="meta">
71
+ <span class="badge">会社名: {company}</span>
72
+ <span class="badge">期間: {period}</span>
73
+ <span class="badge">推定単位: {unit_label}</span>
74
+ </div>
75
+
76
  <div class="cards">
77
+ <div class="card">
78
+ <div class="hd">貸借対照表(主項目)</div>
79
+ <div class="sub">主要残高を表示しています</div>
80
+ {row("総資産", "total_assets", bs)}
81
+ {row("負債合計", "total_liabilities", bs)}
82
+ {row("純資産", "total_equity", bs)}
83
+ {row("流動資産", "current_assets", bs)}
84
+ {row("固定資産", "fixed_assets", bs)}
85
+ {row("流動負債", "current_liabilities", bs)}
86
+ {row("固定負債", "long_term_liabilities", bs)}
87
+ </div>
88
+
89
+ <div class="card">
90
+ <div class="hd">損益計算書(主項目)</div>
91
+ <div class="sub">売上と利益の概況</div>
92
+ {row("売上高", "sales", is_)}
93
+ {row("売上原価", "cost_of_sales", is_)}
94
+ {row("売上総利益", "gross_profit", is_)}
95
+ {row("販管費", "operating_expenses", is_)}
96
+ {row("営業利益", "operating_income", is_)}
97
+ {row("経常利益", "ordinary_income", is_)}
98
+ {row("当期純利益", "net_income", is_)}
99
+ </div>
100
+
101
+ <div class="card">
102
+ <div class="hd">キャッシュフロー(主項目)</div>
103
+ <div class="sub">各CFの方向性</div>
104
+ {row("営業CF", "operating_cash_flow", cf)}
105
+ {row("投資CF", "investing_cash_flow", cf)}
106
+ {row("財務CF", "financing_cash_flow", cf)}
107
+ </div>
108
  </div>
109
  """
110
  return html
111
 
 
 
 
112
 
113
+ # --------- クリックハンドラ ---------
114
+ def on_analyze(company: str, use_vision: bool, files):
115
+ try:
116
+ fin, df, meta, log = parse_pdf(files, company=company or "", use_vision=use_vision)
117
+
118
+ summary_html = render_summary(fin, meta)
119
+ fin_json = json.dumps(fin, ensure_ascii=False, indent=2)
120
+
121
+ unit_label = f"推定単位: {meta.get('unit_label','—')}(スケール: ×{int(meta.get('unit_scale',1)):,})"
122
+ debug = f"[OK] 解析完了\n--- log ---\n{log}"
123
+
124
+ return summary_html, df, fin_json, unit_label, debug
125
+ except Exception as e:
126
+ msg = f"解析に失敗しました: {e}"
127
+ return (
128
+ f"<div style='color:#b11;font-weight:600'>{msg}</div>",
129
+ None,
130
+ "{}",
131
+ "推定単位: —",
132
+ msg,
133
+ )
134
+
135
+ def on_health():
136
+ msgs = []
137
+ try:
138
+ import gradio
139
+ msgs.append(f"Gradio: {gradio.__version__}")
140
+ except Exception as e:
141
+ msgs.append(f"Gradio: 取得失敗 ({e})")
142
+
143
+ import os
144
+ from shutil import which
145
+ for b in ("pdftoppm", "pdftocairo"):
146
+ ok = bool(which(b))
147
+ msgs.append(("✅" if ok else "❌") + f" {b}: " + ("検出" if ok else "見つからず(packages.txt に poppler-utils が必要)"))
148
+ if os.environ.get("OPENAI_API_KEY"):
149
+ msgs.append("✅ OPENAI_API_KEY: 検出")
150
+ else:
151
+ msgs.append("❌ OPENAI_API_KEY: 未設定(Spaceの「Variables and secrets」に設定)")
152
+ return "\n".join(msgs)
153
+
154
+
155
+ # --------- UI本体 ---------
156
+ def build_ui() -> gr.Blocks:
157
+ with gr.Blocks(
158
+ title=APP_TITLE,
159
+ theme=gr.themes.Soft(primary_hue="indigo"),
160
+ fill_height=True,
161
+ analytics_enabled=False,
162
+ ) as demo:
163
+ gr.Markdown(f"## {APP_TITLE}\nPDFから主要財務項目を抽出し、**単位を自動推定**してスケーリングします。Vision失敗時はテキスト抽出にフォールバックします。")
164
 
165
  with gr.Row():
166
+ with gr.Column(scale=1):
167
+ company = gr.Textbox(label="企業名(任意)", placeholder="例:株式会社〇〇")
168
+ use_vision = gr.Checkbox(value=True, label="OpenAI Visionで表を解析(推奨)")
169
+ files = gr.Files(
170
+ label="決算書PDF(複数可)",
171
+ type="filepath",
172
+ file_count="multiple",
173
+ file_types=[".pdf"],
174
+ )
175
+ run_btn = gr.Button("📄 解析する", variant="primary")
176
+ health_btn = gr.Button("🩺 環境チェック")
177
 
178
+ unit_text = gr.Markdown("推定単位: ")
 
 
179
 
180
+ gr.Markdown("※ Vision / 画像化に失敗した場合はテキスト抽出へ自動切替します。Poppler(`poppler-utils`)必須。")
 
181
 
182
  with gr.Column(scale=2):
183
+ summary = gr.HTML(label="サマリー(カード表示)")
184
  with gr.Tabs():
185
+ with gr.Tab("表(編集可)"):
186
+ df_out = gr.Dataframe(headers=["category", "item", "value"], interactive=True, wrap=True)
187
+ with gr.Tab("抽出JSON"):
188
+ fin_json = gr.Code(label="抽出JSON", language="json")
189
+
190
+ debug_out = gr.Textbox(label="ログ", lines=8, interactive=False, show_copy_button=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  run_btn.click(
193
  on_analyze,
194
  inputs=[company, use_vision, files],
195
+ outputs=[summary, df_out, fin_json, unit_text, debug_out],
196
+ concurrency_limit=1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  )
198
+ health_btn.click(on_health, outputs=debug_out, concurrency_limit=1)
199
 
200
  return demo