Nyanpre commited on
Commit
3e36bc4
·
verified ·
1 Parent(s): 3780aa4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -85
app.py CHANGED
@@ -3,6 +3,8 @@ import pandas as pd
3
  from atproto import Client
4
  import time
5
  from datetime import datetime
 
 
6
 
7
  def fetch_all_posts(my_id, my_pw, target_id, progress=gr.Progress()):
8
  try:
@@ -15,8 +17,10 @@ def fetch_all_posts(my_id, my_pw, target_id, progress=gr.Progress()):
15
  total_count_on_profile = profile.posts_count
16
 
17
  posts = []
 
 
18
  cursor = None
19
- max_limit = 1000 # 必要に応じて増やせますが、まずは1000件で安定動作を確認
20
 
21
  progress(0, desc="データ取得を開始します...")
22
 
@@ -25,124 +29,126 @@ def fetch_all_posts(my_id, my_pw, target_id, progress=gr.Progress()):
25
 
26
  for feed_view in response.feed:
27
  post = feed_view.post
 
 
28
  if post.author.handle == target_handle:
29
  rkey = post.uri.split('/')[-1]
30
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
31
 
32
- has_image = 1 if post.embed and hasattr(post.embed, 'images') else 0
33
- has_link = 1 if hasattr(post.record, 'facets') and post.record.facets else 0
34
-
35
  posts.append({
36
  'text': post.record.text,
37
  'created_at': pd.to_datetime(post.record.created_at),
38
  'likes': post.like_count,
39
  'reposts': post.repost_count,
40
- 'url': post_url,
41
- 'has_image': has_image,
42
- 'has_link': has_link
43
  })
