🐈

ChatGPT APIなしでBlueSkyにキャラbotを作ってみた

に公開

BlueSkybotを作ってみたいと思いつつ、chatGPTのAPI課金はしたくないっていう無課金環境でキャラbotを作った手順を紹介していきます。

🗂 投稿文はローカルで大量生成するだけ

使ったのはchatGPT4o。
まずは一般的なキャラ設定。んで、ここでは「キャラbot」を作るからAI人格+趣味趣向やキャラに付属する人物条件も付与。
AI人格設定は誰もが一度は触るだろうし、詳しいことは割愛。
私がAI人格設定するときの条件なんかは以下の記事に詳しく書いています。
https://zenn.dev/nachi_m/articles/e8547e375fd21a
人格設定が完了したらchatGPTローカル上でひたすら投稿文を生成。目安は1000程度。「趣味に関する投稿文」「仕事に関する投稿文」「交友関係に関する投稿文」などシチュエーションを指定すれば幅広い投稿文が生成されます。
大量生成したテキストは asa-aisatsu.txt hiru-aisatsu.txt yoru-aisatsu.txt normal-text.txt sleep-talk.txtなどジャンル分けして保存。

🖥 投稿制御はPythonスクリプトで完結

まず、bot用のBlueSkyアカウントを取得。

設定→プライバシーとセキュリティ→アプリパスワード
このアプリパスワードがBlueSkyAPIに接続するためのログインキーになるから必須。
Pythonコードは以下のような感じ

# 環境変数の取得
MY_BOT_USERNAME = os.getenv("MY_BOT_USERNAME")
APP_PASSWORD = os.getenv("APP_PASSWORD")

