Nyanpre commited on
Commit
a12dbd5
·
verified ·
1 Parent(s): 0973e66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -51
app.py CHANGED
@@ -2,7 +2,7 @@ import gradio as gr
2
  import pandas as pd
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
@@ -33,7 +33,6 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
33
  posts_data = []
34
  reply_users_list = []
35
  repost_users_list = []
36
- like_users_list = []
37
  interactions_for_network = []
38
  interaction_data = {} # { handle: Counter }
39
 
@@ -51,22 +50,26 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
51
 
52
  for feed_view in response.feed:
53
  post = feed_view.post
54
-
55
- # 投稿が正常に取得できている場合のみ処理 (NotFoundPost対策)
56
  if not isinstance(post, PostView):
57
  continue
58
 
59
- # A. 投稿データの保存(本人のポストのみ
60
  if post.author.handle == target_handle:
61
  created_at_raw = getattr(post.record, 'created_at', None)
62
  if created_at_raw:
 
 
 
 
63
  posts_data.append({
64
  'text': getattr(post.record, 'text', ""),
65
- 'created_at': pd.to_datetime(created_at_raw),
 
 
 
66
  'likes': getattr(post, 'like_count', 0),
67
  'reposts': getattr(post, 'repost_count', 0),
68
- 'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0),
69
- 'url': f"https://bsky.app/profile/{target_handle}/post/{post.uri.split('/')[-1]}"
70
  })
71
 
72
  # B. リポストのカウント
@@ -81,7 +84,6 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
81
  # C. リプライのカウント
82
  elif post.author.handle == target_handle:
83
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
84
- # 親ポストが削除されている可能性をチェック
85
  parent_post = feed_view.reply.parent
86
  if isinstance(parent_post, PostView):
87
  u = parent_post.author.handle
@@ -94,27 +96,12 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
94
  cursor = response.cursor
95
  if not cursor: break
96
 
97
- # --- 3. いいねのカウント ---
98
- try:
99
- likes_resp = client.get_actor_likes(actor=profile.did, limit=50)
100
- for l in likes_resp.feed:
101
- # 投稿が削除されていない(PostViewである)ことを確認
102
- if isinstance(l.post, PostView):
103
- u = l.post.author.handle
104
- if u != target_handle:
105
- if u not in interaction_data: interaction_data[u] = Counter()
106
- interaction_data[u]['like'] += 1
107
- like_users_list.append(u)
108
- interactions_for_network.append((target_handle, u))
109
- except: pass
110
-
111
- # --- 4. 解析とHTML作成 ---
112
  if not posts_data:
113
- return "データ取得失敗または投稿がありません", "", "", None, None, "失敗"
114
 
115
  df = pd.DataFrame(posts_data)
116
- df['created_at_dt'] = df['created_at'].dt.tz_localize(None)
117
- days_active = max((datetime.now() - df['created_at_dt'].min()).days, 1)
118
 
119
  stats_html = f"""
120
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;'>
@@ -138,9 +125,8 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
138
 
139
  rank_html = f"""
140
  <div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>
141
- {rank_box("💬 リプライ相手", reply_users_list)}<hr>
142
- {rank_box("🔄 リポスト相手", repost_users_list)}<hr>
143
- {rank_box("❤️ いいね相手", like_users_list)}
144
  </div>