44
-
45
- progress(min(len(posts) / max_limit, 0.95), desc=f"{len(posts)}件取得中...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  cursor = response.cursor
47
- if not cursor: break
48
  time.sleep(0.1)
49
 
50
- if not posts:
51
- return "投稿が見つかりませんでした", "", "", "データなし"
52
 
53
  df = pd.DataFrame(posts)
54
 
55
- # --- 1. 基本スタッツと経過日数計算 ---
56
- # 取得した中で一番古い投稿(リストの最後)を「初投稿」とする
57
- first_post_obj = df.iloc[-1]
58
- first_date = first_post_obj['created_at']
59
- days_active = (datetime.now().replace(tzinfo=None) - first_date.replace(tzinfo=None)).days
60
- days_active = max(days_active, 1) # 0日回避
61
- avg_freq = len(df) / days_active
62
-
63
  stats_html = f"""
64
- <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 20px;'>
65
- <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
66
- <div style='color: #1565c0; font-size: 0.8em;'>総投稿数</div>
67
- <div style='font-size: 1.5em; font-weight: bold;'>{total_count_on_profile}</div>
68
  </div>
69
- <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
70
- <div style='color: #33691e; font-size: 0.8em;'>アカウント経過</div>
71
- <div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
72
  </div>
73
- <div style='background: #fff3e0; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #ffcc80;'>
74
- <div style='color: #e65100; font-size: 0.8em;'>平均投稿頻度</div>
75
- <div style='font-size: 1.5em; font-weight: bold;'>{avg_freq:.2f} <span style='font-size: 0.5em;'>回/日</span></div>
76
  </div>
77
  </div>
78
  """
79
 
80
- # --- 2. 記念すべき初投稿 (分析範囲内) ---
81
- first_post_html = f"""
82
- <div style='background: #ffffff; border: 2px solid #0085ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;'>
83
- <div style='color: #0085ff; font-weight: bold; font-size: 0.9em; margin-bottom: 5px;'>🌱 記念すべき初投稿 ({first_date.strftime('%Y/%m/%d')})</div>
84
- <p style='margin: 5px 0; font-size: 1em; line-height: 1.4;'>{first_post_obj['text']}</p>
85
- <a href='{first_post_obj['url']}' target='_blank' style='color: #0085ff; text-decoration: none; font-size: 0.9em;'>🔗 当時の投稿を開く</a>
86
- </div>
87
- """
 
88
 
89
- # --- 3. ツ割合 ---
90
- img_ratio = (df['has_image'].sum() / len(df)) * 100
91
- link_ratio = (df['has_link'].sum() / len(df)) * 100
92
- content_html = f"""
93
- <div style='display: flex; gap: 10px; margin-bottom: 20px;'>
94
- <div style='flex: 1; background: #fafafa; padding: 10px; border-radius: 8px; border: 1px solid #eee; text-align: center;'>
95
- <div style='font-size: 0.8em; color: #666;'>画像投稿率: {img_ratio:.1f}%</div>
96
- </div>
97
- <div style='flex: 1; background: #fafafa; padding: 10px; border-radius: 8px; border: 1px solid #eee; text-align: center;'>
98
- <div style='font-size: 0.8em; color: #666;'>リンク共有率: {link_ratio:.1f}%</div>
99
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  </div>
101
  """
102
 
103
- # --- 4. ベストポスト Top 3 ---
104
- df['score'] = df['likes'] + df['reposts']
105
- top_posts = df.sort_values(by='score', ascending=False).head(3)
106
- best_posts_html = "<h3>🏆 ベストポスト Top 3</h3>"
107
- for i, (_, row) in enumerate(top_posts.iterrows(), 1):
108
- truncated_text = (row['text'][:80] + '...') if len(row['text']) > 80 else row['text']
109
- best_posts_html += f"""
110
- <div style='border: 1px solid #ddd; padding: 12px; border-radius: 8px; margin-bottom: 10px; background: #fff;'>
111
- <div style='display: flex; justify-content: space-between; align-items: center;'>
112
- <span style='background: #ffd700; padding: 2px 8px; border-radius: 4px; font-weight: bold;'>第{i}位</span>
113
- <span style='color: #666; font-size: 0.8em;'>❤️ {row['likes']} 🔄 {row['reposts']}</span>
114
- </div>
115
- <p style='margin: 8px 0; font-size: 0.9em; color: #333;'>{truncated_text}</p>
116
- <a href='{row['url']}' target='_blank' style='color: #0085ff; text-decoration: none; font-size: 0.8em;'>🔗 投稿を見る</a>
117
- </div>
118
- """
119
-
120
- return stats_html, first_post_html, content_html + best_posts_html, "解析完了!"
121
 
122
  except Exception as e:
123
- return f"<div style='color: red;'>エラー: {str(e)}</div>", "", "", "エラー発生"
124
 
125
- # --- UI構成 ---
126
- with gr.Blocks(css=".gradio-container {background-color: #f9f9f9}") as demo:
127
- gr.Markdown("# 🦋 Bluesky 自己分析ダッシュボード")
128
-
129
  with gr.Row():
130
  with gr.Column(scale=1):
131
- in_id = gr.Textbox(label="自分のID (example.bsky.social)")
132
  in_pw = gr.Textbox(label="アプリパスワード", type="password")
133
- in_target = gr.Textbox(label="解析対象のID")
134
- btn = gr.Button("分析を実行", variant="primary")
135
- out_status = gr.Textbox(label="ステータス")
136
-
137
  with gr.Column(scale=2):
138
- out_stats = gr.HTML() # 基本スタッツ(3列カード)
139
- out_first = gr.HTML() # 初投稿ハイライト
140
- out_main = gr.HTML() # コンテンツ割合 + ベストポスト
141
 
142
- btn.click(
143
- fn=fetch_all_posts,
144
- inputs=[in_id, in_pw, in_target],
145
- outputs=[out_stats, out_first, out_main, out_status]
146
- )
147
 
148
  demo.launch()
 
3
  from atproto import Client
4
  import time
5
  from datetime import datetime
6
+ import re
7
+ from collections import Counter
8
 
9
  def fetch_all_posts(my_id, my_pw, target_id, progress=gr.Progress()):
10
  try:
 
17
  total_count_on_profile = profile.posts_count
18
 
19
  posts = []
20
+ hashtags = []
21
+ interactions = []
22
  cursor = None
23
+ max_limit = 10000
24
 
25
  progress(0, desc="データ取得を開始します...")
26
 
 
29
 
30
  for feed_view in response.feed:
31
  post = feed_view.post
32
+
33
+ # 本人の投稿のみを対象
34
  if post.author.handle == target_handle:
35
  rkey = post.uri.split('/')[-1]
36
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
37
 
38
+ # 1. 基本データ
 
 
39
  posts.append({
40
  'text': post.record.text,
41
  'created_at': pd.to_datetime(post.record.created_at),
42
  'likes': post.like_count,
43
  'reposts': post.repost_count,
44
+ 'url': post_url
 
 
45
  })
46
+
47
+ # 2. ハッシュタグ抽出 (facets または テキストから)
48
+ tags = re.findall(r'#(\w+)', post.record.text)
49
+ hashtags.extend(tags)
50
+
51
+ # 3. メンション・リプライ相手の抽出
52
+ # リプライ
53
+ if feed_view.reply and feed_view.reply.parent:
54
+ parent_author = feed_view.reply.parent.author.handle
55
+ if parent_author != target_handle:
56
+ interactions.append(parent_author)
57
+
58
+ # メンション (facetsから)
59
+ if post.record.facets:
60
+ for facet in post.record.facets:
61
+ for feature in facet.features:
62
+ if hasattr(feature, 'did'):
63
+ # DIDからハンドル名を取得するのは重いため、一旦カウント用
64
+ # ※ここでは簡易化のためリプライ相手を主軸にします
65
+ pass
66
+
67
+ progress(min(len(posts) / max_limit, 0.95), desc=f"{len(posts)}件分析中...")
68
  cursor = response.cursor
69
+ if not cursor or len(posts) >= max_limit: break
70
  time.sleep(0.1)
71
 
72
+ if not posts: return "投稿なし", "", "", "終了"
 
73
 
74
  df = pd.DataFrame(posts)
75
 
76
+ # --- 算 ---
77
+ first_post = df.iloc[-1]
78
+ days_active = max((datetime.now().replace(tzinfo=None) - first_post['created_at'].replace(tzinfo=None)).days, 1)
79
+
80
+ # --- A. 基本スタッツ (HTML) ---
 
 
 
81
  stats_html = f"""
82
+ <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
83
+ <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center;'>
84
+ <div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{total_count_on_profile}</div>
 
85
  </div>
86
+ <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center;'>
87
+ <div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
 
88
  </div>
89
+ <div style='background: #fff3e0; padding: 15px; border-radius: 10px; text-align: center;'>
90
+ <div style='color: #e65100; font-size: 0.8em;'>1日平均</div><div style='font-size: 1.5em; font-weight: bold;'>{len(df)/days_active:.2f}</div>
 
91
  </div>
92
  </div>
93
  """
94
 
95
+ # --- B. ハッシュタグランキング ---
96
+ tag_counts = Counter(hashtags).most_common(5)
97
+ tag_html = "<h4>#️⃣ よく使うタグ</h4><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 20px;'>"
98
+ if tag_counts:
99
+ for tag, count in tag_counts:
100
+ tag_html += f"<span style='background: #eee; padding: 4px 10px; border-radius: 15px; font-size: 0.9em;'>#{tag} ({count})</span>"
101
+ else:
102
+ tag_html += "<span style='color: #999;'>なし</span>"
103
+ tag_html += "</div>"
104
 
105
+ # --- C. 仲良しユーザーラ ---
106
+ user_counts = Counter(interactions).most_common(5)
107
+ user_html = "<h4>🤝 よく交流する相手</h4><div style='margin-bottom: 20px;'>"
108
+ if user_counts:
109
+ for user, count in user_counts:
110
+ user_html += f"<div style='font-size: 0.9em; margin-bottom: 3px;'>@{user} <span style='color: #666;'>({count}回)</span></div>"
111
+ else:
112
+ user_html += "<div style='color: #999;'>なし</div>"
113
+ user_html += "</div>"
114
+
115
+ # --- D. 初投稿 & ベストポスト ---
116
+ top_post = df.sort_values('likes', ascending=False).iloc[0]
117
+
118
+ main_content_html = f"""
119
+ <div style='background: #fff; border: 1px solid #0085ff; padding: 15px; border-radius: 12px; margin-bottom: 15px;'>
120
+ <div style='color: #0085ff; font-weight: bold;'>🌱 分析範囲内の最古ポスト</div>
121
+ <p style='font-size: 0.9em;'>{first_post['text']}</p>
122
+ <a href='{first_post['url']}' target='_blank' style='font-size: 0.8em;'>🔗 投稿を見る</a>
123
+ </div>
124
+ <div style='background: #fff; border: 1px solid #ff4081; padding: 15px; border-radius: 12px;'>
125
+ <div style='color: #ff4081; font-weight: bold;'>🏆 最大いいねポスト</div>
126
+ <p style='font-size: 0.9em;'>{top_post['text']}</p>
127
+ <span style='font-size: 0.8em; color: #666;'>❤️ {top_post['likes']}</span> |
128
+ <a href='{top_post['url']}' target='_blank' style='font-size: 0.8em;'>🔗 投稿を見る</a>
129
  </div>
130
  """
131
 
132
+ return stats_html, tag_html + user_html, main_content_html, "解析完了!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  except Exception as e:
135
+ return f"エラー: {str(e)}", "", "", "失敗"
136
 
137
+ # --- UI ---
138
+ with gr.Blocks(title="Bluesky Summary") as demo:
139
+ gr.Markdown("# 🦋 Bluesky 自己分析くん")
 
140
  with gr.Row():
141
  with gr.Column(scale=1):
142
+ in_id = gr.Textbox(label="自分のハンドル", placeholder="example.bsky.social")
143
  in_pw = gr.Textbox(label="アプリパスワード", type="password")
144
+ in_target = gr.Textbox(label="解析したいID")
145
+ btn = gr.Button("分析開始", variant="primary")
146
+ status = gr.Textbox(label="状況")
 
147
  with gr.Column(scale=2):
148
+ out_stats = gr.HTML() # 基本数字
149
+ out_rank = gr.HTML() # タグ・ユーザー
150
+ out_posts = gr.HTML() # 注目ポスト
151
 
152
+ btn.click(fn=fetch_all_posts, inputs=[in_id, in_pw, in_target], outputs=[out_stats, out_rank, out_posts, status])
 
 
 
 
153
 
154
  demo.launch()