```
import asyncio
import time
from logging import getLogger
from pathlib import Path
from typing import Generator, Iterable, Set, Dict, Tuple

from PIL import Image
from tqdm.asyncio import tqdm

from abstract_interrogator import AbsInterrogator
from configs.path_config import IMAGES_DIR
from debug.timer import timer
from interrogators import interrogators_dict
from tag_merger import merge_and_save_tags
from utils.hash_image_bytes import hash_image_bytes
from utils.tag_db import get_cached_tags, set_cached_tags

logger_local = getLogger(__name__)

# セマフォの最大数を設定（例: 1）
MAX_CONCURRENT_INTERROGATE = 1


def parse_exclude_tags(exclude_tags: list[str]) -> Set[str]:
    if not exclude_tags:
        return set()
    tags: list[str] = []
    for tag_str in exclude_tags:
        for tag in tag_str.split(','):
            tags.append(tag.strip())
    reverse_escaped_tags = [
        tag.replace(' ', '_').replace(r'\(', '(').replace(r'\)', ')')
        for tag in tags
    ]
    return set(tags + reverse_escaped_tags)


def explore_image_files(folder_path: Path, recursive: bool = False) -> Generator[Path, None, None]:
    for path in folder_path.iterdir():
        if path.is_file() and path.suffix.lower() in {'.png', '.jpg', '.jpeg', '.webp'}:
            yield path
        elif recursive and path.is_dir():
            yield from explore_image_files(path, recursive)


async def image_interrogate(
        image_path: Path,
        interrogator_model,
        threshold: float,
        tag_escape: bool,
        exclude_tags: Iterable[str],
        semaphore: asyncio.Semaphore
) -> Tuple[Dict[str, float], bool]:
    hash_b2 = hash_image_bytes(image_path)
    model_name = interrogator_model.name

    # キャッシュがあればセマフォを使わず即座に返す
    if cached := await get_cached_tags(hash_b2, model_name):
        return {tag: 1.0 for tag in cached}, False

    cache_image_path = IMAGES_DIR / image_path.name
    actual_path = cache_image_path if cache_image_path.is_file() else image_path

    im = Image.open(actual_path)

    # セマフォを使ってinterrogateの同時実行を制限
    async with semaphore:
        result = await interrogator_model.interrogate(im)  # super-heavy

    tags_dict = AbsInterrogator.postprocess_tags(
        result[1],
        threshold=threshold,
        escape_tag=tag_escape,
        replace_underscore=tag_escape,
        exclude_tags=exclude_tags
    )
    await set_cached_tags(hash_b2, model_name, list(tags_dict.keys()))

    return tags_dict, True


async def process_single_image(
        file_path: str,
        model: str = 'wd-vit-large-tagger-v3',
        threshold: float = 0.35,
        ext: str = '.txt',
        overwrite: bool = False,
        rawtag: bool = False,
        exclude_tags: list[str] = None,
        use_cpu: bool = False,
        save_caption: bool = False,
        log_time: bool = True,
        semaphore: asyncio.Semaphore = None
) -> Tuple[str, bool]:
    with timer(prefix="process_single_image: "):
        if semaphore is None:
            semaphore = asyncio.Semaphore(MAX_CONCURRENT_INTERROGATE)
        start_time = time.time()

        image_path = Path(file_path)
        interrogator = interrogators_dict[model]

        if use_cpu:
            interrogator.use_cpu()

        exclude_tags_set = parse_exclude_tags(exclude_tags)
        tags_dict, inferred = await image_interrogate(
            image_path,
            interrogator,
            threshold,
            not rawtag,
            exclude_tags_set,
            semaphore
        )
        tags_str = ', '.join(tags_dict.keys())

        if save_caption:
            caption_path = image_path.with_suffix(ext)
            if caption_path.is_file() and not overwrite:
                merge_and_save_tags(caption_path, set(tags_dict.keys()))
            else:
                with open(caption_path, 'w', encoding='utf-8') as fp:
                    fp.write(tags_str)

        if log_time:
            elapsed = time.time() - start_time
            source = "inferred" if inferred else "cached"
            logger_local.info(f"Processed {file_path} ({source}) in {elapsed:.2f}s")

        return tags_str, inferred


async def process_images_in_directory(
        dir_path: str,
        model: str = 'wd-vit-large-tagger-v3',
        threshold: float = 0.35,
        ext: str = '.txt',
        overwrite: bool = False,
        recursive: bool = False,
        rawtag: bool = False,
        exclude_tags: list[str] = None,
        use_cpu: bool = False,
        save_caption: bool = False,
        log_time: bool = True
):
    start_time = time.time()
    root_path = Path(dir_path)
    image_paths = list(explore_image_files(root_path, recursive))

    # セマフォを作成
    semaphore = asyncio.Semaphore(MAX_CONCURRENT_INTERROGATE)

    # 非同期タスクをtqdmでラップ
    tasks = []
    for image_path in image_paths:
        task = asyncio.create_task(
            process_single_image(
                file_path=str(image_path),
                model=model,
                threshold=threshold,
                ext=ext,
                overwrite=overwrite,
                rawtag=rawtag,
                exclude_tags=exclude_tags,
                use_cpu=use_cpu,
                save_caption=save_caption,
                log_time=False,
                semaphore=semaphore
            )
        )
        tasks.append(task)

    # tqdm.asyncioで進捗バーを表示
    results = []
    for f in tqdm.as_completed(tasks, desc="Processing images", total=len(image_paths), unit="image",
                               dynamic_ncols=True):
        result = await f
        results.append(result)

    total = len(image_paths)
    inferred_count = sum(1 for _, inferred in results if inferred)
    cached_count = total - inferred_count

    elapsed = time.time() - start_time
    if log_time:
        logger_local.info(
            f"Processed {total} images in {elapsed:.2f}秒: {inferred_count} inferred, {cached_count} cached"
        )


if __name__ == "__main__":
    asyncio.run(process_images_in_directory("path/to/your/directory"))
```

```
2025-06-15 22:19:29,549 tabs.main_tab_tagger:14 debug_all_tag [INFO]: 全体の推論を実行
Processing images: 100%|██████████| 10/10 [00:02<00:00,  4.45image/s]
2025-06-15 22:19:31,799 debug.timer:26 timer [INFO]: process_single_image: 260.68 ms
2025-06-15 22:19:31,801 debug.timer:26 timer [INFO]: process_single_image: 2241.09 ms
2025-06-15 22:19:31,802 debug.timer:26 timer [INFO]: process_single_image: 2102.84 ms
2025-06-15 22:19:31,802 debug.timer:26 timer [INFO]: process_single_image: 1247.56 ms
2025-06-15 22:19:31,802 debug.timer:26 timer [INFO]: process_single_image: 1175.83 ms
2025-06-15 22:19:31,803 debug.timer:26 timer [INFO]: process_single_image: 493.32 ms
2025-06-15 22:19:31,803 debug.timer:26 timer [INFO]: process_single_image: 745.35 ms
2025-06-15 22:19:31,803 debug.timer:26 timer [INFO]: process_single_image: 1013.13 ms
2025-06-15 22:19:31,804 debug.timer:26 timer [INFO]: process_single_image: 403.98 ms
2025-06-15 22:19:31,804 debug.timer:26 timer [INFO]: process_single_image: 172.08 ms
2025-06-15 22:19:31,805 interrogate_module:186 process_images_in_directory [INFO]: Processed 10 images in 2.26秒: 0 inferred, 10 cached
```

このログから分析できることは？