145
  """
146
 
@@ -149,15 +135,32 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
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 + like_users_list).most_common(12)]
161
  nodes = list(set([target_handle] + top_interactors))
162
 
163
  for (s, t) in interactions_for_network:
@@ -168,21 +171,21 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
168
  pos = nx.spring_layout(G, k=0.6, seed=42)
169
  fig_net = go.Figure()
170
 
171
- # エッジ(線)
172
  edge_x, edge_y = [], []
173
  for edge in G.edges():
174
  edge_x += [pos[edge[0]][0], pos[edge[1]][0], None]
175
  edge_y += [pos[edge[0]][1], pos[edge[1]][1], None]
176
  fig_net.add_trace(go.Scatter(x=edge_x, y=edge_y, mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
177
 
178
- # ホバー用の透明な点
179
  node_hover_texts = []
180
  for node in G.nodes():
181
  if node == target_handle:
182
  node_hover_texts.append(f"<b>{node} (解析対象)</b>")
183
  else:
184
  c = interaction_data.get(node, Counter())
185
- node_hover_texts.append(f"<b>@{node}</b><br>❤️ いいね: {c['like']}<br>🔄 リポスト: {c['repost']}<br>💬 リプライ: {c['reply']}")
186
 
187
  fig_net.add_trace(go.Scatter(
188
  x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
@@ -190,8 +193,7 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
190
  text=[n if n != target_handle else "" for n in G.nodes()],
191
  textposition="bottom center",
192
  marker=dict(size=40, color='rgba(0,0,0,0)'),
193
- hovertext=node_hover_texts,
194
- hoverinfo='text'
195
  ))
196
 
197
  # アイコン画像
@@ -211,24 +213,24 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
211
  margin=dict(t=40, b=0, l=0, r=0)
212
  )
213
 
214
- return stats_html, rank_html, posts_html, fig_bar, fig_net, "解析完了!"
215
 
216
  except Exception as e:
217
  import traceback
218
  print(traceback.format_exc())
219
- return f"エラー: {str(e)}", "", "", None, None, "失敗"
220
 
221
  # --- UI定義 ---
222
- with gr.Blocks(title="Bluesky Dashboard") as demo:
223
- gr.Markdown("# 🦋 Bluesky インタラション・ダッシュボード")
224
 
225
  with gr.Row():
226
  with gr.Column(scale=1):
227
  my_id = gr.Textbox(label="自分のハンドル", placeholder="me.bsky.social")
228
  my_pw = gr.Textbox(label="アプリパスワード", type="password")
229
- target_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social")
230
- freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="グラフ単位", value="日ごと")
231
- btn = gr.Button("析開始", variant="primary")
232
  status = gr.Textbox(label="ステータス", interactive=False)
233
 
234
  with gr.Column(scale=2):
@@ -238,11 +240,15 @@ with gr.Blocks(title="Bluesky Dashboard") as demo:
238
  out_posts = gr.HTML()
239
 
240
  with gr.Row():
241
- out_bar = gr.Plot(label="投稿頻度")
242
- out_net = gr.Plot(label="ユーザーネットワーク (ホバーで詳細表示)")
 
 
 
 
243
 
244
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
245
- outputs=[out_stats, out_rank, out_posts, out_bar, out_net, status])
246
 
247
  if __name__ == "__main__":
248
  demo.launch()
 
2
  import pandas as pd
3
  from atproto import Client
4
  import time
5
+ from datetime import datetime, timedelta
6
  from collections import Counter
7
  import plotly.express as px
8
  import plotly.graph_objects as go
 
33
  posts_data = []
34
  reply_users_list = []
35
  repost_users_list = []
 
36
  interactions_for_network = []
37
  interaction_data = {} # { handle: Counter }
38
 
 
50
 
51
  for feed_view in response.feed:
52
  post = feed_view.post
 
 
53
  if not isinstance(post, PostView):
54
  continue
55
 
56
+ # A. 投稿データの保存(時間に変換
57
  if post.author.handle == target_handle:
58
  created_at_raw = getattr(post.record, 'created_at', None)
59
  if created_at_raw:
60
+ # UTCから日本時間(JST)へ変換 (+9時間)
61
+ dt_utc = pd.to_datetime(created_at_raw)
62
+ dt_jst = dt_utc + timedelta(hours=9)
63
+
64
  posts_data.append({
65
  'text': getattr(post.record, 'text', ""),
66
+ 'created_at': dt_jst,
67
+ 'hour': dt_jst.hour,
68
+ 'weekday': dt_jst.day_name(),
69
+ 'weekday_num': dt_jst.weekday(), # 月=0, 日=6
70
  'likes': getattr(post, 'like_count', 0),
71
  'reposts': getattr(post, 'repost_count', 0),
72
+ 'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
 
73
  })
74
 
75
  # B. リポストのカウント
 
84
  # C. リプライのカウント
85
  elif post.author.handle == target_handle:
86
  if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
 
87
  parent_post = feed_view.reply.parent
88
  if isinstance(parent_post, PostView):
89
  u = parent_post.author.handle
 
96
  cursor = response.cursor
97
  if not cursor: break
98
 
99
+ # --- 3. 解析とHTML作成 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  if not posts_data:
101
+ return "データがありません", "", "", None, None, None, None, "失敗"
102
 
103
  df = pd.DataFrame(posts_data)
104
+ days_active = max((datetime.now() - df['created_at'].min().replace(tzinfo=None)).days, 1)
 
105
 
106
  stats_html = f"""
107
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;'>
 
125
 
126
  rank_html = f"""
127
  <div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>
128
+ {rank_box("💬 よくリプライする相手", reply_users_list)}<hr>
129
+ {rank_box("🔄 よくリポストする相手", repost_users_list)}
 
130
  </div>
131
  """
132
 
 
135
  for _, row in top_posts.iterrows():
136
  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>"
137
 
138
+ # --- 4. グラフ作成 ---
139
+ # A. アクティビティ推移
140
  freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
