Nyanpre commited on
Commit
6da17c4
·
verified ·
1 Parent(s): fce3124

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -83
app.py CHANGED
@@ -3,7 +3,6 @@ import pandas as pd
3
  from atproto import Client
4
  import time
5
  from datetime import datetime
6
- import re
7
  from collections import Counter
8
  import plotly.express as px
9
  import plotly.graph_objects as go
@@ -31,19 +30,17 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
31
  profile = client.get_profile(actor=target_handle)
32
 
33
  posts_data = []
34
- hashtags = []
35
  reply_users_list = []
36
  repost_users_list = []
37
  like_users_list = []
38
  interactions_for_network = []
 
39
 
40
- max_limit = 500
41
-
42
  # --- 2. 投稿フィードの分析 ---
43
  progress(0, desc="フィードを取得中...")
44
  cursor = None
45
 
46
- for _ in range(10):
47
  try:
48
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
49
  except Exception:
@@ -53,62 +50,58 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
53
 
54
  for feed_view in response.feed:
55
  post = feed_view.post
56
-
57
- # ポスト分析
58
- if feed_view.reason:
59
- if hasattr(post, 'author'):
60
- orig_author = post.author.handle
61
- if orig_author != target_handle:
62
- repost_users_list.append(orig_author)
63
- interactions_for_network.append((target_handle, orig_author))
64
- continue
65
-
66
- # 本人投稿分析
67
  if post.author.handle == target_handle:
68
- rkey = post.uri.split('/')[-1]
69
- post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
70
- likes = getattr(post, 'like_count', 0) or 0
71
- reposts = getattr(post, 'repost_count', 0) or 0
72
  created_at_raw = getattr(post.record, 'created_at', None)
73
- text = getattr(post.record, 'text', "") or ""
74
-
75
  if created_at_raw:
76
  posts_data.append({
77
- 'text': text,
78
  'created_at': pd.to_datetime(created_at_raw),
79
- 'likes': likes,
80
- 'reposts': reposts,
81
- 'score': likes + reposts,
82
- 'url': post_url
83
  })
84
- if text:
85
- hashtags.extend(re.findall(r'#(\w+)', text))
86
-
87
- # リプライ相手
 
 
 
 
 
 
 
 
88
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
89
- parent = feed_view.reply.parent
90
- if hasattr(parent, 'author'):
91
- p_handle = parent.author.handle
92
- if p_handle != target_handle:
93
- reply_users_list.append(p_handle)
94
- interactions_for_network.append((target_handle, p_handle))
95
 
96
  cursor = response.cursor
97
- if not cursor or len(posts_data) >= max_limit: break
98
- progress(0.4, desc=f"{len(posts_data)}件取得済み...")
99
 
100
- # --- 3. いいねの取得 ---
101
  try:
102
- likes_resp = client.get_actor_likes(actor=profile.did, limit=40)
103
- for like_item in likes_resp.feed:
104
- l_handle = like_item.post.author.handle
105
- if l_handle != target_handle:
106
- like_users_list.append(l_handle)
 
 
 
 
107
  except: pass
108
 
109
  # --- 4. 解析とHTML作成 ---
110
  if not posts_data:
111
- return "データ不足", "", "", None, None, "失敗"
112
 
113
  df = pd.DataFrame(posts_data)
114
  df['created_at_dt'] = df['created_at'].dt.tz_localize(None)
@@ -120,7 +113,7 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
120
  <small>総ポスト</small><br><b>{getattr(profile, 'posts_count', 0)}</b>
121
  </div>
122
  <div style='background: #f1f8e9; padding: 10px; border-radius: 8px; text-align: center;'>
123
- <small>継続日数</small><br><b>{days_active}日</b>
124
  </div>
125
  <div style='background: #fff3e0; padding: 10px; border-radius: 8px; text-align: center;'>
126
  <small>1日平均</small><br><b>{len(df)/days_active:.2f}</b>
@@ -128,7 +121,6 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
128
  </div>
129
  """
130
 
131
- # ランキングHTML
132
  def rank_box(title, items):
133
  top = Counter(items).most_common(3)
134
  res = f"<b>{title}</b><br>"
@@ -137,71 +129,90 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
137
 
138
  rank_html = f"""
139
  <div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>
