Update app.py
Browse files
app.py
CHANGED
|
@@ -23,10 +23,10 @@ CUSTOM_CSS = """
|
|
| 23 |
.stat-label { font-size: 0.8rem; color: #666; }
|
| 24 |
.stat-value { font-size: 1.4rem; font-weight: bold; color: #0085ff; }
|
| 25 |
.rank-header { font-size: 1rem; font-weight: bold; border-left: 4px solid #0085ff; padding-left: 8px; margin-bottom: 10px; }
|
| 26 |
-
.rank-entry { font-size: 0.9rem; padding: 5px 0; }
|
| 27 |
-
.rank-avatar { width: 28px; height: 28px; border-radius: 50%; }
|
| 28 |
.best-post-item { font-size: 0.9rem; padding: 10px; margin-bottom: 8px; background: #f9f9f9; border-radius: 8px; border: 1px solid #eef; }
|
| 29 |
-
button.primary { height: 50px !important; font-size: 1.1rem !important; }
|
| 30 |
"""
|
| 31 |
|
| 32 |
ADJECTIVES = ["光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の", "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の", "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の", "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"]
|
|
@@ -60,8 +60,7 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 60 |
all_text = ""
|
| 61 |
|
| 62 |
total_posts = profile.posts_count
|
| 63 |
-
max_loops = (total_posts // 100) + 2
|
| 64 |
-
max_loops = min(max_loops, 500)
|
| 65 |
|
| 66 |
cursor = None
|
| 67 |
for i in range(max_loops):
|
|
@@ -72,19 +71,32 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 72 |
txt = getattr(p.record, 'text', "")
|
| 73 |
all_text += txt
|
| 74 |
dt = pd.to_datetime(getattr(p.record, 'created_at')) + timedelta(hours=9)
|
| 75 |
-
posts_data.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
if getattr(f, 'reply', None) and isinstance(f.reply.parent, PostView):
|
| 78 |
u_parent = f.reply.parent.author.handle
|
| 79 |
reply_counts[u_parent] += 1
|
| 80 |
interaction_pairs.append((target_handle, u_parent))
|
| 81 |
-
if u_parent not in user_info_cache:
|
|
|
|
| 82 |
|
| 83 |
cursor = response.cursor
|
| 84 |
if not cursor: break
|
| 85 |
progress((i+1)/max_loops)
|
| 86 |
|
| 87 |
-
df = pd.DataFrame(posts_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
|
| 89 |
|
| 90 |
html = f"""<div class="dashboard-container">
|
|
@@ -99,17 +111,22 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 99 |
html += f"<a href='{r['url']}' target='_blank' style='text-decoration:none; color:inherit;'><div class='best-post-item'>{r['text'][:80]}...<div style='color:#0085ff; font-weight:bold; margin-top:5px;'>❤️ {r['likes']} 🔄 {r['reposts']}</div></div></a>"
|
| 100 |
html += "</div></div>"
|
| 101 |
|
| 102 |
-
# 投稿頻度グラフ
|
| 103 |
-
|
|
|
|
| 104 |
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", height=300)
|
| 105 |
-
fig_bar.update_layout(margin=dict(l=10, r=10, t=30, b=10), dragmode=False)
|
| 106 |
|
| 107 |
# ヒートマップ
|
| 108 |
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 109 |
-
heat_data = df.
|
| 110 |
-
heat_data
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
# 相関図
|
| 115 |
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
|
@@ -144,14 +161,14 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 144 |
images=node_imgs, showlegend=False,
|
| 145 |
xaxis=dict(visible=False, range=[-1.3, 1.3]), yaxis=dict(visible=False, range=[-1.3, 1.3]),
|
| 146 |
plot_bgcolor='white', height=500, margin=dict(t=10, b=10, l=0, r=0),
|
| 147 |
-
dragmode=False
|
| 148 |
)
|
| 149 |
|
| 150 |
return html, fig_bar, fig_heat, fig_net, "解析完了!"
|
| 151 |
except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
|
| 152 |
|
| 153 |
with gr.Blocks() as demo:
|
| 154 |
-
gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:1.6rem;'>🦋 Bluesky
|
| 155 |
with gr.Row():
|
| 156 |
with gr.Column():
|
| 157 |
m_id = gr.Textbox(label="自分のID", placeholder="example.bsky.social")
|
|
@@ -159,18 +176,17 @@ with gr.Blocks() as demo:
|
|
| 159 |
t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social")
|
| 160 |
frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="週ごと")
|
| 161 |
btn = gr.Button("解析実行", variant="primary")
|
| 162 |
-
st = gr.Markdown("<p style='text-align:center;'>
|
| 163 |
|
| 164 |
out_h = gr.HTML()
|
| 165 |
|
| 166 |
with gr.Tabs():
|
| 167 |
-
with gr.TabItem("📊 活動
|
| 168 |
out_b = gr.Plot(label="投稿頻度")
|
| 169 |
-
out_heat = gr.Plot(label="時間
|
| 170 |
with gr.TabItem("🤝 魂の相関図"):
|
| 171 |
out_n = gr.Plot()
|
| 172 |
|
| 173 |
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
|
| 174 |
|
| 175 |
-
# Gradio 6.0仕様: launch時にcssを渡す
|
| 176 |
demo.launch(css=CUSTOM_CSS)
|
|
|
|
| 23 |
.stat-label { font-size: 0.8rem; color: #666; }
|
| 24 |
.stat-value { font-size: 1.4rem; font-weight: bold; color: #0085ff; }
|
| 25 |
.rank-header { font-size: 1rem; font-weight: bold; border-left: 4px solid #0085ff; padding-left: 8px; margin-bottom: 10px; }
|
| 26 |
+
.rank-entry { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; padding: 5px 0; }
|
| 27 |
+
.rank-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
|
| 28 |
.best-post-item { font-size: 0.9rem; padding: 10px; margin-bottom: 8px; background: #f9f9f9; border-radius: 8px; border: 1px solid #eef; }
|
| 29 |
+
button.primary { height: 50px !important; font-size: 1.1rem !important; background: #0085ff !important; color: white !important; }
|
| 30 |
"""
|
| 31 |
|
| 32 |
ADJECTIVES = ["光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の", "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の", "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の", "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"]
|
|
|
|
| 60 |
all_text = ""
|
| 61 |
|
| 62 |
total_posts = profile.posts_count
|
| 63 |
+
max_loops = min((total_posts // 100) + 2, 100) # 最大1万件まで
|
|
|
|
| 64 |
|
| 65 |
cursor = None
|
| 66 |
for i in range(max_loops):
|
|
|
|
| 71 |
txt = getattr(p.record, 'text', "")
|
| 72 |
all_text += txt
|
| 73 |
dt = pd.to_datetime(getattr(p.record, 'created_at')) + timedelta(hours=9)
|
| 74 |
+
posts_data.append({
|
| 75 |
+
'text': txt, 'likes': p.like_count, 'reposts': p.repost_count,
|
| 76 |
+
'created_at': dt, 'url': f"https://bsky.app/profile/{target_handle}/post/{p.uri.split('/')[-1]}",
|
| 77 |
+
'score': p.like_count + p.repost_count, 'hour': dt.hour, 'weekday': dt.day_name()
|
| 78 |
+
})
|
| 79 |
|
| 80 |
if getattr(f, 'reply', None) and isinstance(f.reply.parent, PostView):
|
| 81 |
u_parent = f.reply.parent.author.handle
|
| 82 |
reply_counts[u_parent] += 1
|
| 83 |
interaction_pairs.append((target_handle, u_parent))
|
| 84 |
+
if u_parent not in user_info_cache:
|
| 85 |
+
user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
|
| 86 |
|
| 87 |
cursor = response.cursor
|
| 88 |
if not cursor: break
|
| 89 |
progress((i+1)/max_loops)
|
| 90 |
|
| 91 |
+
df = pd.DataFrame(posts_data)
|
| 92 |
+
if df.empty: return "投稿が見つかりませんでした", None, None, None, "失敗"
|
| 93 |
+
|
| 94 |
+
# 重複削除
|
| 95 |
+
df = df.drop_duplicates(subset=['text'])
|
| 96 |
+
# 重要:DatetimeIndexの再設定
|
| 97 |
+
df['created_at'] = pd.to_datetime(df['created_at'])
|
| 98 |
+
df = df.set_index('created_at').sort_index()
|
| 99 |
+
|
| 100 |
rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
|
| 101 |
|
| 102 |
html = f"""<div class="dashboard-container">
|
|
|
|
| 111 |
html += f"<a href='{r['url']}' target='_blank' style='text-decoration:none; color:inherit;'><div class='best-post-item'>{r['text'][:80]}...<div style='color:#0085ff; font-weight:bold; margin-top:5px;'>❤️ {r['likes']} 🔄 {r['reposts']}</div></div></a>"
|
| 112 |
html += "</div></div>"
|
| 113 |
|
| 114 |
+
# 投稿頻度グラフ(エラー回避版)
|
| 115 |
+
freq_rule = {"週ごと": "W", "月ごと": "M"}[freq_type]
|
| 116 |
+
df_counts = df.resample(freq_rule).size().reset_index(name='count')
|
| 117 |
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", height=300)
|
| 118 |
+
fig_bar.update_layout(margin=dict(l=10, r=10, t=30, b=10), dragmode=False)
|
| 119 |
|
| 120 |
# ヒートマップ
|
| 121 |
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 122 |
+
heat_data = df.copy()
|
| 123 |
+
heat_data['weekday'] = heat_data.index.day_name()
|
| 124 |
+
heat_data['hour'] = heat_data.index.hour
|
| 125 |
+
heat_summary = heat_data.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
|
| 126 |
+
heat_summary.index = ['月','火','水','木','金','土','日']
|
| 127 |
+
|
| 128 |
+
fig_heat = px.imshow(heat_summary, color_continuous_scale='Blues', height=300)
|
| 129 |
+
fig_heat.update_layout(margin=dict(l=10, r=10, t=30, b=10), dragmode=False)
|
| 130 |
|
| 131 |
# 相関図
|
| 132 |
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
|
|
|
| 161 |
images=node_imgs, showlegend=False,
|
| 162 |
xaxis=dict(visible=False, range=[-1.3, 1.3]), yaxis=dict(visible=False, range=[-1.3, 1.3]),
|
| 163 |
plot_bgcolor='white', height=500, margin=dict(t=10, b=10, l=0, r=0),
|
| 164 |
+
dragmode=False
|
| 165 |
)
|
| 166 |
|
| 167 |
return html, fig_bar, fig_heat, fig_net, "解析完了!"
|
| 168 |
except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
|
| 169 |
|
| 170 |
with gr.Blocks() as demo:
|
| 171 |
+
gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:1.6rem;'>🦋 Bluesky Analyzer</p>")
|
| 172 |
with gr.Row():
|
| 173 |
with gr.Column():
|
| 174 |
m_id = gr.Textbox(label="自分のID", placeholder="example.bsky.social")
|
|
|
|
| 176 |
t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social")
|
| 177 |
frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="週ごと")
|
| 178 |
btn = gr.Button("解析実行", variant="primary")
|
| 179 |
+
st = gr.Markdown("<p style='text-align:center;'>情報を入力して実行してください</p>")
|
| 180 |
|
| 181 |
out_h = gr.HTML()
|
| 182 |
|
| 183 |
with gr.Tabs():
|
| 184 |
+
with gr.TabItem("📊 活動ログ"):
|
| 185 |
out_b = gr.Plot(label="投稿頻度")
|
| 186 |
+
out_heat = gr.Plot(label="時間帯ヒートマップ")
|
| 187 |
with gr.TabItem("🤝 魂の相関図"):
|
| 188 |
out_n = gr.Plot()
|
| 189 |
|
| 190 |
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
|
| 191 |
|
|
|
|
| 192 |
demo.launch(css=CUSTOM_CSS)
|