Corin1998's picture
Update app.py
87d8093 verified
import os
import io
import json
import hashlib
import gradio as gr
from pipelines.openai_ingest import (
extract_text_with_openai,
structure_with_openai,
summarize_with_openai,
)
from pipelines.parsing import normalize_resume
from pipelines.merge import merge_normalized_records
from pipelines.skills import extract_skills
from pipelines.anonymize import anonymize_text, render_anonymized_pdf
from pipelines.scoring import compute_quality_score
from pipelines.storage import persist_to_hf
from pipelines.utils import detect_filetype, load_doc_text
APP_TITLE = "候補者インテーク & レジュメ標準化(OpenAI版)"
def process_resumes(files, candidate_id: str, additional_notes: str = ""):
"""
files: gr.Files(type="filepath") から渡る「ファイルパスのリスト」
返り値は Gradio の API スキーマ生成エラーを避けるため、**全て文字列 or ファイル**に統一する。
"""
if not files:
raise gr.Error("少なくとも1ファイルをアップロードしてください。")
partial_records = []
raw_texts = []
# Files(type="filepath") → files はパスのリスト
for path in files:
try:
with open(path, "rb") as rf:
raw_bytes = rf.read()
except Exception as e:
raise gr.Error(f"ファイル読み込みに失敗しました: {path}: {e}")
fname = os.path.basename(path)
filetype = detect_filetype(fname, raw_bytes)
# 1) テキスト抽出:画像/PDFはOpenAI Vision OCR、docx/txtは生文面+OpenAI整形
if filetype in {"pdf", "image"}:
text = extract_text_with_openai(raw_bytes, filename=fname, filetype=filetype)
else:
base_text = load_doc_text(filetype, raw_bytes)
# 生テキストをそのままOpenAIへ渡し、軽く整形した全文を返す
text = extract_text_with_openai(base_text.encode("utf-8"), filename=fname, filetype="txt")
raw_texts.append({"filename": fname, "text": text})
# 2) OpenAIでセクション構造化 → ルールベース正規化
structured = structure_with_openai(text)
normalized = normalize_resume({
"work_experience": structured.get("work_experience_raw", ""),
"education": structured.get("education_raw", ""),
"certifications": structured.get("certifications_raw", ""),
"skills": ", ".join(structured.get("skills_list", [])),
})
partial_records.append({
"source": fname,
"text": text,
"structured": structured,
"normalized": normalized,
})
# 3) 統合(複数ファイル→1候補者)
merged = merge_normalized_records([r["normalized"] for r in partial_records])
# 4) スキル抽出(辞書/正規表現)
merged_text = "\n\n".join([r["text"] for r in partial_records])
skills = extract_skills(merged_text, {
"work_experience": merged.get("raw_sections", {}).get("work_experience", ""),
"education": merged.get("raw_sections", {}).get("education", ""),
"certifications": merged.get("raw_sections", {}).get("certifications", ""),
"skills": ", ".join(merged.get("skills", [])),
})
# 5) 匿名化
anonymized_text, anon_map = anonymize_text(merged_text)
anon_pdf_bytes = render_anonymized_pdf(anonymized_text)
# 6) 品質スコア
score = compute_quality_score(merged_text, merged)
# 7) 要約(300/100/1文)
summaries = summarize_with_openai(merged_text)
# 8) 構造化出力(文字列化して返す)
result_json = {
"candidate_id": candidate_id or hashlib.sha256(merged_text.encode("utf-8")).hexdigest()[:16],
"files": [os.path.basename(p) for p in files],
"merged": merged,
"skills": skills,
"quality_score": score,
"summaries": summaries,
"anonymization_map": anon_map,
"notes": additional_notes,
}
# 9) HF Datasets 保存
dataset_repo = os.environ.get("DATASET_REPO")
commit_info = None
if dataset_repo:
file_hash = result_json["candidate_id"]
commit_info = persist_to_hf(
dataset_repo=dataset_repo,
record=result_json,
anon_pdf_bytes=anon_pdf_bytes,
parquet_path=f"candidates/{file_hash}.parquet",
json_path=f"candidates/{file_hash}.json",
pdf_path=f"candidates/{file_hash}.anon.pdf",
)
# gr.File 用の (filename, bytes) タプル
anon_pdf = (result_json["candidate_id"] + ".anon.pdf", anon_pdf_bytes)
# 返り値は**すべて文字列**(と1つのファイル)に統一
return (
json.dumps(result_json, ensure_ascii=False, indent=2),
json.dumps(skills, ensure_ascii=False, indent=2),
json.dumps(score, ensure_ascii=False, indent=2),
summaries.get("300chars", ""),
summaries.get("100chars", ""),
summaries.get("onesent", ""),
anon_pdf,
json.dumps(commit_info or {"status": "skipped (DATASET_REPO not set)"}, ensure_ascii=False, indent=2),
)
with gr.Blocks(title=APP_TITLE) as demo:
gr.Markdown(f"# {APP_TITLE}\n複数ファイルを統合→OpenAIで読み込み/構造化/要約→匿名化→Datasets保存")
with gr.Row():
in_files = gr.Files(
label="レジュメ類 (PDF/画像/Word/テキスト) 複数可",
file_count="multiple",
file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp", ".docx", ".txt"],
type="filepath", # ← 重要: 'file' は無効。'filepath' か 'binary'
)
candidate_id = gr.Textbox(label="候補者ID(任意。未入力なら自動生成)")
notes = gr.Textbox(label="補足メモ(任意)", lines=3)
run_btn = gr.Button("実行")
with gr.Tab("構造化JSON"):
out_json = gr.Code(label="統合出力 (JSON)")
with gr.Tab("抽出スキル"):
# gr.JSON は API スキーマ生成で例外が出るケースがあるため回避し、文字列(JSON)を表示
out_skills = gr.Code(label="スキル一覧(JSON表示)")
with gr.Tab("品質スコア"):
out_score = gr.Code(label="品質評価(JSON)")
with gr.Tab("要約 (300/100/1文)"):
out_sum_300 = gr.Textbox(label="300字要約")
out_sum_100 = gr.Textbox(label="100字要約")
out_sum_1 = gr.Textbox(label="1文要約")
with gr.Tab("匿名PDF"):
out_pdf = gr.File(label="匿名PDFダウンロード")
with gr.Tab("Datasets 保存ログ"):
out_commit = gr.Code(label="コミット情報")
run_btn.click(
process_resumes,
inputs=[in_files, candidate_id, notes],
outputs=[out_json, out_skills, out_score, out_sum_300, out_sum_100, out_sum_1, out_pdf, out_commit],
)
if __name__ == "__main__":
# Spaces 等で localhost 非公開環境を考慮
demo.launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", "7860")),
share=True,
show_error=True,
analytics_enabled=False,
)