Nyanpre commited on
Commit
0715ca7
·
verified ·
1 Parent(s): a5f594a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -59
app.py CHANGED
@@ -31,45 +31,25 @@ CUSTOM_CSS = """
31
  button.primary { background: #0085ff !important; height: 60px !important; color: white !important; font-size: 1.2rem !important; }
32
  """
33
 
34
- # --- 文言リスト(各30個以上) ---
35
- ADJECTIVES = [
36
- "光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の",
37
- "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の",
38
- "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の",
39
- "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"
40
- ]
41
-
42
- TITLES = [
43
- "投稿者", "クリエイター", "エンターテイナー", "哲学者", "自由人", "守護神",
44
- "表現者", "観測者", "旅人", "語り部", "先駆者", "求道者", "職人", "策士",
45
- "魔術師", "支配者", "住人", "伝道師", "蒐集家", "冒険者", "導き手", "革命家",
46
- "異端児", "詩人", "鑑定士", "研究員", "巨匠", "隠者", "英雄", "新星", "重鎮"
47
- ]
48
-
49
  RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱え料理人"]
50
 
51
  def get_profile_safe(client, actor):
52
  try:
53
  p = client.get_profile(actor=actor)
54
  return {"handle": p.handle, "avatar": p.avatar or "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"}
55
- except:
56
- return {"handle": actor, "avatar": "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"}
57
 
58
  def generate_catchphrase(kanji, posts_df):
59
  adj_list = ADJECTIVES[:]
60
  title_list = TITLES[:]
61
-
62
- # 簡易的な特徴分析
63
  if not posts_df.empty:
64
  avg_hour = posts_df['hour'].mean()
65
  if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
66
- elif 6 <= avg_hour <= 10: adj_list.insert(0, "早起きの")
67
-
68
  if (posts_df['likes'] > 10).any(): title_list.insert(0, "カリスマ")
69
-
70
- adj = random.choice(adj_list)
71
- title = random.choice(title_list)
72
- return f"── {adj} {kanji} を愛する {title} ──"
73
 
74
  def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
75
  try:
@@ -78,7 +58,7 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
78
  target_handle = target_id.replace('@', '').strip()
79
  profile = client.get_profile(actor=target_handle)
80
 
81
- posts_data, interaction_pairs, ends_list = [], [], []
82
  reply_counts = Counter()
83
  user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
84
  all_text = ""
@@ -89,50 +69,44 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
89
  for f in response.feed:
90
  p = f.post
91
  if not isinstance(p, PostView) or p.author.handle != target_handle: continue
92
-
93
  txt = getattr(p.record, 'text', "")
94
  all_text += txt
95
- post_id = p.uri.split('/')[-1]
96
- post_url = f"https://bsky.app/profile/{target_handle}/post/{post_id}"
97
  dt = pd.to_datetime(getattr(p.record, 'created_at')) + timedelta(hours=9)
 
98
 
99
- posts_data.append({'text': txt, 'likes': p.like_count, 'reposts': p.repost_count, 'created_at': dt, 'url': post_url, 'score': p.like_count + p.repost_count, 'hour': dt.hour, 'weekday': dt.day_name()})
100
-
101
- clean = re.sub(r'[!\?!?。\n\s]+$', '', txt)
102
- if len(clean) >= 2:
103
- end = clean[-3:];
104
- if re.search(r'[ぁ-んーwWwW]$', end): ends_list.append(end)
105
-
106
  if getattr(f, 'reply', None) and isinstance(f.reply.parent, PostView):
107
  u_parent = f.reply.parent.author.handle
108
  reply_counts[u_parent] += 1
109
  interaction_pairs.append((target_handle, u_parent))
110
- if u_parent not in user_info_cache:
111
- user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
112
-
113
  cursor = response.cursor
114
  if not cursor: break
115
  progress((i+1)/20)
116
 
117
  df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
