見出し画像

「AIリネーム」生成AIで画像ファイルに中身のわかるファイル名をつけよう! Claude 3 Haikuがイライラを解消。ソースコード公開中

こんにちは!
ノーリーです。ClaudeChatGPT使ってますか?

折角の生成AIを日頃のイライラ解消に使ってみませんか?
AIがあなたの画像ファイルに中身がわかるファイル名をつけてくれますよ。

私は自分のPCにある、記号の羅列の画像ファイルが気に食いません。
中身がわからないからです。

こういうファイルなんとかならんのか

でも捨てようにも、重要なファイルかも知れないじゃないですか。
しかし、いちいち開けて確認するのも面倒過ぎ。
そういうわけで、何百ものファイルが放置されていました。

このイライラを生成AI Claude 3 Haiku で解決します。
コストもごくわずかです。

この記事は、大阪のIT専門学校「清風情報工科学院」の校長・平岡憲人(ノーリー)がお送りします。
清風情報工科学院では、情報処理系の講師を急募しております。
ご興味のある方はこちらの記事を御覧ください。

1.「AIリネーム」でできること

指定したフォルダーの中にある画像ファイルを、生成AIの画像キャプション能力を使って、中身のわかるファイル名にリネームしていくこと。

取り急ぎ公開するので、開発環境がないと動きません。
時間に余裕がある時に、アプリにするつもりです。
まずは、生成AIに興味ある方にお届けします。

必要なもの

Claude 3 HaikuAPIキー
Python
CursorまたはVSCodeの開発環境

Cursor上で動いている様子

処理内容

フォルダ内の画像ファイルをAnthropicのAPIに送り、格安の生成AIであるClaude 3 Haikuのビジョン機能を使って、画像のキャプションを片っ端からつけていく。
そのフォルダに「processed_files.csv」というファイルでリネームの記録を残す。
Claudeには、毎分5回までというリクエスト制限がある。
ウルトラスピードでリネームと言いたいが、わざわざウェイトを入れて12秒に1回動作するようになっている。
寝る前に動かして、夜中に作業してもらって。

2.ソースコード

このプログラムは、自由に使うことができます。
改変も自由です。
CC BY-SA  表示 - 継承 4.0 国際 に従って下さい。

Githubに公開すればよいのだと思いますが、やり方を間違えてもあれなので、とりあえず、ここにソースコードを貼りますね。
言語は、Pythonです。

一番コアなプロンプトは
「この画像に日本語で50字程度のファイル名をつけて」
というシンプルなものです。

"""
AIリネーム   生成AIで画像ファイルに中身のわかるファイル名をつけよう! Claude 3 Haikuがイライラを解消
    指定されたフォルダ内の画像ファイルに、AIを使用してキャプションを生成し、ファイル名をリネームする
  2024/4/16  Version 1.0  HIRAOKA Norito  CC BY-SA  表示 - 継承 4.0 国際
"""
import anthropic # Claude-3-HaikuのAPI呼び出しに必要
import os # ファイルパスの取得に必要
import time # APIのレート制限を回避するための待ち時間の計算に必要
import base64 # 画像ファイルをAPIにわたすために必要
import csv # リネーム記録用
from PIL import Image # サムネール作成用

