# --- START OF MODIFIED FILE app.py --- import gradio as gr from atproto import Client, models import os import io from PIL import Image # --- 配置您的 Bluesky 账户信息 --- BSKY_HANDLE = os.environ.get("BSKY_HANDLE") BSKY_APP_PASSWORD = os.environ.get("BSKY_APP_PASSWORD") # --- 新增:配置缓存目录 --- IMAGE_CACHE_DIR = "image_cache" # 确保缓存目录存在 os.makedirs(IMAGE_CACHE_DIR, exist_ok=True) client = Client() # --- 全局登录 --- try: print("正在尝试登录 Bluesky...") if BSKY_HANDLE and BSKY_APP_PASSWORD: client.login(BSKY_HANDLE, BSKY_APP_PASSWORD) print(f"Bluesky 登录成功!用户: {client.me.handle}") else: print("未找到 Bluesky 环境变量,跳过全局登录。") except Exception as e: print(f"Bluesky 全局登录失败: {e}") pass def fetch_bsky_gallery(max_posts=51): if not client.me: print("客户端未登录,正在尝试重新登录...") if not BSKY_HANDLE or not BSKY_APP_PASSWORD: raise gr.Error("Bluesky 登录失败:请在 Hugging Face Spaces 的 Repository secrets 中设置 BSKY_HANDLE 和 BSKY_APP_PASSWORD。") try: client.login(BSKY_HANDLE, BSKY_APP_PASSWORD) print(f"Bluesky 重新登录成功!用户: {client.me.handle}") except Exception as e: raise gr.Error(f"Bluesky 登录失败,请检查 Handle 或应用密码并重启程序。错误: {e}") print(f"正在为用户 {client.me.handle} 获取帖子...") try: actor_did = client.me.did profile_feed = client.get_author_feed(actor=actor_did, limit=max_posts) print(f"从 API 获取了 {len(profile_feed.feed)} 条帖子,正在开始检查...") gallery_items = [] for i, feed_view in enumerate(profile_feed.feed): post_record = feed_view.post.record # 核心判断逻辑 if post_record.embed and isinstance(post_record.embed, models.AppBskyEmbedImages.Main): print(f"成功匹配到图片帖子 #{i+1}!正在处理...") images = post_record.embed.images post_text = getattr(post_record, 'text', '') for j, image_data in enumerate(images): try: image_cid = image_data.image.ref.link # --- 缓存逻辑开始 --- # 1. 定义缓存文件的路径 cached_image_path = os.path.join(IMAGE_CACHE_DIR, f"{image_cid}.jpg") # 2. 检查缓存是否存在 if os.path.exists(cached_image_path): print(f" > 缓存命中!直接使用本地图片: {cached_image_path}") else: # 3. 如果缓存不存在,则下载并保存 print(f" > 缓存未命中。正在下载 CID: {image_cid}...") params = models.ComAtprotoSyncGetBlob.Params(did=actor_did, cid=image_cid) image_bytes = client.com.atproto.sync.get_blob(params) if image_bytes: # 使用 PIL 打开图片并转换为 RGB img = Image.open(io.BytesIO(image_bytes)) if img.mode == 'RGBA': img = img.convert('RGB') # 保存到缓存目录 img.save(cached_image_path, format='JPEG') print(f" > 图片已成功下载并缓存至: {cached_image_path}") else: print(f" > 下载失败,未获取到图片数据。") continue # 跳过这张图片 # --- 缓存逻辑结束 --- # 将缓存路径(或已存在的本地路径)添加到画廊列表 caption = post_text if j == 0 else "" gallery_items.append((cached_image_path, caption)) except Exception as img_e: print(f"处理单张图片时发生错误: {img_e}") continue else: print(f"帖子 #{i+1} 不包含图片,已跳过。") print(f"\n检查完成。成功获取并处理了 {len(gallery_items)} 张图片。") if not gallery_items: placeholder_image = "https://i.imgur.com/nA8n922.png" return [(placeholder_image, f"在最近的 {max_posts} 条帖子中没有找到符合条件的图片。")] return gallery_items except Exception as e: print(f"获取帖子时发生错误: {e}") raise gr.Error(f"获取 Bluesky 帖子时出错: {e}") with gr.Blocks() as demo: gr.Markdown(""" ### 🎨 AiFeiFei 的 Bluesky 数字艺术画廊 """) with gr.Row(): gallery = gr.Gallery( label="作品集", columns=3, height=600, show_download_button=True, object_fit="contain", show_label=False ) with gr.Row(): refresh_button = gr.Button("🔄 刷新画廊") demo.load(fn=fetch_bsky_gallery, inputs=None, outputs=gallery) refresh_button.click(fn=fetch_bsky_gallery, inputs=None, outputs=gallery) if __name__ == "__main__": demo.launch(debug=True) # --- END OF MODIFIED FILE app.py ---