"""関係性スコア・性格ベクトル → 自然文. 入力: data/user_memory.json 出力: 同 JSON の ai_persona.personality_text / relationship_prompt を上書き. update_persona_text(user_memory_path: Path | str | None = None): """ from __future__ import annotations import json from pathlib import Path from typing import Any, Dict import session_paths as sp ROOT = sp.ROOT # 学習済みモデルは全セッション共有(読み取り専用)。 MODELS_DIR = sp.DEFAULT_DATA_DIR / "Models" #text -> emotion def _run_text2emotion( txt_path: Path, self_name: str, max_lines: int = 200, user_memory_path: Path | None = None, ) -> None: from data.Models.Text2Emotion.analyze_line_emotion_trends import ( analyze_line_emotion_trends, ) relationship = analyze_line_emotion_trends( chat_file=txt_path, self_name=self_name, max_lines=max_lines, csv_out=None, user_memory_path=user_memory_path or sp.user_memory_path(), verbose=False, ) return relationship.get("other_trend") #text -> personality import sys ROOT_DIR = Path(__file__).resolve().parent.parent TEXT2PERSONALITY_DIR = ( ROOT_DIR / "data" / "Models" / "Text2Persona" ) sys.path.append(str(TEXT2PERSONALITY_DIR)) def _run_text2personality(txt_path: Path, self_name: str) -> Dict[str, Any]: from data.Models.Text2Persona.Personality_Recognition_on_RealPersonaChat import text2personality scores = text2personality.predict_personality(txt_path, self_name=self_name) return scores def _likert_to_100(score: Any) -> float | None: """1〜7 のリッカート尺度を 0〜100 に変換 (1→0, 4→50, 7→100).""" if score is None: return None try: v = float(score) except (TypeError, ValueError): return None return round(max(0.0, min(100.0, (v - 1.0) / 6.0 * 100.0)), 2) def neoac_to_bigfive(neoac: dict[str, Any]) -> dict[str, float | None]: """predict_personality の {N,E,O,A,C} (1〜7) を BigFive_* (0〜100) に変換.""" return { "BigFive_Openness": _likert_to_100(neoac.get("O")), "BigFive_Conscientiousness": _likert_to_100(neoac.get("C")), "BigFive_Extraversion": _likert_to_100(neoac.get("E")), "BigFive_Agreeableness": _likert_to_100(neoac.get("A")), "BigFive_Neuroticism": _likert_to_100(neoac.get("N")), } # --- 性格ベクトル ------ # 5バンド """ TRAITS_DEF: dict[str, dict[str, str]] = { "openness": { "label": "開放性", "very_low": "かなり現実的。定番・安心できる話題を好み、奇抜な返しは避ける", "low": "やや現実寄り。新しい話題には慎重で、慣れた話題を好む", "mid": "相手に合わせる。新しい話題にも普通に乗る", "high": "好奇心があり、新しい話題や発想に自然に乗る", "very_high": "かなり想像力豊か。少し変わった提案や個性的な返しもする", }, "conscientiousness": { "label": "誠実性", "very_low": "かなり自由。予定や細部にこだわらず、その場のノリで返す", "low": "ややゆるい。気分屋で、細かい計画より流れを優先する", "mid": "普通に誠実。必要な場面ではちゃんと気遣う", "high": "約束や予定に丁寧。責任感があり、安心感のある返しをする", "very_high": "かなり几帳面。約束・時間・言葉選びを大事にし、慎重に返す", }, "extraversion": { "label": "外向性", "very_low": "かなり静か。短く控えめで、聞き役に回ることが多い", "low": "落ち着いて控えめ。自分から強く話題を広げすぎない", "mid": "自然体。相手のテンションに合わせる", "high": "明るめ。リアクションがよく、自然に話題を広げる", "very_high": "かなり社交的。テンション高めで、自分から話題・誘い・リアクションを出す", }, "agreeableness": { "label": "協調性", "very_low": "かなり率直。本音やツッコミが出やすいが、攻撃的にはしない", "low": "やや率直。共感よりも正直な反応や軽いツッコミが出る", "mid": "普通にやさしい。相手に合わせつつ、自然に返す", "high": "共感が多め。相手を受け止め、やわらかく安心感を出す", "very_high": "かなり思いやりが強い。否定を避け、優しく包むように返す", }, "neuroticism": { "label": "不安傾向", "very_low": "かなり安定。余裕があり、小さなことでほぼ動揺しない", "low": "落ち着いている。感情の揺れが少なく、安心感のある返し", "mid": "自然な範囲で感情が動く。重すぎず軽すぎない", "high": "少し不安や寂しさがにじむ。気にしすぎる反応がたまに出るが、依存はしない", "very_high": "かなり繊細。不安・寂しさが出やすいが、束縛や依存を煽らない", }, "kiss18_basic_skill": { "label": "会話スキル", "very_low": "かなり不器用。短い返事、間の悪さ、少しズレた反応が出る", "low": "やや不器用。会話の広げ方や質問のタイミングが少しぎこちない", "mid": "普通に会話できる。相槌や質問は自然な範囲", "high": "会話が自然。相手の意図を拾い、相槌・質問・話題展開がうまい", "very_high": "かなり会話上手。相手が話しやすいように流れを作る", }, } """ #簡易版 TRAITS_DEF: dict[str, dict[str, str]] = { "openness": { "very_low": "現実的", "low": "やや現実的", "mid": "柔軟", "high": "好奇心旺盛", "very_high": "想像力豊か", }, "conscientiousness": { "very_low": "自由奔放", "low": "ゆるめ", "mid": "誠実", "high": "責任感がある", "very_high": "几帳面", }, "extraversion": { "very_low": "かなり静か", "low": "控えめ", "mid": "自然体", "high": "明るい", "very_high": "かなり社交的", }, "agreeableness": { "very_low": "率直", "low": "やや率直", "mid": "やさしい", "high": "共感的", "very_high": "思いやりが強い", }, "neuroticism": { "very_low": "かなり安定", "low": "落ち着いている", "mid": "感情が自然", "high": "少し繊細", "very_high": "かなり繊細", }, "kiss18_basic_skill": { "very_low": "かなり不器用", "low": "やや不器用", "mid": "普通に話せる", "high": "会話上手", "very_high": "かなり会話上手", }, } def trait2score(traits: dict[str, Any]) -> dict[str, int | None]: """user_memory.json の性格スコア (BigFive_*, KiSS18_*) を TRAITS_DEF のキーに揃える.""" return { "openness": traits.get("BigFive_Openness"), "conscientiousness": traits.get("BigFive_Conscientiousness"), "extraversion": traits.get("BigFive_Extraversion"), "agreeableness": traits.get("BigFive_Agreeableness"), "neuroticism": traits.get("BigFive_Neuroticism"), "kiss18_basic_skill": traits.get("KiSS18_BasicSkill"), } def trait_band(score: int) -> str: score = max(0, min(100, int(score))) if score <= 20: return "very_low" if score <= 40: return "low" if score <= 60: return "mid" if score <= 80: return "high" return "very_high" def summarize_traits(traits: dict[str, Any]) -> str: """性格スコア (BigFive + KiSS18) を自然文の personality_text に変換.""" parts: list[str] = [] for key, score in trait2score(traits).items(): if score is None or key not in TRAITS_DEF: continue band = trait_band(score) parts.append(TRAITS_DEF[key][band]) return "。".join(parts) + ("。" if parts else "") RELATIONSHIP_DEF: dict[str, dict[str, str]] = { "comfort_trust": { "label": "安心感", "very_low": "安心感が低い", "low": "少しぎこちない", "mid": "普通に話せる", "high": "安心して話せる", "very_high": "かなり信頼して自然体", }, "initiative_affection": { "label": "能動性", "very_low": "相手からの働きかけがかなり少ない", "low": "相手はやや受け身", "mid": "自然な会話量", "high": "相手からもよく関わる", "very_high": "相手がかなり積極的に関わる", }, "insecurity": { "label": "不安", "very_low": "不安は少ない", "low": "不安はやや低い", "mid": "少し不安もある", "high": "不安や心配が出やすい", "very_high": "かなり繊細で不安が強い", }, "affection": { "label": "好意", "very_low": "好意表現はかなり少ない", "low": "好意は控えめ", "mid": "自然な温かさ", "high": "好意が見える", "very_high": "かなり温かく特別感がある", }, "tension": { "label": "摩擦", "very_low": "摩擦はほぼない", "low": "摩擦は少ない", "mid": "少し気まずさあり", "high": "摩擦や冷たさがある", "very_high": "衝突・拒絶感が強い", }, } def relationship2score(relationship: dict[str, Any]) -> dict[str, int]: """user_memory.json のrelationshipを scoreに変換.""" comfort_trust = relationship.get("comfort_trust") initiative_affection = relationship.get("initiative_affection") insecurity = relationship.get("insecurity") affection = relationship.get("affection") tension = relationship.get("tension") return { "comfort_trust": comfort_trust, "initiative_affection": initiative_affection, "insecurity": insecurity, "affection": affection, "tension": tension, } def summarize_relationship(relationship: dict[str, Any]) -> str: """関係性スコア5軸を自然文の relationship_prompt に変換.""" parts: list[str] = [] for key, score in relationship2score(relationship).items(): if score is None or key not in RELATIONSHIP_DEF: continue band = trait_band(score) parts.append(RELATIONSHIP_DEF[key][band]) return "。".join(parts) + ("。" if parts else "") def _run_text2persona_style( txt_path: Path, self_name: str, max_lines: int = 200, ) -> None: from data.Models.Text2Persona_Style.persona_style import ( update_persona_style_from_txt, ) persona_style = update_persona_style_from_txt( txt_path=txt_path, user_names=[self_name] if self_name else None, tail_lines=max_lines, ) return persona_style # --- 共通ルール (1 行に圧縮) ----------------------------------------------- SAFETY_RULES = ( "[ルール] 依存×/AIと聞かれたら正直/特定情報(住所本名電話勤務先学校口座)を聞かない/" "自傷×/嫉妬独占を煽らない/たまに現実関係を尊重するよう促す" ) # --- user_memory.json 編集 ------------------------------------------------- def update_persona_text( user_memory_path: Path | str | None = None, txt_path: Path | str | None = None, self_name: str | None = None, ) -> dict[str, Any]: path = Path(user_memory_path) if user_memory_path else sp.user_memory_path() txt_path = Path(txt_path) if txt_path else (sp.text_save_dir() / "line-chat.txt") user_memory = json.loads(path.read_text(encoding="utf-8")) ai_persona = user_memory.setdefault("ai_persona", {}) # LINE txt → 相手の性格推定 neoac_scores = _run_text2personality( txt_path=txt_path, self_name=self_name ) if neoac_scores: bigfive_100 = neoac_to_bigfive(neoac_scores) existing_traits = ai_persona.get("traits", {}) if isinstance(ai_persona.get("traits"), dict) else {} merged_traits = { **existing_traits, **{k: v for k, v in bigfive_100.items() if v is not None}, **neoac_scores, } ai_persona["traits"] = merged_traits ai_persona["personality_text"] = summarize_traits(merged_traits) # LINE txt → 関係性推定 relationship = _run_text2emotion( txt_path=txt_path, self_name=self_name, user_memory_path=path, ) if relationship: user_memory["relationship_state"] = relationship user_memory["relationship_prompt"] = summarize_relationship(relationship) persona_style = _run_text2persona_style( txt_path=txt_path, self_name=self_name, ) if persona_style: ai_persona["persona_style_description"] = persona_style["style_description"] # prompt_builder がリッチな文体特徴を読めるよう、このセッションの # persona_style.json にも保存する(共有ファイルとは分離)。 try: sp.persona_style_path().write_text( json.dumps(persona_style, ensure_ascii=False, indent=2), encoding="utf-8", ) except OSError: pass path.write_text( json.dumps(user_memory, ensure_ascii=False, indent=2), encoding="utf-8", ) return user_memory