Update app.py
Browse files
app.py
CHANGED
|
@@ -47,9 +47,6 @@ def generate_catchphrase(kanji, posts_df):
|
|
| 47 |
if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
|
| 48 |
return f"── {random.choice(adj_list)} {kanji} を愛する {random.choice(title_list)} ──"
|
| 49 |
|
| 50 |
-
# グラフ設定(静止画のように固定)
|
| 51 |
-
PLOT_CONFIG = {'staticPlot': True, 'displayModeBar': False}
|
| 52 |
-
|
| 53 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 54 |
try:
|
| 55 |
client = Client()
|
|
@@ -62,10 +59,9 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 62 |
user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
|
| 63 |
all_text = ""
|
| 64 |
|
| 65 |
-
# ポスト解析ループ(投稿数に応じた回数を実行)
|
| 66 |
total_posts = profile.posts_count
|
| 67 |
-
max_loops = (total_posts // 100) + 2
|
| 68 |
-
max_loops = min(max_loops, 50)
|
| 69 |
|
| 70 |
cursor = None
|
| 71 |
for i in range(max_loops):
|
|
@@ -91,7 +87,6 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 91 |
df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
|
| 92 |
rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
|
| 93 |
|
| 94 |
-
# HTML生成
|
| 95 |
html = f"""<div class="dashboard-container">
|
| 96 |
<div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{generate_catchphrase(rep_kanji, df)}</div></div>
|
| 97 |
<div class="stat-row">
|
|
@@ -104,16 +99,17 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 104 |
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>"
|
| 105 |
html += "</div></div>"
|
| 106 |
|
| 107 |
-
# グラフ
|
| 108 |
df_counts = df.set_index('created_at').resample({"週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count')
|
| 109 |
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", height=300)
|
| 110 |
-
fig_bar.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 111 |
|
|
|
|
| 112 |
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 113 |
heat_data = df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
|
| 114 |
heat_data.index = ['月','火','水','木','金','土','日']
|
| 115 |
fig_heat = px.imshow(heat_data, color_continuous_scale='Blues', height=300)
|
| 116 |
-
fig_heat.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 117 |
|
| 118 |
# 相関図
|
| 119 |
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
|
@@ -123,28 +119,44 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 123 |
pos = nx.spring_layout(G, k=1.3, seed=42)
|
| 124 |
cx, cy = pos[target_handle]
|
| 125 |
for n in pos: pos[n] = (pos[n][0] - cx, pos[n][1] - cy)
|
|
|
|
| 126 |
fig_net = go.Figure()
|
| 127 |
-
for e in G.edges():
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
for n in nodes:
|
| 130 |
img = user_info_cache.get(n, {"avatar": ""})["avatar"]
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
-
# GradioのPlotコンポーネントにConfigを渡すことはできないため、戻り値の段階で制限をかけたグラフを返します
|
| 138 |
return html, fig_bar, fig_heat, fig_net, "解析完了!"
|
| 139 |
except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
|
| 140 |
|
| 141 |
-
with gr.Blocks(
|
| 142 |
gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:1.6rem;'>🦋 Bluesky Analyzer</p>")
|
| 143 |
with gr.Row():
|
| 144 |
with gr.Column():
|
| 145 |
-
m_id = gr.Textbox(label="自分のID", placeholder="example.bsky.social"
|
| 146 |
-
m_pw = gr.Textbox(label="パスワード", type="password"
|
| 147 |
-
t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social"
|
| 148 |
frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="週ごと")
|
| 149 |
btn = gr.Button("解析実行", variant="primary")
|
| 150 |
st = gr.Markdown("<p style='text-align:center;'>IDとパスワードを入力してください</p>")
|
|
@@ -153,12 +165,12 @@ with gr.Blocks(css=CUSTOM_CSS) as demo:
|
|
| 153 |
|
| 154 |
with gr.Tabs():
|
| 155 |
with gr.TabItem("📊 活動ログ"):
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
out_heat = gr.Plot(label="時間帯ヒートマップ", config=PLOT_CONFIG)
|
| 159 |
with gr.TabItem("🤝 魂の相関図"):
|
| 160 |
-
out_n = gr.Plot(
|
| 161 |
|
| 162 |
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
|
| 163 |
|
| 164 |
-
|
|
|
|
|
|
| 47 |
if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
|
| 48 |
return f"── {random.choice(adj_list)} {kanji} を愛する {random.choice(title_list)} ──"
|
| 49 |
|
|
|
|
|
|
|
|
|
|
| 50 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 51 |
try:
|
| 52 |
client = Client()
|
|
|
|
| 59 |
user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
|
| 60 |
all_text = ""
|
| 61 |
|
|
|
|
| 62 |
total_posts = profile.posts_count
|
| 63 |
+
max_loops = (total_posts // 100) + 2
|
| 64 |
+
max_loops = min(max_loops, 50)
|
| 65 |
|
| 66 |
cursor = None
|
| 67 |
for i in range(max_loops):
|
|
|
|
| 87 |
df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
|
| 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">
|
| 91 |
<div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{generate_catchphrase(rep_kanji, df)}</div></div>
|
| 92 |
<div class="stat-row">
|
|
|
|
| 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 |
df_counts = df.set_index('created_at').resample({"週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count')
|
| 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.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
|
| 110 |
heat_data.index = ['月','火','水','木','金','土','日']
|
| 111 |
fig_heat = px.imshow(heat_data, color_continuous_scale='Blues', height=300)
|
| 112 |
+
fig_heat.update_layout(margin=dict(l=10, r=10, t=30, b=10), dragmode=False) # 触れないように設定
|
| 113 |
|
| 114 |
# 相関図
|
| 115 |
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
|
|
|
| 119 |
pos = nx.spring_layout(G, k=1.3, seed=42)
|
| 120 |
cx, cy = pos[target_handle]
|
| 121 |
for n in pos: pos[n] = (pos[n][0] - cx, pos[n][1] - cy)
|
| 122 |
+
|
| 123 |
fig_net = go.Figure()
|
| 124 |
+
for e in G.edges():
|
| 125 |
+
fig_net.add_trace(go.Scatter(x=[pos[e[0]][0], pos[e[1]][0]], y=[pos[e[0]][1], pos[e[1]][1]], mode='lines', line=dict(color='#ccc', width=1), hoverinfo='none'))
|
| 126 |
+
|
| 127 |
+
node_imgs, node_texts, node_x, node_y = [], [], [], []
|
| 128 |
for n in nodes:
|
| 129 |
img = user_info_cache.get(n, {"avatar": ""})["avatar"]
|
| 130 |
+
nx_val, ny_val = pos[n]
|
| 131 |
+
node_x.append(nx_val)
|
| 132 |
+
node_y.append(ny_val)
|
| 133 |
+
node_imgs.append(dict(source=img, xref="x", yref="y", x=nx_val, y=ny_val, sizex=0.22, sizey=0.22, xanchor="center", yanchor="middle", layer="above"))
|
| 134 |
+
rel = "<br><b style='color:#ff4b4b;'>本人</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
|
| 135 |
+
display_name = n[:12] + '..' if len(n) > 12 else n
|
| 136 |
+
node_texts.append(f"<b>{display_name}</b>{rel}")
|
| 137 |
+
|
| 138 |
+
fig_net.add_trace(go.Scatter(
|
| 139 |
+
x=node_x, y=node_y, mode='markers+text', text=node_texts,
|
| 140 |
+
textposition="bottom center", textfont=dict(size=11, color='#333'),
|
| 141 |
+
marker=dict(size=40, color='rgba(0,0,0,0)'), hoverinfo='none'
|
| 142 |
+
))
|
| 143 |
+
fig_net.update_layout(
|
| 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 Analyzer</p>")
|
| 155 |
with gr.Row():
|
| 156 |
with gr.Column():
|
| 157 |
+
m_id = gr.Textbox(label="自分のID", placeholder="example.bsky.social")
|
| 158 |
+
m_pw = gr.Textbox(label="パスワード", type="password")
|
| 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;'>IDとパスワードを入力してください</p>")
|
|
|
|
| 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)
|