def get_caption(image_path):
    """
    画像ファイルのパスを受け取り、Anthropic APIを使用して画像のキャプションを生成する関数。
    
    1. 画像ファイルを開き、必要に応じてサムネイルを作成。
    2. 画像ファイルをbase64エンコードする。
    3. Anthropic APIを呼び出し、画像からキャプションを生成。
    4. 生成されたキャプションの最初の50字を返す。
    
    Parameters:
    image_path (str): キャプションを生成する画像ファイルのパス
    
    Returns:
    str: 生成されたキャプションの最初の50字
    
    Raises:
    IOError: 画像ファイルが開けない場合
    anthropic.exceptions.RateLimitExceededException: APIのレート制限に達した場合
    IndexError, AttributeError: レスポンスからキャプションを抽出できない場合
    Exception: その他の例外が発生した場合
    """

    # Claudeのクライアントを作成(実行はもっと後で)
    client = anthropic.Anthropic(
        # もし環境変数でAPIキーをセットできなければここで指定:
        # api_key="..."
    )

    # 画像ファイルを開いて前処理を行う
    try:
        with Image.open(image_path) as img:
            # 対応フォーマットでない、または画像サイズが大きすぎる場合は
            # サムネイルを作成して一時ファイルに保存
            # コスト削減のためでもあります
            # if img.format not in ["JPEG", "PNG", "GIF"] or max(img.size) > 512:
            #     img.thumbnail((512, 512))
            if img.format not in ["JPEG", "PNG", "GIF"] or max(img.size) > 1000:
                img.thumbnail((1000, 1000))
                temp_image_path = os.path.join(os.path.dirname(image_path), "temp_image.png")
                img.save(temp_image_path, "PNG")
                # image_path = temp_image_path
                media_type = "png"
                print("サムネールを作りました")
            # そのまま使える場合は画像フォーマットを小文字で取得
            else:
                temp_image_path = image_path
                media_type = img.format.lower()
                print("サムネールは作りません")
    # 画像ファイルが開けなかった場合はエラーを出力して例外を再スロー
    except IOError:
        print(f"Error: Unable to open {image_path}")
        raise

    # 画像ファイルをバイナリモードで開き、base64エンコードを行う
    with open(temp_image_path, 'rb') as file:
        data = base64.b64encode(file.read()).decode('utf-8')

        # デバッグ用に画像ファイルのパスを出力
        print("Base64エンコードしました")

    # Anthropic APIを使用して画像からキャプションを生成する
    print("Cloude 3 Haikuを呼びます")
    try:
        message = client.messages.create(
            model="claude-3-haiku-20240307", # HaikuもVision対応なのは神!
            max_tokens=100, # 最大100トークン
            temperature=0.0, # 淡々モード
            system="",
            messages=[
                {
                    "role": "user",
                    "content": [
                        {   # BASE64変換されたキャプションをつける画像
                            "type": "image",
                            "source": {
                                "type": "base64",
                                "media_type": "image/" + media_type,
                                "data": data,
                            }
                        },
                        {   # メインプロンプト
                            "type": "text",
                            "text": "この画像に日本語で50字程度のファイル名をつけて"
                        }
                    ]
                }
            ]
        )
        print(message)  # 認識結果を出力
        input_tokens = message.usage.input_tokens # トークン数取得
        output_tokens = message.usage.output_tokens
        # cost = 
        print(f"消費トークン: IN {input_tokens} OUT {output_tokens}")                                                                               
        # print(f"APIコスト ${cost}")                                                                               
        caption = message.content[0].text
    # APIのレート制限に達した場合のエラー処理
    except anthropic.exceptions.RateLimitExceededException:
        print(f"Error: Rate limit exceeded for {image_path}")
        raise
    # レスポンスからキャプションを抽出できなかった場合のエラー処理 
    except (IndexError, AttributeError):
        print(f"Error: Unable to extract caption from response for {image_path}")
        raise
    # その他の例外が発生した場合のエラー処理
    except Exception as e:
        print(f"Error: {e} for {image_path}")
        raise

    # キャプションが長過ぎることがあることを見越して、最初の50字だけをキャプションとして返す
    return caption[:50]
                                                                                                
