Nyanpre commited on
Commit
9cb89c5
·
verified ·
1 Parent(s): 4044f12

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -73
app.py CHANGED
@@ -8,7 +8,7 @@ import plotly.express as px
8
  import plotly.graph_objects as go
9
  import networkx as nx
10
  import random
11
- from atproto_client.models.app.bsky.feed.defs import PostView, ReplyRef
12
 
13
  def get_profile_info(client, did_or_handle):
14
  try:
@@ -29,13 +29,13 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
29
  profile = client.get_profile(actor=target_handle)
30
 
31
  posts_data = []
 
 
32
  interaction_pairs = []
33
 
34
- pos_words = ["嬉しい", "楽しい", "最高", "ありがとう", "感謝", "好き", "わ", "おめでとう", "楽しみ", "美味しい", "良き", "笑", "w"]
35
- neg_words = ["疲れた", "最悪", "悲しい", "辛い", "苦しい", "嫌い", "しんどい", "ムリ", "残念", "ひどい", "鬱", "おこ", "死ぬ", "ため息"]
36
-
37
- progress(0, desc="フィードを深層解析中...")
38
  cursor = None
 
39
  for _ in range(50):
40
  try:
41
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
@@ -44,124 +44,157 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
44
 
45
  for feed_view in response.feed:
46
  post = feed_view.post
 
47
 
48
- # --- エラ回避用のチェック ---
49
- # 投稿が「PostView」型(正常な投稿)でない場合はスキップ
50
- if not isinstance(post, PostView):
51
- continue
52
-
53
  created_at_raw = getattr(post.record, 'created_at', None)
54
  if created_at_raw:
55
  dt_jst = pd.to_datetime(created_at_raw) + timedelta(hours=9)
56
  if post.author.handle == target_handle:
57
- text = getattr(post.record, 'text', "")
58
- s_val = "Neutral"
59
- if any(w in text for w in pos_words): s_val = "Positive"
60
- elif any(w in text for w in neg_words): s_val = "Negative"
61
-
62
  posts_data.append({
 
63
  'created_at': dt_jst,
64
- 'sentiment': s_val,
65
- 'text': text,
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
  })
70
 
71
- # --- インタラクション抽出のエラー回避 ---
72
  u_author = post.author.handle
 
73
  if feed_view.reason:
74
- interaction_pairs.append((u_author, target_handle))
 
75
 
76
- # リプライ先の投稿存在し、かつ正常な投稿(PostView)であるか確認
77
- reply = getattr(feed_view, 'reply', None)
78
- if reply and isinstance(reply.parent, PostView):
79
- u_parent = reply.parent.author.handle
80
- interaction_pairs.append((u_author, u_parent))
 
 
 
 
81
 
82
  cursor = response.cursor
83
  if not cursor: break
84
 
85
- if not posts_data: return "データなし", "", "", None, None, None, None, "失敗"
86
 
87
  df = pd.DataFrame(posts_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- # --- 感情オーラグラフ ---
90
- df['date'] = df['created_at'].dt.date
91
- aura_df = df.groupby(['date', 'sentiment']).size().reset_index(name='count')
92
- fig_aura = px.area(
93
- aura_df, x="date", y="count", color="sentiment",
94
- title="✨ 心のオーラ・パレット(感情の推移)",
95
- color_discrete_map={'Positive': '#FFADAD', 'Negative': '#A0C4FF', 'Neutral': '#E2E2E2'},
96
- template="plotly_white"
97
- )
98
-
99
- # --- 相関図 ---
100
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
101
  top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
102
  nodes = list(set([target_handle] + top_10))
103
-
104
- RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同じ趣味 of 同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱えの料理人"]
105
  node_attrs = {n: (random.choice(RELATIONSHIPS) if n != target_handle else "(本人)") for n in nodes}
106
 
107
- G = nx.DiGraph()
108
- for (u_f, u_t) in interaction_pairs:
109
- if u_f in nodes and u_t in nodes and u_f != u_t:
110
- if G.has_edge(u_f, u_t): G[u_f][u_t]['weight'] += 1
111
- else: G.add_edge(u_f, u_t, weight=1)
112
 
113
- pos = nx.spring_layout(G, k=1.1, seed=42)
114
  fig_net = go.Figure()
 
115
  for edge in G.edges():
116
- start, end = edge
117
- w = G[start][end]['weight']
118
- fig_net.add_trace(go.Scatter(x=[pos[start][0], pos[end][0], None], y=[pos[start][1], pos[end][1], None],
119
- mode='lines', line=dict(width=min(w, 5), color='rgba(200,200,200,0.6)'), hoverinfo='none'))
120
 
121
  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()]
122
- fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in G.nodes()], y=[pos[n][1] for n in G.nodes()],
123
- mode='markers+text', text=node_labels, textposition="bottom center",
124
- marker=dict(size=40, color='rgba(0,0,0,0)'), hoverinfo='none'))
125
 
126
  node_images = []
127
  for node in G.nodes():
