Nyanpre commited on
Commit
dbf10ba
·
verified ·
1 Parent(s): c042004

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +37 -21
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({'text': txt, 'likes': p.like_count, 'reposts': p.repost_count, 'created_at': dt, 'url': f"https://bsky.app/profile/{target_handle}/post/{p.uri.split('/')[-1]}", 'score': p.like_count + p.repost_count, 'hour': dt.hour, 'weekday': dt.day_name()})
 
 
 
 
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: user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
 
82
 
83
  cursor = response.cursor
84
  if not cursor: break
85
  progress((i+1)/max_loops)
86
 
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">
@@ -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
- 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)]))
@@ -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 Ultimate Profile</p>")
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;'>IDとパスワードを入力してください</p>")
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)