Update app.py
Browse files
app.py
CHANGED
|
@@ -13,269 +13,160 @@ from atproto_client.models.app.bsky.feed.defs import PostView
|
|
| 13 |
|
| 14 |
# --- カスタムCSS ---
|
| 15 |
CUSTOM_CSS = """
|
| 16 |
-
|
| 17 |
-
.
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
}
|
| 23 |
-
.
|
| 24 |
-
|
| 25 |
-
|
| 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 |
-
.rank-item { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
| 37 |
-
.rank-avatar { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
|
| 38 |
-
button.primary {
|
| 39 |
-
background: linear-gradient(135deg, #0085ff 0%, #00bfff 100%) !important;
|
| 40 |
-
border: none !important; color: white !important;
|
| 41 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
"""
|
| 43 |
|
| 44 |
def get_profile_info(client, did_or_handle):
|
| 45 |
try:
|
| 46 |
profile = client.get_profile(actor=did_or_handle)
|
| 47 |
-
return {
|
| 48 |
-
|
| 49 |
-
"handle": profile.handle,
|
| 50 |
-
"display_name": profile.display_name or profile.handle
|
| 51 |
-
}
|
| 52 |
-
except Exception:
|
| 53 |
-
return None
|
| 54 |
|
| 55 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 56 |
try:
|
| 57 |
client = Client()
|
| 58 |
-
my_id
|
| 59 |
target_handle = target_id.replace('@', '').strip()
|
| 60 |
-
client.login(my_id, my_pw.strip())
|
| 61 |
profile = client.get_profile(actor=target_handle)
|
| 62 |
|
| 63 |
-
posts_data = []
|
| 64 |
-
|
| 65 |
-
repost_users_list = []
|
| 66 |
-
interaction_pairs = []
|
| 67 |
-
ends_list = []
|
| 68 |
|
| 69 |
total_posts_count = getattr(profile, 'posts_count', 0)
|
| 70 |
-
max_iterations = (total_posts_count // 100) + 1
|
| 71 |
|
| 72 |
-
progress(0, desc=f"全 {total_posts_count} ポストを解析中...")
|
| 73 |
cursor = None
|
| 74 |
-
|
| 75 |
for i in range(max_iterations):
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
if not isinstance(post, PostView): continue
|
| 84 |
|
| 85 |
-
|
| 86 |
-
if
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
'weekday': dt_jst.day_name(),
|
| 101 |
-
'likes': getattr(post, 'like_count', 0),
|
| 102 |
-
'reposts': getattr(post, 'repost_count', 0),
|
| 103 |
-
'score': getattr(post, 'like_count', 0) + getattr(post, 'repost_count', 0)
|
| 104 |
-
})
|
| 105 |
-
|
| 106 |
-
u_author = post.author.handle
|
| 107 |
-
if feed_view.reason:
|
| 108 |
-
interaction_pairs.append((target_handle, u_author))
|
| 109 |
-
if u_author != target_handle: repost_users_list.append(u_author)
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
cursor = response.cursor
|
| 120 |
-
progress((i
|
| 121 |
if not cursor: break
|
| 122 |
|
| 123 |
-
if not posts_data: return "
|
| 124 |
-
|
| 125 |
-
df = pd.DataFrame(posts_data)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
"""
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
res = f"<div class='rank-card'><div class='rank-header'>{icon} {title}</div>"
|
| 143 |
-
if top:
|
| 144 |
-
for n, c in top:
|
| 145 |
-
info = get_profile_info(client, n) if "@" not in str(n) and "." in str(n) else None
|
| 146 |
-
avatar_html = f"<img src='{info['avatar']}' class='rank-avatar'>" if info else ""
|
| 147 |
-
res += f"<div class='rank-item'>{avatar_html} <b>{n}</b> <span style='margin-left:auto; color:#666;'>{c}回</span></div>"
|
| 148 |
-
else:
|
| 149 |
-
res += "<div style='color:#ccc;'>未検出</div>"
|
| 150 |
-
res += "</div>"
|
| 151 |
-
return res
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
{rank_box_styled('リポスト相手', repost_users_list, '🔄')}
|
| 157 |
-
{rank_box_styled('よく使う語尾', ends_list, '🗣️')}
|
| 158 |
-
</div>
|
| 159 |
-
"""
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
posts_html += "</div>"
|
| 166 |
-
|
| 167 |
-
freq_map = {"日ごと": "D", "週ごと": "W", "月ごと": "M"}
|
| 168 |
-
df_counts = df.set_index('created_at').resample(freq_map[freq_type]).size().reset_index(name='count')
|
| 169 |
-
fig_bar = px.bar(df_counts, x='created_at', y='count', title="投稿推移", color_discrete_sequence=['#0085ff'], template="plotly_white")
|
| 170 |
-
|
| 171 |
-
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 172 |
-
heat_pt = df.groupby(['weekday', 'hour']).size().reset_index(name='count').pivot(index='weekday', columns='hour', values='count').reindex(week_order).fillna(0)
|
| 173 |
-
heat_pt.index = ['月', '火', '水', '木', '金', '土', '日']
|
| 174 |
-
fig_heat = px.imshow(heat_pt, x=list(range(24)), y=heat_pt.index, color_continuous_scale='Blues', title="時間帯別アクティビティ")
|
| 175 |
-
|
| 176 |
-
# --- 相関図の修正 ---
|
| 177 |
-
all_interactors = [p for pair in interaction_pairs for p in pair if p != target_handle]
|
| 178 |
-
top_10 = [u for u, c in Counter(all_interactors).most_common(10)]
|
| 179 |
-
nodes = list(set([target_handle] + top_10))
|
| 180 |
-
RELATIONSHIPS = ["家族", "恋人", "相棒", "ライバル", "師匠", "弟子", "癒やし", "腐れ縁", "憧れ", "守護霊"]
|
| 181 |
-
|
| 182 |
-
# 各ノードにランダムな関係性を割り当て
|
| 183 |
-
node_labels = {}
|
| 184 |
-
for n in nodes:
|
| 185 |
-
rel = random.choice(RELATIONSHIPS) if n != target_handle else "(本人)"
|
| 186 |
-
node_labels[n] = f"<b>{n}</b><br>{rel}"
|
| 187 |
-
|
| 188 |
G = nx.Graph()
|
| 189 |
-
for
|
| 190 |
-
if u1 in nodes and u2 in nodes and u1 != u2:
|
| 191 |
-
|
| 192 |
-
else: G.add_edge(u1, u2, weight=1)
|
| 193 |
-
|
| 194 |
-
pos = nx.spring_layout(G, k=2.0, seed=42)
|
| 195 |
fig_net = go.Figure()
|
| 196 |
-
|
| 197 |
-
# エッジの描画
|
| 198 |
for edge in G.edges():
|
| 199 |
-
fig_net.add_trace(go.Scatter(
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
hoverinfo='none'
|
| 205 |
-
))
|
| 206 |
-
|
| 207 |
-
# ノード画像の設定
|
| 208 |
-
node_images = []
|
| 209 |
-
for node in G.nodes():
|
| 210 |
-
info = get_profile_info(client, node)
|
| 211 |
-
if info:
|
| 212 |
-
node_images.append(dict(
|
| 213 |
-
source=info['avatar'], xref="x", yref="y",
|
| 214 |
-
x=pos[node][0], y=pos[node][1],
|
| 215 |
-
sizex=0.18, sizey=0.18,
|
| 216 |
-
xanchor="center", yanchor="middle", layer="above"
|
| 217 |
-
))
|
| 218 |
-
|
| 219 |
-
# --- ラベルを確実に表示するための修正 ---
|
| 220 |
-
fig_net.add_trace(go.Scatter(
|
| 221 |
-
x=[pos[n][0] for n in G.nodes()],
|
| 222 |
-
y=[pos[n][1] for n in G.nodes()],
|
| 223 |
-
mode='markers+text', # markersも有効化
|
| 224 |
-
marker=dict(size=25, color='rgba(0,0,0,0)'), # 透明なマーカーで当たり判定を確保
|
| 225 |
-
text=[node_labels[n] for n in G.nodes()],
|
| 226 |
-
textposition="bottom center", # 画像の下に配置
|
| 227 |
-
textfont=dict(color="black", size=12),
|
| 228 |
-
hoverinfo='text'
|
| 229 |
-
))
|
| 230 |
-
|
| 231 |
-
fig_net.update_layout(
|
| 232 |
-
images=node_images,
|
| 233 |
-
showlegend=False,
|
| 234 |
-
plot_bgcolor='white',
|
| 235 |
-
height=700,
|
| 236 |
-
xaxis=dict(visible=False, range=[-1.5, 1.5]),
|
| 237 |
-
yaxis=dict(visible=False, range=[-1.5, 1.5]),
|
| 238 |
-
margin=dict(l=0, r=0, t=40, b=0)
|
| 239 |
-
)
|
| 240 |
|
| 241 |
-
return
|
| 242 |
-
|
| 243 |
except Exception as e:
|
| 244 |
-
|
| 245 |
-
print(traceback.format_exc())
|
| 246 |
-
return f"エラー: {str(e)}", "", "", None, None, None, "失敗"
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
gr.Markdown("# <p style='text-align:center; color:#0085ff;'>🦋 Bluesky Ultimate Dashboard</p>")
|
| 251 |
-
|
| 252 |
with gr.Row():
|
| 253 |
-
with gr.Column(
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
with gr.Column(scale=2):
|
| 263 |
-
out_stats = gr.HTML()
|
| 264 |
-
with gr.Row():
|
| 265 |
-
out_rank = gr.HTML()
|
| 266 |
-
out_posts = gr.HTML()
|
| 267 |
-
|
| 268 |
with gr.Tabs():
|
| 269 |
-
with gr.TabItem("📊 活動
|
| 270 |
-
|
| 271 |
-
out_bar = gr.Plot()
|
| 272 |
-
out_heat = gr.Plot()
|
| 273 |
-
with gr.TabItem("🤝 マップ"):
|
| 274 |
-
out_net = gr.Plot()
|
| 275 |
|
| 276 |
-
btn.click(analyze_and_output,
|
| 277 |
-
inputs=[my_id, my_pw, target_id, freq],
|
| 278 |
-
outputs=[out_stats, out_rank, out_posts, out_bar, out_heat, out_net, status])
|
| 279 |
|
| 280 |
-
|
| 281 |
-
demo.launch()
|
|
|
|
| 13 |
|
| 14 |
# --- カスタムCSS ---
|
| 15 |
CUSTOM_CSS = """
|
| 16 |
+
.gradio-container { max-width: 100% !important; padding: 10px !important; background-color: #f0f7ff; }
|
| 17 |
+
.dashboard-container {
|
| 18 |
+
display: grid;
|
| 19 |
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
| 20 |
+
gap: 20px;
|
| 21 |
+
width: 100%;
|
| 22 |
}
|
| 23 |
+
.card {
|
| 24 |
+
background: white; border-radius: 16px; padding: 20px;
|
| 25 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.08); width: 100%; box-sizing: border-box;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
+
.kanji-card { background: linear-gradient(135deg, #0085ff 0%, #00bfff 100%); color: white; text-align: center; }
|
| 28 |
+
.kanji-value { font-size: 5rem; font-weight: 900; line-height: 1; margin: 15px 0; }
|
| 29 |
+
.stats-subgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px; }
|
| 30 |
+
.stat-item { background: rgba(255,255,255,0.2); padding: 10px; border-radius: 10px; text-align: center; }
|
| 31 |
+
.stat-num { font-size: 1.5rem; font-weight: bold; }
|
| 32 |
+
.rank-section { margin-bottom: 20px; }
|
| 33 |
+
.rank-header { font-size: 1.1rem; font-weight: bold; border-left: 4px solid #0085ff; padding-left: 10px; margin-bottom: 15px; color: #333; }
|
| 34 |
+
.rank-entry { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #eee; }
|
| 35 |
+
.best-post-item { background: #f9f9f9; border-radius: 12px; padding: 15px; margin-bottom: 15px; border: 1px solid #eef; line-height: 1.6; }
|
| 36 |
+
.post-meta { font-size: 0.85rem; color: #0085ff; margin-top: 10px; font-weight: bold; }
|
| 37 |
+
button.primary { background: #0085ff !important; height: 60px !important; font-size: 1.2rem !important; color: white !important; }
|
| 38 |
"""
|
| 39 |
|
| 40 |
def get_profile_info(client, did_or_handle):
|
| 41 |
try:
|
| 42 |
profile = client.get_profile(actor=did_or_handle)
|
| 43 |
+
return {"avatar": profile.avatar or "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png", "handle": profile.handle}
|
| 44 |
+
except: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 47 |
try:
|
| 48 |
client = Client()
|
| 49 |
+
client.login(my_id.replace('@', '').strip(), my_pw.strip())
|
| 50 |
target_handle = target_id.replace('@', '').strip()
|
|
|
|
| 51 |
profile = client.get_profile(actor=target_handle)
|
| 52 |
|
| 53 |
+
posts_data, reply_users, repost_users, interaction_pairs, ends_list = [], [], [], [], []
|
| 54 |
+
all_text = ""
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
total_posts_count = getattr(profile, 'posts_count', 0)
|
| 57 |
+
max_iterations = min((total_posts_count // 100) + 1, 30)
|
| 58 |
|
|
|
|
| 59 |
cursor = None
|
|
|
|
| 60 |
for i in range(max_iterations):
|
| 61 |
+
response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
|
| 62 |
+
if not response.feed: break
|
| 63 |
+
|
| 64 |
+
for f in response.feed:
|
| 65 |
+
p = f.post
|
| 66 |
+
# --- 修正ポイント:PostViewではない(削除済みなど)場合はスキップ ---
|
| 67 |
+
if not isinstance(p, PostView): continue
|
|
|
|
| 68 |
|
| 69 |
+
# 投稿内容の解析
|
| 70 |
+
if p.author.handle == target_handle:
|
| 71 |
+
txt = getattr(p.record, 'text', "")
|
| 72 |
+
all_text += txt
|
| 73 |
+
clean = re.sub(r'[!\?!?。\n\s]+$', '', txt)
|
| 74 |
+
if len(clean) >= 2:
|
| 75 |
+
end = clean[-3:];
|
| 76 |
+
if re.search(r'[ぁ-んーwWwW]$', end): ends_list.append(end)
|
| 77 |
+
|
| 78 |
+
posts_data.append({
|
| 79 |
+
'text': txt, 'likes': getattr(p, 'like_count', 0),
|
| 80 |
+
'reposts': getattr(p, 'repost_count', 0),
|
| 81 |
+
'created_at': pd.to_datetime(getattr(p.record, 'created_at')) + timedelta(hours=9),
|
| 82 |
+
'score': getattr(p, 'like_count', 0) + getattr(p, 'repost_count', 0)
|
| 83 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
# 交流の解析
|
| 86 |
+
u_auth = p.author.handle
|
| 87 |
+
if f.reason: interaction_pairs.append((target_handle, u_auth))
|
| 88 |
+
|
| 89 |
+
# リプライ先の安全な確認
|
| 90 |
+
if getattr(f, 'reply', None) and f.reply.parent:
|
| 91 |
+
parent = f.reply.parent
|
| 92 |
+
# リプライ先が削除されていないか確認
|
| 93 |
+
if isinstance(parent, PostView):
|
| 94 |
+
u_parent = parent.author.handle
|
| 95 |
+
interaction_pairs.append((u_auth, u_parent))
|
| 96 |
+
if u_auth == target_handle: reply_users.append(u_parent)
|
| 97 |
|
| 98 |
cursor = response.cursor
|
| 99 |
+
progress((i+1)/max_iterations, desc="データを解析中...")
|
| 100 |
if not cursor: break
|
| 101 |
|
| 102 |
+
if not posts_data: return "有効なポストが見つかりませんでした。", None, None, "失敗"
|
| 103 |
+
|
| 104 |
+
df = pd.DataFrame(posts_data).drop_duplicates(subset=['text']).reset_index(drop=True)
|
| 105 |
+
kanjis = re.findall(r'[一-龠]', all_text)
|
| 106 |
+
rep_kanji = Counter(kanjis).most_common(1)[0][0] if kanjis else "魂"
|
| 107 |
+
|
| 108 |
+
# HTML構成(レスポンシブ)
|
| 109 |
+
html = f"""
|
| 110 |
+
<div class="dashboard-container">
|
| 111 |
+
<div class="card kanji-card">
|
| 112 |
+
<small>象徴する一文字</small><div class="kanji-value">{rep_kanji}</div>
|
| 113 |
+
<div class="stats-subgrid">
|
| 114 |
+
<div class="stat-item"><small>総投稿</small><br><span class="stat-num">{total_posts_count}</span></div>
|
| 115 |
+
<div class="stat-item"><small>解析数</small><br><span class="stat-num">{len(df)}</span></div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="card">
|
| 119 |
+
<div class="rank-section"><div class="rank-header">👥 主な交流相手</div>
|
| 120 |
+
{"".join([f"<div class='rank-entry'><span>{n}</span><b>{c}回</b></div>" for n,c in Counter(reply_users).most_common(3)])}</div>
|
| 121 |
+
<div class="rank-section"><div class="rank-header">🗣️ 特徴的な語尾</div>
|
| 122 |
+
{"".join([f"<div class='rank-entry'><span>{n}</span><b>{c}回</b></div>" for n,c in Counter(ends_list).most_common(3)])}</div>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="card" style="grid-column: 1 / -1;"><div class="rank-header">🏆 ベストポスト</div>
|
| 125 |
"""
|
| 126 |
+
for _, r in df.sort_values('score', ascending=False).head(3).iterrows():
|
| 127 |
+
html += f"<div class='best-post-item'>{r['text']}<div class='post-meta'>❤️ {r['likes']} 🔄 {r['reposts']} — {r['created_at'].strftime('%Y/%m/%d')}</div></div>"
|
| 128 |
+
html += "</div></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
+
# グラフ生成
|
| 131 |
+
df_counts = df.set_index('created_at').resample(freq_map := {"日ごと": "D", "週ごと": "W", "月ごと": "M"}[freq_type]).size().reset_index(name='count')
|
| 132 |
+
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", title="投稿頻度")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
# 相関図
|
| 135 |
+
top_users = [u for u, c in Counter([p for pair in interaction_pairs for p in pair if p != target_handle]).most_common(10)]
|
| 136 |
+
nodes = list(set([target_handle] + top_users))
|
| 137 |
+
RELATIONSHIPS = ["運命の人", "相棒", "ライバル", "師匠", "���子", "癒やし", "犬猿の仲", "腐れ縁", "守護霊"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
G = nx.Graph()
|
| 139 |
+
for u1, u2 in interaction_pairs:
|
| 140 |
+
if u1 in nodes and u2 in nodes and u1 != u2: G.add_edge(u1, u2)
|
| 141 |
+
pos = nx.spring_layout(G, k=1.5, seed=42)
|
|
|
|
|
|
|
|
|
|
| 142 |
fig_net = go.Figure()
|
|
|
|
|
|
|
| 143 |
for edge in G.edges():
|
| 144 |
+
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=1), hoverinfo='none'))
|
| 145 |
+
fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in nodes], y=[pos[n][1] for n in nodes], mode='markers+text',
|
| 146 |
+
text=[f"<b>{n}</b><br>{random.choice(RELATIONSHIPS) if n != target_handle else '(本人)'}" for n in nodes],
|
| 147 |
+
textposition="bottom center", marker=dict(size=12, color='#0085ff'), textfont=dict(size=11)))
|
| 148 |
+
fig_net.update_layout(showlegend=False, xaxis=dict(visible=False), yaxis=dict(visible=False), margin=dict(l=0, r=0, t=40, b=0), plot_bgcolor='white', height=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
return html, fig_bar, fig_net, "解析完了!"
|
|
|
|
| 151 |
except Exception as e:
|
| 152 |
+
return f"エラーが発生しました: {str(e)}", None, None, "失敗"
|
|
|
|
|
|
|
| 153 |
|
| 154 |
+
with gr.Blocks(css=CUSTOM_CSS, title="Bluesky Analysis") as demo:
|
| 155 |
+
gr.Markdown("# <p style='text-align:center; color:#0085ff;'>🦋 Bluesky Analysis Dashboard</p>")
|
|
|
|
|
|
|
| 156 |
with gr.Row():
|
| 157 |
+
with gr.Column():
|
| 158 |
+
m_id = gr.Textbox(label="自分のハンドル", placeholder="user.bsky.social")
|
| 159 |
+
m_pw = gr.Textbox(label="アプリパスワード", type="password")
|
| 160 |
+
t_id = gr.Textbox(label="解析したい相手")
|
| 161 |
+
frq = gr.Radio(["日ごと", "週ごと", "月ごと"], label="グラフ単位", value="日ごと")
|
| 162 |
+
btn = gr.Button("解析���開始", variant="primary")
|
| 163 |
+
st = gr.Markdown("準備完了")
|
| 164 |
+
|
| 165 |
+
out_h = gr.HTML()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
with gr.Tabs():
|
| 167 |
+
with gr.TabItem("📊 活動"): out_b = gr.Plot()
|
| 168 |
+
with gr.TabItem("🤝 相関図"): out_n = gr.Plot()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_n, st])
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
demo.launch()
|
|
|