128
  info = get_profile_info(client, node)
129
  if info:
130
- 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"))
131
- 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)
132
 
133
- 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>"
134
- top_posts = df.sort_values('score', ascending=False).head(3)
135
- posts_html = "<b>🏆 ベストポスト</b><br>"
136
- for _, row in top_posts.iterrows():
137
- 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>"
138
 
139
- return stats_html, "", posts_html, None, None, fig_aura, fig_net, "解析完了!"
140
 
141
  except Exception as e:
142
- return f"エラー: {str(e)}", "", "", None, None, None, None, "失敗"
143
 
144
- with gr.Blocks() as demo:
145
- gr.Markdown("# 🦋 Bluesky 深層オーラ & 相関図 (修正版)")
146
  with gr.Row():
147
  with gr.Column(scale=1):
148
  my_id = gr.Textbox(label="自分のハンドル")
149
  my_pw = gr.Textbox(label="アプリパスワード", type="password")
150
  target_id = gr.Textbox(label="解析対象")
151
- btn = gr.Button("解析実行", variant="primary")
 
152
  status = gr.Textbox(label="ステータス", interactive=False)
153
  with gr.Column(scale=2):
154
  out_stats = gr.HTML()
155
- out_posts = gr.HTML()
156
-
157
- with gr.Tabs():
158
- with gr.TabItem("✨ 心のオーラ"):
159
- out_aura = gr.Plot()
160
- with gr.TabItem("🤝 人間関係相関図"):
161
- out_net = gr.Plot()
162
-
163
- btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, gr.State("日ごと")],
164
- outputs=[out_stats, gr.State(""), out_posts, gr.State(None), gr.State(None), out_aura, out_net, status])
 
 
 
165
 
166
  if __name__ == "__main__":
167
  demo.launch()
 
8
  import plotly.graph_objects as go
9
  import networkx as nx
10
  import random
11
+ from atproto_client.models.app.bsky.feed.defs import PostView
12
 
13
  def get_profile_info(client, did_or_handle):
14
  try:
 
29
  profile = client.get_profile(actor=target_handle)
30
 
31
  posts_data = []
32
+ reply_users_list = []
33
+ repost_users_list = []
34
  interaction_pairs = []
35
 
36
+ progress(0, desc="フィドを解析中...")
 
 
 
37
  cursor = None
38
+ # より多くの繋がりを検出するため50回ループ(最大5000件)
39
  for _ in range(50):
40
  try:
41
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
 
44
 
45
  for feed_view in response.feed:
46
  post = feed_view.post
47
+ if not isinstance(post, PostView): continue
48
 
49
+ # A. 投稿デ
 
 
 
 
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
  posts_data.append({
55
+ 'text': getattr(post.record, 'text', ""),
56
  'created_at': dt_jst,
57
+ 'hour': dt_jst.hour,
58
+ 'weekday': dt_jst.day_name(),
59
  'likes': getattr(post, 'like_count', 0),
60
  'reposts': getattr(post, 'repost_count', 0),
61
  'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
62
  })
63
 
64
+ # B. インタラクション抽出 (自分以外同士の繋がりを強化)
65
  u_author = post.author.handle
66
+ # 1. リポストによる繋がり
67
  if feed_view.reason:
68
+ interaction_pairs.append((target_handle, u_author))
69
+ if u_author != target_handle: repost_users_list.append(u_author)
70
 
71
+ # 2. リプライによる繋り(投稿者と返信先のペアをすべて記録)
72
+ if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
73
+ parent_post = feed_view.reply.parent
74
+ if isinstance(parent_post, PostView):
75
+ u_parent = parent_post.author.handle
76
+ # このペアを記録(自分以外同士も含まれる)
77
+ interaction_pairs.append((u_author, u_parent))
78
+ if u_author == target_handle and u_parent != target_handle:
79
+ reply_users_list.append(u_parent)
80
 
81
  cursor = response.cursor
82
  if not cursor: break
83
 
84
+ if not posts_data: return "データなし", "", "", None, None, None, "失敗"
85
 
86
  df = pd.DataFrame(posts_data)
