Nyanpre commited on
Commit
9febb92
·
verified ·
1 Parent(s): 8ef0c58

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -62
app.py CHANGED
@@ -11,6 +11,34 @@ import random
11
  import re
12
  from atproto_client.models.app.bsky.feed.defs import PostView
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def get_profile_info(client, did_or_handle):
15
  try:
16
  profile = client.get_profile(actor=did_or_handle)
@@ -33,9 +61,9 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
33
  reply_users_list = []
34
  repost_users_list = []
35
  interaction_pairs = []
36
- ends_list = [] # 語尾コレクション用
37
 
38
- progress(0, desc="フィードを解析中...")
39
  cursor = None
40
  for _ in range(50):
41
  try:
@@ -47,18 +75,14 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
47
  post = feed_view.post
48
  if not isinstance(post, PostView): continue
49
 
50
- # A. 投稿データ
51
  created_at_raw = getattr(post.record, 'created_at', None)
52
  if created_at_raw:
53
  dt_jst = pd.to_datetime(created_at_raw) + timedelta(hours=9)
54
  if post.author.handle == target_handle:
55
  text = getattr(post.record, 'text', "")
56
 
57
- # --- 口癖(語尾)抽出ロジック ---
58
- # 記号や改行を除去して文末を特定
59
  clean_text = re.sub(r'[!\?!?。\n\s]+$', '', text)
60
  if len(clean_text) >= 2:
61
- # 文末2〜3文字を抽出(ひらがな中心にヒットしやすく調整)
62
  ending = clean_text[-3:]
63
  if re.search(r'[ぁ-んーwWwW]$', ending):
64
  ends_list.append(ending)
@@ -73,7 +97,6 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
73
  'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
74
  })
75
 
76
- # B. インタラクション抽出
77
  u_author = post.author.handle
78
  if feed_view.reason:
79
  interaction_pairs.append((target_handle, u_author))
@@ -95,112 +118,106 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
95
  df = pd.DataFrame(posts_data)
96
  first_post_date = df['created_at'].min().strftime('%Y/%m/%d')
97
 
98
- # --- HTML作成 ---
99
  stats_html = f"""
100
- <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;'>
101
- <div style='background: #e3f2fd; padding: 10px; border-radius: 8px; text-align: center;'>
102
- <small>総ポスト</small><br><b>{getattr(profile, 'posts_count', 0)}</b>
103
- </div>
104
- <div style='background: #f1f8e9; padding: 10px; border-radius: 8px; text-align: center;'>
105
- <small>活動開始</small><br><b>{first_post_date}</b>
106
- </div>
107
- <div style='background: #fff3e0; padding: 10px; border-radius: 8px; text-align: center;'>
108
- <small>分析件数</small><br><b>{len(df)}件</b>
109
- </div>
110
  </div>
111
  """
112
 
113
- def rank_box(title, items, icon=""):
114
  top = Counter(items).most_common(3)
115
- res = f"<b>{icon} {title}</b><br>"
116
- res += "<br>".join([f"{n} ({c}回)" if "@" not in str(n) else f"@{n} ({c}回)" for n, c in top]) if top else "なし"
 
 
 
 
117
  return res
118
 
119
  rank_html = f"""
120
- <div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>
121
- {rank_box('リプライ相手TOP3', reply_users_list, '💬')}
122
- <hr>
123
- {rank_box('リポスト相手TOP3', repost_users_list, '🔄')}
124
- <hr>
125
- {rank_box('よく使う語尾', ends_list, '🗣️')}
126
  </div>
127
  """
128
 
129
  top_posts = df.sort_values('score', ascending=False).head(3)
130
- posts_html = "<b>🏆 ベストポスト</b><br>"
131
  for _, row in top_posts.iterrows():
132
- posts_html += f"<div style='margin-bottom:8px; font-size:0.85em; border-left:3px solid #0085ff; padding-left:5px;'>{row['text'][:60]}...<br><small>❤️ {row['likes']} 🔄 {row['reposts']}</small></div>"
 
133
 
