見出し画像

Ollama+llama3.2-vision:11bを使った画像のメタタグ生成とR18判定の実装。ローカルLLMで無料で画像判定ができる時代の到来




今回は最近でたllama3.2-visionを使って画像をローカルLLMに読み込ませ、画像にあったタグとR18画像かの判定を試してみます。

完成イメージ


タイトルにもあるように画像を読み込ませたら、タグとレーティングをJSONで出力してくれます。

python3 vision2-tag-csv.py /mnt/c/Users/hoge/Downloads/IMG_3634.jpg
Response Details:
  Model: llama3.2-vision:11b
  Created At (JST): 2024-11-17 21:01:32
  Total Duration: 88.00s
  Load Duration: 2.74s
  Prompt Eval Count: 168
  Prompt Eval Duration: 81.14s
  Eval Count: 25
  Eval Duration: 3.53s

Parsed Content:
Data appended to CSV.
{
  "tag": [
    "人型",
    "海水浴",
    "女性",
    "水着",
    "イメージ"
  ],
  "rating": "general"
}

設計思想

  • ローカルLLMの画像読み込み機能の使い道の一つがタグ生成とR18判定だと思ったから

  • 特にR18判定はクラウドLLMだと難しいと思われるため利点がある

  • 90bでなく軽量な方の11bなのは大量に画像を処理するには11bのほうが有効だと思われるため

  • 11bだとMacBookAir M1 メモリ16GBでも動く!

  • 最終的には特定のフォルダを読み込ませて画像を大量に処理させたい

準備

  • Ollamaをローカルにインストールしllama3.2-vision:11bをダウンロードしておく

  • PythonからはAPI接続でモデルを利用するためOllamaを起動しておくこと

  • 下記でモジュールのインストール

pip install ollama

STEP1 これは何の画像ですか?

まず公式のサンプル(https://ollama.com/blog/llama3.2-vision)を少し改造してコマンドライン引数からファイルを読み込ませるようにし、さらに日本語プロンプトにします

import ollama
import sys

inputImageFile = sys.argv[1]

response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content': 'これはなんの画像ですか?',
        'images': [inputImageFile]
    }]
)

print(response)

下記の画像を読み込ませます
python3 vision1-jp.py XXXX.JPG

レスポンスはこんな形です。Pythonのハッシュのダンプになっています

{'model': 'llama3.2-vision:11b', 'created_at': '2024-11-17T12:20:46.2932528Z', 'message': {'role': 'assistant', 'content': 'この画像は、ペンギンの画像です。特にコウテイル属のものと思われます。'}, 'done_reason': 'stop', 'done': True, 'total_duration': 85364485200, 'load_duration': 3002522900, 'prompt_eval_count': 19, 'prompt_eval_duration': 78052000000, 'eval_count': 25, 'eval_duration': 3568000000}

STEP2 画像のタグを生成するようプロンプトで指示

import ollama
import sys

inputImageFile = sys.argv[1]

