File size: 9,225 Bytes
fcc7d9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import gradio as gr
import json
import torch
from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer
import os
import sys
import traceback

# --- 1. モデルとパイプラインの初期化 ---
# NOTE: モデルの初期化はアプリケーション起動時に一度だけ実行し、グローバル変数に格納します。

# モデル初期化のヘルパー関数
def safe_initialize_pipeline(task, model_name, fallback_model_name=None, is_zero_shot=False):
    """Hugging Faceパイプラインを安全に初期化する関数"""
    try:
        if is_zero_shot:
            # Zero-Shot Classification専用の初期化
            classifier = pipeline(
                task,
                model=model_name, 
                tokenizer=model_name,
                device=0 if torch.cuda.is_available() else -1
            )
        else:
            # 汎用パイプラインの初期化
            classifier = pipeline(
                task,
                model=model_name,
                tokenizer=model_name,
            )
        print(f"✅ {task} パイプラインをモデル: {model_name} でロードしました。")
        return classifier
    except Exception as e:
        print(f"❌ {task} の初期化に失敗 (モデル: {model_name})。代替モデルを試行します。")
        # エラー発生時の代替処理
        if fallback_model_name:
             try:
                # 汎用分類モデルをロード
                model = AutoModelForSequenceClassification.from_pretrained(fallback_model_name)
                tokenizer = AutoTokenizer.from_pretrained(fallback_model_name)
                # 分類タスクとして再定義
                classifier = pipeline(
                    "text-classification",
                    model=model,
                    tokenizer=tokenizer,
                    id2label={0: "Negative", 1: "Positive"}
                )
                print(f"✅ {task} パイプラインを代替モデル: {fallback_model_name} でロードしました。")
                return classifier
             except Exception as fallback_e:
                print(f"致命的なエラー: 代替モデルのロードにも失敗しました。")
                print(f"詳細: {fallback_e}")
                sys.exit(1)
        else:
             print(f"致命的なエラー: フォールバックモデルが定義されていません。")
             sys.exit(1)


# モデル定義
TONE_MODEL_NAME = "cl-tohoku/bert-base-japanese-whole-word-masking"
STAR_MODEL_NAME = "izumi-lab/bert-base-japanese-v2"
FALLBACK_MODEL_NAME = TONE_MODEL_NAME

# モデルの初期化 (アプリケーション開始時に一度だけ実行)
print("--- モデル初期化開始 ---")
TONE_CLASSIFIER = safe_initialize_pipeline("sentiment-analysis", TONE_MODEL_NAME, FALLBACK_MODEL_NAME)
STAR_CLASSIFIER = safe_initialize_pipeline("zero-shot-classification", STAR_MODEL_NAME, FALLBACK_MODEL_NAME, is_zero_shot=True)
print("--- モデル初期化完了 ---")


# --- 2. 分析ロジック関数 ---

def analyze_tone(text, classifier):
    """テキストの感情・トーンを分析し、熱意スコアを算出する。"""
    try:
        results = classifier(text)
        result = results[0]
        # 'Positive' のスコアを抽出 (モデルの出力に依存)
        sentiment_score = result.get('score', 0.5) 
        enthusiasm_score = round(sentiment_score * 100, 2)
        return {
            "enthusiasm_score": enthusiasm_score
        }
    except Exception as e:
        print(f"感情・トーン分析中にエラーが発生しました: {e}")
        return {"enthusiasm_score": 50.0} # エラー時は中間スコアを返す

def check_star_suitability(self_pr_text, classifier):
    """STAR法適合度をチェックし、不足要素を特定する。"""
    try:
        candidate_labels = ["状況 (Situation)", "課題 (Task)", "行動 (Action)", "結果 (Result)"]
        star_results = classifier(
            self_pr_text, 
            candidate_labels, 
            multi_label=True
        )
        # スコアとラベルを辞書にまとめる
        star_scores = {label: round(score * 100, 2) for label, score in zip(star_results['labels'], star_results['scores'])}
        
        # 適合度と不足要素の判定ロジック
        insufficiency_threshold = 70.0 
        missing_elements = []
        total_score = 0
        for label, score in star_scores.items():
            total_score += score
            if score < insufficiency_threshold:
                missing_elements.append(label)

        # 総合適合度スコア (4要素の平均スコア)
        suitability_score = round(total_score / len(candidate_labels), 2)
        
        # JSON出力データ
        analysis_reason = {
            "適合度スコア": suitability_score,
            "論理的": suitability_score >= 75.0,
            "不足要素": missing_elements,
            "詳細スコア": star_scores
        }

        return {
            "suitability_score": suitability_score,
            "analysis_json": analysis_reason
        }
    except Exception as e:
        print(f"STAR法適合度チェック中にエラーが発生しました: {e}")
        print(traceback.format_exc())
        return {
            "suitability_score": 50.0,
            "analysis_json": {
                "適合度スコア": 50.0,
                "論理的": False,
                "不足要素": ["エラーのため評価不可"],
                "詳細スコア": {"Error": 50.0}
            }
        }


