Nyanpre commited on
Commit
2b87004
·
verified ·
1 Parent(s): c27d88c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -77
app.py CHANGED
@@ -17,26 +17,27 @@ def get_profile_info(client, did_or_handle):
17
  "avatar": profile.avatar if profile.avatar else "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png",
18
  "handle": profile.handle
19
  }
20
- except Exception: return None
 
21
 
22
  def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
23
  try:
24
  client = Client()
25
- client.login(my_id.replace('@', '').strip(), my_pw.strip())
26
  target_handle = target_id.replace('@', '').strip()
 
27
  profile = client.get_profile(actor=target_handle)
28
 
29
  posts_data = []
30
- interaction_pairs = []
31
-
32
- # 感情分析用の簡易辞書
33
- pos_words = ["嬉しい", "楽しい", "最高", "ありがとう", "感謝", "好き", "わーい", "おめでとう", "楽しみ", "美味しい"]
34
- neg_words = ["疲れた", "最悪", "悲しい", "辛い", "苦しい", "嫌い", "しんどい", "ムリ", "残念", "ひどい"]
35
- sentiment_counts = {"Positive": 0, "Negative": 0, "Neutral": 0}
36
 
37
- progress(0, desc="フィードを解析中...")
38
  cursor = None
39
- for _ in range(40):
40
  try:
41
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
42
  except Exception: break
@@ -46,26 +47,29 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
46
  post = feed_view.post
47
  if not isinstance(post, PostView): continue
48
 
49
- text = getattr(post.record, 'text', "")
50
- if post.author.handle == target_handle:
51
- # 簡易感情分析
52
- is_pos = any(w in text for w in pos_words)
53
- is_neg = any(w in text for w in neg_words)
54
- if is_pos: sentiment_counts["Positive"] += 1
55
- elif is_neg: sentiment_counts["Negative"] += 1
56
- else: sentiment_counts["Neutral"] += 1
57
-
58
- dt_jst = pd.to_datetime(getattr(post.record, 'created_at')) + timedelta(hours=9)
59
- posts_data.append({
60
- 'text': text, 'created_at': dt_jst, 'hour': dt_jst.hour,
61
- 'weekday': dt_jst.day_name(),
62
- 'likes': getattr(post, 'like_count', 0), 'reposts': getattr(post, 'repost_count', 0),
63
- 'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
64
- })
65
-
66
- # インタラクション (自分以外同士もペアにする)
 
 
 
67
  u_author = post.author.handle
68
- if feed_view.reason: interaction_pairs.append((target_handle, u_author))
69
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
70
  u_parent = feed_view.reply.parent.author.handle
71
  interaction_pairs.append((u_author, u_parent))
@@ -73,81 +77,97 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
73
  cursor = response.cursor
74
  if not cursor: break
75
 
76
- if not posts_data: return "データなし", "", "", None, None, None, "失敗"
77
- df = pd.DataFrame(posts_data)
78
 
79
- # --- 1. 感情色パレット (Pie Chart) ---
80
- fig_aura = px.pie(
81
- names=list(sentiment_counts.keys()),
82
- values=list(sentiment_counts.values()),
83
- title="現在の感情オーラ・パレット",
84
- color=list(sentiment_counts.keys()),
85
- color_discrete_map={'Positive': '#ff99cc', 'Negative': '#9999ff', 'Neutral': '#dddddd'}
 
 
 
 
 
86
  )
 
87
 
88
- # --- 2. 曜日×時間 ヒートマップ ---
89
- week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
90
- heat_pt = df.groupby(['weekday', 'hour']).size().reset_index(name='count').pivot(index='weekday', columns='hour', values='count').reindex(week_order).fillna(0)
91
- heat_pt.index = ['月', '', '', '木', '', '土', '日']
92
- fig_heat = px.imshow(heat_pt, color_continuous_scale='Blues', title="週間アクティビティ")
93
 
94
- # --- 3. 相関図 (関係性上書き & 自分以外同士) ---
95
- RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同じ趣味の同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱えの料理人"]
96
-
97
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
98
  top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