response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content':  '''
        あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
        なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
        例1 {"tag": ["虎", "動物", "公園", "女性","アイス"] }
        のような形で。
        タグ名は日本語にしてください。
        その他の文言は返さないでください。
        ''',
        'images': [inputImageFile]
    }]
)

print(response)

あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"] }
のような形で。
タグ名は日本語にしてください。
その他の文言は返さないでください。

プロンプト

これぐらい厳密にプロンプトを書かないとタグを正確に返してくれません。
先ほどのペンギンを入力します
python3 vision2-tag2-jp.py xxxx.JPG

{'model': 'llama3.2-vision:11b', 'created_at': '2024-11-17T12:28:20.250479Z', 'message': {'role': 'assistant', 'content': 'あなたが画像を読み込んで内容のタグを出力する役割をもったプログラムです。\n\n「ピンギン」というタグ名と、「動物」などの類似した言葉を持つタグ名を5つ選びます。\n\n\n{"tag": ["ピンギン","動物","海水浴","アイス","女性"] }\n\n日本語で書きました。'}, 'done_reason': 'stop', 'done': True, 'total_duration': 98108769400, 'load_duration': 2985385900, 'prompt_eval_count': 125, 'prompt_eval_duration': 81042000000, 'eval_count': 91, 'eval_duration': 13320000000}

contentにタグ以外が入ってるのが気に入らないですが["ピンギン","動物","海水浴","アイス","女性"] }と帰ってきています。ただ4,5番目のタグはプロンプトに惑わされている感じです。
もう一回同じプログラムを打ち込んでみます

{'model': 'llama3.2-vision:11b', 'created_at': '2024-11-17T12:30:01.3894599Z', 'message': {'role': 'assistant', 'content': '{"tag": ["ペンギン","水","海洋生物","動物","鳥"] }'}, 'done_reason': 'stop', 'done': True, 'total_duration': 4127259700, 'load_duration': 86335500, 'prompt_eval_count': 125, 'prompt_eval_duration': 164000000, 'eval_count': 22, 'eval_duration': 3115000000}

今後は綺麗に ["ペンギン","水","海洋生物","動物","鳥"]と生成しています。
こういった曖昧さは生成AIにはつきものです。

STEP3 出力を綺麗にする。Pythonの変数の整形とJSONパース


プロンプトはそのままにして出力を見やすくします

import ollama
import sys
import json
from datetime import datetime, timedelta, timezone

inputImageFile = sys.argv[1]

# 画像を解析してレスポンスを取得
response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content': '''
        あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
        なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
        例1 {"tag": ["虎", "動物", "公園", "女性","アイス"] }
        のような形で。
        タグ名は日本語にしてください。
        JSON以外の文字列は返答しないでください。
        ''',
        'images': [inputImageFile]
    }]
)

# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
    # 余分な小数点以下をトリム
    if "." in utc_time_str:
        utc_time_str = utc_time_str.split(".")[0] + "Z"
    # UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
    utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
    # 日本時間(UTC+9)に変換
    japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
    # 日本時間をフォーマット
    return japan_time.strftime("%Y-%m-%d %H:%M:%S")

# レスポンスをきれいに表示
print("Response Details:")
try:
    # 時間の変換
    created_at_japan = convert_to_japan_time(response['created_at'])

    # 秒に変換して表示
    total_duration_sec = response['total_duration'] / 1_000_000_000
    load_duration_sec = response['load_duration'] / 1_000_000_000
    prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
    eval_duration_sec = response['eval_duration'] / 1_000_000_000

    print(f"  Model: {response['model']}")
    print(f"  Created At (JST): {created_at_japan}")
    print(f"  Total Duration: {total_duration_sec:.2f}s")
    print(f"  Load Duration: {load_duration_sec:.2f}s")
    print(f"  Prompt Eval Count: {response['prompt_eval_count']}")
    print(f"  Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
    print(f"  Eval Count: {response['eval_count']}")
    print(f"  Eval Duration: {eval_duration_sec:.2f}s")

    # JSONパースを試行
    content = response['message']['content']
    print("\nParsed Content:")
    parsed_json = json.loads(content)  # JSONパース
    print(json.dumps(parsed_json, indent=2, ensure_ascii=False))  # 見やすく整形して表示

except json.JSONDecodeError:
    print("\nError: Failed to parse 'content' as JSON.")
    print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
    print(f"\nError: Missing key in response: {e}")
except Exception as e:
    print(f"\nUnexpected Error: {e}")

今度はこちらの画像を読み込ませてみます

python3 vision2-tag3.py xxxx.png

Response Details:
  Model: llama3.2-vision:11b
  Created At (JST): 2024-11-17 21:37:01
  Total Duration: 86.87s
  Load Duration: 3.02s
  Prompt Eval Count: 127
  Prompt Eval Duration: 80.96s
  Eval Count: 18
  Eval Duration: 2.65s

Parsed Content:
{
  "tag": [
    "少女",
    "剣",
    "赤眼",
    "白服",
    "黒髪"
  ]
}

きれいに返ってきてますね!
90bでなくllama3.2-vision:11bでもタグ生成であれば日本語でも十分使えるのがわかると想います。

{ "tag": [ "少女", "剣", "赤眼", "白服", "黒髪" ] }

タグの部分

STEP4 R18判定

プロンプトを改良しR18判定をしてもらいます

あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
  なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
のような形で。
タグ名は日本語にしてください。
JSON以外の文字列は返答しないでください。

プロンプト
import ollama
import sys
import json
from datetime import datetime, timedelta, timezone

inputImageFile = sys.argv[1]

# 画像を解析してレスポンスを取得
response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content': '''
        あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
        なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
        またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
        例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
        のような形で。
        タグ名は日本語にしてください。
        JSON以外の文字列は返答しないでください。
        ''',
        'images': [inputImageFile]
    }]
)

# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
    # 余分な小数点以下をトリム
    if "." in utc_time_str:
        utc_time_str = utc_time_str.split(".")[0] + "Z"
    # UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
    utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
    # 日本時間(UTC+9)に変換
    japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
    # 日本時間をフォーマット
    return japan_time.strftime("%Y-%m-%d %H:%M:%S")

# レスポンスをきれいに表示
print("Response Details:")
try:
    # 時間の変換
    created_at_japan = convert_to_japan_time(response['created_at'])

    # 秒に変換して表示
    total_duration_sec = response['total_duration'] / 1_000_000_000
    load_duration_sec = response['load_duration'] / 1_000_000_000
    prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
    eval_duration_sec = response['eval_duration'] / 1_000_000_000

    print(f"  Model: {response['model']}")
    print(f"  Created At (JST): {created_at_japan}")
    print(f"  Total Duration: {total_duration_sec:.2f}s")
    print(f"  Load Duration: {load_duration_sec:.2f}s")
    print(f"  Prompt Eval Count: {response['prompt_eval_count']}")
    print(f"  Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
    print(f"  Eval Count: {response['eval_count']}")
    print(f"  Eval Duration: {eval_duration_sec:.2f}s")

    # JSONパースを試行
    content = response['message']['content']
    print("\nParsed Content:")
    parsed_json = json.loads(content)  # JSONパース
    print(json.dumps(parsed_json, indent=2, ensure_ascii=False))  # 見やすく整形して表示

except json.JSONDecodeError:
    print("\nError: Failed to parse 'content' as JSON.")
    print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
    print(f"\nError: Missing key in response: {e}")
except Exception as e:
    print(f"\nUnexpected Error: {e}")


さすがにR18画像はここには乗せられませんが
getchuコムから適当にエロアニメ画像を拾って読み込ませてみました

Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 18:16:27
Total Duration: 94.47s
Load Duration: 0.02s
Prompt Eval Count: 168
Prompt Eval Duration: 85.10s
Eval Count: 61
Eval Duration: 9.24s

Parsed Content:

Error: Failed to parse 'content' as JSON.
Raw Content: この画像を表すタグ名5つをJSON形式で書きます。

{"tag": ["女性", "乳房", "下着", "パンツ", "性感染症"], "rating": "R18" }
この画像は成人向けの画像です。

正しくエロ画像も判定してくれています。それっぽいワードがタグとして並んでいます。これがローカルLLMの強みでしょう
JSONのパースに失敗しています。AI君がおそらくタグ以外にも色々文言を付け加えているためタグのJSONパースに失敗します。
色々というのは「これは成年画像です」などプロンプトが複雑になっているのでそれに呼応して余計なことを返答する回数も増えてしまっています。

STEP5 R18判定をしつつ、LLMの想定外の返答に対応する


llamaが指定したJSONタグ以外の文言をレスポンスしていてもその文言にはたいていJSONタグがはいっているのでそれを読み取る処理も加えます

失敗レスポンス例

Content: タグ名が成人向けであるため、プログラムは画像を解析し、タグ名とそれに合わせた「rating」の値をJSON形式で出力します。

json { "tag": ["少女","猫耳","水着","プール","パンツ"], "rating": "R18" }

本当はJSONタグだけ返してほしい
import ollama
import sys
import json
import re
from datetime import datetime, timedelta, timezone

inputImageFile = sys.argv[1]

# 画像を解析してレスポンスを取得
response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content': '''
        あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
        なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
        またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
        例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
        のような形で。
        タグ名は日本語にしてください。
        JSON以外の文字列は返答しないでください。
        ''',
        'images': [inputImageFile]
    }]
)

# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
    # 余分な小数点以下をトリム
    if "." in utc_time_str:
        utc_time_str = utc_time_str.split(".")[0] + "Z"
    # UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
    utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
    # 日本時間(UTC+9)に変換
    japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
    # 日本時間をフォーマット
    return japan_time.strftime("%Y-%m-%d %H:%M:%S")

# content中からJSON形式を抽出する関数
def extract_json_from_content(content):
    json_pattern = r'{.*"tag".*}'
    match = re.search(json_pattern, content, re.DOTALL)
    if match:
        return match.group(0)
    return None

# レスポンスをきれいに表示
print("Response Details:")
try:
    # 時間の変換
    created_at_japan = convert_to_japan_time(response['created_at'])

    # 秒に変換して表示
    total_duration_sec = response['total_duration'] / 1_000_000_000
    load_duration_sec = response['load_duration'] / 1_000_000_000
    prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
    eval_duration_sec = response['eval_duration'] / 1_000_000_000

    print(f"  Model: {response['model']}")
    print(f"  Created At (JST): {created_at_japan}")
    print(f"  Total Duration: {total_duration_sec:.2f}s")
    print(f"  Load Duration: {load_duration_sec:.2f}s")
    print(f"  Prompt Eval Count: {response['prompt_eval_count']}")
    print(f"  Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
    print(f"  Eval Count: {response['eval_count']}")
    print(f"  Eval Duration: {eval_duration_sec:.2f}s")

    # JSONパースを試行
    content = response['message']['content']
    print("\nParsed Content:")
    try:
        parsed_json = json.loads(content)  # JSONパース
    except json.JSONDecodeError:
        # JSONパースエラーが発生した場合、contentからJSON部分を抽出
        print(content)
        print("\nError: Failed to parse 'content' as JSON. Attempting to extract JSON from content...")
        extracted_json = extract_json_from_content(content)
        if extracted_json:
            print("Extracted JSON found:")
            parsed_json = json.loads(extracted_json)
        else:
            raise ValueError("No valid JSON could be extracted from the content.")

    # 抽出・パースしたJSONをきれいに整形して表示
    print(json.dumps(parsed_json, indent=2, ensure_ascii=False))

except json.JSONDecodeError:
    print("\nError: Unable to parse content into JSON and no valid JSON could be extracted.")
    print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
    print(f"\nError: Missing key in response: {e}")
except Exception as e:
    print(f"\nUnexpected Error: {e}")

これでJSONパースに1度失敗してもタグを拾える様になりました!

Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 18:30:06
Total Duration: 98.75s
Load Duration: 2.64s
Prompt Eval Count: 168
Prompt Eval Duration: 84.56s
Eval Count: 75
Eval Duration: 11.45s

Parsed Content:

Error: Failed to parse 'content' as JSON. Attempting to extract JSON from content...
Extracted JSON found:
{
"tag": [
"女性",
"性別",
"着衣なし",
"成人向け",
"赤毛"
],
"rating": "R18"
}

R18じゃない画像を読み込ませてみます

Response Details:
  Model: llama3.2-vision:11b
  Created At (JST): 2024-11-17 19:21:22
  Total Duration: 90.72s
  Load Duration: 0.08s
  Prompt Eval Count: 168
  Prompt Eval Duration: 84.27s
  Eval Count: 40
  Eval Duration: 5.65s

Parsed Content:
Data appended to CSV.
{
  "tag": [
    "古代",
    "博物館",
    "像",
    "像",
    "祀"
  ],
  "rating": "general"
}

"rating": "general"

問題ない判定

ただし信頼は禁物、あくまでも簡易判定ということで正確ではない

こちらのラインが微妙な学園アイドルマスターの画像ですが
R18されたりされなかったりします

1回目 全年齢判定

Response Details:
  Model: llama3.2-vision:11b
  Created At (JST): 2024-11-17 21:55:20
  Total Duration: 96.51s
  Load Duration: 2.97s
  Prompt Eval Count: 168
  Prompt Eval Duration: 83.55s
  Eval Count: 62
  Eval Duration: 9.70s

Parsed Content:
この画像には以下のタグが含まれます。

{"tag": ["女性", "写真","水着","浴衣","花嫁"], "rating": "general"}

これは成人向け画像ではないので、 rating の値に "general" という文字列を付けました。

Error: Failed to parse 'content' as JSON. Attempting to extract JSON from content...
Extracted JSON found:
{
  "tag": [
    "女性",
    "写真",
    "水着",
    "浴衣",
    "花嫁"
  ],
  "rating": "general"
}

2回目 R18判定

Response Details:
  Model: llama3.2-vision:11b
  Created At (JST): 2024-11-17 21:56:32
  Total Duration: 42.17s
  Load Duration: 0.02s
  Prompt Eval Count: 168
  Prompt Eval Duration: 0.15s
  Eval Count: 271
  Eval Duration: 41.76s

Parsed Content:
この画像に含まれるタグ名について、5つ選んでJSON形式で書くという質問です。

まず、この画像が成人向けであることから、"rating": "R18" を加えてください。

次に、以下のタグを選択します。
- 「アイドル」: この画像には青い衣装を着た女性が写っていることから、アイドルと呼ばれるようで、タグとして採用します。
- 「温泉」: この画像は、温泉施設の中で撮影されています。タグとして「温泉」を採用します。
- 「水着」: この画像には水着を着た女性が写っており、タグとして「水着」を採用します。
- 「花嫁衣装」: この画像には、白い衣装を着た女性が写っていることから、「花嫁衣装」というタグを選びます。
- 「アイス」: これはこの画像に含まれるアイテムです。

以上の理由により、この画像に関する以下のJSON形式のデータを作成できます。


{"tag": ["アイドル", "温泉","水着","花嫁衣装","アイス"], "rating": "R18" }

Error: Failed to parse 'content' as JSON. Attempting to extract JSON from content...
Extracted JSON found:
{
  "tag": [
    "アイドル",
    "温泉",
    "水着",
    "花嫁衣装",
    "アイス"
  ],
  "rating": "R18"
}

STEP6(完成) バッチで回すことを想定しCSVに結果を書き出す

完成した最終的なコードがこちら

import ollama
import sys
import json
import re
import csv
from datetime import datetime, timedelta, timezone
import os  # ファイルの存在確認に使用

inputImageFile = sys.argv[1]
csv_file = "output.csv"

# 画像を解析してレスポンスを取得
response = ollama.chat(
    model='llama3.2-vision:11b',
    messages=[{
        'role': 'user',
        'content': '''
        あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
        なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
        またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
        例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
        のような形で。
        タグ名は日本語にしてください。
        JSON以外の文字列は返答しないでください。
        ''',
        'images': [inputImageFile]
    }]
)

# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
    if "." in utc_time_str:
        utc_time_str = utc_time_str.split(".")[0] + "Z"
    utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
    japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
    return japan_time.strftime("%Y-%m-%d %H:%M:%S")

# content中からJSON形式を抽出する関数
def extract_json_from_content(content):
    json_pattern = r'{.*"tag".*}'
    match = re.search(json_pattern, content, re.DOTALL)
    if match:
        return match.group(0)
    return None

# CSV追記書き込み
def append_to_csv(filename, model, created_at, total_duration, load_duration, prompt_eval_count,
                  prompt_eval_duration, eval_count, eval_duration, content, tags, rating):
    # ヘッダー行を定義
    headers = [
        "filename", "Model", "Created At (JST)", "Total Duration", "Load Duration", 
        "Prompt Eval Count", "Prompt Eval Duration", "Eval Count", "Eval Duration", 
        "content", "tag1", "tag2", "tag3", "tag4", "tag5", "rating"
    ]
    # ファイルが存在しない場合はヘッダーを追加
    file_exists = os.path.isfile(csv_file)
    with open(csv_file, mode="a", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        if not file_exists:
            writer.writerow(headers)  # ヘッダー行の書き込み
        # データ行の書き込み
        row = [
            filename, model, created_at, f"{total_duration:.2f}s", f"{load_duration:.2f}s",
            prompt_eval_count, f"{prompt_eval_duration:.2f}s", eval_count, f"{eval_duration:.2f}s",
            content
        ] + tags + [""] * (5 - len(tags)) + [rating]  # タグが5つ未満の場合は空文字を追加
        writer.writerow(row)

# レスポンスをきれいに表示
print("Response Details:")
try:
    created_at_japan = convert_to_japan_time(response['created_at'])
    total_duration_sec = response['total_duration'] / 1_000_000_000
    load_duration_sec = response['load_duration'] / 1_000_000_000
    prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
    eval_duration_sec = response['eval_duration'] / 1_000_000_000

    print(f"  Model: {response['model']}")
    print(f"  Created At (JST): {created_at_japan}")
    print(f"  Total Duration: {total_duration_sec:.2f}s")
    print(f"  Load Duration: {load_duration_sec:.2f}s")
    print(f"  Prompt Eval Count: {response['prompt_eval_count']}")
    print(f"  Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
    print(f"  Eval Count: {response['eval_count']}")
    print(f"  Eval Duration: {eval_duration_sec:.2f}s")

    content = response['message']['content']
    print("\nParsed Content:")
    tags = []
    rating = ""
    try:
        parsed_json = json.loads(content)
        tags = parsed_json.get("tag", [])
        rating = parsed_json.get("rating", "")
    except json.JSONDecodeError:
        print(content)
        print("\nError: Failed to parse 'content' as JSON. Attempting to extract JSON from content...")
        extracted_json = extract_json_from_content(content)
        if extracted_json:
            print("Extracted JSON found:")
            parsed_json = json.loads(extracted_json)
            tags = parsed_json.get("tag", [])
            rating = parsed_json.get("rating", "")
        else:
            print("No valid JSON could be extracted from the content.")
    finally:
        # CSV書き込み
        append_to_csv(
            filename=inputImageFile,
            model=response['model'],
            created_at=created_at_japan,
            total_duration=total_duration_sec,
            load_duration=load_duration_sec,
            prompt_eval_count=response['prompt_eval_count'],
            prompt_eval_duration=prompt_eval_duration_sec,
            eval_count=response['eval_count'],
            eval_duration=eval_duration_sec,
            content=content,
            tags=tags,
            rating=rating
        )
        print("Data appended to CSV.")

    print(json.dumps({"tag": tags, "rating": rating}, indent=2, ensure_ascii=False))

except Exception as e:
    print(f"\nUnexpected Error: {e}")


こんなCSVができあがります

まとめ

(電気代を無視すれば)ローカルLLMで画像のタグ生成、R18判定ができるいい時代になりました。クラウドAPIの料金を気にせずに自由にプログラミングできるのはかなりいいのではないでしょうか?
まだまだスピードには難ありですが、他にもいろんな使い道がありそうなので今後も模索していこうと想います。

この記事が気に入ったらサポートをしてみませんか?