Corin1998 commited on
Commit
c2ea3e1
·
verified ·
1 Parent(s): 058ed82

Update ui/ui_app.py

Browse files
Files changed (1) hide show
  1. ui/ui_app.py +123 -58
ui/ui_app.py CHANGED
@@ -1,15 +1,20 @@
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 "—"
@@ -17,7 +22,6 @@ def _fmt_num(v):
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):
@@ -29,54 +33,39 @@ def _get(dct, path, default=None):
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)}
@@ -85,10 +74,9 @@ def render_summary(fin: dict, meta: dict) -> str:
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_)}
@@ -97,31 +85,43 @@ def render_summary(fin: dict, meta: dict) -> str:
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 (
@@ -130,27 +130,46 @@ def on_analyze(company: str, use_vision: bool, files):
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:
@@ -160,8 +179,9 @@ def build_ui() -> gr.Blocks:
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="例:株式会社〇〇")
@@ -176,25 +196,70 @@ def build_ui() -> gr.Blocks:
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
 
1
  # ui/ui_app.py
2
  from __future__ import annotations
3
  import json
 
4
  import gradio as gr
5
+ import plotly.graph_objects as go
6
 
7
+ from core.extract import parse_pdf # (fin, df, meta, log)
8
+ from core.external_scoring import (
9
+ get_external_template_df,
10
+ fill_missing_with_external,
11
+ score_external_from_df,
12
+ )
13
+ from core.ai_judgement import suggest_external_fields, make_ai_memo
14
 
15
+ APP_TITLE = "📊 企業スコアリング/PDF解析(Vision・単位自動推定)+ 外部評価 & AI評価"
16
 
17
+ # --------- UI補助 ---------
 
18
  def _fmt_num(v):
19
  if v is None or v == "":
20
  return "—"
 
22
  x = float(v)
23
  except Exception:
24
  return str(v)
 
25
  return f"{x:,.0f}"
26
 
27
  def _get(dct, path, default=None):
 
33
  return cur
34
 
35
  def render_summary(fin: dict, meta: dict) -> str:
 
36
  unit_label = meta.get("unit_label", "(不明)")
37
  company = _get(fin, ["company", "name"], "—")
38
  period = f"{_get(fin, ['period','start_date'],'—')} 〜 {_get(fin, ['period','end_date'],'—')}"
 
39
  bs = fin.get("balance_sheet", {}) or {}
40
  is_ = fin.get("income_statement", {}) or {}
41
  cf = fin.get("cash_flows", {}) or {}
42
 
43
  def row(label, key, src):
44
+ return f"<div class='row'><div class='k'>{label}</div><div class='v'>{_fmt_num(src.get(key))}</div></div>"
 
 
 
 
45
 
46
+ return f"""
47
  <style>
48
  .cards {{ display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:12px; }}
49
  @media (max-width: 1024px) {{ .cards {{ grid-template-columns: 1fr; }} }}
50
+ .card {{ background:#fff; border-radius:14px; box-shadow:0 4px 14px rgba(0,0,0,0.06);
51
+ padding:16px 16px 10px; border:1px solid #eee; }}
52
+ .hd {{ font-weight:700; font-size:14px; margin-bottom:6px; color:#334; }}
 
 
53
  .sub {{ color:#667; font-size:12px; margin-bottom:10px; }}
54
  .row {{ display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px dashed #eef; }}
55
  .row:last-child {{ border-bottom:none; }}
56
+ .k {{ color:#566; }} .v {{ font-weight:600; color:#111; }}
57
+ .meta {{ margin:8px 0 14px; color:#556; font-size:12px; display:flex; gap:18px; flex-wrap:wrap; }}
58
+ .badge {{ background:#f6f7fb; border:1px solid #e7e9f2; padding:4px 8px; border-radius:999px; font-size:12px; }}
 
 
 
 
 
59
  </style>
 
60
  <div class="meta">
61
  <span class="badge">会社名: {company}</span>
62
  <span class="badge">期間: {period}</span>
63
  <span class="badge">推定単位: {unit_label}</span>
64
  </div>
 
65
  <div class="cards">
66
  <div class="card">
67
  <div class="hd">貸借対照表(主項目)</div>
68
+ <div class="sub">主要残高</div>
69
  {row("総資産", "total_assets", bs)}
70
  {row("負債合計", "total_liabilities", bs)}
71
  {row("純資産", "total_equity", bs)}
 
74
  {row("流動負債", "current_liabilities", bs)}
75
  {row("固定負債", "long_term_liabilities", bs)}
76
  </div>
 
77
  <div class="card">
78
  <div class="hd">損益計算書(主項目)</div>
79
+ <div class="sub">売上と利益</div>
80
  {row("売上高", "sales", is_)}
81
  {row("売上原価", "cost_of_sales", is_)}
82
  {row("売上総利益", "gross_profit", is_)}
 
85
  {row("経常利益", "ordinary_income", is_)}
86
  {row("当期純利益", "net_income", is_)}
87
  </div>
 
88
  <div class="card">
89
  <div class="hd">キャッシュフロー(主項目)</div>
90
+ <div class="sub">方向性</div>
91
  {row("営業CF", "operating_cash_flow", cf)}
92
  {row("投資CF", "investing_cash_flow", cf)}
93
  {row("財務CF", "financing_cash_flow", cf)}
94
  </div>
95
  </div>
96
  """
 
