Nyanpre commited on
Commit
40a70e7
·
verified ·
1 Parent(s): 38792c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -85
app.py CHANGED
@@ -8,78 +8,87 @@ from collections import Counter
8
 
9
  def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
10
  try:
11
- # --- 1. ログインと初期設定 ---
12
  client = Client()
13
  my_id = my_id.replace('@', '').strip()
14
  target_handle = target_id.replace('@', '').strip()
15
  client.login(my_id, my_pw.strip())
16
 
17
  profile = client.get_profile(actor=target_handle)
18
- total_count_on_profile = profile.posts_count
19
 
20
  posts_data = []
21
  hashtags = []
22
- interactions = []
23
- cursor = None
 
 
24
  max_limit = 1000
25
-
26
- # --- 2. データ取得ループ ---
27
- progress(0, desc="Blueskyからデータを取得中...")
28
 
 
 
 
29
  while len(posts_data) < max_limit:
30
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
31
-
32
  for feed_view in response.feed:
33
- if not hasattr(feed_view.post, 'author') or feed_view.post is None:
34
- continue
 
 
 
 
 
35
 
36
  post = feed_view.post
37
  if post.author.handle == target_handle:
38
  rkey = post.uri.split('/')[-1]
39
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
40
 
41
- # スコア計算 (いいね + リポスト)
42
- engagement_score = post.like_count + post.repost_count
43
-
44
  posts_data.append({
45
  'text': post.record.text,
46
  'created_at': pd.to_datetime(post.record.created_at),
47
  'likes': post.like_count,
48
  'reposts': post.repost_count,
49
- 'score': engagement_score,
50
  'url': post_url
51
  })
52
 
53
- # タグとインタラクション
54
  hashtags.extend(re.findall(r'#(\w+)', post.record.text))
 
 
55
  if feed_view.reply and feed_view.reply.parent:
56
- parent = feed_view.reply.parent
57
- if hasattr(parent, 'author') and parent.author:
58
- if parent.author.handle != target_handle:
59
- interactions.append(parent.author.handle)
60
 
61
- progress(min(len(posts_data) / max_limit, 0.95), desc=f"{len(posts_data)}件取得済み...")
62
-
63
  cursor = response.cursor
64
  if not cursor or len(posts_data) >= max_limit: break
65
- time.sleep(0.1)
66
-
67
- if not posts_data:
68
- return "有効な投稿が見つかりませんでした", "", "", "終了"
69
-
70
- # --- 3. 解析処理 ---
71
- progress(0.95, desc="解析レポートを作成中...")
 
 
 
 
 
 
 
 
 
 
 
 
72
  df = pd.DataFrame(posts_data)
73
-
74
- # スタッツ計算
75
  first_post = df.iloc[-1]
76
  days_active = max((datetime.now().replace(tzinfo=None) - first_post['created_at'].replace(tzinfo=None)).days, 1)
77
 
78
- # HTML: 基本スタッツ
79
  stats_html = f"""
80
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
81
  <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
82
- <div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{total_count_on_profile}</div>
83
  </div>
84
  <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
85
  <div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
@@ -90,62 +99,34 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
90
  </div>
91
  """
92
 
93
- # HTML: ランキング (タグ上位3つ)
94
- tag_counts = Counter(hashtags).most_common(3)
95
- user_counts = Counter(interactions).most_common(5)
96
-
97
- rank_html = "<h4>#️⃣ よく使うタグ (Top 3)</h4><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 15px;'>"
98
- rank_html += "".join([f"<span style='background: #eee; padding: 4px 10px; border-radius: 15px; font-size: 0.8em;'>#{t} ({c})</span>" for t, c in tag_counts]) or "なし"
99
- rank_html += "</div><h4>🤝 よく交流する相手</h4>"
100
- rank_html += "".join([f"<div style='font-size: 0.9em;'>@{u} ({c}回)</div>" for u, c in user_counts]) or "なし"
101
-
102
- # HTML: 初投稿 & ベストポスト Top 3
103
- top_3_posts = df.sort_values('score', ascending=False).head(3)
104
-
105
- posts_html = f"""
106
- <div style='background: #e8f5e9; padding: 12px; border-radius: 8px; margin-bottom: 20px; border-left: 5px solid #4caf50;'>
107
- <small style='color: #2e7d32; font-weight: bold;'>🌱 取得範囲内の初投稿 ({first_post['created_at'].strftime('%Y/%m/%d')})</small>
108
- <p style='font-size: 0.9em; margin: 5px 0;'>{first_post['text']}</p>
109
- <a href='{first_post['url']}' target='_blank' style='font-size: 0.8em; color: #4caf50;'>投稿を見る</a>
110
  </div>
111
- <h4 style='margin-bottom: 10px; color: #ff4081;'>🏆 ベストポスト Top 3</h4>
112
  """