# --- 3. Gradioインターフェース用メイン処理 ---

def run_es_analysis(es_text, self_pr_text):
    """
    Gradioの入力を受け取り、AI分析を実行して結果を返す関数。
    """
    if not es_text or not self_pr_text:
        return "入力テキストが不足しています。", 0.0, 0.0, json.dumps({"Error": "入力データ不足"})

    # 1. 感情・トーン分析
    tone_analysis = analyze_tone(es_text, TONE_CLASSIFIER)
    enthusiasm_score = tone_analysis['enthusiasm_score']
    
    # 2. STAR法適合度チェック
    star_analysis = check_star_suitability(self_pr_text, STAR_CLASSIFIER)
    star_score = star_analysis['suitability_score']
    analysis_json_data = star_analysis['analysis_json']
    
    # 3. 最終スコアの統合
    # 感情スコア 50%, STARスコア 50% の重み付け
    final_potential_score = (enthusiasm_score * 0.5) + (star_score * 0.5)
    
    # 結果の整形
    final_score_str = f"統合スコア: {round(final_potential_score, 2)} / 100"
    
    # JSON出力を整形して文字列化
    json_output = json.dumps(analysis_json_data, indent=4, ensure_ascii=False)
    
    return final_score_str, enthusiasm_score, star_score, json_output

# --- 4. Gradioインターフェースの定義 ---

# テストデータ
DEFAULT_ES_TEXT = """
貴社が推進する「グローバル×ローカル」のデジタル変革戦略に深く共感し、志望いたしました。私は大学時代、学園祭の実行委員長として、従来の集客方法に課題を感じ、SNSを活用した新しいプロモーション戦略を立案・実行しました。これにより、来場者数を前年比150%に増加させるという明確な結果を出しました。この経験から学んだ、現状を打破するための課題設定力と、関係者を巻き込む実行力を貴社で活かし、世界中の顧客に感動を届けるシステムを構築したいと強く願っています。
"""
DEFAULT_SELF_PR = """
私は大学時代、学園祭の実行委員長として、従来の集客方法に課題を感じ、SNSを活用した新しいプロモーション戦略を立案・実行しました。これにより、来場者数を前年比150%に増加させるという明確な結果を出しました。課題はSNSチームのメンバーのモチベーション管理でしたが、個別の面談を通じて主体的な関与を引き出し、最終的に全員が目標達成に貢献しました。
"""


# Gradioインターフェースの構築
iface = gr.Interface(
    fn=run_es_analysis,
    title="新卒ES AI評価デモンストレーション",
    description="エントリーシートの内容を、感情・トーン、およびSTAR法に基づいた論理構造の観点から自動評価します。",
    inputs=[
        gr.Textbox(label="ES全体テキスト (志望動機など)", lines=10, value=DEFAULT_ES_TEXT),
        gr.Textbox(label="自己PRセクション (STAR法チェック対象)", lines=8, value=DEFAULT_SELF_PR)
    ],
    outputs=[
        gr.Textbox(label="🌟 最終ポテンシャル評価スコア (統合)", type="text"),
        gr.Number(label="熱意・トーン分析スコア (50%)", precision=2),
        gr.Number(label="STAR法 総合適合度スコア (50%)", precision=2),
        gr.JSON(label="論理構造の詳細分析 (JSON出力)")
    ],
    allow_flagging="never",
    theme="soft"
)

# if __name__ == "__main__":
#     # Gradioアプリの実行 (ローカル実行用)
#     iface.launch()