97
 
98
+ def _radar_from_category_scores(cat_scores: dict) -> go.Figure:
99
+ if not cat_scores:
100
+ return go.Figure()
101
+ labels = list(cat_scores.keys())
102
+ values = [cat_scores[k] for k in labels]
103
+ fig = go.Figure()
104
+ fig.add_trace(go.Scatterpolar(r=values + values[:1], theta=labels + labels[:1], fill="toself"))
105
+ fig.update_layout(
106
+ polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
107
+ margin=dict(l=20, r=20, t=30, b=20),
108
+ showlegend=False,
109
+ height=380,
110
+ title="カテゴリ別スコア(外部・定量)",
111
+ )
112
+ return fig
113
 
114
+ # --------- ハンドラ ---------
115
  def on_analyze(company: str, use_vision: bool, files):
116
  try:
117
  fin, df, meta, log = parse_pdf(files, company=company or "", use_vision=use_vision)
 
118
  summary_html = render_summary(fin, meta)
119
  fin_json = json.dumps(fin, ensure_ascii=False, indent=2)
120
+ unit_text = f"推定単位: {meta.get('unit_label','—')}(スケール: ×{int(meta.get('unit_scale',1)):,})"
 
121
  debug = f"[OK] 解析完了\n--- log ---\n{log}"
122
+ # 外部評価テンプレ初期化
123
+ df_ext = get_external_template_df()
124
+ return summary_html, df, fin_json, unit_text, debug, fin, df_ext
125
  except Exception as e:
126
  msg = f"解析に失敗しました: {e}"
127
  return (
 
130
  "{}",
131
  "推定単位: —",
132
  msg,
133
+ {}, # fin_state
134
+ get_external_template_df(),
135
  )
136
 
137
  def on_health():
 
 
 
 
 
 
 
138
  import os
139
  from shutil import which
140
+ msgs = []
141
  for b in ("pdftoppm", "pdftocairo"):
142
  ok = bool(which(b))
143
  msgs.append(("✅" if ok else "❌") + f" {b}: " + ("検出" if ok else "見つからず(packages.txt に poppler-utils が必要)"))
144
+ msgs.append(("✅" if os.environ.get("OPENAI_API_KEY") else "❌") + " OPENAI_API_KEY")
 
 
 
145
  return "\n".join(msgs)
146
 
147
+ def on_ext_fill(company: str, fin_state: dict, df_ext_in):
148
+ try:
149
+ sugg = suggest_external_fields(company, fin_state or {})
150
+ df_filled = fill_missing_with_external(df_ext_in, sugg)
151
+ return df_filled, json.dumps(sugg, ensure_ascii=False, indent=2)
152
+ except Exception as e:
153
+ return df_ext_in, f"LLM推定に失敗: {e}"
154
+
155
+ def on_ext_score(df_ext_in):
156
+ try:
157
+ ext = score_external_from_df(df_ext_in)
158
+ chart = _radar_from_category_scores(ext.get("category_scores", {}))
159
+ total_md = f"**外部評価(定量)合計:** {ext['external_total']:.1f} / 100"
160
+ return json.dumps(ext, ensure_ascii=False, indent=2), chart, total_md
161
+ except Exception as e:
162
+ return f"外部評価に失敗: {e}", go.Figure(), "—"
163
+
164
+ def on_ai_eval(company: str, fin_state: dict, df_ext_in):
165
+ try:
166
+ ext = score_external_from_df(df_ext_in) # 最新入力で算出
167
+ ai = make_ai_memo(company, fin_state or {}, ext)
168
+ memo = ai.get("memo_md", "")
169
+ score = ai.get("ai_score", 50)
170
+ return memo, float(score)
171
+ except Exception as e:
172
+ return f"AI評価の生成に失敗: {e}", 50.0
173
 