134
  # --- グラフ作成 ---
135
  freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
136
  df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
137
- fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿の推移", color_discrete_sequence=['#0085ff'])
138
 
139
  week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
140
  heat_pt = df.groupby(['weekday', 'hour']).size().reset_index(name='count').pivot(index='weekday', columns='hour', values='count').reindex(week_order).fillna(0)
141
  heat_pt.index = ['月', '火', '水', '木', '金', '土', '日']
142
- fig_heat = px.imshow(heat_pt, labels=dict(x="時間 (24h)", y="曜日", color="投稿数"), x=list(range(24)), y=heat_pt.index, color_continuous_scale='Blues', title="間アクティビティ")
143
 
144
  # --- 相関図 ---
145
- progress(0.8, desc="相関図を生成中...")
146
- RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同じ趣味 of 同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱えの料理人"]
147
-
148
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
149
- top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
150
  nodes = list(set([target_handle] + top_10))
151
- node_attrs = {n: (random.choice(RELATIONSHIPS) if n != target_handle else "(本人)") for n in nodes}
152
-
153
  G = nx.Graph()
154
  for (u1, u2) in interaction_pairs:
155
  if u1 in nodes and u2 in nodes and u1 != u2:
156
  if G.has_edge(u1, u2): G[u1][u2]['weight'] += 1
157
  else: G.add_edge(u1, u2, weight=1)
158
 
159
- pos = nx.spring_layout(G, k=1.0, seed=42)
160
  fig_net = go.Figure()
161
-
162
  for edge in G.edges():
163
  w = G[edge[0]][edge[1]]['weight']
164
- 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'))
165
-
166
- 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()]
167
- 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)'), hoverinfo='none'))
168
 
169
  node_images = []
170
  for node in G.nodes():
171
  info = get_profile_info(client, node)
172
  if info:
173
- node_images.append(dict(source=info['avatar'], xref="x", yref="y", x=pos[node][0], y=pos[node][1], sizex=0.22, sizey=0.22, xanchor="center", yanchor="middle", layer="above"))
174
-
175
- 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=700, title="🦋 脳内ユーザー相関図 (自分以外も繋がります)")
176
 
177
- return stats_html, rank_html, posts_html, fig_bar, fig_heat, fig_net, "解析完了!"
178
 
179
  except Exception as e:
180
- return f"エラー: {str(e)}", "", "", None, None, None, "失敗"
181
 
182
- with gr.Blocks(title="Bluesky Ultimate Analyzer") as demo:
183
- gr.Markdown("# 🦋 Bluesky アクティビティ & 脳内相関図")
 
 
184
  with gr.Row():
185
  with gr.Column(scale=1):
186
- my_id = gr.Textbox(label="自分のハンドル")
187
- my_pw = gr.Textbox(label="アプリパスワード", type="password")
188
- target_id = gr.Textbox(label="解析対象")
189
- freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="推移グラフの単位", value="日ごと")
190
- btn = gr.Button("解析を開始する", variant="primary")
191
- status = gr.Textbox(label="ステータス", interactive=False)
 
 
192
  with gr.Column(scale=2):
193
  out_stats = gr.HTML()
194
  with gr.Row():
195
  out_rank = gr.HTML()
196
  out_posts = gr.HTML()
197
 
198
- with gr.Tab("アクティビティ推移"):
199
- out_bar = gr.Plot(label="投稿頻度の推移")
200
- out_heat = gr.Plot(label="曜日×時間ヒートマップ")
201
-
202
- with gr.Tab("ユーザー相関図"):
203
- out_net = gr.Plot(label="脳内相")
 
204
 
205
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
206
  outputs=[out_stats, out_rank, out_posts, out_bar, out_heat, out_net, status])
 
11
  import re
12
  from atproto_client.models.app.bsky.feed.defs import PostView
13
 