99
  nodes = list(set([target_handle] + top_10))
 
 
100
  node_attrs = {n: (random.choice(RELATIONSHIPS) if n != target_handle else "(本人)") for n in nodes}
101
 
102
- G = nx.Graph()
103
- for (u1, u2) in interaction_pairs:
104
- if u1 in nodes and u2 in nodes and u1 != u2:
105
- if G.has_edge(u1, u2): G[u1][u2]['weight'] += 1
106
- else: G.add_edge(u1, u2, weight=1)
107
 
108
- pos = nx.spring_layout(G, k=0.9, seed=42)
109
  fig_net = go.Figure()
 
110
  for edge in G.edges():
111
- w = G[edge[0]][edge[1]]['weight']
112
- fig_net.add_trace(go.Scatter(x=[pos[edge[0]][0], pos[edge[1]][0], None], y=[pos[edge[0]][1], pos[edge[1]][1], None], mode='lines', line=dict(color='#ddd', width=min(w, 5)), hoverinfo='none'))
 
 
113
 
114
  node_labels = [f"<b>{n}</b>" if n == target_handle else f"@{n}<br><span style='color:red;'>【{node_attrs[n]}】</span>" for n in G.nodes()]
115
- fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()], mode='markers+text', text=node_labels, textposition="bottom center", marker=dict(size=45, color='rgba(0,0,0,0)')))
 
 
116
 
117
  node_images = []
118
  for node in G.nodes():
119
  info = get_profile_info(client, node)
120
- if info: node_images.append(dict(source=info['avatar'], xref="x", yref="y", x=pos[node][0], y=pos[node][1], sizex=0.2, sizey=0.2, xanchor="center", yanchor="middle", layer="above"))
121
- fig_net.update_layout(images=node_images, showlegend=False, plot_bgcolor='white', xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), height=650, title="🦋 ドラマチック相関図 (自分以外も繋がります)")
 
122
 
123
- # 統計とベストポスト
124
- stats_html = f"<div style='background:#f0f8ff;padding:10px;border-radius:10px;text-align:center;'><b>{target_handle}</b> の解析結果<br><small>活動開始(初ポスト): {df['created_at'].min().strftime('%Y/%m/%d')}</small></div>"
125
- top_posts = df.sort_values(by='score', ascending=False).head(3)
126
  posts_html = "<b>🏆 ベストポスト</b><br>"
127
- for _, r in top_posts.iterrows(): posts_html += f"<div style='border-left:3px solid #0085ff;padding-left:5px;margin-bottom:8px;'>{r['text'][:55]}...<br><small>❤️ {r['likes']} 🔄 {r['reposts']}</small></div>"
 
128
 
129
- return stats_html, posts_html, fig_aura, fig_heat, fig_net, "解析完了!"
130
 
131
- except Exception as e: return f"エラー: {str(e)}", "", None, None, None, "失敗"
 
132
 
133
- with gr.Blocks() as demo:
134
- gr.Markdown("# 🦋 Bluesky アクティビティ & 感情色パレ相関図")
135
  with gr.Row():
136
  with gr.Column(scale=1):
137
- my_id, my_pw = gr.Textbox(label="自分のID"), gr.Textbox(label="パスワード", type="password")
 
138
  target_id = gr.Textbox(label="解析対象")
139
- btn = gr.Button("解析開始", variant="primary")
140
- status = gr.Textbox(label="状態", interactive=False)
141
  with gr.Column(scale=2):
142
- out_stats, out_posts = gr.HTML(), gr.HTML()
143
-
 
144
  with gr.Tabs():
145
- with gr.TabItem("人間関係相関図"): out_net = gr.Plot()
146
- with gr.TabItem("活動リズム & 感情パレット"):
147
- with gr.Row():
148
- out_heat = gr.Plot()
149
- out_aura = gr.Plot()
150
 
151
- btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, gr.State("日ごと")], outputs=[out_stats, out_posts, out_aura, out_heat, out_net, status])
 
152
 
