Nyanpre commited on
Commit
a9ca70e
·
verified ·
1 Parent(s): 6c7b2ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -27
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 # 安全策として+100件分多く回す
68
- max_loops = min(max_loops, 50) # 最大5000件までの安全上限
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(): 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'))
128
- node_imgs, node_texts = [], []
 
 
129
  for n in nodes:
130
  img = user_info_cache.get(n, {"avatar": ""})["avatar"]
131
- node_imgs.append(dict(source=img, xref="x", yref="y", x=pos[n][0], y=pos[n][1], sizex=0.2, sizey=0.2, xanchor="center", yanchor="middle", layer="above"))
132
- rel = "<br><b style='color:#ff4b4b;'>(本人)</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
133
- node_texts.append(f"<b>{n[:12]}</b>{rel}")
134
- 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', text=node_texts, marker=dict(size=40, color='rgba(0,0,0,0)'), textposition="bottom center", hoverinfo='none'))
135
- fig_net.update_layout(images=node_imgs, showlegend=False, xaxis=dict(visible=False, range=[-1.2, 1.2]), yaxis=dict(visible=False, range=[-1.2, 1.2]), plot_bgcolor='white', height=500, margin=dict(t=10, b=10, l=0, r=0))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(css=CUSTOM_CSS) as demo:
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", lines=1)
146
- m_pw = gr.Textbox(label="パスワード", type="password", lines=1)
147
- t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social", lines=1)
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
- # show_label=False と config を適用
157
- out_b = gr.Plot(label="投稿頻度", config=PLOT_CONFIG)
158
- out_heat = gr.Plot(label="時間帯ヒートマップ", config=PLOT_CONFIG)
159
  with gr.TabItem("🤝 魂の相関図"):
160
- out_n = gr.Plot(config=PLOT_CONFIG)
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
- demo.launch()
 
 
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)