14
+ # --- カスタムCSS ---
15
+ CUSTOM_CSS = """
16
+ body { background-color: #f0f7ff; }
17
+ .gradio-container { font-family: 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif; }
18
+ .stat-card {
19
+ background: white; padding: 20px; border-radius: 15px;
20
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05); text-align: center;
21
+ border-bottom: 4px solid #0085ff;
22
+ }
23
+ .stat-value { font-size: 1.8em; font-weight: bold; color: #0085ff; }
24
+ .rank-card {
25
+ background: #ffffff; padding: 15px; border-radius: 15px;
26
+ border: 1px solid #e1e8ed; font-size: 0.9em;
27
+ }
28
+ .rank-header {
29
+ color: #111; font-weight: bold; margin-bottom: 10px;
30
+ border-bottom: 1px solid #eee; padding-bottom: 5px;
31
+ }
32
+ .best-post-card {
33
+ background: #f8f9fa; border-left: 5px solid #0085ff;
34
+ padding: 10px; margin-bottom: 10px; border-radius: 5px;
35
+ }
36
+ button.primary {
37
+ background: linear-gradient(135deg, #0085ff 0%, #00bfff 100%) !important;
38
+ border: none !important; color: white !important;
39
+ }
40
+ """
41
+
42
  def get_profile_info(client, did_or_handle):
43
  try:
44
  profile = client.get_profile(actor=did_or_handle)
 
61
  reply_users_list = []
62
  repost_users_list = []
63
  interaction_pairs = []
64
+ ends_list = []
65
 
66
+ progress(0, desc="Blueskyの風を解析中...")
67
  cursor = None
68
  for _ in range(50):
69
  try:
 
75
  post = feed_view.post
76
  if not isinstance(post, PostView): continue
77
 
 
78
  created_at_raw = getattr(post.record, 'created_at', None)
79
  if created_at_raw:
80
  dt_jst = pd.to_datetime(created_at_raw) + timedelta(hours=9)
81
  if post.author.handle == target_handle:
82
  text = getattr(post.record, 'text', "")
83
 
 
 
84
  clean_text = re.sub(r'[!\?!?。\n\s]+$', '', text)
85
  if len(clean_text) >= 2:
 
86
  ending = clean_text[-3:]
87
  if re.search(r'[ぁ-んーwWwW]$', ending):
88
  ends_list.append(ending)
 
97
  'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
98
  })
99
 
 
100
  u_author = post.author.handle
101
  if feed_view.reason:
102
  interaction_pairs.append((target_handle, u_author))
 
118
  df = pd.DataFrame(posts_data)
119
  first_post_date = df['created_at'].min().strftime('%Y/%m/%d')
120
 
121
+ # --- 新・HTMLレイアウト ---
122
  stats_html = f"""
123
+ <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px;'>
124
+ <div class='stat-card'><small>総ポスト数</small><div class='stat-value'>{getattr(profile, 'posts_count', 0)}</div></div>
125
+ <div class='stat-card'><small>観測開始日</small><div class='stat-value' style='font-size:1.2em; padding: 6px 0;'>{first_post_date}</div></div>
126
+ <div class='stat-card'><small>分析サンプル</small><div class='stat-value'>{len(df)}</div></div>
 
 
 
 
 
 
127
  </div>
128
  """
129
 
130
+ def rank_box_styled(title, items, icon=""):
131
  top = Counter(items).most_common(3)
132
+ res = f"<div class='rank-card'><div class='rank-header'>{icon} {title}</div>"
133
+ if top:
134
+ res += "".join([f"<div style='margin-bottom:4px;'><b>{n}</b> <span style='color:#666; float:right;'>{c}回</span></div>" for n, c in top])
135
+ else:
136
+ res += "<div style='color:#ccc;'>未検出</div>"
137
+ res += "</div>"
138
  return res
139
 
140
  rank_html = f"""
141
+ <div style='display: flex; flex-direction: column; gap: 10px;'>
142
+ {rank_box_styled('リプライ相手', reply_users_list, '💬')}
143
+ {rank_box_styled('リポスト相手', repost_users_list, '🔄')}
144
+ {rank_box_styled('よく使う語尾', ends_list, '🗣️')}
 
 
145
  </div>
146
  """
147
 