113
-
114
- for i, (_, row) in enumerate(top_3_posts.iterrows(), 1):
115
- posts_html += f"""
116
- <div style='background: #fff; padding: 12px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ffc107; position: relative;'>
117
- <span style='position: absolute; top: 10px; right: 10px; font-size: 0.75em; color: #666;'>❤️ {row['likes']} 🔄 {row['reposts']}</span>
118
- <small style='font-weight: bold; color: #ffb300;'>第{i}位</small>
119
- <p style='font-size: 0.9em; margin: 5px 0; padding-right: 60px;'>{row['text'][:100]}{'...' if len(row['text']) > 100 else ''}</p>
120
- <a href='{row['url']}' target='_blank' style='font-size: 0.8em; color: #ff4081;'>投稿を見る</a>
121
- </div>
122
- """
123
-
124
- progress(1.0, desc="完了!")
125
- return stats_html, rank_html, posts_html, "解析が完了しました!"
126
 
127
- except Exception as e:
128
- return f"エラー: {str(e)}", "", "", "失敗しました"
 
 
 
129
 
130
- # --- UI (変更なし) ---
131
- with gr.Blocks(title="Bluesky Summary") as demo:
132
- gr.Markdown("# 🦋 Bluesky 自己分析ダッシュボード")
133
- with gr.Row():
134
- with gr.Column(scale=1):
135
- in_id = gr.Textbox(label="自分のハンドル", placeholder="example.bsky.social")
136
- in_pw = gr.Textbox(label="アプリパスワード", type="password")
137
- in_target = gr.Textbox(label="解析したいID")
138
- btn = gr.Button("分析を開始", variant="primary")
139
- status = gr.Textbox(label="ステータス", interactive=False)
140
- with gr.Column(scale=2):
141
- out_stats = gr.HTML()
142
- out_rank = gr.HTML()
143
- out_posts = gr.HTML()
144
 
145
- btn.click(
146
- fn=analyze_and_output,
147
- inputs=[in_id, in_pw, in_target],
148
- outputs=[out_stats, out_rank, out_posts, status]
149
- )
150
 
151
- demo.launch()
 
8
 
9
  def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
10
  try:
 
11
  client = Client()
12
  my_id = my_id.replace('@', '').strip()
13
  target_handle = target_id.replace('@', '').strip()
14
  client.login(my_id, my_pw.strip())
15
 
16
  profile = client.get_profile(actor=target_handle)
 
17
 
18
  posts_data = []
19
  hashtags = []
20
+ reply_users = [] # メンション・リプライ
21
+ repost_users = [] # リポストした相手
22
+ like_users = [] # いいねした相手
23
+
24
  max_limit = 1000
 
 
 
25
 
26
+ # --- 1. 投稿フィードの分析 (リプライ・リポスト) ---
27
+ progress(0, desc="フィードを取得中...")
28
+ cursor = None
29
  while len(posts_data) < max_limit:
30
  response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
 
31
  for feed_view in response.feed:
32
+ # リポストの分析
33
+ if feed_view.reason and hasattr(feed_view.reason, 'by'):
34
+ # 自分がリポストした投稿の、元の投稿者
35
+ orig_author = feed_view.post.author.handle
36
+ if orig_author != target_handle:
37
+ repost_users.append(orig_author)
38
+ continue # リポスト自体は本文解析から除外
39
 
40
  post = feed_view.post
41
  if post.author.handle == target_handle:
42
  rkey = post.uri.split('/')[-1]
43
  post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
44
 
 
 
 
45
  posts_data.append({
46
  'text': post.record.text,
47
  'created_at': pd.to_datetime(post.record.created_at),
48
  'likes': post.like_count,
49
  'reposts': post.repost_count,
50
+ 'score': post.like_count + post.repost_count,
51
  'url': post_url
52
  })
53
 
 
54
  hashtags.extend(re.findall(r'#(\w+)', post.record.text))
55
+
56
+ # リプライ・メンション相手
57
  if feed_view.reply and feed_view.reply.parent:
58
+ if hasattr(feed_view.reply.parent, 'author'):
59
+ parent_author = feed_view.reply.parent.author.handle
60
+ if parent_author != target_handle:
61
+ reply_users.append(parent_author)
62
 
 
 
63
  cursor = response.cursor
64
  if not cursor or len(posts_data) >= max_limit: break
65
+ progress(len(posts_data)/max_limit * 0.5, desc=f"フィード解析中...")
66
+
67
+ # --- 2. いいね一覧の分析 ---
68
+ progress(0.6, desc="いいねした相手を分析中...")
69
+ like_cursor = None
70
+ # いいねは直近200件程度をサンプルとして取得(API負荷考慮)
71
+ for _ in range(2):
72
+ likes_resp = client.get_actor_likes(actor=profile.did, limit=100, cursor=like_cursor)
73
+ for like_item in likes_resp.feed:
74
+ if like_item.post and like_item.post.author:
75
+ l_author = like_item.post.author.handle
76
+ if l_author != target_handle:
77
+ like_users.append(l_author)
78
+ like_cursor = likes_resp.cursor
79
+ if not like_cursor: break
80
+
81
+ if not posts_data: return "データなし", "", "", "終了"
82
+
83
+ # --- 3. HTML作成 ---
84
  df = pd.DataFrame(posts_data)
 
 
85
  first_post = df.iloc[-1]
86
  days_active = max((datetime.now().replace(tzinfo=None) - first_post['created_at'].replace(tzinfo=None)).days, 1)
87
 
 
88
  stats_html = f"""
89
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
90
  <div style='background: #e3f2fd; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #90caf9;'>
91
+ <div style='color: #1565c0; font-size: 0.8em;'>総ポスト</div><div style='font-size: 1.5em; font-weight: bold;'>{profile.posts_count}</div>
92
  </div>
93
  <div style='background: #f1f8e9; padding: 15px; border-radius: 10px; text-align: center; border: 1px solid #c5e1a5;'>
94
  <div style='color: #33691e; font-size: 0.8em;'>継続日数</div><div style='font-size: 1.5em; font-weight: bold;'>{days_active}日</div>
 
99
  </div>
100
  """
101
 
102
+ # ランキングHTML
103
+ def make_rank_list(title, icon, counter_list):
104
+ html = f"<b>{icon} {title}</b><div style='font-size: 0.85em; color: #444; margin: 5px 0 15px 0;'>"
105
+ if not counter_list: return html + "データなし</div>"
106
+ for name, count in Counter(counter_list).most_common(3):
107
+ html += f"<div>@{name} ({count})</div>"
108
+ return html + "</div>"
109
+
110
+ rank_html = f"""
111
+ <div style='background: #fafafa; padding: 15px; border-radius: 10px; border: 1px solid #eee;'>
112
+ {make_rank_list("よくリプライする相手", "💬", reply_users)}
113
+ {make_rank_list("よくリポストする相手", "🔄", repost_users)}
114
+ {make_rank_list("よくいいねする相手", "❤️", like_users)}
115
+ <b>#️⃣ よく使うタグ</b><div style='display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;'>
116
+ {"".join([f"<span style='background: #eee; padding: 3px 8px; border-radius: 10px; font-size: 0.75em;'>#{t}</span>" for t, _ in Counter(hashtags).most_common(3)]) or "なし"}
117
+ </div>
 
118
  </div>
 
119
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ # ベストポスト等 (前回同様)
122
+ top_3 = df.sort_values('score', ascending=False).head(3)
123
+ posts_html = f"<h4>🌱 初投稿 ({first_post['created_at'].strftime('%Y/%m/%d')})</h4><p style='font-size: 0.9em; background:#f9f9f9; padding:10px; border-radius:5px;'>{first_post['text']}</p><h4 style='color: #ff4081;'>🏆 ベストポスト Top 3</h4>"
124
+ for i, (_, row) in enumerate(top_3.iterrows(), 1):
125
+ posts_html += f"<div style='border: 1px solid #ffc107; padding: 10px; margin-bottom: 8px; border-radius: 8px; font-size: 0.9em;'><b>第{i}位</b> (❤️{row['likes']} 🔄{row['reposts']})<br>{row['text'][:80]}...<br><a href='{row['url']}' target='_blank' style='color:#ff4081;'>投稿を見る</a></div>"
126
 
127
+ return stats_html, rank_html, posts_html, "解析完了!"
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ except Exception as e:
130
+ return f"エラー: {str(e)}", "", "", "失敗"
 
 
 
131
 
132
+ # (Gradio UI部分は変更なし)