Corin1998's picture
Update app.py
516889a verified
# app.py
from typing import List, Dict, Any, Optional
import re
import json
import gradio as gr
from transformers import pipeline
# =========================
# モデル定義(Hugging Face)
# =========================
# 日本語感情分析モデル
SENTIMENT_MODEL_NAME = "koheiduck/bert-japanese-finetuned-sentiment"
# 多言語ゼロショット分類モデル(STAR分類・トーン評価用)
NLI_MODEL_NAME = "joeddav/xlm-roberta-large-xnli"
# 必要に応じて device=0 (GPU) に変更してください
sentiment_classifier = pipeline(
"sentiment-analysis",
model=SENTIMENT_MODEL_NAME,
tokenizer=SENTIMENT_MODEL_NAME,
)
zero_shot_classifier = pipeline(
"zero-shot-classification",
model=NLI_MODEL_NAME,
tokenizer=NLI_MODEL_NAME,
)
# =========================
# ユーティリティ
# =========================
_SENT_SPLIT_RE = re.compile(r"[。!?\n]+")
def split_sentences(text: str) -> List[str]:
"""日本語の文をざっくり分割する簡易関数"""
sentences = [s.strip() for s in _SENT_SPLIT_RE.split(text) if s.strip()]
return sentences
# =========================
# 感情・トーン分析
# =========================
def analyze_sentiment(text: str) -> Dict[str, Any]:
"""
日本語テキストのネガポジ分析。
出力例:
{
"label": "POSITIVE",
"score": 0.98
}
"""
if not text.strip():
return {"label": "NEUTRAL", "score": 0.0}
result = sentiment_classifier(text)[0]
return {
"label": result["label"],
"score": float(result["score"]),
}
def analyze_tone(text: str) -> Dict[str, Any]:
"""
ゼロショット分類で「熱意・主体性・一貫性・論理性・協調性」などをスコアリング。
multi_label=True なので、1文が複数のトーンを兼ねることも許容。
出力例:
{
"labels": {
"熱意": 0.92,
"主体性": 0.81,
"一貫性": 0.55,
"論理性": 0.73,
"協調性": 0.40
}
}
"""
if not text.strip():
return {"labels": {}}
tone_labels = ["熱意", "主体性", "一貫性", "論理性", "協調性"]
result = zero_shot_classifier(
text,
candidate_labels=tone_labels,
multi_label=True,
)
label_scores = {
label: float(score)
for label, score in zip(result["labels"], result["scores"])
}
ordered = {label: label_scores.get(label, 0.0) for label in tone_labels}
return {"labels": ordered}
# =========================
# STAR 構造分析
# =========================
STAR_LABELS_JP = [
"Situation(状況)",
"Task(課題)",
"Action(行動)",
"Result(結果)",
"Other(その他)",
]
STAR_KEY_MAP = {
"Situation(状況)": "S",
"Task(課題)": "T",
"Action(行動)": "A",
"Result(結果)": "R",
"Other(その他)": "O",
}
def classify_sentence_star(sentence: str) -> Dict[str, Any]:
"""
1文を STAR のどれに近いかゼロショット分類する。
multi_label=False とし、最も近いラベルのみ採用。
出力例:
{
"sentence": "...",
"star_label": "S",
"raw_label": "Situation(状況)",
"score": 0.87
}
"""
if not sentence.strip():
return {
"sentence": sentence,
"star_label": "O",
"raw_label": "Other(その他)",
"score": 0.0,
}
result = zero_shot_classifier(
sentence,
candidate_labels=STAR_LABELS_JP,
multi_label=False,
)
raw_label = result["labels"][0]
score = float(result["scores"][0])
star_label = STAR_KEY_MAP.get(raw_label, "O")
return {
"sentence": sentence,
"star_label": star_label,
"raw_label": raw_label,
"score": score,
}
def analyze_star_structure(text: str) -> Dict[str, Any]:
"""
自己PRテキストの STAR 構造(S/T/A/R がどの程度含まれているか)を分析。
出力例:
{
"coverage": {
"S": true,
"T": true,
"A": true,
"R": false
},
"star_score": 0.75,
"per_sentence": [...],
"missing_elements": ["R"],
"comment": "結果(Result)の記述が弱いため、成果をより具体的に書くと良いです。"
}
"""
sentences = split_sentences(text)
if not sentences:
return {
"coverage": {k: False for k in ["S", "T", "A", "R"]},
"star_score": 0.0,
"per_sentence": [],
"missing_elements": ["S", "T", "A", "R"],
"comment": "文章が空か極端に短いため、STAR 構造の判定ができません。",
}
per_sentence_results = [
classify_sentence_star(s) for s in sentences
]
coverage = {k: False for k in ["S", "T", "A", "R"]}
for r in per_sentence_results:
key = r["star_label"]
if key in coverage:
coverage[key] = True
missing = [k for k, v in coverage.items() if not v]
star_score = sum(1 for v in coverage.values() if v) / 4.0
# コメント生成(シンプルなヒューリスティック)
if not missing:
comment = "STAR の各要素(状況・課題・行動・結果)が一通り含まれています。構成としてバランスは良好です。"
else:
parts = []
mapping_jp = {"S": "状況(Situation)", "T": "課題(Task)", "A": "行動(Action)", "R": "結果(Result)"}
for m in missing:
parts.append(mapping_jp[m])
missing_jp = "・".join(parts)
comment = (
f"{missing_jp} の要素が弱い/不足しています。"
"不足している要素を具体的に書き足すと、より論理的で説得力のある自己PRになります。"
)
return {
"coverage": coverage,
"star_score": star_score,
"per_sentence": per_sentence_results,
"missing_elements": missing,
"comment": comment,
}
# =========================
# ES 全体の評価インターフェース
# =========================
def evaluate_entry_sheet(
self_pr: str,
motivation: Optional[str] = None,
) -> Dict[str, Any]:
"""
ES の自己PR(必須)、志望動機(任意)を入力として、
感情・トーン・STAR 構造をまとめて評価するインターフェース。
"""
texts = [self_pr]
if motivation:
texts.append(motivation)
full_text = "\n".join([t for t in texts if t])
sentiment = analyze_sentiment(full_text)
tone = analyze_tone(full_text)
star = analyze_star_structure(self_pr) # STAR は主に自己PRに対して実行
return {
"input": {
"self_pr": self_pr,
"motivation": motivation,
},
"sentiment": sentiment,
"tone": tone,
"star": star,
}
# =========================
# Gradio 用ラッパー
# =========================
def evaluate_es(self_pr: str, motivation: str):
"""
Gradio から呼ばれるラッパー関数。
self_pr: 自己PR
motivation: 志望動機(空でもOK)
"""
if not self_pr.strip() and not motivation.strip():
return {
"error": "自己PRか志望動機のいずれかは入力してください。"
}
result = evaluate_entry_sheet(
self_pr=self_pr,
motivation=motivation if motivation.strip() else None,
)
return result
with gr.Blocks() as demo:
gr.Markdown(
"""
# 新卒エントリーシート AI評価デモ(Hugging Face 上で完結)
- 日本語感情分析モデル + 多言語ゼロショット分類モデルを用いて、
ES の **感情・トーン** と **STAR 構造(Situation / Task / Action / Result)** を自動評価します。
- 出力は JSON 形式なので、そのまま既存パイプラインに組み込み可能です。
"""
)
with gr.Row():
self_pr_input = gr.Textbox(
label="自己PR(必須)",
lines=12,
placeholder="自己PRを入力してください。",
)
motivation_input = gr.Textbox(
label="志望動機(任意)",
lines=12,
placeholder="志望動機があれば入力してください。(空でも可)",
)
btn = gr.Button("AIで評価する")
output_json = gr.JSON(
label="評価結果(JSON)",
value={},
)
btn.click(
fn=evaluate_es,
inputs=[self_pr_input, motivation_input],
outputs=output_json,
)
if __name__ == "__main__":
demo.launch()