153
- if __name__ == "__main__": demo.launch()
 
 
17
  "avatar": profile.avatar if profile.avatar else "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png",
18
  "handle": profile.handle
19
  }
20
+ except Exception:
21
+ return None
22
 
23
  def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
24
  try:
25
  client = Client()
26
+ my_id = my_id.replace('@', '').strip()
27
  target_handle = target_id.replace('@', '').strip()
28
+ client.login(my_id, my_pw.strip())
29
  profile = client.get_profile(actor=target_handle)
30
 
31
  posts_data = []
32
+ interaction_pairs = []
33
+
34
+ # 感情キーワード
35
+ pos_words = ["嬉しい", "楽しい", "最高", "ありがとう", "感謝", "好き", "わーい", "おめでとう", "楽しみ", "美味しい", "良き", "かな", "笑", "w"]
36
+ neg_words = ["疲れた", "最悪", "悲しい", "辛い", "苦しい", "嫌い", "しんどい", "ムリ", "残念", "ひどい", "鬱", "おこ", "死ぬ", "ため息"]
 
37
 
38
+ progress(0, desc="フィードを深層解析中...")
39
  cursor = None
40
+ for _ in range(50):
41
  try:
42
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
43
  except Exception: break
 
47
  post = feed_view.post
48
  if not isinstance(post, PostView): continue
49
 
50
+ created_at_raw = getattr(post.record, 'created_at', None)
51
+ if created_at_raw:
52
+ dt_jst = pd.to_datetime(created_at_raw) + timedelta(hours=9)
53
+ if post.author.handle == target_handle:
54
+ text = getattr(post.record, 'text', "")
55
+
56
+ # 感情スコアリング
57
+ s_val = "Neutral"
58
+ if any(w in text for w in pos_words): s_val = "Positive"
59
+ elif any(w in text for w in neg_words): s_val = "Negative"
60
+
61
+ posts_data.append({
62
+ 'created_at': dt_jst,
63
+ 'sentiment': s_val,
64
+ 'text': text,
65
+ 'likes': getattr(post, 'like_count', 0),
66
+ 'reposts': getattr(post, 'repost_count', 0),
67
+ 'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
68
+ })
69
+
70
+ # インタラクション抽出
71
  u_author = post.author.handle
72
+ if feed_view.reason: interaction_pairs.append((u_author, target_handle))
73
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
74
  u_parent = feed_view.reply.parent.author.handle
75
  interaction_pairs.append((u_author, u_parent))
 
77
  cursor = response.cursor
78
  if not cursor: break
79
 
80
+ if not posts_data: return "データなし", "", "", None, None, None, None, "失敗"
 
81
 
82
+ df = pd.DataFrame(posts_data)
83
+
84
+ # --- 新・感情パレット(オーラ・タイムライン) ---
85
+ # 日ごとの感情比率を計算
86
+ df['date'] = df['created_at'].dt.date
87
+ aura_df = df.groupby(['date', 'sentiment']).size().reset_index(name='count')
88
+
89
+ fig_aura = px.area(
90
+ aura_df, x="date", y="count", color="sentiment",
91
+ title="✨ 心のオーラ・パレット(感情の推移)",
92
+ color_discrete_map={'Positive': '#FFADAD', 'Negative': '#A0C4FF', 'Neutral': '#E2E2E2'},
93
+ line_group="sentiment", template="plotly_white"
94
  )
95
+ fig_aura.update_layout(hovermode="x unified", yaxis_title="感情の強さ")
96
 
97
+ # --- 他のグラフ ---
98
+ freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
99
+ df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
100
+ fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿エネルギーの波動", color_discrete_sequence=['#4CC9F0'])
 
101
 
102
+ # --- 相関図 (DiGraph) ---
 
 
103
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
104
  top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
105
  nodes = list(set([target_handle] + top_10))
106
+
107
+ RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同じ趣味 of 同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱えの料理人"]
108
  node_attrs = {n: (random.choice(RELATIONSHIPS) if n != target_handle else "(本人)") for n in nodes}
109
 
