# 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()