LineChatbot / core /persona.py
raystermomo's picture
Deploy romance chat to Hugging Face Spaces
6cd1906
Raw
History Blame Contribute Delete
13.9 kB
"""関係性スコア・性格ベクトル → 自然文.
入力: 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