Update app.py
Browse files
app.py
CHANGED
|
@@ -8,51 +8,179 @@ from collections import Counter
|
|
| 8 |
import plotly.express as px
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
import networkx as nx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 13 |
try:
|
| 14 |
-
# --- 1. ログイン
|
| 15 |
client = Client()
|
| 16 |
-
|
| 17 |
target_handle = target_id.replace('@', '').strip()
|
|
|
|
|
|
|
| 18 |
profile = client.get_profile(actor=target_handle)
|
| 19 |
|
| 20 |
posts_data = []
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
|
|
|
|
|
|
| 23 |
cursor = None
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
for feed_view in response.feed:
|
|
|
|
| 27 |
post = feed_view.post
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
if not posts_data:
|
| 49 |
-
return
|
| 50 |
|
| 51 |
df = pd.DataFrame(posts_data)
|
| 52 |
-
df['created_at'] = df['created_at'].dt.tz_localize(None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
|
| 57 |
df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
|
| 58 |
|
|
@@ -62,72 +190,123 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 62 |
template="plotly_white")
|
| 63 |
fig_bar.update_traces(marker_color='#0085ff')
|
| 64 |
|
| 65 |
-
# ---
|
| 66 |
G = nx.Graph()
|
| 67 |
-
#
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
for edge in G.edges():
|
| 76 |
-
x0, y0 = pos[edge[0]]
|
| 77 |
-
x1, y1 = pos[edge[1]]
|
| 78 |
-
edge_x.extend([x0, x1, None])
|
| 79 |
-
edge_y.extend([y0, y1, None])
|
| 80 |
-
|
| 81 |
-
edge_trace = go.Scatter(x=edge_x, y=edge_y, line=dict(width=0.5, color='#888'), hoverinfo='none', mode='lines')
|
| 82 |
-
|
| 83 |
-
node_x, node_y, node_text, node_size = [], [], [], []
|
| 84 |
-
for node in G.nodes():
|
| 85 |
-
x, y = pos[node]
|
| 86 |
-
node_x.append(x)
|
| 87 |
-
node_y.append(y)
|
| 88 |
-
node_text.append(f"@{node}<br>接続数: {len(list(G.neighbors(node)))}")
|
| 89 |
-
# 中心ユーザー(本人)を大きく表示
|
| 90 |
-
node_size.append(30 if node == target_handle else 15)
|
| 91 |
-
|
| 92 |
-
node_trace = go.Scatter(
|
| 93 |
-
x=node_x, y=node_y, mode='markers', hoverinfo='text', text=node_text,
|
| 94 |
-
marker=dict(showscale=True, colorscale='YlGnBu', size=node_size,
|
| 95 |
-
colorbar=dict(thickness=15, title='Node Connections'),
|
| 96 |
-
line_width=2))
|
| 97 |
-
|
| 98 |
-
fig_network = go.Figure(data=[edge_trace, node_trace],
|
| 99 |
-
layout=go.Layout(title='関連ユーザーネットワーク (Reply & Repost)',
|
| 100 |
-
showlegend=False, hovermode='closest',
|
| 101 |
-
margin=dict(b=0,l=0,r=0,t=40),
|
| 102 |
-
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| 103 |
-
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)))
|
| 104 |
-
|
| 105 |
-
return fig_bar, fig_network, "解析完了"
|
| 106 |
|
| 107 |
except Exception as e:
|
| 108 |
-
|
|
|
|
| 109 |
|
| 110 |
# --- Gradio UI ---
|
| 111 |
-
with gr.Blocks() as demo:
|
| 112 |
-
gr.Markdown("# 🦋 Bluesky
|
| 113 |
|
| 114 |
with gr.Row():
|
| 115 |
with gr.Column(scale=1):
|
| 116 |
-
in_id = gr.Textbox(label="自分のID")
|
| 117 |
-
in_pw = gr.Textbox(label="アプリパスワード", type="password")
|
| 118 |
-
in_target = gr.Textbox(label="解析
|
| 119 |
-
in_freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="グラフの単位", value="日ごと")
|
| 120 |
-
btn = gr.Button("分析
|
| 121 |
-
status = gr.Textbox(label="ステータス")
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
with gr.Row():
|
| 124 |
out_bar = gr.Plot(label="投稿数推移")
|
| 125 |
-
|
|
|
|
| 126 |
with gr.Row():
|
| 127 |
-
out_net = gr.Plot(label="関連ユーザーネットワーク")
|
| 128 |
|
| 129 |
-
btn.click(
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
| 132 |
|
| 133 |
-
demo.launch()
|
|
|
|
|
|
| 8 |
import plotly.express as px
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
import networkx as nx
|
| 11 |
+
import io
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
# プロフィール画像を取得するヘルパー関数
|
| 15 |
+
def get_profile_image(client, did_or_handle):
|
| 16 |
+
try:
|
| 17 |
+
profile = client.get_profile(actor=did_or_handle)
|
| 18 |
+
return profile.avatar if profile and profile.avatar else None
|
| 19 |
+
except Exception:
|
| 20 |
+
return None
|
| 21 |
|
| 22 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 23 |
try:
|
| 24 |
+
# --- 1. ログイン ---
|
| 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 |
+
|
| 30 |
profile = client.get_profile(actor=target_handle)
|
| 31 |
|
| 32 |
posts_data = []
|
| 33 |
+
hashtags = []
|
| 34 |
+
reply_users_list = []
|
| 35 |
+
repost_users_list = []
|
| 36 |
+
like_users_list = []
|
| 37 |
+
|
| 38 |
+
# ネットワーク図作成のための相互作用リスト
|
| 39 |
+
interactions_for_network = [] # (source_handle, target_handle, type)
|
| 40 |
+
user_avatars = {target_handle: getattr(profile, 'avatar', None)} # 顔アイコンURLを保存
|
| 41 |
+
|
| 42 |
+
max_limit = 500
|
| 43 |
|
| 44 |
+
# --- 2. 投稿フィードの分析 ---
|
| 45 |
+
progress(0, desc="フィードと相互作用データを取得中...")
|
| 46 |
cursor = None
|
| 47 |
+
|
| 48 |
+
for _ in range(10): # 最大1000件取得を想定しつつ無限ループ防止
|
| 49 |
+
try:
|
| 50 |
+
response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
|
| 51 |
+
except Exception:
|
| 52 |
+
break
|
| 53 |
+
|
| 54 |
+
if not response or not hasattr(response, 'feed') or not response.feed:
|
| 55 |
+
break
|
| 56 |
+
|
| 57 |
for feed_view in response.feed:
|
| 58 |
+
if not getattr(feed_view, 'post', None): continue
|
| 59 |
post = feed_view.post
|
| 60 |
+
|
| 61 |
+
# リポスト分析
|
| 62 |
+
if feed_view.reason:
|
| 63 |
+
if hasattr(post, 'author') and post.author:
|
| 64 |
+
orig_author_handle = post.author.handle
|
| 65 |
+
if orig_author_handle != target_handle:
|
| 66 |
+
repost_users_list.append(orig_author_handle)
|
| 67 |
+
interactions_for_network.append((target_handle, orig_author_handle, 'repost'))
|
| 68 |
+
if orig_author_handle not in user_avatars:
|
| 69 |
+
user_avatars[orig_author_handle] = get_profile_image(client, orig_author_handle)
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
# 本人投稿分析
|
| 73 |
+
if hasattr(post, 'author') and post.author and post.author.handle == target_handle:
|
| 74 |
+
if not hasattr(post, 'record'): continue
|
| 75 |
|
| 76 |
+
rkey = post.uri.split('/')[-1]
|
| 77 |
+
post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
|
| 78 |
+
|
| 79 |
+
likes = getattr(post, 'like_count', 0) or 0
|
| 80 |
+
reposts = getattr(post, 'repost_count', 0) or 0
|
| 81 |
+
created_at_raw = getattr(post.record, 'created_at', None)
|
| 82 |
+
text = getattr(post.record, 'text', "") or ""
|
| 83 |
+
|
| 84 |
+
if created_at_raw:
|
| 85 |
+
posts_data.append({
|
| 86 |
+
'text': text,
|
| 87 |
+
'created_at': pd.to_datetime(created_at_raw),
|
| 88 |
+
'likes': likes,
|
| 89 |
+
'reposts': reposts,
|
| 90 |
+
'score': likes + reposts,
|
| 91 |
+
'url': post_url
|
| 92 |
+
})
|
| 93 |
+
if text:
|
| 94 |
+
hashtags.extend(re.findall(r'#(\w+)', text))
|
| 95 |
+
|
| 96 |
+
# リプライ相手
|
| 97 |
+
if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
|
| 98 |
+
parent = feed_view.reply.parent
|
| 99 |
+
if hasattr(parent, 'author') and parent.author:
|
| 100 |
+
p_handle = parent.author.handle
|
| 101 |
+
if p_handle != target_handle:
|
| 102 |
+
reply_users_list.append(p_handle)
|
| 103 |
+
interactions_for_network.append((target_handle, p_handle, 'reply'))
|
| 104 |
+
if p_handle not in user_avatars:
|
| 105 |
+
user_avatars[p_handle] = get_profile_image(client, p_handle)
|
| 106 |
|
| 107 |
+
cursor = getattr(response, 'cursor', None)
|
| 108 |
+
if not cursor or len(posts_data) >= max_limit: break
|
| 109 |
+
progress(min(len(posts_data)/max_limit * 0.5, 0.5), desc=f"{len(posts_data)}件取得中...")
|
| 110 |
+
|
| 111 |
+
# --- 3. いいね一覧の分析 ---
|
| 112 |
+
progress(0.6, desc="いいねを分析中...")
|
| 113 |
+
try:
|
| 114 |
+
likes_resp = client.get_actor_likes(actor=profile.did, limit=50) # いいねの取得は少し少なめに
|
| 115 |
+
if likes_resp and hasattr(likes_resp, 'feed'):
|
| 116 |
+
for like_item in likes_resp.feed:
|
| 117 |
+
if like_item.post and hasattr(like_item.post, 'author') and like_item.post.author:
|
| 118 |
+
l_handle = like_item.post.author.handle
|
| 119 |
+
if l_handle != target_handle:
|
| 120 |
+
like_users_list.append(l_handle)
|
| 121 |
+
# ネットワーク図には追加しないが、ユーザー情報を取得するならここで
|
| 122 |
+
if l_handle not in user_avatars:
|
| 123 |
+
user_avatars[l_handle] = get_profile_image(client, l_handle)
|
| 124 |
+
except Exception:
|
| 125 |
+
pass # いいね取得でエラーが出ても続行
|
| 126 |
+
|
| 127 |
+
# --- 4. 解析結果のHTML作成 ---
|
| 128 |
if not posts_data:
|
| 129 |
+
return "有効な投稿データが見つかりませんでした。", "", "", None, None, "完了(データ不足)"
|
| 130 |
|
| 131 |
df = pd.DataFrame(posts_data)
|
| 132 |
+
df['created_at'] = df['created_at'].dt.tz_localize(None) # タイムゾーンを削除
|
| 133 |
+
df = df.sort_values('created_at', ascending=True)
|
| 134 |
+
|
| 135 |
+
first_post_time = df.iloc[0]['created_at'].replace(tzinfo=None)
|
| 136 |
+
days_active = max((datetime.now().replace(tzinfo=None) - first_post_time).days, 1)
|
| 137 |
+
|
| 138 |
+
stats_html = f"""
|
| 139 |
+
<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
|
| 140 |
+
<div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
|
| 141 |
+
<div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{getattr(profile, 'posts_count', 0)}</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
|
| 144 |
+
<div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
|
| 145 |
+
</div>
|
| 146 |
+
<div style='background: #fff3e0; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #ffcc80;'>
|
| 147 |
+
<div style='color: #e65100; font-size: 0.8em;'>1日平均</div><div style='font-size: 1.5em; font-weight: bold;'>{len(df)/days_active:.2f}</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
"""
|
| 151 |
|
| 152 |
+
def make_rank_list(title, icon, counter_list):
|
| 153 |
+
html = f"<b>{icon} {title}</b><div style='font-size: 0.85em; color: #444; margin: 5px 0 12px 0;'>"
|
| 154 |
+
if not counter_list: return html + "データなし</div>"
|
| 155 |
+
items = Counter(counter_list).most_common(3)
|
| 156 |
+
for name, count in items:
|
| 157 |
+
html += f"<div>@{name} ({count}回)</div>"
|
| 158 |
+
return html + "</div>"
|
| 159 |
+
|
| 160 |
+
rank_html = f"""
|
| 161 |
+
<div style='background: #fafafa; padding: 15px; border-radius: 10px; border: 1px solid #eee;'>
|
| 162 |
+
{make_rank_list("よくリプライする相手", "💬", reply_users_list)}
|
| 163 |
+
{make_rank_list("よくリポストする相手", "🔄", repost_users_list)}
|
| 164 |
+
{make_rank_list("よくいいねする相手", "❤️", like_users_list)}
|
| 165 |
+
<b>#️⃣ よく使うタグ</b><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px;'>
|
| 166 |
+
{"".join([f"<span style='background: #eee; padding: 3px 8px; border-radius: 10px; font-size: 0.75em;'>#{t}</span>" for t, _ in Counter(hashtags).most_common(3)]) or "なし"}
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
top_3 = df.sort_values('score', ascending=False).head(3)
|
| 172 |
+
posts_html = f"<h4>🌱 初投稿 ({df.iloc[0]['created_at'].strftime('%Y/%m/%d')})</h4><p style='font-size: 0.9em; background:#f9f9f9; padding:10px; border-radius:5px;'>{df.iloc[0]['text']}</p><h4>🏆 ベストポスト Top 3</h4>"
|
| 173 |
+
|
| 174 |
+
for i, (_, row) in enumerate(top_3.iterrows(), 1):
|
| 175 |
+
posts_html += f"""
|
| 176 |
+
<div style='border: 1px solid #eee; padding: 10px; margin-bottom: 8px; border-radius: 8px; background: white;'>
|
| 177 |
+
<b>第{i}位</b> (❤️{row['likes']} 🔄{row['reposts']})<br>
|
| 178 |
+
<p style='margin:5px 0;'>{row['text'][:80]}...</p>
|
| 179 |
+
<a href='{row['url']}' target='_blank' style='color:#d81b60; font-size:0.8em;'>表示</a>
|
| 180 |
+
</div>
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
# --- 5. 投稿数の棒グラフ (Plotly) ---
|
| 184 |
freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
|
| 185 |
df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
|
| 186 |
|
|
|
|
| 190 |
template="plotly_white")
|
| 191 |
fig_bar.update_traces(marker_color='#0085ff')
|
| 192 |
|
| 193 |
+
# --- 6. ユーザーネットワーク図 (NetworkX + Plotly + 顔アイコン) ---
|
| 194 |
G = nx.Graph()
|
| 195 |
+
# 相互作用の多い順に上位10ユーザーを抽出
|
| 196 |
+
all_interactions_nodes = [item[1] for item in interactions_for_network] # 相手側のユーザー
|
| 197 |
+
top_10_users_counter = Counter(all_interactions_nodes).most_common(9) # 本人を除いた9人
|
| 198 |
+
top_10_handles = [target_handle] + [user for user, _ in top_10_users_counter]
|
| 199 |
+
|
| 200 |
+
for source, target, _type in interactions_for_network:
|
| 201 |
+
if source in top_10_handles and target in top_10_handles:
|
| 202 |
+
if not G.has_edge(source, target):
|
| 203 |
+
G.add_edge(source, target, weight=1) # 初回
|
| 204 |
+
else:
|
| 205 |
+
G[source][target]['weight'] += 1 # 相互作用回数を重み付け
|
| 206 |
|
| 207 |
+
# ノードが1つ以下ならネットワーク図は作らない
|
| 208 |
+
if G.number_of_nodes() < 2:
|
| 209 |
+
fig_network = go.Figure().update_layout(title="ネットワーク図:相互作用が不足しています")
|
| 210 |
+
else:
|
| 211 |
+
pos = nx.spring_layout(G, k=0.5, iterations=50) # レイアウト計算
|
| 212 |
+
|
| 213 |
+
edge_x, edge_y, edge_weights = [], [], []
|
| 214 |
+
for u, v, data in G.edges(data=True):
|
| 215 |
+
x0, y0 = pos[u]
|
| 216 |
+
x1, y1 = pos[v]
|
| 217 |
+
edge_x.extend([x0, x1, None])
|
| 218 |
+
edge_y.extend([y0, y1, None])
|
| 219 |
+
edge_weights.append(data['weight'])
|
| 220 |
+
|
| 221 |
+
edge_trace = go.Scatter(x=edge_x, y=edge_y, line=dict(width=[w*0.5 for w in edge_weights], color='#888'),
|
| 222 |
+
hoverinfo='none', mode='lines', opacity=0.7)
|
| 223 |
+
|
| 224 |
+
node_x, node_y, node_text, node_images = [], [], [], []
|
| 225 |
+
for node in G.nodes():
|
| 226 |
+
x, y = pos[node]
|
| 227 |
+
node_x.append(x)
|
| 228 |
+
node_y.append(y)
|
| 229 |
+
node_text.append(f"@{node}<br>つながり: {len(list(G.neighbors(node)))}")
|
| 230 |
+
|
| 231 |
+
avatar_url = user_avatars.get(node, None)
|
| 232 |
+
node_images.append(avatar_url if avatar_url else 'https://img.icons8.com/ios-filled/50/000000/user--v1.png') # デフォルトアイコン
|
| 233 |
+
|
| 234 |
+
node_trace = go.Scatter(
|
| 235 |
+
x=node_x, y=node_y, mode='markers', hoverinfo='text', text=node_text,
|
| 236 |
+
marker=dict(
|
| 237 |
+
symbol='circle', # ダミー
|
| 238 |
+
size=30,
|
| 239 |
+
color='rgba(0,0,0,0)', # 透明にして画像を表示
|
| 240 |
+
line_width=0,
|
| 241 |
+
opacity=0 # 透明
|
| 242 |
+
)
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# アイコンを追加するためのアノテーション
|
| 246 |
+
annotations = []
|
| 247 |
+
for i, (x, y) in enumerate(zip(node_x, node_y)):
|
| 248 |
+
annotations.append(
|
| 249 |
+
go.layout.Annotation(
|
| 250 |
+
x=x, y=y,
|
| 251 |
+
xref="x", yref="y",
|
| 252 |
+
text="",
|
| 253 |
+
showarrow=False,
|
| 254 |
+
images=[dict(
|
| 255 |
+
source=node_images[i],
|
| 256 |
+
xref="x", yref="y",
|
| 257 |
+
x=x, y=y,
|
| 258 |
+
sizex=0.08, sizey=0.08, # アイコンのサイズ調��
|
| 259 |
+
xanchor="center", yanchor="middle"
|
| 260 |
+
)]
|
| 261 |
+
)
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
fig_network = go.Figure(data=[edge_trace, node_trace],
|
| 265 |
+
layout=go.Layout(title=f'{target_handle}の関連ユーザーネットワーク (Top {len(top_10_handles)} users)',
|
| 266 |
+
showlegend=False, hovermode='closest',
|
| 267 |
+
margin=dict(b=0,l=0,r=0,t=40),
|
| 268 |
+
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| 269 |
+
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| 270 |
+
annotations=annotations # アイコンをアノテーションとして追加
|
| 271 |
+
))
|
| 272 |
|
| 273 |
+
return stats_html, rank_html, posts_html, fig_bar, fig_network, "解析が完了しました!"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
except Exception as e:
|
| 276 |
+
import traceback
|
| 277 |
+
return f"エラーが発生しました: {str(e)}", "", "", None, None, "失敗"
|
| 278 |
|
| 279 |
# --- Gradio UI ---
|
| 280 |
+
with gr.Blocks(title="Bluesky Analysis") as demo:
|
| 281 |
+
gr.Markdown("# 🦋 Bluesky アクティビティ&ネットワーク分析")
|
| 282 |
|
| 283 |
with gr.Row():
|
| 284 |
with gr.Column(scale=1):
|
| 285 |
+
in_id = gr.Textbox(label="自分のBluesky ID (例: example.bsky.social)")
|
| 286 |
+
in_pw = gr.Textbox(label="Bluesky アプリパスワード", type="password")
|
| 287 |
+
in_target = gr.Textbox(label="解析したいユーザーのBluesky ID (例: target.bsky.social)")
|
| 288 |
+
in_freq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="投稿数グラフの単位", value="日ごと")
|
| 289 |
+
btn = gr.Button("分析開始", variant="primary")
|
| 290 |
+
status = gr.Textbox(label="ステータス", interactive=False)
|
| 291 |
+
|
| 292 |
+
with gr.Column(scale=2):
|
| 293 |
+
out_stats = gr.HTML(label="概要統計")
|
| 294 |
+
out_rank = gr.HTML(label="関連ユーザーとハッシュタグ")
|
| 295 |
+
out_posts = gr.HTML(label="ベスト投稿 Top 3")
|
| 296 |
+
|
| 297 |
+
gr.Markdown("## グラフで見るアクティビティ")
|
| 298 |
with gr.Row():
|
| 299 |
out_bar = gr.Plot(label="投稿数推移")
|
| 300 |
+
|
| 301 |
+
gr.Markdown("## 関連ユーザーネットワーク")
|
| 302 |
with gr.Row():
|
| 303 |
+
out_net = gr.Plot(label="主要関連ユーザーネットワーク")
|
| 304 |
|
| 305 |
+
btn.click(
|
| 306 |
+
fn=analyze_and_output,
|
| 307 |
+
inputs=[in_id, in_pw, in_target, in_freq],
|
| 308 |
+
outputs=[out_stats, out_rank, out_posts, out_bar, out_net, status]
|
| 309 |
+
)
|
| 310 |
|
| 311 |
+
demo.launch()
|
| 312 |
+
```http://googleusercontent.com/image_generation_content/1
|