141
+ df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
142
+ fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿数の推移", color_discrete_sequence=['#0085ff'])
143
+
144
+ # B. 時間帯ヒトマップ
145
+ hour_counts = df['hour'].value_counts().reindex(range(24), fill_value=0).reset_index()
146
+ hour_counts.columns = ['hour', 'count']
147
+ fig_hour = px.bar(hour_counts, x='hour', y='count', title="時間帯別アクティビティ (日本時間)",
148
+ labels={'hour':'時間 (24h)', 'count':'投稿数'}, color='count', color_continuous_scale='Viridis')
149
+ fig_hour.update_layout(xaxis=dict(tickmode='linear', tick0=0, dtick=2))
150
+
151
+ # C. 曜日別アクティビティ
152
+ week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
153
+ week_ja = {'Monday':'月', 'Tuesday':'火', 'Wednesday':'水', 'Thursday':'木', 'Friday':'金', 'Saturday':'土', 'Sunday':'日'}
154
+ df['weekday_ja'] = df['weekday'].map(week_ja)
155
+ week_counts = df['weekday'].value_counts().reindex(week_order, fill_value=0).reset_index()
156
+ week_counts['weekday_ja'] = week_counts['weekday'].map(week_ja)
157
+ fig_week = px.pie(week_counts, values='count', names='weekday_ja', title="曜日別比率",
158
+ color_discrete_sequence=px.colors.qualitative.Pastel)
159
+
160
+ # --- 5. ネットワーク図 ---
161
  progress(0.8, desc="ネットワーク図を生成中...")
162
  G = nx.Graph()
163
+ top_interactors = [u for u, c in Counter(reply_users_list + repost_users_list).most_common(12)]
164
  nodes = list(set([target_handle] + top_interactors))
165
 
166
  for (s, t) in interactions_for_network:
 
171
  pos = nx.spring_layout(G, k=0.6, seed=42)
172
  fig_net = go.Figure()
173
 
174
+ # エッジ
175
  edge_x, edge_y = [], []
176
  for edge in G.edges():
177
  edge_x += [pos[edge[0]][0], pos[edge[1]][0], None]
178
  edge_y += [pos[edge[0]][1], pos[edge[1]][1], None]
179
  fig_net.add_trace(go.Scatter(x=edge_x, y=edge_y, mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
180
 
181
+ # ホバー層(透明マーカー)
182
  node_hover_texts = []
183
  for node in G.nodes():
184
  if node == target_handle:
185
  node_hover_texts.append(f"<b>{node} (解析対象)</b>")
186
  else:
187
  c = interaction_data.get(node, Counter())
188
+ node_hover_texts.append(f"<b>@{node}</b><br>🔄 リポスト: {c['repost']}<br>💬 リプライ回数: {c['reply']}")
189
 
190
  fig_net.add_trace(go.Scatter(
191
  x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
 
193
  text=[n if n != target_handle else "" for n in G.nodes()],
194
  textposition="bottom center",
195
  marker=dict(size=40, color='rgba(0,0,0,0)'),
196
+ hovertext=node_hover_texts, hoverinfo='text'
 
197
  ))
198
 
199
  # アイコン画像
 
213
  margin=dict(t=40, b=0, l=0, r=0)
214
  )
215
 
216
+ return stats_html, rank_html, posts_html, fig_bar, fig_hour, fig_week, fig_net, "解析完了!"
217
 
218
  except Exception as e:
219
  import traceback
220
  print(traceback.format_exc())
221
+ return f"エラー: {str(e)}", "", "", None, None, None, None, "失敗"
222
 
223
  # --- UI定義 ---
224
+ with gr.Blocks(title="Bluesky Active Analyzer") as demo:
225
+ gr.Markdown("# 🦋 Bluesky ティビティ解析ダッシュボード")
226
 
227
  with gr.Row():
228
  with gr.Column(scale=1):
229
  my_id = gr.Textbox(label="自分のハンドル", placeholder="me.bsky.social")
230
  my_pw = gr.Textbox(label="アプリパスワード", type="password")
231
+ target_id = gr.Textbox(label="解析対象のハンドル", placeholder="target.bsky.social")
232
+ freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="推移グラフ単位", value="日ごと")
233
+ btn = gr.Button("開始する", variant="primary")
234
  status = gr.Textbox(label="ステータス", interactive=False)
235
 
236
  with gr.Column(scale=2):
 
240
  out_posts = gr.HTML()
241
 
242
  with gr.Row():
243
+ out_bar = gr.Plot(label="投稿頻度の推移")
244
+ out_net = gr.Plot(label="ユーザーネットワーク")
245
+
246
+ with gr.Row():
247
+ out_hour = gr.Plot(label="時間帯別アクティビティ")
248
+ out_week = gr.Plot(label="曜日別アクティビティ")
249
 
250
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
251
+ outputs=[out_stats, out_rank, out_posts, out_bar, out_hour, out_week, out_net, status])
252
 
253
  if __name__ == "__main__":
254
  demo.launch()