118
- kanjis = re.findall(r'[一-龠]', all_text)
119
- rep_kanji = Counter(kanjis).most_common(1)[0][0] if kanjis else "魂"
120
- catchphrase = generate_catchphrase(rep_kanji, df)
121
-
122
- # HTML
123
  html = f"""<div class="dashboard-container">
124
- <div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{catchphrase}</div></div>
125
  <div class="card stat-card"><div class="stat-label">🚀 総投稿数</div><div class="stat-value">{profile.posts_count}</div></div>
126
  <div class="card stat-card"><div class="stat-label">🔍 今回の解析数</div><div class="stat-value">{len(df)}</div></div>
127
- <div class="card"><div class="rank-header">👥 よく絡む人</div>
128
- {"".join([f"<div class='rank-entry'><img src='{user_info_cache.get(h, get_profile_safe(client, h))['avatar']}' class='rank-avatar'><b>{h}</b><span style='margin-left:auto'>{c}回</span></div>" for h,c in reply_counts.most_common(3)])}</div>
129
  <div class="card" style="grid-column: 1 / -1;"><div class="rank-header">🏆 ベストポスト</div>"""
130
  for _, r in df.sort_values('score', ascending=False).head(3).iterrows():
131
  html += f"<a href='{r['url']}' target='_blank' class='post-link'><div class='best-post-item'>{r['text']}<div class='post-meta'>❤️ {r['likes']} 🔄 {r['reposts']} — {r['created_at'].strftime('%Y/%m/%d')}</div></div></a>"
132
  html += "</div></div>"
133
 
134
- fig_bar = px.bar(df.set_index('created_at').resample({"日ごと":"D","週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count'), x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white")
 
 
135
 
 
 
 
 
 
 
136
  # 相関図
137
  nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(8)]))
138
  G = nx.Graph()
@@ -141,36 +115,35 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
141
  pos = nx.spring_layout(G, k=1.5, seed=42)
142
  cx, cy = pos[target_handle]
143
  for n in pos: pos[n] = (pos[n][0] - cx, pos[n][1] - cy)
144
-
145
  fig_net = go.Figure()
146
  for e in G.edges(): fig_net.add_trace(go.Scatter(x=[pos[e[0]][0], pos[e[1]][0]], y=[pos[e[0]][1], pos[e[1]][1]], mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
147
-
148
- node_imgs = []
149
- node_texts = []
150
  for n in nodes:
151
  img = user_info_cache.get(n, {"avatar": ""})["avatar"]
152
  node_imgs.append(dict(source=img, xref="x", yref="y", x=pos[n][0], y=pos[n][1], sizex=0.22, sizey=0.22, xanchor="center", yanchor="middle", layer="above"))
153
  rel = "<br><b style='color:#ff4b4b;'>(本人)</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
154
  node_texts.append(f"<b>{n}</b>{rel}")
155
-
156
  fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in nodes], y=[pos[n][1] for n in nodes], mode='markers+text', text=node_texts, marker=dict(size=50, color='rgba(0,0,0,0)'), textposition="bottom center", hoverinfo='none'))
157
  fig_net.update_layout(images=node_imgs, showlegend=False, xaxis=dict(visible=False, range=[-1.3, 1.3]), yaxis=dict(visible=False, range=[-1.3, 1.3]), plot_bgcolor='white', height=650, margin=dict(t=20, b=20, l=0, r=0))
158
 
159
- return html, fig_bar, fig_net, "解析完了!"
160
- except Exception as e: return f"エラー: {e}", None, None, "失敗"
161
 
162
  with gr.Blocks(css=CUSTOM_CSS) as demo:
163
  gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:2rem;'>🦋 Bluesky Analyzer</p>")
164
  with gr.Row():
165
  with gr.Column():
166
  m_id, m_pw, t_id = gr.Textbox(label="自分のハンドル"), gr.Textbox(label="アプリパスワード", type="password"), gr.Textbox(label="解析対象")
167
- frq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="グラフ単位", value="ごと")
168
  btn = gr.Button("解析実行", variant="primary")
