-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemoteChristianImageFilenameGenerator.py
More file actions
314 lines (257 loc) · 12.1 KB
/
Copy pathRemoteChristianImageFilenameGenerator.py
File metadata and controls
314 lines (257 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import os
import time
from pathlib import Path
from openai import OpenAI
from PIL import Image
import base64
from tqdm import tqdm
import argparse
import re
import numpy as np
from io import BytesIO
# ─── Config ────────────────────────────────────────────────────────────────
SERVER_URL = "https://openrouter.ai/api/v1"
API_KEY = "<please specify your api key in command args>" # ← your real key
MODEL = "x-ai/grok-4.1-fast"
FOLDER = r"./images" # ← change this
BATCH_FILE_NAME = "rename_images.bat"
PROCESSED_LOG_NAME = "processed_images.log" # one absolute path per line
MAX_FILENAME_WORDS = 7
#SUPPORTED_EXT = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif'}
SUPPORTED_EXT = {'.jpg', '.jpeg', '.jfif', '.jpe', '.jfi', # all JPEG family variants
'.png',
'.webp',
'.bmp',
'.gif',
'.tiff', '.tif'}
# Sharpness-based resize settings
SHARPNESS_HIGH = 200
SHARPNESS_MED = 50
MIN_WIDTH = 256
MAX_WIDTH = 1024
PROMPT_TEMPLATE = """You are an expert in Biblical and Traditional Christian imagery, including scenes from the Old and New Testaments, depictions of Jesus Christ, Mary, saints, apostles, angels, demons, miracles, parables, symbols like the cross, ichthys, dove, lamb, or architectural elements like cathedrals, altars, and stained glass in a religious context. Your task is to analyze the provided image and generate a single, concise filename (e.g., "descriptive_name") that accurately describes its content.
First, classify if the image primarily depicts a Biblical event, figure, symbol, or Traditional Christian theme (e.g., Nativity, Crucifixion, Last Supper, saints' lives, sacraments, or ecclesiastical art). If it does, prioritize a filename that directly references the specific Biblical or Christian element, using accurate terminology (e.g., "Jesus_Healing_the_Blind" instead of generic).
If the image does not clearly depict Biblical or Traditional Christian content, check for any subtle or thematic connection (e.g., a garden might relate to "Garden_of_Eden" if fitting, or a shepherd to "Good_Shepherd"). Only apply this if the link is reasonable and enhances accuracy—do not force it.
If no Biblical or Christian connection applies, fall back to a neutral, secular description based on the main subjects, actions, colors, style, or composition (e.g., "(secular) Red_Sports_Car_on_Highway").
For the filename use 5-15 words max, underscore-separated, descriptive nouns/adjectives, no articles/prepositions unless essential, no file extension. Output only the filename—nothing else."""
def get_sharpness(img: Image.Image) -> float:
gray = img.convert('L')
array = np.asarray(gray).astype(float)
lap = (
-4.0 * array[1:-1, 1:-1]
+ array[:-2, 1:-1]
+ array[2:, 1:-1]
+ array[1:-1, :-2]
+ array[1:-1, 2:]
)
return float(np.var(lap))
def prepare_image_for_model(img_path: Path) -> str:
img = Image.open(img_path)
# ─── Critical fix: convert to RGB (drop alpha/transparency) ─────────────
if img.mode in ('RGBA', 'LA', 'P'): # P can have alpha in some cases
# Option A: simple discard alpha (black background)
img = img.convert('RGB')
# Option B: composite on white background (better for most art/icons)
# background = Image.new('RGB', img.size, (255, 255, 255))
# img = Image.alpha_composite(background, img.convert('RGBA')).convert('RGB')
sharpness = get_sharpness(img) # sharpness now works on RGB
if sharpness > SHARPNESS_HIGH:
target_width = 512
elif sharpness > SHARPNESS_MED:
target_width = 384
else:
target_width = MIN_WIDTH
target_width = max(MIN_WIDTH, min(MAX_WIDTH, target_width))
if img.width > target_width:
ratio = target_width / float(img.width)
new_height = int(img.height * ratio)
img = img.resize((target_width, new_height), Image.Resampling.LANCZOS)
buffer = BytesIO()
img.save(buffer, format="JPEG", quality=85)
return base64.b64encode(buffer.getvalue()).decode("utf-8")
def get_suggested_name(client: OpenAI, img_path: Path) -> str | None:
try:
base64_img = prepare_image_for_model(img_path)
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": PROMPT_TEMPLATE},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_img}"}
}
]
}
],
temperature=0.1,
max_tokens=60,
)
content = response.choices[0].message.content
if not isinstance(content, str):
return None
name = content.strip()
name = re.sub(r'[^a-z0-9_-]', '', name.lower())
name = re.sub(r'-+', '-', name).strip('-_')
if len(name) < 5:
return None
return name
except Exception as e:
print(f"Error processing {img_path.name}: {e}")
return None
def escape_batch_filename(name: str) -> str:
for c in '&%!?^':
name = name.replace(c, f'^{c}')
return name
def append_rename_command(batch_path: Path, old_path: Path, new_name: str):
old_full = str(old_path.absolute())
cmd = f'ren "{old_full}" {escape_batch_filename(new_name)}\n'
with open(batch_path, 'a', encoding='utf-8', errors='replace') as f:
f.write(cmd)
def load_processed_set(log_path: Path) -> set[str]:
if not log_path.exists():
return set()
with open(log_path, encoding='utf-8', errors='replace') as f:
return {line.strip() for line in f if line.strip()}
def append_processed(log_path: Path, img_path: Path):
with open(log_path, 'a', encoding='utf-8', errors='replace') as f:
f.write(f"{img_path.absolute()}\n")
def main():
global MODEL
parser = argparse.ArgumentParser(
description=(
"Scan images in a folder, ask an OpenAI-compatible vision model for "
"filename suggestions, write rename commands to a batch file, and "
"track already-processed images for resumable runs."
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--server-url",
type=str,
default=SERVER_URL,
help="OpenAI-compatible API base URL.",
)
parser.add_argument(
"--api-key",
type=str,
default=API_KEY,
help="API key used to authenticate model requests.",
)
parser.add_argument(
"--model",
type=str,
default=MODEL,
help="Model identifier for image-to-filename generation.",
)
parser.add_argument(
"--folder",
type=str,
default=FOLDER,
help="Root folder to scan recursively for supported image files.",
)
parser.add_argument(
"--batch-file-name",
type=str,
default=BATCH_FILE_NAME,
help="Output batch filename that receives generated rename commands.",
)
parser.add_argument(
"--processed-images-log-name",
"--processed-log-name",
dest="processed_images_log_name",
type=str,
default=PROCESSED_LOG_NAME,
help="Log filename storing absolute paths already processed.",
)
parser.add_argument(
"--reset",
action="store_true",
help="Delete processed log before running and process all images from scratch.",
)
args = parser.parse_args()
MODEL = args.model
masked_api_key = args.api_key
if masked_api_key:
if len(masked_api_key) <= 8:
masked_api_key = "*" * len(masked_api_key)
else:
masked_api_key = f"{masked_api_key[:4]}...{masked_api_key[-4:]}"
print("Program summary:")
print(" Scans image files in the target folder (recursive).")
print(" Sends each image to a vision model for a suggested filename.")
print(" Appends rename commands to a batch file.")
print(" Tracks processed images to avoid duplicate work.")
print("Run settings:")
print(f" server_url={args.server_url}")
print(f" api_key={masked_api_key}")
print(f" model={args.model}")
print(f" folder={Path(args.folder).resolve()}")
print(f" batch_file_name={args.batch_file_name}")
print(f" processed_images_log_name={args.processed_images_log_name}")
print(f" reset={args.reset}")
client = OpenAI(base_url=args.server_url, api_key=args.api_key)
root = Path(args.folder).resolve()
batch_file = root / args.batch_file_name
processed_log = root / args.processed_images_log_name
# ─── Resume / reset logic ──────────────────────────────────────────────
if args.reset and processed_log.exists():
print("Reset requested → deleting processed log")
processed_log.unlink()
already_processed = load_processed_set(processed_log)
print(f"Already processed files (from log): {len(already_processed)}")
# Optional: reset batch file on --reset (uncomment if desired)
# if args.reset and batch_file.exists():
# batch_file.unlink()
if not batch_file.exists():
with open(batch_file, 'w', encoding='utf-8') as f:
f.write("echo Starting rename operations...\n\n")
images = sorted(p for p in root.rglob("*") if p.suffix.lower() in SUPPORTED_EXT)
# ^ sorted() gives more stable/reproducible order across runs
print(f"Found {len(images)} images in total")
print(f"Will process {len(images) - len(already_processed & {str(p) for p in images})} new/remaining images")
print(f"Batch file : {batch_file}")
print(f"Processed log: {processed_log}")
processed_count = 0
skipped_count = 0
generated = 0
for img_path in tqdm(images, desc="Processing"):
abs_path_str = str(img_path.absolute())
if abs_path_str in already_processed:
skipped_count += 1
continue
suggested = get_suggested_name(client, img_path)
if not suggested:
print(f" → SKIPPED (no suggestion) {img_path.name}")
# You may still want to mark as processed so we don't retry forever
append_processed(processed_log, img_path)
processed_count += 1
continue
new_name = f"{suggested}{img_path.suffix.lower()}"
# Collision handling
counter = 1
candidate = new_name
new_path = img_path.with_name(candidate)
while new_path.exists() and new_path != img_path:
candidate = f"{suggested}-{counter}{img_path.suffix.lower()}"
new_path = img_path.with_name(candidate)
counter += 1
# Write rename command
append_rename_command(batch_file, img_path, candidate)
generated += 1
# Mark as done
append_processed(processed_log, img_path)
processed_count += 1
print(f" → {img_path.name} → {candidate}")
time.sleep(0.3) # gentle cooldown
print("\nFinished this run.")
print(f" Already processed (skipped) : {skipped_count}")
print(f" Commands generated this run : {generated}")
print(f" Newly processed this run : {processed_count}")
print(f"Batch file : {batch_file}")
print(f"Processed log : {processed_log}")
print("\nRun again to continue from where it left off (or use --reset to start over).")
if __name__ == "__main__":
main()