110
+ G = nx.DiGraph()
111
+ for (u_f, u_t) in interaction_pairs:
112
+ if u_f in nodes and u_t in nodes and u_f != u_t:
113
+ if G.has_edge(u_f, u_t): G[u_f][u_t]['weight'] += 1
114
+ else: G.add_edge(u_f, u_t, weight=1)
115
 
116
+ pos = nx.spring_layout(G, k=1.1, seed=42)
117
  fig_net = go.Figure()
118
+
119
  for edge in G.edges():
120
+ start, end = edge
121
+ w = G[start][end]['weight']
122
+ fig_net.add_trace(go.Scatter(x=[pos[start][0], pos[end][0], None], y=[pos[start][1], pos[end][1], None],
123
+ mode='lines', line=dict(width=min(w, 5), color='rgba(200,200,200,0.6)'), hoverinfo='none'))
124
 
125
  node_labels = [f"<b>{n}</b>" if n == target_handle else f"@{n}<br><span style='color:red;'>【{node_attrs[n]}】</span>" for n in G.nodes()]
126
+ fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
127
+ mode='markers+text', text=node_labels, textposition="bottom center",
128
+ marker=dict(size=40, color='rgba(0,0,0,0)'), hoverinfo='none'))
129
 
130
  node_images = []
131
  for node in G.nodes():
132
  info = get_profile_info(client, node)
133
+ if info:
134
+ node_images.append(dict(source=info['avatar'], xref="x", yref="y", x=pos[node][0], y=pos[node][1], sizex=0.2, sizey=0.2, xanchor="center", yanchor="middle", layer="above"))
135
+ fig_net.update_layout(images=node_images, showlegend=False, plot_bgcolor='white', xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), height=650)
136
 
137
+ # HTML
138
+ stats_html = f"<div style='text-align:center; background:#f9f9f9; padding:15px; border-radius:15px;'><b>{target_handle}</b> の深層プロフィール<br><small>初ポスト: {df['created_at'].min().strftime('%Y/%m/%d')}</small></div>"
139
+ top_posts = df.sort_values('score', ascending=False).head(3)
140
  posts_html = "<b>🏆 ベストポスト</b><br>"
141
+ for _, row in top_posts.iterrows():
142
+ posts_html += f"<div style='margin-bottom:8px; border-left:4px solid #4CC9F0; padding-left:8px; font-size:0.9em;'>{row['text'][:60]}...<br><small>❤️ {row['likes']} 🔄 {row['reposts']}</small></div>"
143
 
144
+ return stats_html, "", posts_html, fig_bar, None, fig_aura, fig_net, "解析完了!"
145
 
146
+ except Exception as e:
147
+ return f"エラー: {str(e)}", "", "", None, None, None, None, "失敗"
148
 
149
+ with gr.Blocks(title="Bluesky Aura & Relationship") as demo:
150
+ gr.Markdown("# 🦋 Bluesky 深層オーラ & ドラマチ相関図")
151
  with gr.Row():
152
  with gr.Column(scale=1):
153
+ my_id = gr.Textbox(label="自分のハン")
154
+ my_pw = gr.Textbox(label="アプリパスワード", type="password")
155
  target_id = gr.Textbox(label="解析対象")
156
+ btn = gr.Button("フル解析スタート", variant="primary")
157
+ status = gr.Textbox(label="ステータス", interactive=False)
158
  with gr.Column(scale=2):
159
+ out_stats = gr.HTML()
160
+ out_posts = gr.HTML()
161
+
162
  with gr.Tabs():
163
+ with gr.TabItem("✨ 心のオーラ"):
164
+ out_aura = gr.Plot()
165
+ out_bar = gr.Plot()
166
+ with gr.TabItem("🤝 人間関係相関図"):
167
+ out_net = gr.Plot()
168
 
169
+ btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, gr.State("日ごと")],
170
+ outputs=[out_stats, gr.State(""), out_posts, out_bar, gr.State(None), out_aura, out_net, status])
171
 
172
+ if __name__ == "__main__":
173
+ demo.launch()