# ログの設定
logging.basicConfig(
    filename='bluesky_bot.log',
    level=logging.ERROR,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# 最大リトライ回数
MAX_RETRY = 5

# 環境変数の未設定チェック
if not MY_BOT_USERNAME or not APP_PASSWORD:
    error_message = "環境変数 MY_BOT_USERNAME または APP_PASSWORD が正しく設定されていません。"
    print(error_message)
    logging.error(error_message)
    
    # 環境変数がない場合、適切な終了コードを設定して終了
    sys.exit(1)  # 1: 異常終了

# レートリミットのリセット時間を取得する関数
def get_rate_limit_reset():
    url = "https://bsky.social/xrpc/com.atproto.server.getSession"
    try:
        response = requests.get(url, timeout=5)  # タイムアウトを設定
        response.raise_for_status()  # HTTPエラー(4xx, 5xx)の場合は例外を発生させる
        
        reset_time = int(response.headers.get("ratelimit-reset", 0))
        return reset_time
    
    except Timeout:
        error_message = "レートリミット情報取得がタイムアウトしました。"
        logging.error(error_message)
        print(error_message)
    
    except HTTPError as e:
        error_message = f"HTTPエラー: {e.response.status_code} - {e.response.reason}"
        logging.error(error_message)
        print(error_message)
    
    except ConnectionError:
        error_message = "ネットワークエラー: サーバーに接続できませんでした。"
        logging.error(error_message)
        print(error_message)
    
    except RequestException as e:
        error_message = f"予期しないリクエストエラー: {e}"
        logging.error(error_message)
        print(error_message)
    
    return 0  # エラー時は0を返す

# レートリミットが解除されるまで待機
def wait_until_rate_limit_reset():
    reset_time = get_rate_limit_reset()
    if reset_time > 0:
        wait_time = reset_time - int(time.time())
        if wait_time > 0:
            print(f"レートリミットがリセットされるまで {wait_time} 秒待機します...")
            logging.warning(f"レートリミット超過: {wait_time}秒待機")
            time.sleep(wait_time)

# クライアントの初期化関数(UnauthorizedError 対応)
def initialize_client():
    from atproto import Client  # 遅延インポート(他の処理と分離)
    
    client = Client()
    attempt = 0

    while attempt < MAX_RETRY:  # 最大試行回数までリトライ
        try:
            print(f"Trying to login with: {repr(MY_BOT_USERNAME)} / {'*' * len(APP_PASSWORD) if APP_PASSWORD else 'None'}")
            client.login(MY_BOT_USERNAME, APP_PASSWORD)
            print("Bluesky APIに接続しました。")
            return client  # 成功時は client を返す
        
        except UnauthorizedError as e:
            attempt += 1
            error_message = f"認証エラー: {e}. {attempt} 回目の試行"
            print(error_message)
            logging.error(error_message)
            
            if attempt >= MAX_RETRY:
                print("最大試行回数に達しました。プログラムを終了します。")
                logging.error("最大試行回数に達し、認証に失敗しました。")
                return None  # None を返して異常終了(呼び出し元で処理)

            time.sleep(10)  # 10秒待機してリトライ

        except Exception as e:
            error_message = f"予期しないエラーが発生しました: {e}"
            print(error_message)
            logging.error(error_message)
            return None  # 予期しないエラー発生時は即座に終了

    return None  # ここに到達することはないが念のため

これでPython制御下でBlueSkyにログイン。
投稿もPython制御。

# 投稿作成関数
def post_content(client, text, image_blob=None):
    try:
        if image_blob:
            response = client.send_post(text=text, images=[image_blob])
        else:
            response = client.send_post(text=text)
        if hasattr(response, 'error') and response.error:
            error_message = f"投稿に失敗しました: {response['error']}"
            print(error_message)
            logging.error(error_message)
        else:
            print("投稿が成功しました。")
    except Exception as e:
        error_message = f"投稿中にエラーが発生しました: {e}"
        print(error_message)
        logging.error(error_message)

# ファイルからテキストをランダムに選択する関数
def get_random_text(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            lines = file.readlines()
            return random.choice(lines).strip()
    except FileNotFoundError:
        error_message = f"ファイルが見つかりません: {file_path}"
        print(error_message)
        logging.error(error_message)
        return None

# 朝昼晩の挨拶投稿関数
def post_greeting(client):
    current_hour = datetime.now().hour

    if 5 <= current_hour < 12:
        greeting = get_random_text('asa-aisatsu.txt')
    elif 12 <= current_hour < 17:
        greeting = get_random_text('hiru-aisatsu.txt')
    elif current_hour == 23:
        greeting = get_random_text('yoru-aisatsu.txt')
    else:
        greeting = None

    if greeting:
        post_content(client, greeting)
        print(f"挨拶を投稿しました: {greeting}")
    else:
        print("適切な挨拶のテキストが見つかりませんでした。")

# 通常投稿関数
def post_regular_text(client, first_run=False):
    current_hour = datetime.now().hour

    if 1 <= current_hour < 6:
        text = get_random_text('sleep-talk.txt')
        interval = 3600  # 深夜帯は1時間に1回
    else:
        text = get_random_text('normal-text.txt')
        interval = random.randint(900, 1800)  # 15~30分間隔(1時間に2~4回)

    # 初回実行時に間隔を空ける
    if first_run:
        print(f"初回実行のため、{interval} 秒待機します...")
        time.sleep(interval)

    if text:
        post_content(client, text)
        print(f"通常投稿を行いました: {text}")
    else:
        print("通常投稿のテキストが見つかりませんでした。")
        logging.error("通常投稿のテキストが見つかりませんでした。")

    # 次回の投稿スケジュール設定
    schedule.every(interval).seconds.do(post_regular_text, client, first_run=False)

post_regular_text(client, first_run=True) 

ここで生成した投稿文を分けて保存する必要があったんですよ。

🔁 botの“日常サイクル”

投稿文生成の時に保存ファイルを asa-aisatsu.txt hiru-aisatsu.txt yoru-aisatsu.txt normal-text.txt sleep-talk.txt で分けましたが、これは疑似的な日常感を演出するため。

  • 6時:おはよう
  • 12時:昼ごはん系
  • 23時:おやすみ
  • 23時01分~5時59分まで:寝言風
    挨拶系テキストも複数生成してその日の気分の揺れをランダム取得で表現できる。

現状の課題はnormal-text.txtが時間帯整理されてないから夜に仕事のこと投稿しちゃったりしてるけど、ここはタグ管理で整理すべきか「オフでも仕事のこと考えちゃう」ってことで放置するか悩み中。

📌 最小構成でbotを回す設計ポイント

  • ChatGPT API未使用(ローカル生成で済ます)
  • BlueSky APIは無料 → atprotoで対応
  • 投稿はキャラの時間感覚を見せるだけ
    要は初手でGPTぶん回して投稿文大量生成するだけでOK!簡単!!

それでいま稼働中なのが結城 悠真bot.ver1.0
https://bsky.app/profile/yukiyuma-bot.bsky.social

Discussion