174
  # --------- UI本体 ---------
175
  def build_ui() -> gr.Blocks:
 
179
  fill_height=True,
180
  analytics_enabled=False,
181
  ) as demo:
182
+ gr.Markdown(f"## {APP_TITLE}\n- PDFから主要財務項目を抽出し、**単位自動推定**で円換算します。\n- 外部評価(定量)は**ルールベース**、AI評価は**別基準**で中立に要約・採点します。")
183
 
184
+ fin_state = gr.State({})
185
  with gr.Row():
186
  with gr.Column(scale=1):
187
  company = gr.Textbox(label="企業名(任意)", placeholder="例:株式会社〇〇")
 
196
  health_btn = gr.Button("🩺 環境チェック")
197
 
198
  unit_text = gr.Markdown("推定単位: —")
199
+ debug_out = gr.Textbox(label="ログ", lines=6, interactive=False, show_copy_button=True)
 
200
 
201
  with gr.Column(scale=2):
202
  summary = gr.HTML(label="サマリー(カード表示)")
203
  with gr.Tabs():
204
+ with gr.Tab("抽出テーブル(編集可)"):
205
  df_out = gr.Dataframe(headers=["category", "item", "value"], interactive=True, wrap=True)
206
  with gr.Tab("抽出JSON"):
207
  fin_json = gr.Code(label="抽出JSON", language="json")
208
 
209
+ # --- 外部評価 ---
210
+ gr.Markdown("### 外部評価(定量・ルールベース)")
211
+ with gr.Row():
212
+ with gr.Column(scale=2):
213
+ df_ext = gr.Dataframe(
214
+ label="外部入力テンプレ(不足は空欄でOK)",
215
+ value=get_external_template_df(),
216
+ interactive=True,
217
+ wrap=True,
218
+ headers=["カテゴリー", "入力項目", "値"],
219
+ )
220
+ with gr.Column(scale=1):
221
+ fill_btn = gr.Button("✨ LLMで不足項目を推定して下書き")
222
+ fill_json = gr.Code(label="推定値(JSON)", language="json")
223
+ score_btn = gr.Button("🧮 外部評価を計算", variant="primary")
224
+ total_md = gr.Markdown("—")
225
+ with gr.Row():
226
+ ext_json = gr.Code(label="外部評価の結果(JSON)", language="json")
227
+ ext_chart = gr.Plot(label="カテゴリ別レーダー")
228
 
229
+ # --- AI評価 ---
230
+ gr.Markdown("### AI評価(中立メモ+別基準スコア)")
231
+ with gr.Row():
232
+ ai_btn = gr.Button("🤖 AI評価を生成")
233
+ with gr.Row():
234
+ ai_memo = gr.Markdown()
235
+ ai_score = gr.Number(label="AIスコア(0-100)", precision=1, interactive=False)
236
+
237
+ # --- ワイヤリング ---
238
  run_btn.click(
239
  on_analyze,
240
  inputs=[company, use_vision, files],
241
+ outputs=[summary, df_out, fin_json, unit_text, debug_out, fin_state, df_ext],
242
  concurrency_limit=1,
243
  )
244
  health_btn.click(on_health, outputs=debug_out, concurrency_limit=1)
245
 
246
+ fill_btn.click(
247
+ on_ext_fill,
248
+ inputs=[company, fin_state, df_ext],
249
+ outputs=[df_ext, fill_json],
250
+ concurrency_limit=1,
251
+ )
252
+ score_btn.click(
253
+ on_ext_score,
254
+ inputs=[df_ext],
255
+ outputs=[ext_json, ext_chart, total_md],
256
+ concurrency_limit=1,
257
+ )
258
+ ai_btn.click(
259
+ on_ai_eval,
260
+ inputs=[company, fin_state, df_ext],
261
+ outputs=[ai_memo, ai_score],
262
+ concurrency_limit=1,
263
+ )
264
+
265
  return demo