169
  st = gr.Markdown("認証情報を入力してください")
170
  out_h = gr.HTML()
171
  with gr.Tabs():
172
- with gr.TabItem("📊 活動ログ"): out_b = gr.Plot()
173
- with gr.TabItem("🤝 魂の相関図"): out_n = gr.Plot()
174
- btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_n, st])
 
 
 
175
 
176
  demo.launch()
 
31
  button.primary { background: #0085ff !important; height: 60px !important; color: white !important; font-size: 1.2rem !important; }
32
  """
33
 
34
+ # 文言リスト
35
+ ADJECTIVES = ["光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の", "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の", "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の", "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"]
36
+ TITLES = ["投稿者", "クリエイター", "エンターテイナー", "哲学者", "自由人", "守護神", "表現者", "観測者", "旅人", "語り部", "先駆者", "求道者", "職人", "策士", "魔術師", "支配者", "住人", "道師", "蒐集家", "冒険者", "導き手", "革命家", "異端児", "詩人", "鑑定士", "研究員", "巨匠", "隠者", "英雄", "新星", "重鎮"]
 
 
 
 
 
 
 
 
 
 
 
 
37
  RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱え料理人"]
38
 
39
  def get_profile_safe(client, actor):
40
  try:
41
  p = client.get_profile(actor=actor)
42
  return {"handle": p.handle, "avatar": p.avatar or "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"}
43
+ except: return {"handle": actor, "avatar": "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"}
 
44
 
45
  def generate_catchphrase(kanji, posts_df):
46
  adj_list = ADJECTIVES[:]
47
  title_list = TITLES[:]
 
 
48
  if not posts_df.empty:
49
  avg_hour = posts_df['hour'].mean()
50
  if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
 
 
51
  if (posts_df['likes'] > 10).any(): title_list.insert(0, "カリスマ")
52
+ return f"── {random.choice(adj_list)} {kanji} を愛する {random.choice(title_list)} ──"
 
 
 
53
 
54
  def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
55
  try:
 
58
  target_handle = target_id.replace('@', '').strip()
59
  profile = client.get_profile(actor=target_handle)
60
 
61
+ posts_data, interaction_pairs = [], []
62
  reply_counts = Counter()
63
  user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
64
  all_text = ""
 
69
  for f in response.feed:
70
  p = f.post
71
  if not isinstance(p, PostView) or p.author.handle != target_handle: continue
 
72
  txt = getattr(p.record, 'text', "")
73
  all_text += txt
 
 
74
  dt = pd.to_datetime(getattr(p.record, 'created_at')) + timedelta(hours=9)
75
+ posts_data.append({'text': txt, 'likes': p.like_count, 'reposts': p.repost_count, 'created_at': dt, 'url': f"https://bsky.app/profile/{target_handle}/post/{p.uri.split('/')[-1]}", 'score': p.like_count + p.repost_count, 'hour': dt.hour, 'weekday': dt.day_name()})
76
 
 
 
 
 
 
 
 
77
  if getattr(f, 'reply', None) and isinstance(f.reply.parent, PostView):
78
  u_parent = f.reply.parent.author.handle
79
  reply_counts[u_parent] += 1
80
  interaction_pairs.append((target_handle, u_parent))
81
+ if u_parent not in user_info_cache: user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
 
 
82
  cursor = response.cursor
83
  if not cursor: break
84
  progress((i+1)/20)
85
 
86
  df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
87
+ rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
88
+
89
+ # HTML生成
 
 
90
  html = f"""<div class="dashboard-container">
91
+ <div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{generate_catchphrase(rep_kanji, df)}</div></div>
92
  <div class="card stat-card"><div class="stat-label">🚀 総投稿数</div><div class="stat-value">{profile.posts_count}</div></div>
93
  <div class="card stat-card"><div class="stat-label">🔍 今回の解析数</div><div class="stat-value">{len(df)}</div></div>
94
+ <div class="card"><div class="rank-header">👥 よく絡む人</div>{"".join([f"<div class='rank-entry'><img src='{user_info_cache.get(h, get_profile_safe(client, h))['avatar']}' class='rank-avatar'><b>{h}</b><span style='margin-left:auto'>{c}回</span></div>" for h,c in reply_counts.most_common(3)])}</div>
 
95
  <div class="card" style="grid-column: 1 / -1;"><div class="rank-header">🏆 ベストポスト</div>"""
96
  for _, r in df.sort_values('score', ascending=False).head(3).iterrows():
97
  html += f"<a href='{r['url']}' target='_blank' class='post-link'><div class='best-post-item'>{r['text']}<div class='post-meta'>❤️ {r['likes']} 🔄 {r['reposts']} — {r['created_at'].strftime('%Y/%m/%d')}</div></div></a>"
98
  html += "</div></div>"
99
 
100
+ # グラフ: 活動ログ
101
+ df_counts = df.set_index('created_at').resample({"週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count')
102
+ fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", title="投稿頻度")
103
 
104
+ # グラフ: ヒートマップ
105
+ week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
106
+ heat_data = df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
107
+ heat_data.index = ['月','火','水','木','金','土','日']
108
+ fig_heat = px.imshow(heat_data, color_continuous_scale='Blues', title="活動時間帯(JST)")
109
+
110
  # 相関図
111
  nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(8)]))
112
  G = nx.Graph()
 
115
  pos = nx.spring_layout(G, k=1.5, seed=42)
116
  cx, cy = pos[target_handle]
117
  for n in pos: pos[n] = (pos[n][0] - cx, pos[n][1] - cy)
 
118
  fig_net = go.Figure()
119
  for e in G.edges(): fig_net.add_trace(go.Scatter(x=[pos[e[0]][0], pos[e[1]][0]], y=[pos[e[0]][1], pos[e[1]][1]], mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
120
+ node_imgs, node_texts = [], []
 
 
121
  for n in nodes:
122
  img = user_info_cache.get(n, {"avatar": ""})["avatar"]
123
  node_imgs.append(dict(source=img, xref="x", yref="y", x=pos[n][0], y=pos[n][1], sizex=0.22, sizey=0.22, xanchor="center", yanchor="middle", layer="above"))
124
  rel = "<br><b style='color:#ff4b4b;'>(本人)</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
125
  node_texts.append(f"<b>{n}</b>{rel}")
 
126
  fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in nodes], y=[pos[n][1] for n in nodes], mode='markers+text', text=node_texts, marker=dict(size=50, color='rgba(0,0,0,0)'), textposition="bottom center", hoverinfo='none'))
127
  fig_net.update_layout(images=node_imgs, showlegend=False, xaxis=dict(visible=False, range=[-1.3, 1.3]), yaxis=dict(visible=False, range=[-1.3, 1.3]), plot_bgcolor='white', height=650, margin=dict(t=20, b=20, l=0, r=0))
128
 
129
+ return html, fig_bar, fig_heat, fig_net, "解析完了!"
130
+ except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
131
 
132
  with gr.Blocks(css=CUSTOM_CSS) as demo:
133
  gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:2rem;'>🦋 Bluesky Analyzer</p>")
134
  with gr.Row():
135
  with gr.Column():
136
  m_id, m_pw, t_id = gr.Textbox(label="自分のハンドル"), gr.Textbox(label="アプリパスワード", type="password"), gr.Textbox(label="解析対象")
137
+ frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="ごと")
138
  btn = gr.Button("解析実行", variant="primary")
139
  st = gr.Markdown("認証情報を入力してください")
140
  out_h = gr.HTML()
141
  with gr.Tabs():
142
+ with gr.TabItem("📊 活動ログ"):
143
+ out_b = gr.Plot()
144
+ out_heat = gr.Plot()
145
+ with gr.TabItem("🤝 魂の相関図"):
146
+ out_n = gr.Plot()
147
+ btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
148
 
149
  demo.launch()