140
- {rank_box("💬 リプライ", reply_users_list)}<hr>
141
- {rank_box("🔄 リポスト", repost_users_list)}<hr>
142
- {rank_box("❤️ いいね", like_users_list)}
143
  </div>
144
  """
145
 
146
- # ベスト投稿
147
  top_posts = df.sort_values('score', ascending=False).head(3)
148
  posts_html = "<b>🏆 ベストポスト</b><br>"
149
  for _, row in top_posts.iterrows():
150
  posts_html += f"<div style='margin-bottom:8px; font-size:0.85em; border-left:3px solid #0085ff; padding-left:5px;'>{row['text'][:60]}... (❤️{row['likes']})</div>"
151
 
152
- # --- 5. グラフ作成 ---
153
  freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
154
  df_counts = df.set_index('created_at_dt').resample(freq_map[freq_type]).size().reset_index(name='count')
155
  fig_bar = px.bar(df_counts, x='created_at_dt', y='count', title="アクティビティ推移")
156
 
157
- # --- 6. アイコン付きネットワーク図 ---
158
  progress(0.8, desc="ネットワーク図を生成中...")
159
  G = nx.Graph()
160
- top_interactors = [u for u, c in Counter(reply_users_list + repost_users_list).most_common(9)]
161
- nodes = [target_handle] + top_interactors
 
162
 
163
- # 相互作用の重み付け
164
- for s, t in interactions_for_network:
165
  if s in nodes and t in nodes:
166
  if G.has_edge(s, t): G[s][t]['weight'] += 1
167
  else: G.add_edge(s, t, weight=1)
168
 
169
- pos = nx.spring_layout(G, k=0.5)
170
-
171
- # アイコンURLの取得
172
- node_images = []
173
- for node in G.nodes():
174
- info = get_profile_info(client, node)
175
- img_url = info['avatar'] if info else ""
176
- node_images.append(dict(
177
- source=img_url, xref="x", yref="y", x=pos[node][0], y=pos[node][1],
178
- sizex=0.15, sizey=0.15, xanchor="center", yanchor="middle", layer="above"
179
- ))
180
 
 
181
  edge_x, edge_y = [], []
182
  for edge in G.edges():
183
  edge_x += [pos[edge[0]][0], pos[edge[1]][0], None]
184
  edge_y += [pos[edge[0]][1], pos[edge[1]][1], None]
185
-
186
- fig_net = go.Figure()
187
  fig_net.add_trace(go.Scatter(x=edge_x, y=edge_y, mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
188
- fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
189
- mode='markers+text', text=list(G.nodes()), textposition="bottom center",
190
- marker=dict(size=30, color='rgba(0,0,0,0)')))
191
-
192
- fig_net.update_layout(images=node_images, showlegend=False,
193
- xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
194
- yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
195
- plot_bgcolor='white', margin=dict(t=40, b=0, l=0, r=0))
196
 
197
- return stats_html, rank_html, posts_html, fig_bar, fig_net, "解析完了!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
 
 
199
  except Exception as e:
 
 
200
  return f"エラー: {str(e)}", "", "", None, None, "失敗"
201
 
202
  # --- UI定義 ---
203
  with gr.Blocks(title="Bluesky Dashboard") as demo:
204
- gr.Markdown("# 🦋 Bluesky 全機能統合ダッシュボード")
205
 
206
  with gr.Row():
207
  with gr.Column(scale=1):
@@ -220,9 +231,10 @@ with gr.Blocks(title="Bluesky Dashboard") as demo:
220
 
221
  with gr.Row():
222
  out_bar = gr.Plot(label="投稿頻度")
223
- out_net = gr.Plot(label="ユーザーネットワーク(アイコン付)")
224
 
225
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
226
  outputs=[out_stats, out_rank, out_posts, out_bar, out_net, status])
227
 
228
- demo.launch()
 
 
3
  from atproto import Client
4
  import time
5
  from datetime import datetime
 
6
  from collections import Counter
7
  import plotly.express as px
8
  import plotly.graph_objects as go
 
30
  profile = client.get_profile(actor=target_handle)
31
 
32
  posts_data = []
 
33
  reply_users_list = []
34
  repost_users_list = []
35
  like_users_list = []
36
  interactions_for_network = []
37
+ interaction_data = {} # { handle: Counter }
38
 
 
 
39
  # --- 2. 投稿フィードの分析 ---
40
  progress(0, desc="フィードを取得中...")
41
  cursor = None
42
 
43
+ for _ in range(50):
44
  try:
45
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
46
  except Exception:
 
50
 
51
  for feed_view in response.feed:
52
  post = feed_view.post
53
+
54
+ # A. 投稿データの保存(本人のポストのみ)
 
 
 
 
 
 
 
 
 
55
  if post.author.handle == target_handle:
 
 
 
 
56
  created_at_raw = getattr(post.record, 'created_at', None)
 
 
57
  if created_at_raw:
58
  posts_data.append({
59
+ 'text': getattr(post.record, 'text', ""),
60
  'created_at': pd.to_datetime(created_at_raw),
61
+ 'likes': getattr(post, 'like_count', 0),
62
+ 'reposts': getattr(post, 'repost_count', 0),
63
+ 'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0),
64
+ 'url': f"https://bsky.app/profile/{target_handle}/post/{post.uri.split('/')[-1]}"
65
  })
66
+
67
+ # B. リポストのカウント
68
+ if feed_view.reason and hasattr(post, 'author'):
69
+ u = post.author.handle
70
+ if u != target_handle:
71
+ if u not in interaction_data: interaction_data[u] = Counter()
72
+ interaction_data[u]['repost'] += 1
73
+ repost_users_list.append(u)
74
+ interactions_for_network.append((target_handle, u))
75
+
76
+ # C. リプライのカウント
77
+ elif post.author.handle == target_handle:
78
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
79
+ u = feed_view.reply.parent.author.handle
80
+ if u != target_handle:
81
+ if u not in interaction_data: interaction_data[u] = Counter()
82
+ interaction_data[u]['reply'] += 1
83
+ reply_users_list.append(u)
84
+ interactions_for_network.append((target_handle, u))
85
 
86
  cursor = response.cursor
87
+ if not cursor: break
 
88
 
89
+ # --- 3. いいねのカウント ---
90
  try:
91
+ likes_resp = client.get_actor_likes(actor=profile.did, limit=50)
92
+ for l in likes_resp.feed:
93
+ u = l.post.author.handle
94
+ if u != target_handle:
95
+ if u not in interaction_data: interaction_data[u] = Counter()
96
+ interaction_data[u]['like'] += 1
97
+ like_users_list.append(u)
98
+ # いいねもネットワークの線として考慮する
99
+ interactions_for_network.append((target_handle, u))
100
  except: pass
101
 
102
  # --- 4. 解析とHTML作成 ---
103
  if not posts_data:
104
+ return "データ取得失敗または投稿がありません", "", "", None, None, "失敗"
105
 
106
  df = pd.DataFrame(posts_data)
107
  df['created_at_dt'] = df['created_at'].dt.tz_localize(None)
 
113
  <small>総ポスト</small><br><b>{getattr(profile, 'posts_count', 0)}</b>
114
  </div>
115
  <div style='background: #f1f8e9; padding: 10px; border-radius: 8px; text-align: center;'>
116
+ <small>解析期間</small><br><b>{days_active}日</b>
117
  </div>
118
  <div style='background: #fff3e0; padding: 10px; border-radius: 8px; text-align: center;'>
119
  <small>1日平均</small><br><b>{len(df)/days_active:.2f}</b>
 
121
  </div>
122
  """