def main(folder_path):
    """
    指定されたフォルダ内の画像ファイルに対して、AIを使用してキャプションを生成し、そのキャプションをもとにファイル名をリネームする関数。
    
    Args:
        folder_path (str): 処理対象のフォルダのパス
        
    Returns:
        None
        
    Notes:
        - 処理済みのファイル情報はCSVファイルに記録される
        - 既に処理済みのファイルや、ファイル名に漢字が含まれるファイルはスキップされる
        - APIの利用制限エラーが発生した場合は1時間待機後に再試行される
        - ファイル名の重複を避けるため、必要に応じて連番が付与される
    """
    # 処理済みファイルのリストを初期化
    processed_files = []
    # フォルダパスが指定されていない場合、カレントディレクトリを使用
    if folder_path is None:
        folder_path = '.'
    print("---------------------------------------------")
    print("Selected folder: "+ folder_path)
    print("---------------------------------------------")
    
    # 処理済みファイルのリストを格納するCSVファイルのパスを作成
    csv_file_path = os.path.join(folder_path, 'processed_files.csv')
    
    # CSVファイルが存在する場合は、処理済みファイルのリストを読み込む
    try:
        with open(csv_file_path, 'r', encoding='utf-8') as csvfile:
            reader = csv.DictReader(csvfile)
            processed_files = [row['original_filename'] for row in reader if row['new_filename'] != 'Skipped'] 
        with open(csv_file_path, 'r', encoding='utf-8') as csvfile:
            reader = csv.DictReader(csvfile)
            processed_files = processed_files + [row['new_filename'] for row in reader if row['new_filename'] != 'Skipped']         
            print("以前にRenameした記録がありました")
            print(processed_files)
            #exit()
    
    # CSVファイルが存在しない場合は、空のリストを作成
    except FileNotFoundError:
        processed_files = []
        with open(os.path.join(folder_path, 'processed_files.csv'), 'a', newline='', encoding='utf-8') as csvfile:
            fieldnames = ['original_filename', 'new_filename', 'remarks']
            # CSVファイルへの書き込み用のDictWriterオブジェクトを作成
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            # ヘッダー行を書き込む
            writer.writeheader()
            print("新たにRename記録を作成します")

    # 指定されたフォルダ内のファイル一覧を取得、フォルダーを除外
    matching_files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
    print("対象ファイル数: " + str(len(matching_files)))
    print("---------------------------------------------")
                                              
    # 処理済みファイルのリストを追記モードで開く
    with open(os.path.join(folder_path, 'processed_files.csv'), 'a', newline='', encoding='utf-8') as csvfile:
        # CSVファイルのヘッダー(フィールド名)を指定
        fieldnames = ['original_filename', 'new_filename', 'remarks']
        # CSVファイルへの書き込み用のDictWriterオブジェクトを作成
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

        i = 0
        # matching_filesリストの各ファイル名に対して処理を行う
        for filename in matching_files:
            i = i+1
            # 既に処理済みのファイルと、ファイル名に漢字が含まれるファイルはスキップする  
            if filename not in processed_files:
                # 例外処理のtry文
                try:
                    if not any(char >= '\u4e00' and char <= '\u9fff' for char in filename):
                        # 画像ファイルからキャプションを生成する
                        image_file_path = os.path.join(folder_path, filename)
                        print("■" + str(i) + " File: " + filename)

                        caption = get_caption(image_file_path)
                        # キャプションからファイル名を生成する
                        new_filename = "".join(e for e in caption if e.isalnum()) + os.path.splitext(filename)[1]
                        print("修正候補: " + new_filename)
                        # ファイル名を変更する
                        # 新しいファイル名とパスを設定
                        renamed_filename = new_filename
                        renamed_file_path = os.path.join(folder_path, new_filename)
                        # 同名のファイルが既に存在する場合
                        if os.path.exists(renamed_file_path):
                            # 連番を付けて重複を回避
                            print("同名のファイルがありました")
                            i = 1
                            while True:
                                renamed_filename = f"{os.path.splitext(new_filename)[0]}({i}){os.path.splitext(new_filename)[1]}"
                                renamed_file_path = os.path.join(folder_path, renamed_filename)
                                if not os.path.exists(renamed_file_path):
                                    break
                                i += 1
                            # ファイル名を変更
                            os.rename(os.path.join(folder_path, filename), renamed_file_path)
                            # 変更内容をCSVに記録
                            writer.writerow({'original_filename': filename, 'new_filename': renamed_filename, 'remarks': 'Renamed'})
                        # 同名のファイルが存在しない場合
                        else:
                            # ファイル名を変更
                            os.rename(os.path.join(folder_path, filename), renamed_file_path)
                            # 変更内容をCSVに記録
                            writer.writerow({'original_filename': filename, 'new_filename': renamed_filename, 'remarks': 'Renamed'})
                        # 変更前後のファイル名を表示
                        print(filename + " -> " + renamed_filename)
                        # APIコール後に12秒間待機
                        time.sleep(12) 
                        continue

                    # 漢字のファイル名のスキップを記録する
                    else:
                        print(str(i) + f" Skipping kanji file name: {filename}")
                        writer.writerow({'original_filename': filename, 'new_filename': 'Skipped', 'remarks': 'Kanji file name'})
                        continue
            
                    # APIの利用制限エラーを処理する
                except Exception as e:
                    if "Rate limit exceeded" in str(e):
                        print(f"Error: Rate limit exceeded for {filename}")
                        writer.writerow({'original_filename': filename, 'new_filename': 'Untitled', 'remarks': 'Rate limit exceeded'})
                        time.sleep(60 * 60)  # 1時間待機
                        continue
                    else:
                        print(f"Error: {str(e)}")
                        writer.writerow({'original_filename': filename, 'new_filename': 'Skipped', 'remarks': 'Skipped due to error'})
                        continue

            # 既に処理済みのファイルのスキップを記録する
            else:
                print(str(i) + f" Skipping already processed file: {filename}")
                writer.writerow({'original_filename': filename, 'new_filename': filename, 'remarks': 'Already processed'})
                continue

        print("---------------------------------------------")
        print("Rename finished.")

