Corin1998 commited on
Commit
9492ff0
·
verified ·
1 Parent(s): 8752b28

Upload 7 files

Browse files
Files changed (4) hide show
  1. app.py +152 -94
  2. finance_core.py +34 -9
  3. llm_extract.py +47 -4
  4. schemas.py +6 -0
app.py CHANGED
@@ -10,7 +10,7 @@ import gradio as gr
10
  import yaml
11
  from openai import OpenAI
12
 
13
- from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion
14
  from finance_core import (
15
  compute_ratios,
16
  credit_decision,
@@ -23,6 +23,7 @@ from llm_extract import (
23
  upload_file_to_openai,
24
  extract_financials_from_files,
25
  suggest_multiples_with_llm,
 
26
  )
27
 
28
  HF_SPACE = os.environ.get("HF_SPACE_NAME", "hf-credit-loan-investment-app")
@@ -107,7 +108,7 @@ def _read_file_input(f):
107
  pass
108
  raise ValueError(f"Unsupported file input type: {type(f)}")
109
 
110
- # --- 単位検出&換算ヘルパー(PDF本文を走査して「単位:百万円」等を検出) ---
111
  def _concat_pdf_text(paths: List[str], max_chars: int = 180_000) -> str:
112
  try:
113
  from pypdf import PdfReader
@@ -132,19 +133,13 @@ def _concat_pdf_text(paths: List[str], max_chars: int = 180_000) -> str:
132
  return "\n\n".join(out)[:max_chars]
133
 
134
  def detect_unit_multiplier_from_paths(paths: List[str]) -> Tuple[float, str]:
135
- """
136
- PDF本文から単位を推定して (乗数, ラベル) を返す。
137
- 例: ('百万円'→1_000_000, '千円'→1_000, '万円'→10_000, '円'→1,
138
- 'millions'→1_000_000, 'thousands'→1_000)
139
- 見つからなければ (1, '不明')
140
- """
141
  text = _concat_pdf_text(paths)
142
  if not text:
143
  return 1.0, "不明"
144
 
145
  lower = text.lower()
146
 
147
- # 日本語パターン(優先度:百万円→千円→万円→円)
148
  if re.search(r"単位[::]\s*百万円", text) or re.search(r"(百万円)", text):
149
  return 1_000_000.0, "百万円"
150
  if re.search(r"単位[::]\s*千円", text) or re.search(r"(千円)", text):
@@ -160,7 +155,6 @@ def detect_unit_multiplier_from_paths(paths: List[str]) -> Tuple[float, str]:
160
  if re.search(r"in\s+thousands\s+of\s+(yen|jpy|usd|dollars?)", lower) or re.search(r"\b(jpy|¥|\$|usd)\s*\(\s*thousands?\s*\)", lower):
161
  return 1_000.0, "thousands"
162
 
163
- # コンテキストで単独出現
164
  if re.search(r"百万円", text):
165
  return 1_000_000.0, "百万円"
166
 
@@ -174,7 +168,6 @@ _NUM_FIELDS = [
174
  ]
175
 
176
  def scale_extract_inplace(extract: FinancialExtract, multiplier: float) -> None:
177
- """抽出済みオブジェクトの数値を指定乗数でインプレース換算する(Noneは無視)。"""
178
  if not multiplier or multiplier == 1:
179
  return
180
  for period in extract.periods:
@@ -186,12 +179,102 @@ def scale_extract_inplace(extract: FinancialExtract, multiplier: float) -> None:
186
  except Exception:
187
  pass
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  def analyze(
191
  files: List,
192
  company_name: str,
193
  industry_hint: str,
194
  currency_hint: str,
 
195
  base_rate: float,
196
  want_credit: bool,
197
  want_loan: bool,
@@ -207,7 +290,7 @@ def analyze(
207
  if not files or len(files) == 0:
208
  raise gr.Error("決算書ファイル(PDF/画像)を1つ以上アップロードしてください。")
209
 
210
- # 1) Upload files to OpenAI and extract structured financials via vision
211
  try:
212
  file_ids = []
213
  for f in files:
@@ -216,14 +299,14 @@ def analyze(
216
  except Exception as e:
217
  raise gr.Error(f"ファイルのアップロードに失敗しました: {e}")
218
 
219
- # Local paths for text & unit fallback
220
  local_paths = []
221
  for f in files:
222
  if isinstance(f, (str, bytes)) or hasattr(f, "__fspath__"):
223
  local_paths.append(os.fspath(f))
224
 
 
225
  try:
226
- # Prefer version that accepts local_paths; fallback if not supported
227
  try:
228
  extract = extract_financials_from_files(
229
  client=client,
@@ -246,13 +329,13 @@ def analyze(
246
  except Exception as e:
247
  raise gr.Error(f"LLM抽出に失敗しました: {e}")
248
 
249
- # allow override company name / industry if provided
250
  if company_name:
251
  extract.company_name = company_name
252
  if industry_hint:
253
  extract.industry = industry_hint
254
 
255
- # --- 単位検出&換算(円/ドル等の素単位に正規化)---
256
  unit_info = {"source_label": "不明", "multiplier": 1}
257
  try:
258
  if local_paths:
@@ -264,15 +347,17 @@ def analyze(
264
  if debug:
265
  print(f"[unit-detect] warning: {e}")
266
 
267
- # 2) Compute derived ratios and risk score
268
  ratios = compute_ratios(extract)
269
 
270
- # 3) Decisions
271
  decisions: Dict[str, Any] = {}
272
  if want_credit:
273
  decisions["credit"] = credit_decision(extract, ratios, POLICIES)
274
  if want_loan:
275
  decisions["loan"] = loan_decision(extract, ratios, base_rate or BASE_RATE, POLICIES)
 
 
276
  if want_invest:
277
  multiples: Optional[MultipleSuggestion] = None
278
  try:
@@ -285,13 +370,27 @@ def analyze(
285
  )
286
  except Exception:
287
  multiples = None
288
- decisions["investment"] = investment_decision(extract, ratios, POLICIES, multiples)
 
 
 
 
 
 
 
 
 
 
289
 
290
- # 4) Build a combined report (dict) and return displays
291
- report = build_report_dict(extract, ratios, decisions, unit_info=unit_info)
 
 
 
 
292
  report_json = json.dumps(report, ensure_ascii=False, indent=2)
293
 
294
- # Save a downloadable JSON (ensure directory exists; Gradio v5 は /tmp を推奨)
295
  ts = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
296
  data_dir = os.environ.get("HF_DATA_DIR", "/tmp")
297
  os.makedirs(data_dir, exist_ok=True)
@@ -299,93 +398,52 @@ def analyze(
299
  with open(out_path, "w", encoding="utf-8") as f:
300
  f.write(report_json)
301
 
302
- # Pretty sections for Gradio
303
- summary_md = []
304
- summary_md.append(f"### 企業名\n{extract.company_name or '(不明)'}")
305
- if extract.industry:
306
- summary_md.append(f"### 業種(推定/指定)\n{extract.industry}")
307
- if extract.currency:
308
- summary_md.append(f"### 通貨\n{extract.currency}")
309
- if extract.fiscal_year_end:
310
- summary_md.append(f"### 決算期末\n{extract.fiscal_year_end}")
311
-
312
- summary_md.append("### 単位(検出結果)")
313
- summary_md.append(f"- ソース表記: {unit_info['source_label']} / 乗数: x{unit_info['multiplier']:,}" + ("(数値は換算済み)" if unit_info["multiplier"] != 1 else ""))
314
-
315
- summary_md.append("### 指標(主要)")
316
- summary_md.append(
317
- f"- 売上高: {ratios.get('revenue')}\n"
318
- f"- 営業利益(EBIT): {ratios.get('ebit')}\n"
319
- f"- EBITDA: {ratios.get('ebitda')}\n"
320
- f"- 当期純利益: {ratios.get('net_income')}\n"
321
- f"- 流動比率: {ratios.get('current_ratio')}\n"
322
- f"- 当座比率: {ratios.get('quick_ratio')}\n"
323
- f"- D/Eレシオ: {ratios.get('debt_to_equity')}\n"
324
- f"- インタレストカバレッジ: {ratios.get('interest_coverage')}\n"
325
- f"- 売上成長率: {ratios.get('revenue_growth_pct')}"
326
- )
327
-
328
- if "credit" in decisions:
329
- c = decisions["credit"]
330
- summary_md.append("### 与信判断(提案)")
331
- summary_md.append(
332
- f"- 与信ランク: **{c['rating']}**(スコア {c['risk_score']}/100)\n"
333
- f"- 取引サイト: **{c['site_days']}日**\n"
334
- f"- 取引可能上限: **{c['transaction_limit_display']}**\n"
335
- f"- 見直しタイミング: **{c['review_cycle']}**"
336
- )
337
-
338
- if "loan" in decisions:
339
- l = decisions["loan"]
340
- summary_md.append("### 融資判断(提案)")
341
- summary_md.append(
342
- f"- 融資上限額(概算): **{l['max_principal_display']}**\n"
343
- f"- 期間案: **{l['term_years']}年**\n"
344
- f"- 参考金利: **{l['interest_rate_pct']}%**\n"
345
- f"- 目標DSCR: **{l['target_dscr']}**"
346
- )
347
-
348
- if "investment" in decisions:
349
- inv = decisions["investment"]
350
- summary_md.append("### 投資判断(提案)")
351
- summary_md.append(
352
- f"- 推定企業価値(EV): **{inv['ev_display']}**\n"
353
- f"- 推定時価総額: **{inv['market_cap_display']}**\n"
354
- f"- 想定投資レンジ: **{inv['recommended_check_size_display']}**\n"
355
- f"- 魅力度: **{inv['attractiveness']}/5**(成長性: {inv['growth_label']})"
356
- )
357
 
358
- return "\n\n".join(summary_md), json.loads(report_json), out_path
359
 
360
 
361
  def build_ui():
362
- with gr.Blocks(theme=gr.themes.Soft(), css="footer {visibility: hidden}") as demo:
363
- gr.Markdown("# 決算書→与信・融資・投資判断(HF+OpenAI)")
 
 
 
364
  with gr.Row():
365
- with gr.Column():
366
  files = gr.File(
367
  label="決算書ファイル(PDF/JPG/PNG, 複数可)",
368
  file_types=[".pdf", ".png", ".jpg", ".jpeg"],
369
  file_count="multiple",
370
- type="filepath", # return string paths (stable across Gradio versions)
371
  )
372
- company_name = gr.Textbox(label="会社名(任意)")
373
- industry_hint = gr.Textbox(label="業種(任意)")
374
  currency_hint = gr.Textbox(label="通貨(任意, 例: JPY, USD)")
 
 
 
 
375
  base_rate = gr.Number(label="ベース金利(%/年)", value=BASE_RATE)
376
- want_credit = gr.Checkbox(label="与信判断", value=True)
377
- want_loan = gr.Checkbox(label="融資判断", value=True)
378
- want_invest = gr.Checkbox(label="投資判断", value=True)
379
- debug = gr.Checkbox(label="デバッグモード(LLM抽出JSONを厳密化)", value=False)
 
380
  run_btn = gr.Button("分析する", variant="primary")
381
- with gr.Column():
382
- summary = gr.Markdown()
383
- report = gr.JSON(label="詳細レポート(JSON)")
384
- download = gr.File(label="レポート(JSONダウンロード)")
 
 
 
 
385
 
386
  run_btn.click(
387
  analyze,
388
- inputs=[files, company_name, industry_hint, currency_hint, base_rate, want_credit, want_loan, want_invest, debug],
389
  outputs=[summary, report, download],
390
  )
391
  return demo
@@ -393,4 +451,4 @@ def build_ui():
393
 
394
  if __name__ == "__main__":
395
  demo = build_ui()
396
- demo.launch(allowed_paths=["/tmp", "/mnt/data"]) # /tmp を既定保存先にしつつ、必要なら /mnt/data も許可
 
10
  import yaml
11
  from openai import OpenAI
12
 
13
+ from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion, MarketOutlook
14
  from finance_core import (
15
  compute_ratios,
16
  credit_decision,
 
23
  upload_file_to_openai,
24
  extract_financials_from_files,
25
  suggest_multiples_with_llm,
26
+ suggest_market_outlook_with_llm,
27
  )
28
 
29
  HF_SPACE = os.environ.get("HF_SPACE_NAME", "hf-credit-loan-investment-app")
 
108
  pass
109
  raise ValueError(f"Unsupported file input type: {type(f)}")
110
 
111
+ # --- 単位検出&換算(PDFの「単位:百万円」等を検出) ---
112
  def _concat_pdf_text(paths: List[str], max_chars: int = 180_000) -> str:
113
  try:
114
  from pypdf import PdfReader
 
133
  return "\n\n".join(out)[:max_chars]
134
 
135
  def detect_unit_multiplier_from_paths(paths: List[str]) -> Tuple[float, str]:
 
 
 
 
 
 
136
  text = _concat_pdf_text(paths)
137
  if not text:
138
  return 1.0, "不明"
139
 
140
  lower = text.lower()
141
 
142
+ # 日本語パターン
143
  if re.search(r"単位[::]\s*百万円", text) or re.search(r"(百万円)", text):
144
  return 1_000_000.0, "百万円"
145
  if re.search(r"単位[::]\s*千円", text) or re.search(r"(千円)", text):
 
155
  if re.search(r"in\s+thousands\s+of\s+(yen|jpy|usd|dollars?)", lower) or re.search(r"\b(jpy|¥|\$|usd)\s*\(\s*thousands?\s*\)", lower):
156
  return 1_000.0, "thousands"
157
 
 
158
  if re.search(r"百万円", text):
159
  return 1_000_000.0, "百万円"
160
 
 
168
  ]
169
 
170
  def scale_extract_inplace(extract: FinancialExtract, multiplier: float) -> None:
 
171
  if not multiplier or multiplier == 1:
172
  return
173
  for period in extract.periods:
 
179
  except Exception:
180
  pass
181
 
182
+ # --- サマリ生成(見やすい要約) ---
183
+ def build_human_summary(extract: FinancialExtract, ratios: Dict[str, Any], decisions: Dict[str, Any],
184
+ unit_info: Dict[str, Any], market: Optional[MarketOutlook]) -> str:
185
+ pieces = []
186
+ pieces.append(f"### 企業名\n{extract.company_name or '(不明)'}")
187
+ if extract.industry:
188
+ pieces.append(f"### 業種(推定/指定)\n{extract.industry}")
189
+ if extract.currency:
190
+ pieces.append(f"### 通貨\n{extract.currency}")
191
+ if extract.fiscal_year_end:
192
+ pieces.append(f"### 決算期末\n{extract.fiscal_year_end}")
193
+
194
+ # 評価サマリ(トップに集約)
195
+ badge = []
196
+ if "credit" in decisions:
197
+ badge.append(f"与信: **{decisions['credit']['rating']}**")
198
+ if "investment" in decisions:
199
+ badge.append(f"投資魅力度: **{decisions['investment']['attractiveness']}/5**")
200
+ if market:
201
+ badge.append(f"市場期待: **{market.expectation_label}**({market.expected_market_cagr:.1f}%)")
202
+ if ratios.get("revenue_growth_pct"):
203
+ badge.append(f"売上成長率: **{ratios['revenue_growth_pct']}**")
204
+ pieces.append("### 評価サマリ\n" + " / ".join(badge))
205
+
206
+ # 単位
207
+ pieces.append("### 単位(検出結果)")
208
+ pieces.append(
209
+ f"- ソース表記: {unit_info['source_label']} / 乗数: x{unit_info['multiplier']:,}"
210
+ + ("(数値は換算済み)" if unit_info["multiplier"] != 1 else "")
211
+ )
212
+
213
+ # 主要指標
214
+ pieces.append("### 指標(主要)")
215
+ pieces.append(
216
+ f"- 売上高: {ratios.get('revenue')}\n"
217
+ f"- 営業利益(EBIT): {ratios.get('ebit')}\n"
218
+ f"- EBITDA: {ratios.get('ebitda')}\n"
219
+ f"- 当期純利益: {ratios.get('net_income')}\n"
220
+ f"- 流動比率: {ratios.get('current_ratio')}\n"
221
+ f"- 当座比率: {ratios.get('quick_ratio')}\n"
222
+ f"- D/Eレシオ: {ratios.get('debt_to_equity')}\n"
223
+ f"- インタレストカバレッジ: {ratios.get('interest_coverage')}\n"
224
+ f"- 売上成長率: {ratios.get('revenue_growth_pct')}"
225
+ )
226
+
227
+ # 与信
228
+ if "credit" in decisions:
229
+ c = decisions["credit"]
230
+ pieces.append("### 与信判断(提案)")
231
+ pieces.append(
232
+ f"- 与信ランク: **{c['rating']}**(スコア {c['risk_score']}/100)\n"
233
+ f"- 取引サイト: **{c['site_days']}日**\n"
234
+ f"- 取引可能上限: **{c['transaction_limit_display']}**\n"
235
+ f"- 見直しタイミング: **{c['review_cycle']}**"
236
+ )
237
+
238
+ # 融資
239
+ if "loan" in decisions:
240
+ l = decisions["loan"]
241
+ pieces.append("### 融資判断(提案)")
242
+ pieces.append(
243
+ f"- 融資上限額(概算): **{l['max_principal_display']}**\n"
244
+ f"- 期間案: **{l['term_years']}年**\n"
245
+ f"- 参考金利: **{l['interest_rate_pct']}%**\n"
246
+ f"- 目標DSCR: **{l['target_dscr']}**"
247
+ )
248
+
249
+ # 投資+市場期待
250
+ if "investment" in decisions:
251
+ inv = decisions["investment"]
252
+ pieces.append("### 投資判断(提案)")
253
+ pieces.append(
254
+ f"- 推定企業価値(EV): **{inv['ev_display']}**\n"
255
+ f"- 推定時価総額: **{inv['market_cap_display']}**\n"
256
+ f"- 想定投資レンジ: **{inv['recommended_check_size_display']}**\n"
257
+ f"- 魅力度: **{inv['attractiveness']}/5**(成長性: {inv['growth_label']})"
258
+ )
259
+ if inv.get("market_factor"):
260
+ pieces.append(f"- 市場期待補正: x{inv['market_factor']:.2f}(投資レンジへ反映)")
261
+ if market:
262
+ pieces.append("### 市場規模拡大の期待(LLM評価)")
263
+ pieces.append(
264
+ f"- 期待度: **{market.expectation_label}**(スコア {market.expectation_score}/5)\n"
265
+ f"- 想定市場CAGR(3-5年): **{market.expected_market_cagr:.1f}%**\n"
266
+ f"- 根拠要約: {market.rationale or '—'}"
267
+ )
268
+
269
+ return "\n\n".join(pieces)
270
+
271
 
272
  def analyze(
273
  files: List,
274
  company_name: str,
275
  industry_hint: str,
276
  currency_hint: str,
277
+ market_notes: str,
278
  base_rate: float,
279
  want_credit: bool,
280
  want_loan: bool,
 
290
  if not files or len(files) == 0:
291
  raise gr.Error("決算書ファイル(PDF/画像)を1つ以上アップロードしてください。")
292
 
293
+ # 1) Upload to OpenAI
294
  try:
295
  file_ids = []
296
  for f in files:
 
299
  except Exception as e:
300
  raise gr.Error(f"ファイルのアップロードに失敗しました: {e}")
301
 
302
+ # Local paths for text/unit fallback
303
  local_paths = []
304
  for f in files:
305
  if isinstance(f, (str, bytes)) or hasattr(f, "__fspath__"):
306
  local_paths.append(os.fspath(f))
307
 
308
+ # 2) Vision抽出(失敗時はテキストフォールバック)
309
  try:
 
310
  try:
311
  extract = extract_financials_from_files(
312
  client=client,
 
329
  except Exception as e:
330
  raise gr.Error(f"LLM抽出に失敗しました: {e}")
331
 
332
+ # allow overrides
333
  if company_name:
334
  extract.company_name = company_name
335
  if industry_hint:
336
  extract.industry = industry_hint
337
 
338
+ # 3) 単位検出→換算(素単位化)
339
  unit_info = {"source_label": "不明", "multiplier": 1}
340
  try:
341
  if local_paths:
 
347
  if debug:
348
  print(f"[unit-detect] warning: {e}")
349
 
350
+ # 4) 指標計算
351
  ratios = compute_ratios(extract)
352
 
353
+ # 5) 意思決定
354
  decisions: Dict[str, Any] = {}
355
  if want_credit:
356
  decisions["credit"] = credit_decision(extract, ratios, POLICIES)
357
  if want_loan:
358
  decisions["loan"] = loan_decision(extract, ratios, base_rate or BASE_RATE, POLICIES)
359
+
360
+ market_outlook: Optional[MarketOutlook] = None
361
  if want_invest:
362
  multiples: Optional[MultipleSuggestion] = None
363
  try:
 
370
  )
371
  except Exception:
372
  multiples = None
373
+ try:
374
+ market_outlook = suggest_market_outlook_with_llm(
375
+ client=client,
376
+ text_model=TEXT_MODEL,
377
+ industry=extract.industry or industry_hint or "",
378
+ market_notes=market_notes or "",
379
+ region="JP",
380
+ debug=debug,
381
+ )
382
+ except Exception:
383
+ market_outlook = None
384
 
385
+ decisions["investment"] = investment_decision(
386
+ extract, ratios, POLICIES, multiples, market_outlook=market_outlook
387
+ )
388
+
389
+ # 6) レポート構築
390
+ report = build_report_dict(extract, ratios, decisions, unit_info=unit_info, market_outlook=market_outlook)
391
  report_json = json.dumps(report, ensure_ascii=False, indent=2)
392
 
393
+ # 保存(Gradio v5は /tmp が安全)
394
  ts = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
395
  data_dir = os.environ.get("HF_DATA_DIR", "/tmp")
396
  os.makedirs(data_dir, exist_ok=True)
 
398
  with open(out_path, "w", encoding="utf-8") as f:
399
  f.write(report_json)
400
 
401
+ # 7) UI 向けサマリ
402
+ summary_md = build_human_summary(extract, ratios, decisions, unit_info, market_outlook)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
+ return summary_md, json.loads(report_json), out_path
405
 
406
 
407
  def build_ui():
408
+ with gr.Blocks(theme=gr.themes.Soft(), css="""
409
+ footer {visibility: hidden}
410
+ .badge {display:inline-block;padding:4px 8px;border-radius:9999px;background:#eef; margin-right:6px}
411
+ """) as demo:
412
+ gr.Markdown("## 決算書→与信・融資・投資判断(HF + OpenAI)")
413
  with gr.Row():
414
+ with gr.Column(scale=1):
415
  files = gr.File(
416
  label="決算書ファイル(PDF/JPG/PNG, 複数可)",
417
  file_types=[".pdf", ".png", ".jpg", ".jpeg"],
418
  file_count="multiple",
419
+ type="filepath",
420
  )
421
+ company_name = gr.Textbox(label="会社名(任意)", placeholder="例:株式会社サンプル")
422
+ industry_hint = gr.Textbox(label="業種(任意)", placeholder="例:ソフトウェア、食品、物流 等")
423
  currency_hint = gr.Textbox(label="通貨(任意, 例: JPY, USD)")
424
+ market_notes = gr.Textbox(
425
+ label="市場拡大の期待(自由記述)",
426
+ placeholder="例:生成AI関連の新製品投入、医療×SaaS拡販、EV向け部材、海外展開 等"
427
+ )
428
  base_rate = gr.Number(label="ベース金利(%/年)", value=BASE_RATE)
429
+ with gr.Row():
430
+ want_credit = gr.Checkbox(label="与信判断", value=True)
431
+ want_loan = gr.Checkbox(label="融資判断", value=True)
432
+ want_invest = gr.Checkbox(label="投資判断", value=True)
433
+ debug = gr.Checkbox(label="デバッグモード(厳格JSON/ログ)", value=False)
434
  run_btn = gr.Button("分析する", variant="primary")
435
+ with gr.Column(scale=1):
436
+ with gr.Tabs():
437
+ with gr.TabItem("概要(評価サマリ)"):
438
+ summary = gr.Markdown()
439
+ with gr.TabItem("詳細JSON"):
440
+ report = gr.JSON(label="詳細レポート(JSON)")
441
+ with gr.TabItem("ダウンロード"):
442
+ download = gr.File(label="レポート(JSON)")
443
 
444
  run_btn.click(
445
  analyze,
446
+ inputs=[files, company_name, industry_hint, currency_hint, market_notes, base_rate, want_credit, want_loan, want_invest, debug],
447
  outputs=[summary, report, download],
448
  )
449
  return demo
 
451
 
452
  if __name__ == "__main__":
453
  demo = build_ui()
454
+ demo.launch(allowed_paths=["/tmp", "/mnt/data"])
finance_core.py CHANGED
@@ -2,9 +2,9 @@
2
  from __future__ import annotations
3
  from typing import Dict, Any, Optional
4
 
5
- from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion
6
 
7
- def _fmt_currency(x: Optional[float], currency: str) -> str:
8
  if x is None:
9
  return "—"
10
  try:
@@ -199,8 +199,13 @@ def loan_decision(extract: FinancialExtract, ratios: Dict[str, Any], base_rate_p
199
  return {"max_principal": max_principal, "max_principal_display": _fmt_currency(max_principal, currency),
200
  "term_years": term_years, "interest_rate_pct": round(interest_rate_pct, 2), "target_dscr": target_dscr}
201
 
202
- def investment_decision(extract: FinancialExtract, ratios: Dict[str, Any], policies: Dict[str, Any],
203
- multiples: Optional[MultipleSuggestion]) -> Dict[str, Any]:
 
 
 
 
 
204
  latest = extract.latest()
205
  currency = extract.currency or "JPY"
206
  if not latest:
@@ -236,6 +241,7 @@ def investment_decision(extract: FinancialExtract, ratios: Dict[str, Any], polic
236
  pct = {"A": 0.15, "B": 0.12, "C": 0.08, "D": 0.04, "E": 0.0}[rating]
237
  check = market_cap * pct
238
 
 
239
  attractiveness = 1
240
  if rating == "A":
241
  attractiveness = 5 if glabel == "High" else 4
@@ -246,16 +252,33 @@ def investment_decision(extract: FinancialExtract, ratios: Dict[str, Any], polic
246
  elif rating == "D":
247
  attractiveness = 1
248
 
249
- return {"ev": ev, "ev_display": _fmt_currency(ev, currency),
250
- "market_cap": market_cap, "market_cap_display": _fmt_currency(market_cap, currency),
251
- "recommended_check_size": check, "recommended_check_size_display": _fmt_currency(check, currency),
252
- "attractiveness": attractiveness, "growth_label": glabel}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  def build_report_dict(
255
  extract: FinancialExtract,
256
  ratios: Dict[str, Any],
257
  decisions: Dict[str, Any],
258
- unit_info: Optional[Dict[str, Any]] = None, # ← 追加
 
259
  ) -> Dict[str, Any]:
260
  out = {
261
  "metadata": {
@@ -271,4 +294,6 @@ def build_report_dict(
271
  }
272
  if unit_info:
273
  out["unit_detection"] = unit_info
 
 
274
  return out
 
2
  from __future__ import annotations
3
  from typing import Dict, Any, Optional
4
 
5
+ from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion, MarketOutlook
6
 
7
+ def _fmt_currency(x, currency: str) -> str:
8
  if x is None:
9
  return "—"
10
  try:
 
199
  return {"max_principal": max_principal, "max_principal_display": _fmt_currency(max_principal, currency),
200
  "term_years": term_years, "interest_rate_pct": round(interest_rate_pct, 2), "target_dscr": target_dscr}
201
 
202
+ def investment_decision(
203
+ extract: FinancialExtract,
204
+ ratios: Dict[str, Any],
205
+ policies: Dict[str, Any],
206
+ multiples: Optional[MultipleSuggestion],
207
+ market_outlook: Optional[MarketOutlook] = None,
208
+ ) -> Dict[str, Any]:
209
  latest = extract.latest()
210
  currency = extract.currency or "JPY"
211
  if not latest:
 
241
  pct = {"A": 0.15, "B": 0.12, "C": 0.08, "D": 0.04, "E": 0.0}[rating]
242
  check = market_cap * pct
243
 
244
+ # ベース魅力度(内部健全性+成長性)
245
  attractiveness = 1
246
  if rating == "A":
247
  attractiveness = 5 if glabel == "High" else 4
 
252
  elif rating == "D":
253
  attractiveness = 1
254
 
255
+ market_factor = 1.0
256
+ if market_outlook:
257
+ # 期待スコアに応じて投資レンジを±15%調整
258
+ factor_map = {1: 0.85, 2: 0.93, 3: 1.00, 4: 1.07, 5: 1.15}
259
+ market_factor = factor_map.get(int(market_outlook.expectation_score), 1.0)
260
+ # 魅力度にも±1段階の微調整(1〜5にクランプ)
261
+ if market_outlook.expectation_score >= 4:
262
+ attractiveness = min(5, attractiveness + 1)
263
+ elif market_outlook.expectation_score <= 2:
264
+ attractiveness = max(1, attractiveness - 1)
265
+
266
+ check *= market_factor
267
+
268
+ return {
269
+ "ev": ev, "ev_display": _fmt_currency(ev, currency),
270
+ "market_cap": market_cap, "market_cap_display": _fmt_currency(market_cap, currency),
271
+ "recommended_check_size": check, "recommended_check_size_display": _fmt_currency(check, currency),
272
+ "attractiveness": attractiveness, "growth_label": glabel,
273
+ "market_factor": round(market_factor, 2)
274
+ }
275
 
276
  def build_report_dict(
277
  extract: FinancialExtract,
278
  ratios: Dict[str, Any],
279
  decisions: Dict[str, Any],
280
+ unit_info: Optional[Dict[str, Any]] = None,
281
+ market_outlook: Optional[MarketOutlook] = None,
282
  ) -> Dict[str, Any]:
283
  out = {
284
  "metadata": {
 
294
  }
295
  if unit_info:
296
  out["unit_detection"] = unit_info
297
+ if market_outlook:
298
+ out["market_outlook"] = market_outlook.model_dump()
299
  return out
llm_extract.py CHANGED
@@ -4,7 +4,7 @@ import os, json
4
  from typing import List, Optional
5
  from openai import OpenAI
6
  from pydantic import ValidationError
7
- from schemas import FinancialExtract, MultipleSuggestion
8
 
9
  VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-4o-mini")
10
  TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
@@ -77,7 +77,7 @@ def extract_financials_from_files(
77
  currency_hint: Optional[str],
78
  model: str = VISION_MODEL,
79
  debug: bool = False,
80
- local_paths: Optional[List[str]] = None, # ← フォールバック用
81
  ) -> FinancialExtract:
82
 
83
  schema = FinancialExtract.model_json_schema()
@@ -95,7 +95,7 @@ def extract_financials_from_files(
95
  if currency_hint:
96
  base_user += f"\nCurrency hint: {currency_hint}"
97
 
98
- # 1) Vision + file_id で試す(response_format 未使用)
99
  try:
100
  resp = client.responses.create(
101
  model=model,
@@ -115,7 +115,7 @@ def extract_financials_from_files(
115
  return FinancialExtract.model_validate(data)
116
 
117
  except Exception as e_vision:
118
- # 2) テキスト抽出 → TEXT_MODEL で構造化
119
  if not local_paths:
120
  raise RuntimeError(f"Vision抽出に失敗し、かつローカルPDFテキストがありません: {e_vision}")
121
 
@@ -168,3 +168,46 @@ def suggest_multiples_with_llm(client: OpenAI, text_model: str, industry: str, r
168
  if debug:
169
  raise
170
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from typing import List, Optional
5
  from openai import OpenAI
6
  from pydantic import ValidationError
7
+ from schemas import FinancialExtract, MultipleSuggestion, MarketOutlook
8
 
9
  VISION_MODEL = os.environ.get("OPENAI_VISION_MODEL", "gpt-4o-mini")
10
  TEXT_MODEL = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
 
77
  currency_hint: Optional[str],
78
  model: str = VISION_MODEL,
79
  debug: bool = False,
80
+ local_paths: Optional[List[str]] = None,
81
  ) -> FinancialExtract:
82
 
83
  schema = FinancialExtract.model_json_schema()
 
95
  if currency_hint:
96
  base_user += f"\nCurrency hint: {currency_hint}"
97
 
98
+ # 1) Vision + file_id
99
  try:
100
  resp = client.responses.create(
101
  model=model,
 
115
  return FinancialExtract.model_validate(data)
116
 
117
  except Exception as e_vision:
118
+ # 2) Fallback: PDFテキスト→TEXTモデルで構造化
119
  if not local_paths:
120
  raise RuntimeError(f"Vision抽出に失敗し、かつローカルPDFテキストがありません: {e_vision}")
121
 
 
168
  if debug:
169
  raise
170
  return None
171
+
172
+ def suggest_market_outlook_with_llm(
173
+ client: OpenAI,
174
+ text_model: str,
175
+ industry: str,
176
+ market_notes: str,
177
+ region: str = "JP",
178
+ debug: bool = False,
179
+ ) -> Optional[MarketOutlook]:
180
+ """取扱商品・新製品・展開計画などに基づく、3-5年の市場拡大期待を定性的にスコア化。"""
181
+ if not (industry or market_notes):
182
+ return None
183
+
184
+ system = "You are a market analyst. Rate market expansion expectations for the product portfolio in the next 3-5 years."
185
+ user = (
186
+ f"Industry: {industry or 'Unknown'}\nRegion: {region}\n"
187
+ f"Notes: {market_notes or 'None'}\n"
188
+ "Return STRICT JSON with keys:\n"
189
+ " expectation_label (one of: Very Low, Low, Medium, High, Very High),\n"
190
+ " expectation_score (integer 1-5),\n"
191
+ " expected_market_cagr (float, %),\n"
192
+ " rationale (<=60 words).\n"
193
+ "No prose outside JSON."
194
+ )
195
+
196
+ try:
197
+ resp = client.responses.create(
198
+ model=text_model,
199
+ input=[
200
+ {"role": "system", "content": [{"type": "input_text", "text": system}]},
201
+ {"role": "user", "content": [{"type": "input_text", "text": user}]},
202
+ ],
203
+ max_output_tokens=250,
204
+ )
205
+ raw = _safe_output_text(resp)
206
+ if not raw:
207
+ return None
208
+ data = _json_loads_strict(raw)
209
+ return MarketOutlook.model_validate(data)
210
+ except Exception:
211
+ if debug:
212
+ raise
213
+ return None
schemas.py CHANGED
@@ -44,3 +44,9 @@ class FinancialExtract(BaseModel):
44
  class MultipleSuggestion(BaseModel):
45
  revenue_multiple: float = 1.5
46
  ebitda_multiple: float = 8.0
 
 
 
 
 
 
 
44
  class MultipleSuggestion(BaseModel):
45
  revenue_multiple: float = 1.5
46
  ebitda_multiple: float = 8.0
47
+
48
+ class MarketOutlook(BaseModel):
49
+ expectation_label: str # Very Low / Low / Medium / High / Very High
50
+ expectation_score: int # 1..5
51
+ expected_market_cagr: float # percentage
52
+ rationale: Optional[str] = None