123
 
 
124
  def rank_box(title, items):
125
  top = Counter(items).most_common(3)
126
  res = f"<b>{title}</b><br>"
 
129
 
130
  rank_html = f"""
131
  <div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>
132
+ {rank_box("💬 リプライ相手", reply_users_list)}<hr>
133
+ {rank_box("🔄 リポスト相手", repost_users_list)}<hr>
134
+ {rank_box("❤️ いいね相手", like_users_list)}
135
  </div>
136
  """
137
 
 
138
  top_posts = df.sort_values('score', ascending=False).head(3)
139
  posts_html = "<b>🏆 ベストポスト</b><br>"
140
  for _, row in top_posts.iterrows():
141
  posts_html += f"<div style='margin-bottom:8px; font-size:0.85em; border-left:3px solid #0085ff; padding-left:5px;'>{row['text'][:60]}... (❤️{row['likes']})</div>"
142
 
143
+ # --- 5. 投稿頻度グラフ ---
144
  freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
145
  df_counts = df.set_index('created_at_dt').resample(freq_map[freq_type]).size().reset_index(name='count')
146
  fig_bar = px.bar(df_counts, x='created_at_dt', y='count', title="アクティビティ推移")
147
 
148
+ # --- 6. アイコン付きネットワーク図の生成 ---
149
  progress(0.8, desc="ネットワーク図を生成中...")