87
+ # 初ポスト日(取得できた範囲での最古)
88
+ first_post_date = df['created_at'].min().strftime('%Y/%m/%d')
89
+
90
+ # --- HTML作成 ---
91
+ stats_html = f"""
92
+ <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;'>
93
+ <div style='background: #e3f2fd; padding: 10px; border-radius: 8px; text-align: center;'>
94
+ <small>総ポスト</small><br><b>{getattr(profile, 'posts_count', 0)}</b>
95
+ </div>
96
+ <div style='background: #f1f8e9; padding: 10px; border-radius: 8px; text-align: center;'>
97
+ <small>活動開始(初ポスト)</small><br><b>{first_post_date}</b>
98
+ </div>
99
+ <div style='background: #fff3e0; padding: 10px; border-radius: 8px; text-align: center;'>
100
+ <small>分析件数</small><br><b>{len(df)}件</b>
101
+ </div>
102
+ </div>
103
+ """
104
+
105
+ def rank_box(title, items):
106
+ top = Counter(items).most_common(3)
107
+ res = f"<b>{title}</b><br>"
108
+ res += "<br>".join([f"@{n} ({c}回)" for n, c in top]) if top else "なし"
109
+ return res
110
+
111
+ rank_html = f"<div style='background: #f9f9f9; padding: 15px; border-radius: 10px; font-size: 0.9em;'>{rank_box('💬 リプライ相手TOP3', reply_users_list)}<hr>{rank_box('🔄 リポスト相手TOP3', repost_users_list)}</div>"
112
+
113
+ top_posts = df.sort_values('score', ascending=False).head(3)
114
+ posts_html = "<b>🏆 ベストポスト</b><br>"
115
+ for _, row in top_posts.iterrows():
116
+ 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>"
117
+
118
+ # --- グラフ作成 ---
119
+ freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
120
+ df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
121
+ fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿数の推移", color_discrete_sequence=['#0085ff'])
122
+
123
+ week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
124
+ week_ja = ['月', '火', '水', '木', '金', '土', '日']
125
+ heat_df = df.groupby(['weekday', 'hour']).size().reset_index(name='count')
126
+ heat_pt = heat_df.pivot(index='weekday', columns='hour', values='count').reindex(week_order).fillna(0)
127
+ heat_pt.index = week_ja
128
+ fig_heat = px.imshow(heat_pt, labels=dict(x="時間 (24h)", y="曜日", color="投稿数"), x=list(range(24)), y=week_ja, color_continuous_scale='Blues', title="週間アクティビティ・ヒートマップ")
129
+
130
+ # --- 相関図 (関係性上書き & 自分以外同士の線) ---
131
+ progress(0.8, desc="相関図を生成中...")
132
+ RELATIONSHIPS = [
133
+ "家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁",
134
+ "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同じ趣味 of 同志",
135
+ "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者",
136
+ "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱えの料理人"
137
+ ]
138
 
 
 
 
 
 
 
 
 
 
 
 
139
  all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
140
  top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
141
  nodes = list(set([target_handle] + top_10))
 
 
142
  node_attrs = {n: (random.choice(RELATIONSHIPS) if n != target_handle else "(本人)") for n in nodes}
143
 
144
+ G = nx.Graph()
145
+ for (u1, u2) in interaction_pairs:
146
+ if u1 in nodes and u2 in nodes and u1 != u2:
147
+ if G.has_edge(u1, u2): G[u1][u2]['weight'] += 1
148
+ else: G.add_edge(u1, u2, weight=1)
149
 
150
+ pos = nx.spring_layout(G, k=1.0, seed=42) # 少し広めに配置
151
  fig_net = go.Figure()
152
+
153
  for edge in G.edges():
154
+ w = G[edge[0]][edge[1]]['weight']
155
+ 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'))
 
 
156
 
157
  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()]
158
+ 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'))
 
 
159
 
160
  node_images = []
161
  for node in G.nodes():
162
  info = get_profile_info(client, node)
163
  if info:
164
+ 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"))
 
165
 
166
+ 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="🦋 脳内ユザー相関図 (自分以外も繋がります)")
 
 
 
 
167
 
168
+ return stats_html, rank_html, posts_html, fig_bar, fig_heat, fig_net, "解析完了!"
169
 
170
  except Exception as e:
171
+ return f"エラー: {str(e)}", "", "", None, None, None, "失敗"
172
 
173
+ with gr.Blocks(title="Bluesky Ultimate Analyzer") as demo:
174
+ gr.Markdown("# 🦋 Bluesky アクティビティ & 脳内相関図")
175
  with gr.Row():
176
  with gr.Column(scale=1):
177
  my_id = gr.Textbox(label="自分のハンドル")
178
  my_pw = gr.Textbox(label="アプリパスワード", type="password")
179
  target_id = gr.Textbox(label="解析対象")
180
+ freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="推移グラフの単位", value="日ごと")
181
+ btn = gr.Button("解析を開始する", variant="primary")
182
  status = gr.Textbox(label="ステータス", interactive=False)
183
  with gr.Column(scale=2):
184
  out_stats = gr.HTML()
185
+ with gr.Row():
186
+ out_rank = gr.HTML()
187
+ out_posts = gr.HTML()
188
+
189
+ with gr.Tab("アクティビティ推移"):
190
+ out_bar = gr.Plot(label="投稿頻度の推移")
191
+ out_heat = gr.Plot(label="曜日×時間ヒートマップ")
192
+
193
+ with gr.Tab("ユーザー相関図"):
194
+ out_net = gr.Plot(label="脳内相関図")
195
+
196
+ btn.click(analyze_and_output, inputs=[my_id, my_pw, target_id, freq],
197
+ outputs=[out_stats, out_rank, out_posts, out_bar, out_heat, out_net, status])
198
 
199
  if __name__ == "__main__":
200
  demo.launch()