import argparse import re import json from pathlib import Path from collections import Counter PERSONA_STYLE_PATH = ( Path(__file__).resolve().parents[2] / "persona_style.json" ) MEDIA_MARKERS = {"画像", "動画", "スタンプ", "写真", "ボイスメッセージ", "音声", "アルバム", "連絡先"} CALL_MARKERS = {"応答なし", "不在着信", "キャンセル"} URL_RE = re.compile(r"https?://|www\.") CALL_DURATION_RE = re.compile(r"^\d{1,2}:\d{2}$") DELETED_MESSAGE_RE = re.compile(r"メッセージの送信を取り消しました") LINE_DATE_RE = re.compile(r"^\d{4}\.\d{2}\.\d{2}\s+") LINE_MESSAGE_RE = re.compile(r"^(\d{1,2}:\d{2})\s+(.+)$") POLITE_PATTERNS = [ "です", "ます", "でした", "ました", "ください", "でしょう", "ですね" ] CASUAL_PATTERNS = [ "じゃん", "だよ", "だね", "やん", "かも", "やろ", "やで", "やねん", "ちゃう", "めっちゃ", "そやな", "そやね", "やからな", "やからね", "やってん" ] CASUAL_ENDING_PATTERN = re.compile( r"(じゃん|だよ|だね|やん|かも|やろ|やで|" r"やな|やねん|やった|やってん|んや|ちゃう|" r"(?= 30: return { "level": "high", "description": "会話数が十分あり、比較的安定した推定" } if sample_count >= 10: return { "level": "medium", "description": "ある程度の会話数があり、参考にしやすい推定" } if sample_count > 0: return { "level": "low", "description": "会話数が少ないため、まだ仮の推定" } return { "level": "none", "description": "会話データがないため推定不可" } def extract_favorite_phrases(texts): joined = "\n".join(texts) counts = Counter() for phrase in COMMON_PHRASE_CANDIDATES: c = joined.count(phrase) if c > 0: counts[phrase] = c return counts.most_common(10) def extract_style_features(messages, partner_role="partner"): texts = extract_partner_messages(messages, partner_role=partner_role) if not texts: return { "sample_count": 0, "avg_length": 0, "short_reply_rate": 0, "question_rate": 0, "emoji_rate": 0, "laugh_rate": 0, "polite_rate": 0, "casual_rate": 0, "reliability": estimate_reliability(0), "ending_patterns": [], "favorite_phrases": [] } total = len(texts) lengths = [len(t.strip()) for t in texts] short_reply_count = sum(length <= 8 for length in lengths) question_count = sum(("?" in t or "?" in t) for t in texts) emoji_count = sum(bool(EMOJI_PATTERN.search(t)) for t in texts) laugh_count = sum(has_laugh_expression(t) for t in texts) polite_count = sum(any(p in t for p in POLITE_PATTERNS) for t in texts) casual_count = sum(has_casual_expression(t) for t in texts) return { "sample_count": total, "avg_length": round(sum(lengths) / total, 2), "short_reply_rate": round(short_reply_count / total, 3), "question_rate": round(question_count / total, 3), "emoji_rate": round(emoji_count / total, 3), "laugh_rate": round(laugh_count / total, 3), "polite_rate": round(polite_count / total, 3), "casual_rate": round(casual_count / total, 3), "reliability": estimate_reliability(total), "ending_patterns": extract_endings(texts), "favorite_phrases": extract_favorite_phrases(texts) } def describe_style(features): descriptions = [] avg_length = features["avg_length"] short_rate = features["short_reply_rate"] polite_rate = features["polite_rate"] casual_rate = features["casual_rate"] laugh_rate = features["laugh_rate"] emoji_rate = features["emoji_rate"] question_rate = features["question_rate"] reliability = features["reliability"] if reliability["level"] in ["low", "none"]: descriptions.append(reliability["description"]) if avg_length <= 10 or short_rate >= 0.6: descriptions.append("短文で返す傾向が強い") elif avg_length >= 30: descriptions.append("比較的長めに話す傾向がある") else: descriptions.append("文の長さは中程度") if polite_rate >= 0.5: descriptions.append("丁寧語が多い") elif casual_rate >= 0.3: descriptions.append("カジュアルなタメ口が多い") else: descriptions.append("硬すぎない自然な口調") if laugh_rate >= 0.25: descriptions.append("「笑」や「w」などの笑い表現をよく使う") elif laugh_rate >= 0.08: descriptions.append("笑い表現をたまに使う") else: descriptions.append("笑い表現は控えめ") if emoji_rate >= 0.2: descriptions.append("絵文字を比較的よく使う") else: descriptions.append("絵文字は少なめ") if question_rate >= 0.25: descriptions.append("質問で会話を続けることが多い") endings = [e for e, _ in features.get("ending_patterns", [])[:5]] if endings: descriptions.append(f"よく出る語尾は「{'」「'.join(endings)}」") phrases = [p for p, _ in features.get("favorite_phrases", [])[:5]] if phrases: descriptions.append(f"よく使う表現は「{'」「'.join(phrases)}」") return "。".join(descriptions) + "。" def update_persona_style(messages, partner_role="partner"): features = extract_style_features(messages, partner_role=partner_role) style_description = describe_style(features) data = { "style_features": features, "style_description": style_description } save_json(PERSONA_STYLE_PATH, data) return data def update_persona_style_from_txt( txt_path, user_names=None, tail_lines=200, encoding="utf-8" ): """LINE .txtから相手の文体を分析してpersona_style.jsonへ保存する.""" messages = extract_messages_from_txt( txt_path=txt_path, user_names=user_names, tail_lines=tail_lines, encoding=encoding ) return update_persona_style(messages, partner_role="partner") def main(): parser = argparse.ArgumentParser( description="Analyze persona style from a LINE chat export." ) parser.add_argument( "txt_path", type=Path, help="LINE export .txt file. If named [LINE]name.txt, name is used as the partner.", ) parser.add_argument( "--user-name", dest="user_names", action="append", default=None, help="Your LINE display name. Can be passed multiple times. Defaults to Rayta.", ) parser.add_argument( "--tail-lines", type=int, default=200, help="Most recent N lines to analyze (0 = all).", ) parser.add_argument("--encoding", default="utf-8") args = parser.parse_args() data = update_persona_style_from_txt( txt_path=args.txt_path, user_names=args.user_names or ["Rayta"], tail_lines=None if args.tail_lines == 0 else args.tail_lines, encoding=args.encoding, ) print(data["style_description"]) print("saved to:", PERSONA_STYLE_PATH) if __name__ == "__main__": main()