148
  top_posts = df.sort_values('score', ascending=False).head(3)
149
+ posts_html = "<div class='rank-card'><div class='rank-header'>🏆 ベストポスト</div>"
150
  for _, row in top_posts.iterrows():
151
+ posts_html += f"<div class='best-post-card'>{row['text'][:80]}...<br><small style='color:#0085ff;'>❤️ {row['likes']} &nbsp; 🔄 {row['reposts']}</small></div>"
152
+ posts_html += "</div>"
153
 
154
  # --- グラフ作成 ---
155
  freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
156
  df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
157
+ fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿エネルギーの推移", color_discrete_sequence=['#0085ff'], template="plotly_white")
158
 
159
  week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
160
  heat_pt = df.groupby(['weekday', 'hour']).size().reset_index(name='count').pivot(index='weekday', columns='hour', values='count').reindex(week_order).fillna(0)
161
  heat_pt.index = ['月', '火', '水', '木', '金', '土', '日']
162
+ fig_heat = px.imshow(heat_pt, x=list(range(24)), y=heat_pt.index, color_continuous_scale='Blues', title="帯別アクティビティ")
163
 
164
  # --- 相関図 ---
 
 
 
165
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
166
+ top_10 = [u for u, c in Counter(all_interactors).most_common(12)]
167
  nodes = list(set([target_handle] + top_10))
168
+
 
169
  G = nx.Graph()
170
  for (u1, u2) in interaction_pairs:
171
  if u1 in nodes and u2 in nodes and u1 != u2:
172
  if G.has_edge(u1, u2): G[u1][u2]['weight'] += 1
173
  else: G.add_edge(u1, u2, weight=1)
174
 
175
+ pos = nx.spring_layout(G, k=1.2, seed=42)
176
  fig_net = go.Figure()
 
177
  for edge in G.edges():
178
  w = G[edge[0]][edge[1]]['weight']
179
+ 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='#cfd8dc', width=min(w, 4)), hoverinfo='none'))
 
 
 
180
 
181
  node_images = []
182
  for node in G.nodes():
183
  info = get_profile_info(client, node)
184
  if info:
185
+ 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"))
186
+
187
+ fig_net.update_layout(images=node_images, showlegend=False, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), height=700)
188
 
189
+ return stats_html, rank_html, posts_html, fig_bar, fig_heat, fig_net, "解析完了しました!"
190
 
191
  except Exception as e:
192
+ return f"エラーが発生しました: {str(e)}", "", "", None, None, None, "失敗"
193
 
194
+ # --- インターフェース ---
195
+ with gr.Blocks(css=CUSTOM_CSS, title="Bluesky Ultimate Dashboard") as demo:
196
+ gr.Markdown("# <p style='text-align:center; color:#0085ff;'>🦋 Bluesky Ultimate Dashboard</p>")
197
+
198
  with gr.Row():
199
  with gr.Column(scale=1):
200
+ with gr.Group():
201
+ my_id = gr.Textbox(label="自分のハンル (@なし)", placeholder="example.bsky.social")
202
+ my_pw = gr.Textbox(label="アプリパスワード", type="password")
203
+ target_id = gr.Textbox(label="解析したい相手", placeholder="target.bsky.social")
204
+ freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="グラフ単位", value="日ごと")
205
+ btn = gr.Button("解析を開始する", variant="primary")
206
+ status = gr.Markdown("待機中...")
207
+
208
  with gr.Column(scale=2):
209
  out_stats = gr.HTML()
210
  with gr.Row():
211
  out_rank = gr.HTML()
212
  out_posts = gr.HTML()
213
 
214
+ with gr.Tabs():
215
+ with gr.TabItem("📊 活動データ"):
216
+ with gr.Row():
217
+ out_bar = gr.Plot()
218
+ out_heat = gr.Plot()
219
+ with gr.TabItem("🤝 人間係マップ"):
220
+ out_net = gr.Plot()
221
 
222
  btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
223
  outputs=[out_stats, out_rank, out_posts, out_bar, out_heat, out_net, status])