Instructions to use stevenlearns/caption-tool with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use stevenlearns/caption-tool with Transformers:
# Use a pipeline as a high-level helper # Warning: Pipeline type "image-to-text" is no longer supported in transformers v5. # You must load the model directly (see below) or downgrade to v4.x with: # 'pip install "transformers<5.0.0' from transformers import pipeline pipe = pipeline("image-to-text", model="stevenlearns/caption-tool")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("stevenlearns/caption-tool", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- .gitignore +11 -0
- LICENSE +21 -0
- README.md +144 -0
- caption.py +361 -0
- caption_config.json +8 -0
- install.sh +186 -0
- requirements.txt +8 -0
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
|
| 6 |
+
# Virtual environment (created by install.sh)
|
| 7 |
+
.venv/
|
| 8 |
+
|
| 9 |
+
# OS / editor noise
|
| 10 |
+
.DS_Store
|
| 11 |
+
*.swp
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 oohfixer.com
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Caption Tool
|
| 2 |
+
|
| 3 |
+
A small, friendly tool that writes text descriptions (captions) for every image
|
| 4 |
+
in a folder — all on your own computer.
|
| 5 |
+
|
| 6 |
+
- **No API key. No account. No internet** (after the first-time model download).
|
| 7 |
+
- **Private:** your images never leave your machine.
|
| 8 |
+
- **Lightweight:** runs on a GPU with as little as **2 GB of VRAM**.
|
| 9 |
+
- **Whole folders at once:** point it at a directory and it captions everything.
|
| 10 |
+
|
| 11 |
+
Made by [oohfixer.com](https://oohfixer.com).
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## What it does
|
| 16 |
+
|
| 17 |
+
For every image it finds, Caption Tool writes a matching text file next to it:
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
vacation.jpg → vacation.txt
|
| 21 |
+
cat.png → cat.txt
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
That's it. The caption sits in the `.txt` file, ready to use for training
|
| 25 |
+
image models, organizing photos, or search.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## Install
|
| 30 |
+
|
| 31 |
+
You need **Python 3.13 or older** (3.14 doesn't work with this tool's
|
| 32 |
+
dependencies).
|
| 33 |
+
|
| 34 |
+
### One command (recommended)
|
| 35 |
+
|
| 36 |
+
Open a terminal and run:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
curl -fsSL https://oohfixer.com/caption-tool/install.sh | bash
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
This downloads the tool, installs what it needs in its own isolated
|
| 43 |
+
environment, and adds a **`captions`** command you can run from anywhere. When it
|
| 44 |
+
finishes, open a new terminal (or run `source ~/.bashrc`).
|
| 45 |
+
|
| 46 |
+
Then just type:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
captions
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Manual install
|
| 53 |
+
|
| 54 |
+
If you'd rather set it up by hand:
|
| 55 |
+
|
| 56 |
+
1. Download this folder and open a terminal in it.
|
| 57 |
+
2. Install the requirements:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
pip install -r requirements.txt
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
3. Run it:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
python caption.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## Use it
|
| 72 |
+
|
| 73 |
+
Just run it:
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
python caption.py
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
It will ask you for a folder of images, show the settings, and caption them.
|
| 80 |
+
Sit back — a progress bar shows how far it's gotten.
|
| 81 |
+
|
| 82 |
+
### Want to skip the questions?
|
| 83 |
+
|
| 84 |
+
If you already know what you want, you can do it in one line:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
python caption.py --path "/path/to/your/images"
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
Useful extras:
|
| 91 |
+
|
| 92 |
+
| Option | What it does |
|
| 93 |
+
| --- | --- |
|
| 94 |
+
| `--path "FOLDER"` | Caption this folder right away (no prompt). |
|
| 95 |
+
| `--force` | Re-caption images that already have a `.txt` file. |
|
| 96 |
+
| `--model florence2-base` | Pick a different model (see below). |
|
| 97 |
+
| `--max-new-tokens 512` | Allow longer captions. |
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## Choosing a model
|
| 102 |
+
|
| 103 |
+
When you run it, you can pick which AI model writes the captions. Bigger = more
|
| 104 |
+
detailed, but uses more VRAM.
|
| 105 |
+
|
| 106 |
+
| Model | Size | Best for |
|
| 107 |
+
| --- | --- | --- |
|
| 108 |
+
| **florence2-large-ft** (default) | ~1.5 GB | Best quality. Needs a normal GPU. |
|
| 109 |
+
| florence2-large | ~1.5 GB | Slightly less accurate. |
|
| 110 |
+
| **florence2-base** | ~0.45 GB | **2 GB GPUs**, or when you want it fast. |
|
| 111 |
+
|
| 112 |
+
If your card only has 2 GB of VRAM, choose **florence2-base**.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## Prefix and suffix (optional)
|
| 117 |
+
|
| 118 |
+
If you're captioning images to train your own image model, you may want a
|
| 119 |
+
**trigger word** on every caption (for example, your subject's name). Set a
|
| 120 |
+
**prefix** and the tool adds it to the front of every caption automatically.
|
| 121 |
+
A **suffix** does the same at the end.
|
| 122 |
+
|
| 123 |
+
Leave them empty if you just want plain descriptions.
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## Settings
|
| 128 |
+
|
| 129 |
+
Settings are stored in `caption_config.json` next to the script. You can edit
|
| 130 |
+
that file directly, or change things from the tool's menu. The defaults:
|
| 131 |
+
|
| 132 |
+
- Skip images that already have a caption (so re-running is safe).
|
| 133 |
+
- At least ~15 words per caption (it re-captions anything shorter).
|
| 134 |
+
- 256 token cap on caption length.
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## Questions
|
| 139 |
+
|
| 140 |
+
- **Does it need to be online?** Only the very first time, to download the
|
| 141 |
+
model. After that, everything runs offline.
|
| 142 |
+
- **Where do my images go?** Nowhere. They're processed locally and the caption
|
| 143 |
+
is saved next to each image.
|
| 144 |
+
- **What image types?** jpg, png, webp, bmp, tiff.
|
caption.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Caption Tool — caption entire folders of images locally.
|
| 4 |
+
|
| 5 |
+
Runs 100% on your machine. No API key, no internet after the first model
|
| 6 |
+
download. Works on GPUs with as little as 2 GB of VRAM (use Florence-2-base).
|
| 7 |
+
|
| 8 |
+
For every image it writes a matching <image>.txt caption file next to it.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
import warnings
|
| 17 |
+
|
| 18 |
+
warnings.filterwarnings("ignore")
|
| 19 |
+
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
| 20 |
+
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
| 21 |
+
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
from rich.console import Console
|
| 25 |
+
from rich.panel import Panel
|
| 26 |
+
from rich.prompt import Prompt, Confirm
|
| 27 |
+
from rich.progress import (
|
| 28 |
+
Progress,
|
| 29 |
+
SpinnerColumn,
|
| 30 |
+
BarColumn,
|
| 31 |
+
TextColumn,
|
| 32 |
+
TimeElapsedColumn,
|
| 33 |
+
TaskProgressColumn,
|
| 34 |
+
)
|
| 35 |
+
from rich.live import Live
|
| 36 |
+
from rich.table import Table
|
| 37 |
+
from rich.text import Text
|
| 38 |
+
|
| 39 |
+
console = Console()
|
| 40 |
+
|
| 41 |
+
CONFIG_PATH = Path(__file__).parent / "caption_config.json"
|
| 42 |
+
|
| 43 |
+
# Model choices. Sizes are approximate fp16 GPU footprint.
|
| 44 |
+
MODELS = {
|
| 45 |
+
"florence2-large-ft": ("microsoft/Florence-2-large-ft", "~1.5 GB — best quality (recommended)"),
|
| 46 |
+
"florence2-large": ("microsoft/Florence-2-large", "~1.5 GB — base, slightly less accurate"),
|
| 47 |
+
"florence2-base": ("microsoft/Florence-2-base", "~0.45 GB — fast, great for 2 GB GPUs"),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
DEFAULT_CONFIG = {
|
| 51 |
+
"model": "microsoft/Florence-2-large-ft",
|
| 52 |
+
"prefix": "",
|
| 53 |
+
"suffix": "",
|
| 54 |
+
"max_new_tokens": 256,
|
| 55 |
+
"min_words": 15,
|
| 56 |
+
"skip_existing": True,
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _model_family(model_id: str) -> str:
|
| 61 |
+
return "florence2" if "florence" in model_id.lower() else "unknown"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif"}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_config() -> dict:
|
| 68 |
+
if CONFIG_PATH.exists():
|
| 69 |
+
with open(CONFIG_PATH) as f:
|
| 70 |
+
cfg = json.load(f)
|
| 71 |
+
return {**DEFAULT_CONFIG, **cfg}
|
| 72 |
+
return DEFAULT_CONFIG.copy()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def save_config(cfg: dict):
|
| 76 |
+
with open(CONFIG_PATH, "w") as f:
|
| 77 |
+
json.dump(cfg, f, indent=2)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def find_images(path: Path) -> list[Path]:
|
| 81 |
+
if path.is_file():
|
| 82 |
+
if path.suffix.lower() in IMAGE_EXTENSIONS:
|
| 83 |
+
return [path]
|
| 84 |
+
return []
|
| 85 |
+
return sorted(p for p in path.rglob("*") if p.suffix.lower() in IMAGE_EXTENSIONS)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def load_model(model_id: str):
|
| 89 |
+
"""Load the captioning model onto the GPU, or CPU if no GPU is present."""
|
| 90 |
+
import torch
|
| 91 |
+
from transformers import AutoProcessor, AutoModelForCausalLM
|
| 92 |
+
|
| 93 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 94 |
+
device_label = (
|
| 95 |
+
f"CUDA ({torch.cuda.get_device_name(0)})" if device.type == "cuda" else "CPU"
|
| 96 |
+
)
|
| 97 |
+
dtype = torch.float16 if device.type == "cuda" else torch.float32
|
| 98 |
+
family = _model_family(model_id)
|
| 99 |
+
|
| 100 |
+
with console.status(f"[bold cyan]Loading [white]{model_id}[/white] on {device_label}...", spinner="dots"):
|
| 101 |
+
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
|
| 102 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 103 |
+
model_id, torch_dtype=dtype, trust_remote_code=True
|
| 104 |
+
).to(device)
|
| 105 |
+
model.eval()
|
| 106 |
+
|
| 107 |
+
return (processor, model, device, dtype, family), device_label
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _generate_caption(processor, model, img, device, dtype, max_new_tokens: int, task: str) -> str:
|
| 111 |
+
"""Run one Florence-2 caption generation for the given task prompt."""
|
| 112 |
+
import torch
|
| 113 |
+
|
| 114 |
+
inputs = processor(text=task, images=img, return_tensors="pt")
|
| 115 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 116 |
+
if "pixel_values" in inputs:
|
| 117 |
+
inputs["pixel_values"] = inputs["pixel_values"].to(dtype)
|
| 118 |
+
output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, num_beams=3)
|
| 119 |
+
raw = processor.batch_decode(output_ids, skip_special_tokens=False)[0]
|
| 120 |
+
parsed = processor.post_process_generation(raw, task=task, image_size=(img.width, img.height))
|
| 121 |
+
return parsed[task].strip()
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def caption_image(model_bundle, image_path: Path, prefix: str, suffix: str,
|
| 125 |
+
max_new_tokens: int, min_words: int = 0) -> str:
|
| 126 |
+
import torch
|
| 127 |
+
from PIL import Image
|
| 128 |
+
|
| 129 |
+
processor, model, device, dtype, family = model_bundle
|
| 130 |
+
img = Image.open(image_path).convert("RGB")
|
| 131 |
+
|
| 132 |
+
with torch.no_grad():
|
| 133 |
+
caption = _generate_caption(
|
| 134 |
+
processor, model, img, device, dtype, max_new_tokens, "<MORE_DETAILED_CAPTION>"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Florence-2 has no native minimum length, so re-run with a stronger
|
| 138 |
+
# prompt if the first caption came out too short.
|
| 139 |
+
if min_words and len(caption.split()) < min_words:
|
| 140 |
+
stronger = (
|
| 141 |
+
"Describe this image in extensive detail for a training caption. "
|
| 142 |
+
"Cover the subject, clothing, hair, lighting, pose, expression, "
|
| 143 |
+
"background, and camera angle."
|
| 144 |
+
)
|
| 145 |
+
caption = _generate_caption(
|
| 146 |
+
processor, model, img, device, dtype, max_new_tokens, stronger
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
parts = [p for p in (prefix.strip(), caption, suffix.strip()) if p]
|
| 150 |
+
return ", ".join(parts) if prefix or suffix else caption
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def select_model(current: str) -> str:
|
| 154 |
+
console.print("\n[bold]Available models:[/bold]")
|
| 155 |
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
| 156 |
+
for key, (model_id, desc) in MODELS.items():
|
| 157 |
+
marker = "→" if model_id == current else " "
|
| 158 |
+
table.add_row(f"[cyan]{marker} {key}[/cyan]", model_id, f"[dim]{desc}[/dim]")
|
| 159 |
+
console.print(table)
|
| 160 |
+
|
| 161 |
+
choice = Prompt.ask(
|
| 162 |
+
"\nModel shortname or full HF model ID",
|
| 163 |
+
default=next((k for k, (m, _) in MODELS.items() if m == current), current),
|
| 164 |
+
)
|
| 165 |
+
if choice in MODELS:
|
| 166 |
+
return MODELS[choice][0]
|
| 167 |
+
return choice # allow any Hugging Face model ID
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def parse_args():
|
| 171 |
+
p = argparse.ArgumentParser(description="Caption Tool — local batch image captioner.")
|
| 172 |
+
p.add_argument("--path", help="Image folder or file (skips the path prompt)")
|
| 173 |
+
p.add_argument("--model", help="Override the model (shortname or HF ID)")
|
| 174 |
+
p.add_argument("--max-new-tokens", type=int, help="Override max new tokens")
|
| 175 |
+
p.add_argument("--force", action="store_true", help="Re-caption even if a .txt already exists")
|
| 176 |
+
return p.parse_args()
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _hsl_hex(hue: float) -> str:
|
| 180 |
+
"""Hue (0-360) → hex color string, full saturation, mid lightness."""
|
| 181 |
+
hue %= 360
|
| 182 |
+
s, l = 1.0, 0.55
|
| 183 |
+
c = (1 - abs(2 * l - 1)) * s
|
| 184 |
+
hp = hue / 60
|
| 185 |
+
x = c * (1 - abs((hp % 2) - 1))
|
| 186 |
+
m = l - c / 2
|
| 187 |
+
if hp < 1:
|
| 188 |
+
r, g, b = c, x, 0
|
| 189 |
+
elif hp < 2:
|
| 190 |
+
r, g, b = x, c, 0
|
| 191 |
+
elif hp < 3:
|
| 192 |
+
r, g, b = 0, c, x
|
| 193 |
+
elif hp < 4:
|
| 194 |
+
r, g, b = 0, x, c
|
| 195 |
+
elif hp < 5:
|
| 196 |
+
r, g, b = x, 0, c
|
| 197 |
+
else:
|
| 198 |
+
r, g, b = c, 0, x
|
| 199 |
+
R = int((r + m) * 255)
|
| 200 |
+
G = int((g + m) * 255)
|
| 201 |
+
B = int((b + m) * 255)
|
| 202 |
+
return f"#{R:02x}{G:02x}{B:02x}"
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _header_text(offset: int) -> "Text":
|
| 206 |
+
"""Build the title panel content with a rainbow-shimmer oohfixer.com."""
|
| 207 |
+
t = Text()
|
| 208 |
+
t.append("Caption Tool", style="bold white")
|
| 209 |
+
t.append("\n")
|
| 210 |
+
t.append("Caption entire image folders locally — no API key required", style="dim")
|
| 211 |
+
t.append("\n")
|
| 212 |
+
for i, ch in enumerate("oohfixer.com"):
|
| 213 |
+
hue = (i * 18 + offset) % 360
|
| 214 |
+
t.append(ch, style=f"bold {_hsl_hex(hue)}")
|
| 215 |
+
return t
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def show_header():
|
| 219 |
+
"""Print the title. Animate a rainbow shimmer on a real terminal; static otherwise."""
|
| 220 |
+
panel = lambda off: Panel(_header_text(off), style="bold blue", expand=False) # noqa: E731
|
| 221 |
+
if console.is_terminal:
|
| 222 |
+
with Live(console=console, refresh_per_second=24, transient=False) as live:
|
| 223 |
+
for frame in range(40):
|
| 224 |
+
live.update(panel(frame * 14))
|
| 225 |
+
time.sleep(0.03)
|
| 226 |
+
else:
|
| 227 |
+
console.print(panel(0))
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def main():
|
| 231 |
+
args = parse_args()
|
| 232 |
+
|
| 233 |
+
show_header()
|
| 234 |
+
|
| 235 |
+
cfg = load_config()
|
| 236 |
+
|
| 237 |
+
# --- Path input (use --path if given) ---
|
| 238 |
+
if args.path:
|
| 239 |
+
target = Path(args.path).expanduser().resolve()
|
| 240 |
+
else:
|
| 241 |
+
raw = Prompt.ask("\n[bold]Image folder or file path[/bold]")
|
| 242 |
+
target = Path(raw).expanduser().resolve()
|
| 243 |
+
|
| 244 |
+
if not target.exists():
|
| 245 |
+
console.print(f"[red]✗ Path not found:[/red] {target}")
|
| 246 |
+
sys.exit(1)
|
| 247 |
+
|
| 248 |
+
images = find_images(target)
|
| 249 |
+
if not images:
|
| 250 |
+
console.print("[red]✗ No images found.[/red]")
|
| 251 |
+
sys.exit(1)
|
| 252 |
+
|
| 253 |
+
# CLI / config overrides
|
| 254 |
+
if args.model:
|
| 255 |
+
cfg["model"] = args.model
|
| 256 |
+
if args.max_new_tokens:
|
| 257 |
+
cfg["max_new_tokens"] = args.max_new_tokens
|
| 258 |
+
if args.force:
|
| 259 |
+
cfg["skip_existing"] = False
|
| 260 |
+
|
| 261 |
+
# Filter already-captioned unless skipping is disabled
|
| 262 |
+
pending = images
|
| 263 |
+
if cfg["skip_existing"]:
|
| 264 |
+
pending = [p for p in images if not p.with_suffix(".txt").exists()]
|
| 265 |
+
skipped = len(images) - len(pending)
|
| 266 |
+
else:
|
| 267 |
+
skipped = 0
|
| 268 |
+
|
| 269 |
+
console.print(f"\n[green]✓[/green] Found [bold]{len(images)}[/bold] image(s)", end="")
|
| 270 |
+
if skipped:
|
| 271 |
+
console.print(f" [dim]({skipped} already captioned, skipping)[/dim]", end="")
|
| 272 |
+
console.print()
|
| 273 |
+
|
| 274 |
+
if not pending:
|
| 275 |
+
console.print("[yellow]All images already have captions. Run with --force to redo them.[/yellow]")
|
| 276 |
+
sys.exit(0)
|
| 277 |
+
|
| 278 |
+
# --- Settings summary + optional changes (skip prompts when --path given) ---
|
| 279 |
+
console.print(Panel(
|
| 280 |
+
f"[cyan]Model:[/cyan] {cfg['model']}\n"
|
| 281 |
+
f"[cyan]Prefix:[/cyan] {cfg['prefix'] or '[dim](none)[/dim]'}\n"
|
| 282 |
+
f"[cyan]Suffix:[/cyan] {cfg['suffix'] or '[dim](none)[/dim]'}\n"
|
| 283 |
+
f"[cyan]Max tokens:[/cyan] {cfg['max_new_tokens']}\n"
|
| 284 |
+
f"[cyan]Min words:[/cyan] {cfg['min_words']} (0 = no minimum)\n"
|
| 285 |
+
f"[cyan]Skip existing:[/cyan] {cfg['skip_existing']}",
|
| 286 |
+
title="[bold]Current Settings[/bold]",
|
| 287 |
+
expand=False,
|
| 288 |
+
))
|
| 289 |
+
|
| 290 |
+
# Only offer to change settings when running interactively (no --path).
|
| 291 |
+
if not args.path and Confirm.ask("Change settings?", default=False):
|
| 292 |
+
cfg["model"] = select_model(cfg["model"])
|
| 293 |
+
cfg["prefix"] = Prompt.ask("Caption prefix (trigger word, etc.)", default=cfg["prefix"])
|
| 294 |
+
cfg["suffix"] = Prompt.ask("Caption suffix", default=cfg["suffix"])
|
| 295 |
+
cfg["max_new_tokens"] = int(Prompt.ask("Max new tokens", default=str(cfg["max_new_tokens"])))
|
| 296 |
+
cfg["min_words"] = int(Prompt.ask("Min words (re-caption if shorter)", default=str(cfg["min_words"])))
|
| 297 |
+
cfg["skip_existing"] = Confirm.ask("Skip already-captioned images?", default=cfg["skip_existing"])
|
| 298 |
+
save_config(cfg)
|
| 299 |
+
console.print("[dim]Settings saved.[/dim]")
|
| 300 |
+
|
| 301 |
+
proceed = args.path or Confirm.ask(f"\nCaption [bold]{len(pending)}[/bold] image(s)?", default=True)
|
| 302 |
+
if not proceed:
|
| 303 |
+
console.print("[dim]Aborted.[/dim]")
|
| 304 |
+
sys.exit(0)
|
| 305 |
+
|
| 306 |
+
# --- Load model ---
|
| 307 |
+
console.print()
|
| 308 |
+
try:
|
| 309 |
+
model_bundle, device_label = load_model(cfg["model"])
|
| 310 |
+
except Exception as e:
|
| 311 |
+
console.print(f"[red]✗ Failed to load model:[/red] {e}")
|
| 312 |
+
sys.exit(1)
|
| 313 |
+
|
| 314 |
+
console.print(f"[green]✓[/green] Model ready on [bold]{device_label}[/bold]\n")
|
| 315 |
+
|
| 316 |
+
# --- Caption loop ---
|
| 317 |
+
errors = []
|
| 318 |
+
done = 0
|
| 319 |
+
|
| 320 |
+
with Progress(
|
| 321 |
+
SpinnerColumn(),
|
| 322 |
+
TextColumn("[bold cyan]{task.description}"),
|
| 323 |
+
BarColumn(),
|
| 324 |
+
TaskProgressColumn(),
|
| 325 |
+
TimeElapsedColumn(),
|
| 326 |
+
console=console,
|
| 327 |
+
transient=False,
|
| 328 |
+
) as progress:
|
| 329 |
+
task = progress.add_task("Captioning...", total=len(pending))
|
| 330 |
+
|
| 331 |
+
for img_path in pending:
|
| 332 |
+
progress.update(task, description=f"[bold cyan]{img_path.name}")
|
| 333 |
+
try:
|
| 334 |
+
caption = caption_image(
|
| 335 |
+
model_bundle, img_path,
|
| 336 |
+
cfg["prefix"], cfg["suffix"], cfg["max_new_tokens"], cfg["min_words"]
|
| 337 |
+
)
|
| 338 |
+
txt_path = img_path.with_suffix(".txt")
|
| 339 |
+
txt_path.write_text(caption, encoding="utf-8")
|
| 340 |
+
done += 1
|
| 341 |
+
except Exception as e:
|
| 342 |
+
errors.append((img_path.name, str(e)))
|
| 343 |
+
progress.advance(task)
|
| 344 |
+
|
| 345 |
+
# --- Summary ---
|
| 346 |
+
console.print()
|
| 347 |
+
if errors:
|
| 348 |
+
console.print(f"[green]✓ {done} captioned[/green] [red]✗ {len(errors)} failed[/red]")
|
| 349 |
+
for name, err in errors:
|
| 350 |
+
console.print(f" [red]{name}:[/red] {err}")
|
| 351 |
+
else:
|
| 352 |
+
console.print(Panel(
|
| 353 |
+
f"[bold green]✓ {done} image(s) captioned successfully[/bold green]\n"
|
| 354 |
+
f"[dim]Output: .txt files alongside each image[/dim]",
|
| 355 |
+
style="green",
|
| 356 |
+
expand=False,
|
| 357 |
+
))
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
if __name__ == "__main__":
|
| 361 |
+
main()
|
caption_config.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "microsoft/Florence-2-large-ft",
|
| 3 |
+
"prefix": "",
|
| 4 |
+
"suffix": "",
|
| 5 |
+
"max_new_tokens": 256,
|
| 6 |
+
"min_words": 15,
|
| 7 |
+
"skip_existing": true
|
| 8 |
+
}
|
install.sh
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
#
|
| 3 |
+
# Caption Tool installer
|
| 4 |
+
#
|
| 5 |
+
# One-command install (host this file, then):
|
| 6 |
+
# curl -fsSL https://oohfixer.com/caption-tool/install.sh | bash
|
| 7 |
+
#
|
| 8 |
+
# Local install (run from this folder):
|
| 9 |
+
# bash install.sh
|
| 10 |
+
#
|
| 11 |
+
# Environment overrides:
|
| 12 |
+
# CAPTION_TOOL_BASE -> base URL the files are served from (download mode)
|
| 13 |
+
# CAPTION_TOOL_DIR -> install location (default ~/.local/share/caption_tool)
|
| 14 |
+
|
| 15 |
+
set -euo pipefail
|
| 16 |
+
|
| 17 |
+
BASE_URL="${CAPTION_TOOL_BASE:-https://oohfixer.com/caption-tool}"
|
| 18 |
+
INSTALL_DIR="${CAPTION_TOOL_DIR:-$HOME/.local/share/caption_tool}"
|
| 19 |
+
BIN_DIR="$HOME/.local/bin"
|
| 20 |
+
VENV="$INSTALL_DIR/.venv"
|
| 21 |
+
|
| 22 |
+
info() { printf '%s\n' "$1"; }
|
| 23 |
+
err() { printf 'Error: %s\n' "$1" >&2; }
|
| 24 |
+
|
| 25 |
+
# Ask a yes/no question on the terminal so it works even when this script is
|
| 26 |
+
# piped in via curl (where stdin is the script itself, not the keyboard).
|
| 27 |
+
prompt_yes_no() {
|
| 28 |
+
local ans
|
| 29 |
+
if [ -r /dev/tty ]; then
|
| 30 |
+
read -r -p "$1 [y/N] " ans < /dev/tty || ans=""
|
| 31 |
+
else
|
| 32 |
+
read -r -p "$1 [y/N] " ans || ans=""
|
| 33 |
+
fi
|
| 34 |
+
case "$ans" in
|
| 35 |
+
y|Y|yes|YES|Yes) return 0 ;;
|
| 36 |
+
*) return 1 ;;
|
| 37 |
+
esac
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Best-effort install of Python 3.13 via the system package manager.
|
| 41 |
+
install_python() {
|
| 42 |
+
local os_id=""
|
| 43 |
+
if [ -f /etc/os-release ]; then
|
| 44 |
+
# shellcheck disable=SC1091
|
| 45 |
+
os_id="$(. /etc/os-release && echo "$ID")"
|
| 46 |
+
fi
|
| 47 |
+
|
| 48 |
+
case "$os_id" in
|
| 49 |
+
ubuntu|debian|linuxmint|pop|raspbian|kali)
|
| 50 |
+
sudo apt-get update
|
| 51 |
+
if ! sudo apt-get install -y python3.13 python3.13-venv; then
|
| 52 |
+
sudo apt-get install -y software-properties-common
|
| 53 |
+
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
| 54 |
+
sudo apt-get update
|
| 55 |
+
sudo apt-get install -y python3.13 python3.13-venv
|
| 56 |
+
fi
|
| 57 |
+
;;
|
| 58 |
+
fedora|rhel|centos|rocky|almalinux)
|
| 59 |
+
sudo dnf install -y python3.13
|
| 60 |
+
;;
|
| 61 |
+
arch|manjaro)
|
| 62 |
+
err "Arch-based systems do not provide older Python easily."
|
| 63 |
+
info "Install python-3.13 from the AUR, then re-run this installer."
|
| 64 |
+
return 1
|
| 65 |
+
;;
|
| 66 |
+
*)
|
| 67 |
+
if command -v brew >/dev/null 2>&1; then
|
| 68 |
+
brew install python@3.13
|
| 69 |
+
else
|
| 70 |
+
err "Could not detect your OS package manager."
|
| 71 |
+
info "Install Python 3.13 manually (https://www.python.org/downloads/), then re-run."
|
| 72 |
+
return 1
|
| 73 |
+
fi
|
| 74 |
+
;;
|
| 75 |
+
esac
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# --- 1. Locate or install a suitable Python --------------------------------
|
| 79 |
+
PYBIN=""
|
| 80 |
+
if command -v python3 >/dev/null 2>&1; then
|
| 81 |
+
PYVER="$(python3 --version 2>&1 | sed 's/Python //')"
|
| 82 |
+
PYMAJ="$(printf '%s' "$PYVER" | cut -d. -f1)"
|
| 83 |
+
PYMIN="$(printf '%s' "$PYVER" | cut -d. -f2)"
|
| 84 |
+
if [ "$PYMAJ" -gt 3 ] || { [ "$PYMAJ" -eq 3 ] && [ "$PYMIN" -ge 14 ]; }; then
|
| 85 |
+
if prompt_yes_no "Python $PYVER is installed, but Caption Tool needs 3.13 or older. Install Python 3.13 now?"; then
|
| 86 |
+
install_python || true
|
| 87 |
+
PYBIN="$(command -v python3.13 || true)"
|
| 88 |
+
fi
|
| 89 |
+
else
|
| 90 |
+
PYBIN="$(command -v python3)"
|
| 91 |
+
fi
|
| 92 |
+
else
|
| 93 |
+
if prompt_yes_no "Python 3 was not found. Install Python 3.13 now?"; then
|
| 94 |
+
install_python || true
|
| 95 |
+
PYBIN="$(command -v python3.13 || command -v python3 || true)"
|
| 96 |
+
fi
|
| 97 |
+
fi
|
| 98 |
+
|
| 99 |
+
if [ -z "$PYBIN" ]; then
|
| 100 |
+
err "A Python 3.13 (or older) interpreter is required."
|
| 101 |
+
info "Install it, then re-run: bash install.sh"
|
| 102 |
+
exit 1
|
| 103 |
+
fi
|
| 104 |
+
|
| 105 |
+
# Re-verify the chosen interpreter is acceptable.
|
| 106 |
+
PYVER="$("$PYBIN" --version 2>&1 | sed 's/Python //')"
|
| 107 |
+
PYMAJ="$(printf '%s' "$PYVER" | cut -d. -f1)"
|
| 108 |
+
PYMIN="$(printf '%s' "$PYVER" | cut -d. -f2)"
|
| 109 |
+
if [ "$PYMAJ" -gt 3 ] || { [ "$PYMAJ" -eq 3 ] && [ "$PYMIN" -ge 14 ]; }; then
|
| 110 |
+
err "Python $PYVER is still too new. Caption Tool requires 3.13 or older."
|
| 111 |
+
exit 6
|
| 112 |
+
fi
|
| 113 |
+
|
| 114 |
+
# --- 2. Locate the source files (local copy or download) --------------------
|
| 115 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" && pwd)"
|
| 116 |
+
if [ -f "$SCRIPT_DIR/caption.py" ]; then
|
| 117 |
+
SRC_MODE="local"
|
| 118 |
+
SRC="$SCRIPT_DIR"
|
| 119 |
+
else
|
| 120 |
+
SRC_MODE="download"
|
| 121 |
+
SRC="$BASE_URL"
|
| 122 |
+
fi
|
| 123 |
+
|
| 124 |
+
# --- 3. Create install dir + virtual environment ----------------------------
|
| 125 |
+
info "Installing Caption Tool"
|
| 126 |
+
info " Location: $INSTALL_DIR"
|
| 127 |
+
info ""
|
| 128 |
+
|
| 129 |
+
info "Step 1/5 Preparing the isolated Python environment..."
|
| 130 |
+
mkdir -p "$INSTALL_DIR"
|
| 131 |
+
if [ ! -d "$VENV" ]; then
|
| 132 |
+
"$PYBIN" -m venv "$VENV"
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
# --- 4. Fetch the tool files -------------------------------------------------
|
| 136 |
+
info "Step 2/5 Getting the tool files..."
|
| 137 |
+
if [ "$SRC_MODE" = "local" ]; then
|
| 138 |
+
cp "$SRC/caption.py" "$INSTALL_DIR/"
|
| 139 |
+
cp "$SRC/caption_config.json" "$INSTALL_DIR/"
|
| 140 |
+
cp "$SRC/requirements.txt" "$INSTALL_DIR/"
|
| 141 |
+
else
|
| 142 |
+
curl -fsSL "$SRC/caption.py" -o "$INSTALL_DIR/caption.py"
|
| 143 |
+
curl -fsSL "$SRC/caption_config.json" -o "$INSTALL_DIR/caption_config.json"
|
| 144 |
+
curl -fsSL "$SRC/requirements.txt" -o "$INSTALL_DIR/requirements.txt"
|
| 145 |
+
fi
|
| 146 |
+
|
| 147 |
+
# --- 5. Install dependencies -------------------------------------------------
|
| 148 |
+
info "Step 3/5 Installing dependencies (this can take a few minutes)..."
|
| 149 |
+
"$VENV/bin/pip" install --quiet --upgrade pip
|
| 150 |
+
"$VENV/bin/pip" install --quiet -r "$INSTALL_DIR/requirements.txt"
|
| 151 |
+
|
| 152 |
+
# --- 6. Create the global 'captions' command ---------------------------------
|
| 153 |
+
info "Step 4/5 Creating the 'captions' command..."
|
| 154 |
+
mkdir -p "$BIN_DIR"
|
| 155 |
+
cat > "$BIN_DIR/captions" <<EOF
|
| 156 |
+
#!/usr/bin/env bash
|
| 157 |
+
exec "$VENV/bin/python" "$INSTALL_DIR/caption.py" "\$@"
|
| 158 |
+
EOF
|
| 159 |
+
chmod +x "$BIN_DIR/captions"
|
| 160 |
+
|
| 161 |
+
# --- 7. Make sure ~/.local/bin is on PATH ------------------------------------
|
| 162 |
+
NEED_RESTART=0
|
| 163 |
+
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
|
| 164 |
+
RC="$HOME/.bashrc"
|
| 165 |
+
case "${SHELL:-}" in
|
| 166 |
+
*zsh) RC="$HOME/.zshrc" ;;
|
| 167 |
+
esac
|
| 168 |
+
if [ -f "$RC" ] && ! grep -q 'caption_tool_path' "$RC"; then
|
| 169 |
+
printf '\nexport PATH="%s:$PATH" # caption_tool_path\n' "$BIN_DIR" >> "$RC"
|
| 170 |
+
fi
|
| 171 |
+
NEED_RESTART=1
|
| 172 |
+
fi
|
| 173 |
+
|
| 174 |
+
# --- 8. Done -----------------------------------------------------------------
|
| 175 |
+
info "Step 5/5 Done."
|
| 176 |
+
info ""
|
| 177 |
+
info "Caption Tool is installed."
|
| 178 |
+
if [ "$NEED_RESTART" = "1" ]; then
|
| 179 |
+
info "Run the next line (or open a new terminal) to use 'captions' anywhere:"
|
| 180 |
+
info " source $RC"
|
| 181 |
+
else
|
| 182 |
+
info "The command 'captions' is ready to use now."
|
| 183 |
+
fi
|
| 184 |
+
info ""
|
| 185 |
+
info "Start captioning: captions"
|
| 186 |
+
info "Or point at a folder: captions --path \"/path/to/images\""
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Caption Tool — local image captioner
|
| 2 |
+
# Needs Python 3.13 or older. Python 3.14 breaks the tokenizer build.
|
| 3 |
+
|
| 4 |
+
torch>=2.1.0
|
| 5 |
+
torchvision>=0.20.0
|
| 6 |
+
transformers==4.46.3
|
| 7 |
+
Pillow>=10.0.0
|
| 8 |
+
rich>=13.0.0
|