150
  G = nx.Graph()
151
+ # 交流のある上位ユーザーを抽出
152
+ top_interactors = [u for u, c in Counter(reply_users_list + repost_users_list + like_users_list).most_common(12)]
153
+ nodes = list(set([target_handle] + top_interactors))
154
 
155
+ for (s, t) in interactions_for_network:
 
156
  if s in nodes and t in nodes:
157
  if G.has_edge(s, t): G[s][t]['weight'] += 1
158
  else: G.add_edge(s, t, weight=1)
159
 
160
+ pos = nx.spring_layout(G, k=0.6, seed=42)
161
+ fig_net = go.Figure()
 
 
 
 
 
 
 
 
 
162
 
163
+ # エッジ(線)
164
  edge_x, edge_y = [], []
165
  for edge in G.edges():
166
  edge_x += [pos[edge[0]][0], pos[edge[1]][0], None]
167
  edge_y += [pos[edge[0]][1], pos[edge[1]][1], None]
 
 
168
  fig_net.add_trace(go.Scatter(x=edge_x, y=edge_y, mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
 
 
 
 
 
 
 
 
169
 
170
+ # ホバー用の透明な点(データ内訳)
171
+ node_hover_texts = []
172
+ for node in G.nodes():
173
+ if node == target_handle:
174
+ node_hover_texts.append(f"<b>{node} (解析対象)</b>")
175
+ else:
176
+ c = interaction_data.get(node, Counter())
177
+ node_hover_texts.append(f"<b>@{node}</b><br>❤️ いいね: {c['like']}<br>🔄 リポスト: {c['repost']}<br>💬 リプライ: {c['reply']}")
178
+
179
+ fig_net.add_trace(go.Scatter(
180
+ x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
181
+ mode='markers+text',
182
+ text=[n if n != target_handle else "" for n in G.nodes()],
183
+ textposition="bottom center",
184
+ marker=dict(size=40, color='rgba(0,0,0,0)'),
185
+ hovertext=node_hover_texts,
186
+ hoverinfo='text'
187
+ ))
188
+
189
+ # アイコン画像
190
+ node_images = []
191
+ for node in G.nodes():
192
+ info = get_profile_info(client, node)
193
+ if info:
194
+ node_images.append(dict(
195
+ source=info['avatar'], xref="x", yref="y", x=pos[node][0], y=pos[node][1],
196
+ sizex=0.18, sizey=0.18, xanchor="center", yanchor="middle", layer="above"
197
+ ))
198
+
199
+ fig_net.update_layout(
200
+ images=node_images, showlegend=False, plot_bgcolor='white',
201
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
202
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
203
+ margin=dict(t=40, b=0, l=0, r=0)
204
+ )
205
 
206
+ return stats_html, rank_html, posts_html, fig_bar, fig_net, "解析完了!"
207
+
208
  except Exception as e:
209
+ import traceback
210
+ print(traceback.format_exc())
211
  return f"エラー: {str(e)}", "", "", None, None, "失敗"
212
 
213
  # --- UI定義 ---
214
  with gr.Blocks(title="Bluesky Dashboard") as demo:
215
+ gr.Markdown("# 🦋 Bluesky インタラクション・ダッシュボード")
216
 
217
  with gr.Row():
218
  with gr.Column(scale=1):
 
231
 
232
  with gr.Row():
233
  out_bar = gr.Plot(label="投稿頻度")
234
+ out_net = gr.Plot(label="ユーザーネットワーク (ホバーで詳細表示)")
235
 
236
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
237
  outputs=[out_stats, out_rank, out_posts, out_bar, out_net, status])
238
 
239
+ if __name__ == "__main__":
240
+ demo.launch()