if __name__ == "__main__":
    '''
    ユーザーにフォルダー選択ダイアログを表示し、
    選択されたフォルダーのパスを引数としてmain関数を呼び出す
    '''

    # tkinterモジュールをtkという名前でインポート
    import tkinter as tk
    # tkinterモジュールからfiledialogをインポート
    from tkinter import filedialog

    # tkinterのルートウィンドウを作成
    root = tk.Tk()
    # ルートウィンドウを非表示にする
    root.withdraw()

    # ユーザーにフォルダー選択ダイアログを表示し、選択されたフォルダーのパスを取得
    folder_path = filedialog.askdirectory(title="フォルダーを選択してください")
    # 選択されたフォルダーのパスを引数としてmain関数を呼び出す
    main(folder_path)

ソースコードには、コメントをつけておきました。
読んでいただけば、処理の流れはわかると思います。

Windows環境で動作確認しています。
Macでも動くと思いますが、確認していません。

ClaudeのAPIキーは、環境変数の「ANTHROPIC_API_KEY」に設定しておいて下さい。

動作などに問題がありましたら、コメントにてお知らせ下さい。

3.備考

ClaudeのAPIキーの調達方法

ClaudeのAPIキーの取得方法は、次のように行います。
まず、次の記事の1と2の要領で、開発者アカウントをつくります。
初期に$5の無料クーポンが与えられます。
うまくやればこれで1000枚くらいできるはずです。

開発者アカウントがつくれたら、ダッシュボードからAPIキーの生成コーナーに行って生成し、取得したAPIキーを手元のPCの環境変数の「ANTHROPIC_API_KEY」に設定します。
やり方は、次の記事など参考になさって下さい。

Cursorの導入方法

Microsoftがオープンソースで作って公開しているVisual Studio Code(VSCode)という開発者用のエディタがあります。
これにAI機能を組み込んで、電動アシスト自転車の要領で、AIアシストエディタにしてしまったのがCursorです。
私は、生成AI塾の元木さんに教えてもらい、現在ではCursorなしの開発は考えられないくらい愛用しています。
また、開発に使うだけでなく、企画書など書く時にも使い始めています。

これを機会に、使ってみられてはいかがでしょうか。

Python

Pythonは、MicrosoftがExcelの中に組み込んでしまった注目の言語です。
Pythonは、現代における「BASIC」の位置にあります。
マイコンの黎明期には、BASIC。
その後、Visual Basic。
いま、Pythonです。
インタープリタという共通点があります。

専門家の領域では、AI関係の開発、データ分析などに使われています。
生成AIに興味のある方は、この際、Pythonに挑戦なさって下さい。

この動画では、開発環境として上に述べたVSCodeが使われています。
VSCodeの設定とCursorの設定はほぼ同じなので、VSCodeの説明をCursorに読み替えてご覧ください。

コスト

1枚あたりいくらくらい費用がかかるか。
ざっくりいうと、画像1枚0.04円くらいです。
   百枚で 4円
   千枚で40円
大量にやるとお金が気になりますね。
下げる方法は下で述べます。
利用明細の見方は、次の記事の3の(8)を見て下さい。

「AIリネーム」のプログラムでは、画像のサイズを縦横1000ピクセルに縮小してキャプションをつけさせています。
これを500くらいにすれば、コストは単純計算で1/4程度になるはずです。
ただ、精度が下がってきます。

get_caption関数のこの部分が該当処理に関わるところです。

        # if img.format not in ["JPEG", "PNG", "GIF"] or max(img.size) > 512:
        #     img.thumbnail((512, 512))
        if img.format not in ["JPEG", "PNG", "GIF"] or max(img.size) > 1000:
            img.thumbnail((1000, 1000))

現在のif文をコメントアウトし、コメントアウトしてあるコードの冒頭の「#」を削除して下さい。
これで、画像1枚0.01円くらいになるはずです。
百枚で 1円
   千枚で10円
これだと結構リーズナブル
じゃないですか?

もっとも、こういうのに使うのなら、ローカルLLM(パソコン内で生成AI)ほしくなるんですよねぇ。
そうすりゃタダだ。
そう遠くない内に、こういう機能はOSの標準機能になると予想します。

4.まとめ

以上、「AIリネーム」の簡単な説明でした。
役に立ったら「いいね」お願いします!

本校・清風情報工科学院では、生成AIを活用した教育に取り組んでいます。
昨年度は、IT教育の主にシステム開発のところに使っていました。
今年度は、画像生成や日本語教育でも使っていきます。
ご興味がある方、本校で教鞭をおとりになりませんか?

清風情報工科学院では、情報処理系の講師を急募しております。
こちらの記事を御覧ください。


よろしければサポートお願いします! いただいたサポートはクリエイターとしての活動費に使わせていただきます! (