schenesort 2.1.1__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schenesort/cli.py +185 -30
- schenesort/config.py +9 -1
- schenesort/tui/app.py +21 -8
- schenesort/tui/widgets/image_preview.py +4 -2
- {schenesort-2.1.1.dist-info → schenesort-2.3.0.dist-info}/METADATA +79 -15
- schenesort-2.3.0.dist-info/RECORD +15 -0
- schenesort-2.1.1.dist-info/RECORD +0 -15
- {schenesort-2.1.1.dist-info → schenesort-2.3.0.dist-info}/WHEEL +0 -0
- {schenesort-2.1.1.dist-info → schenesort-2.3.0.dist-info}/entry_points.txt +0 -0
- {schenesort-2.1.1.dist-info → schenesort-2.3.0.dist-info}/licenses/LICENSE +0 -0
schenesort/cli.py
CHANGED
|
@@ -444,6 +444,9 @@ def get(
|
|
|
444
444
|
paths_only: Annotated[
|
|
445
445
|
bool, typer.Option("--paths-only", "-p", help="Output only file paths (for scripting)")
|
|
446
446
|
] = False,
|
|
447
|
+
browse: Annotated[
|
|
448
|
+
bool, typer.Option("--browse", "-b", help="Open results in TUI browser")
|
|
449
|
+
] = False,
|
|
447
450
|
) -> None:
|
|
448
451
|
"""Query wallpapers by metadata attributes."""
|
|
449
452
|
from schenesort.db import WallpaperDB
|
|
@@ -472,7 +475,13 @@ def get(
|
|
|
472
475
|
typer.echo("No wallpapers found matching criteria.", err=True)
|
|
473
476
|
raise typer.Exit(1)
|
|
474
477
|
|
|
475
|
-
if
|
|
478
|
+
if browse:
|
|
479
|
+
from schenesort.tui import WallpaperBrowser
|
|
480
|
+
|
|
481
|
+
files = [Path(r["path"]) for r in results]
|
|
482
|
+
app_instance = WallpaperBrowser(files=files)
|
|
483
|
+
app_instance.run()
|
|
484
|
+
elif paths_only:
|
|
476
485
|
for r in results:
|
|
477
486
|
typer.echo(r["path"])
|
|
478
487
|
else:
|
|
@@ -542,7 +551,10 @@ def stats() -> None:
|
|
|
542
551
|
|
|
543
552
|
@app.command()
|
|
544
553
|
def browse(
|
|
545
|
-
path: Annotated[
|
|
554
|
+
path: Annotated[
|
|
555
|
+
Path | None,
|
|
556
|
+
typer.Argument(help="Directory or file to browse (default: config wallpaper path)"),
|
|
557
|
+
] = None,
|
|
546
558
|
recursive: Annotated[
|
|
547
559
|
bool, typer.Option("--recursive", "-r", help="Browse directories recursively")
|
|
548
560
|
] = False,
|
|
@@ -550,6 +562,15 @@ def browse(
|
|
|
550
562
|
"""Browse wallpapers in a terminal UI with image preview and metadata display."""
|
|
551
563
|
from schenesort.tui import WallpaperBrowser
|
|
552
564
|
|
|
565
|
+
if path is None:
|
|
566
|
+
cfg = load_config()
|
|
567
|
+
if cfg.wallpaper_path:
|
|
568
|
+
path = Path(cfg.wallpaper_path).expanduser()
|
|
569
|
+
else:
|
|
570
|
+
typer.echo("Error: No path provided and no wallpaper path configured.", err=True)
|
|
571
|
+
typer.echo("Set paths.wallpaper in config or provide a path argument.", err=True)
|
|
572
|
+
raise typer.Exit(1)
|
|
573
|
+
|
|
553
574
|
path = path.resolve()
|
|
554
575
|
|
|
555
576
|
if not path.exists():
|
|
@@ -572,22 +593,17 @@ def config(
|
|
|
572
593
|
config_path = get_config_path()
|
|
573
594
|
|
|
574
595
|
if create:
|
|
596
|
+
already_existed = config_path.exists()
|
|
575
597
|
path = create_default_config()
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
598
|
+
typer.echo(f"Config file: {path}")
|
|
599
|
+
typer.echo("(already existed)" if already_existed else "(created)")
|
|
600
|
+
|
|
601
|
+
if config_path.exists():
|
|
602
|
+
typer.echo(f"Config file: {config_path}\n")
|
|
603
|
+
typer.echo(config_path.read_text())
|
|
604
|
+
elif not create:
|
|
581
605
|
typer.echo(f"Config file: {config_path}")
|
|
582
|
-
|
|
583
|
-
typer.echo("\nCurrent settings:")
|
|
584
|
-
cfg = load_config()
|
|
585
|
-
typer.echo(f" ollama.host: {cfg.ollama_host or '(default: localhost:11434)'}")
|
|
586
|
-
typer.echo(f" ollama.model: {cfg.ollama_model}")
|
|
587
|
-
if cfg.wallpaper_path:
|
|
588
|
-
typer.echo(f" paths.wallpaper: {cfg.wallpaper_path}")
|
|
589
|
-
else:
|
|
590
|
-
typer.echo("(file does not exist, use --create to create)")
|
|
606
|
+
typer.echo("(file does not exist, use --create to create)")
|
|
591
607
|
|
|
592
608
|
|
|
593
609
|
DEFAULT_MODEL = "llava"
|
|
@@ -1108,20 +1124,27 @@ def metadata_generate(
|
|
|
1108
1124
|
metadata = existing
|
|
1109
1125
|
metadata.description = str(description)
|
|
1110
1126
|
metadata.ai_model = effective_model
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if isinstance(
|
|
1116
|
-
metadata.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if isinstance(
|
|
1122
|
-
metadata.
|
|
1123
|
-
|
|
1124
|
-
|
|
1127
|
+
scene = result.get("scene")
|
|
1128
|
+
if isinstance(scene, str):
|
|
1129
|
+
metadata.scene = scene
|
|
1130
|
+
tags = result.get("tags")
|
|
1131
|
+
if isinstance(tags, list):
|
|
1132
|
+
metadata.tags = tags
|
|
1133
|
+
mood = result.get("mood")
|
|
1134
|
+
if isinstance(mood, list):
|
|
1135
|
+
metadata.mood = mood
|
|
1136
|
+
style = result.get("style")
|
|
1137
|
+
if isinstance(style, str):
|
|
1138
|
+
metadata.style = style
|
|
1139
|
+
colors = result.get("colors")
|
|
1140
|
+
if isinstance(colors, list):
|
|
1141
|
+
metadata.colors = colors
|
|
1142
|
+
time_val = result.get("time")
|
|
1143
|
+
if isinstance(time_val, str):
|
|
1144
|
+
metadata.time_of_day = time_val
|
|
1145
|
+
subject = result.get("subject")
|
|
1146
|
+
if isinstance(subject, str):
|
|
1147
|
+
metadata.subject = subject
|
|
1125
1148
|
|
|
1126
1149
|
# Add image dimensions
|
|
1127
1150
|
width, height = get_image_dimensions(target_path)
|
|
@@ -1202,6 +1225,138 @@ def metadata_update_dimensions(
|
|
|
1202
1225
|
typer.echo(f"\n{action} {updated_count} sidecar(s), skipped {skipped_count}.")
|
|
1203
1226
|
|
|
1204
1227
|
|
|
1228
|
+
@app.command()
|
|
1229
|
+
def collage(
|
|
1230
|
+
output: Annotated[Path, typer.Argument(help="Output PNG file path")],
|
|
1231
|
+
tag: Annotated[str | None, typer.Option("--tag", "-t", help="Filter by tag")] = None,
|
|
1232
|
+
mood: Annotated[str | None, typer.Option("--mood", "-m", help="Filter by mood")] = None,
|
|
1233
|
+
color: Annotated[str | None, typer.Option("--color", "-c", help="Filter by color")] = None,
|
|
1234
|
+
style: Annotated[str | None, typer.Option("--style", "-s", help="Filter by style")] = None,
|
|
1235
|
+
subject: Annotated[str | None, typer.Option("--subject", help="Filter by subject")] = None,
|
|
1236
|
+
time: Annotated[str | None, typer.Option("--time", help="Filter by time of day")] = None,
|
|
1237
|
+
screen: Annotated[
|
|
1238
|
+
str | None, typer.Option("--screen", help="Filter by recommended screen (4K, 1440p, etc)")
|
|
1239
|
+
] = None,
|
|
1240
|
+
min_width: Annotated[
|
|
1241
|
+
int | None, typer.Option("--min-width", help="Minimum width in pixels")
|
|
1242
|
+
] = None,
|
|
1243
|
+
min_height: Annotated[
|
|
1244
|
+
int | None, typer.Option("--min-height", help="Minimum height in pixels")
|
|
1245
|
+
] = None,
|
|
1246
|
+
search: Annotated[
|
|
1247
|
+
str | None, typer.Option("--search", "-q", help="Search description, scene, style, subject")
|
|
1248
|
+
] = None,
|
|
1249
|
+
cols: Annotated[int, typer.Option("--cols", help="Number of columns (1-4)")] = 2,
|
|
1250
|
+
rows: Annotated[int, typer.Option("--rows", help="Number of rows (1-4)")] = 2,
|
|
1251
|
+
tile_width: Annotated[
|
|
1252
|
+
int, typer.Option("--tile-width", "-w", help="Width of each tile in pixels")
|
|
1253
|
+
] = 480,
|
|
1254
|
+
tile_height: Annotated[
|
|
1255
|
+
int, typer.Option("--tile-height", "-h", help="Height of each tile in pixels")
|
|
1256
|
+
] = 270,
|
|
1257
|
+
random: Annotated[bool, typer.Option("--random", "-R", help="Select images randomly")] = True,
|
|
1258
|
+
) -> None:
|
|
1259
|
+
"""Create a collage of wallpapers matching the given criteria (max 4x4 grid)."""
|
|
1260
|
+
from PIL import Image
|
|
1261
|
+
|
|
1262
|
+
from schenesort.db import WallpaperDB
|
|
1263
|
+
|
|
1264
|
+
# Validate grid size
|
|
1265
|
+
if cols < 1 or cols > 4:
|
|
1266
|
+
typer.echo("Error: --cols must be between 1 and 4.", err=True)
|
|
1267
|
+
raise typer.Exit(1)
|
|
1268
|
+
if rows < 1 or rows > 4:
|
|
1269
|
+
typer.echo("Error: --rows must be between 1 and 4.", err=True)
|
|
1270
|
+
raise typer.Exit(1)
|
|
1271
|
+
|
|
1272
|
+
num_images = cols * rows
|
|
1273
|
+
|
|
1274
|
+
with WallpaperDB() as db:
|
|
1275
|
+
results = db.query(
|
|
1276
|
+
tag=tag,
|
|
1277
|
+
mood=mood,
|
|
1278
|
+
color=color,
|
|
1279
|
+
style=style,
|
|
1280
|
+
subject=subject,
|
|
1281
|
+
time_of_day=time,
|
|
1282
|
+
screen=screen,
|
|
1283
|
+
min_width=min_width,
|
|
1284
|
+
min_height=min_height,
|
|
1285
|
+
search=search,
|
|
1286
|
+
limit=num_images,
|
|
1287
|
+
random=random,
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
if not results:
|
|
1291
|
+
typer.echo("No wallpapers found matching criteria.", err=True)
|
|
1292
|
+
raise typer.Exit(1)
|
|
1293
|
+
|
|
1294
|
+
if len(results) < num_images:
|
|
1295
|
+
typer.echo(
|
|
1296
|
+
f"Warning: Only found {len(results)} image(s), "
|
|
1297
|
+
f"need {num_images} for {cols}x{rows} grid."
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
# Create collage canvas
|
|
1301
|
+
collage_width = cols * tile_width
|
|
1302
|
+
collage_height = rows * tile_height
|
|
1303
|
+
collage_img = Image.new("RGB", (collage_width, collage_height), color=(0, 0, 0))
|
|
1304
|
+
|
|
1305
|
+
typer.echo(f"Creating {cols}x{rows} collage ({collage_width}x{collage_height})...")
|
|
1306
|
+
|
|
1307
|
+
for idx, r in enumerate(results):
|
|
1308
|
+
if idx >= num_images:
|
|
1309
|
+
break
|
|
1310
|
+
|
|
1311
|
+
filepath = Path(r["path"])
|
|
1312
|
+
row = idx // cols
|
|
1313
|
+
col = idx % cols
|
|
1314
|
+
|
|
1315
|
+
try:
|
|
1316
|
+
with Image.open(filepath) as img:
|
|
1317
|
+
# Convert to RGB if necessary (handles RGBA, palette, etc.)
|
|
1318
|
+
if img.mode != "RGB":
|
|
1319
|
+
img = img.convert("RGB")
|
|
1320
|
+
|
|
1321
|
+
# Resize to fit tile while preserving aspect ratio, then crop to fill
|
|
1322
|
+
img_ratio = img.width / img.height
|
|
1323
|
+
tile_ratio = tile_width / tile_height
|
|
1324
|
+
|
|
1325
|
+
if img_ratio > tile_ratio:
|
|
1326
|
+
# Image is wider - fit by height, crop width
|
|
1327
|
+
new_height = tile_height
|
|
1328
|
+
new_width = int(tile_height * img_ratio)
|
|
1329
|
+
else:
|
|
1330
|
+
# Image is taller - fit by width, crop height
|
|
1331
|
+
new_width = tile_width
|
|
1332
|
+
new_height = int(tile_width / img_ratio)
|
|
1333
|
+
|
|
1334
|
+
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
1335
|
+
|
|
1336
|
+
# Center crop to tile size
|
|
1337
|
+
left = (new_width - tile_width) // 2
|
|
1338
|
+
top = (new_height - tile_height) // 2
|
|
1339
|
+
img = img.crop((left, top, left + tile_width, top + tile_height))
|
|
1340
|
+
|
|
1341
|
+
# Paste into collage
|
|
1342
|
+
x = col * tile_width
|
|
1343
|
+
y = row * tile_height
|
|
1344
|
+
collage_img.paste(img, (x, y))
|
|
1345
|
+
|
|
1346
|
+
typer.echo(f" [{row},{col}] {filepath.name}")
|
|
1347
|
+
|
|
1348
|
+
except Exception as e:
|
|
1349
|
+
typer.echo(f" [{row},{col}] Failed to load {filepath.name}: {e}", err=True)
|
|
1350
|
+
|
|
1351
|
+
# Save collage
|
|
1352
|
+
output = output.resolve()
|
|
1353
|
+
if not output.suffix.lower() == ".png":
|
|
1354
|
+
output = output.with_suffix(".png")
|
|
1355
|
+
|
|
1356
|
+
collage_img.save(output, "PNG")
|
|
1357
|
+
typer.echo(f"\nSaved collage to: {output}")
|
|
1358
|
+
|
|
1359
|
+
|
|
1205
1360
|
@metadata_app.command("embed")
|
|
1206
1361
|
def metadata_embed(
|
|
1207
1362
|
path: Annotated[Path, typer.Argument(help="Image file or directory")],
|
schenesort/config.py
CHANGED
|
@@ -73,7 +73,15 @@ def load_config() -> Config:
|
|
|
73
73
|
|
|
74
74
|
return config
|
|
75
75
|
|
|
76
|
-
except
|
|
76
|
+
except tomllib.TOMLDecodeError as e:
|
|
77
|
+
import sys
|
|
78
|
+
|
|
79
|
+
print(f"Warning: Failed to parse config file: {e}", file=sys.stderr)
|
|
80
|
+
return Config()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
import sys
|
|
83
|
+
|
|
84
|
+
print(f"Warning: Failed to load config file: {e}", file=sys.stderr)
|
|
77
85
|
return Config()
|
|
78
86
|
|
|
79
87
|
|
schenesort/tui/app.py
CHANGED
|
@@ -73,10 +73,16 @@ class WallpaperBrowser(App):
|
|
|
73
73
|
Binding("end", "last_image", "Last", show=False),
|
|
74
74
|
]
|
|
75
75
|
|
|
76
|
-
def __init__(
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
path: Path | None = None,
|
|
79
|
+
recursive: bool = False,
|
|
80
|
+
files: list[Path] | None = None,
|
|
81
|
+
) -> None:
|
|
77
82
|
super().__init__()
|
|
78
83
|
self._base_path = path
|
|
79
84
|
self._recursive = recursive
|
|
85
|
+
self._files = files # Pre-specified file list (from query results)
|
|
80
86
|
self._images: list[Path] = []
|
|
81
87
|
self._current_index: int = 0
|
|
82
88
|
|
|
@@ -99,7 +105,15 @@ class WallpaperBrowser(App):
|
|
|
99
105
|
self._update_status("No images found", "")
|
|
100
106
|
|
|
101
107
|
def _load_images(self) -> None:
|
|
102
|
-
"""Load list of images from the specified path."""
|
|
108
|
+
"""Load list of images from the specified path or file list."""
|
|
109
|
+
# If files were provided directly (e.g., from query results), use them
|
|
110
|
+
if self._files:
|
|
111
|
+
self._images = [f for f in self._files if f.exists()]
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if self._base_path is None:
|
|
115
|
+
return
|
|
116
|
+
|
|
103
117
|
if self._base_path.is_file():
|
|
104
118
|
if self._base_path.suffix.lower() in VALID_IMAGE_EXTENSIONS:
|
|
105
119
|
self._images = [self._base_path]
|
|
@@ -128,12 +142,11 @@ class WallpaperBrowser(App):
|
|
|
128
142
|
panel.update_metadata(metadata, current_image.name)
|
|
129
143
|
|
|
130
144
|
# Update status
|
|
131
|
-
self.
|
|
132
|
-
str(current_image.relative_to(self._base_path))
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
145
|
+
if self._base_path and current_image.is_relative_to(self._base_path):
|
|
146
|
+
display_path = str(current_image.relative_to(self._base_path))
|
|
147
|
+
else:
|
|
148
|
+
display_path = current_image.name
|
|
149
|
+
self._update_status(display_path, f"{self._current_index + 1}/{len(self._images)}")
|
|
137
150
|
|
|
138
151
|
def _update_status(self, left: str, right: str) -> None:
|
|
139
152
|
"""Update the status bar."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: schenesort
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Wallpaper collection management CLI tool
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -12,20 +12,43 @@ Requires-Dist: textual>=0.95.0
|
|
|
12
12
|
Requires-Dist: typer>=0.21.1
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
|
-
# Schenesort v2.
|
|
15
|
+
# Schenesort v2.3.0
|
|
16
16
|
|
|
17
|
-
A CLI tool for managing wallpaper collections with
|
|
17
|
+
A CLI tool for managing wallpaper collections with model generated metadata, terminal UI browsing, and SQLite-based
|
|
18
|
+
querying.
|
|
19
|
+
|
|
20
|
+
schenesort takes a directory of random wallpapers, with random filenames, and uses olama with a decent vision model to
|
|
21
|
+
- look at each wallpaper
|
|
22
|
+
- rename the wallpaper to something sensible
|
|
23
|
+
- drop a XMP sidecar with metadata about the file
|
|
24
|
+
|
|
25
|
+
Once you have a collection of wallpapers re-named with metadata sidecars, run the indexer over it to create a sqlitedb
|
|
26
|
+
that can be queried to retrieve suggestions based on tags, colours, names and the like. Then use `get` to get a
|
|
27
|
+
wallpaper path `feh $(schenesort get -1 -p)` or `hyprctl hyprpaper wallpaper "eDP-1,$(schenesort get -1 -p)"`
|
|
28
|
+
|
|
29
|
+

|
|
18
30
|
|
|
19
31
|
## Installation
|
|
20
32
|
|
|
21
33
|
```bash
|
|
34
|
+
uv tool install schenesort
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For development:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/sthysel/schenesort.git
|
|
41
|
+
cd schenesort
|
|
22
42
|
uv sync
|
|
23
43
|
```
|
|
24
44
|
|
|
25
45
|
## Quick Start
|
|
26
46
|
|
|
47
|
+
To generate metadata you need a olama server serving llava nearby, it takes a minute to set one up see [ollama
|
|
48
|
+
setup](./ollama-setup.md).
|
|
49
|
+
|
|
27
50
|
```bash
|
|
28
|
-
# Generate
|
|
51
|
+
# Generate metadata for images
|
|
29
52
|
schenesort metadata generate ~/wallpapers -r
|
|
30
53
|
|
|
31
54
|
# Browse with TUI
|
|
@@ -54,6 +77,7 @@ schenesort get -1 -p | xargs feh # random wallpaper
|
|
|
54
77
|
| `info` | Show collection file statistics |
|
|
55
78
|
| `describe` | AI-rename images based on content (Ollama) |
|
|
56
79
|
| `models` | List available Ollama models |
|
|
80
|
+
| `collage` | Create a collage grid from matching wallpapers |
|
|
57
81
|
| `metadata show` | Display XMP sidecar metadata |
|
|
58
82
|
| `metadata set` | Manually set metadata fields |
|
|
59
83
|
| `metadata generate` | Generate metadata with AI (Ollama) |
|
|
@@ -67,8 +91,14 @@ Browse your wallpaper collection with image preview and metadata display:
|
|
|
67
91
|
```bash
|
|
68
92
|
schenesort browse ~/wallpapers
|
|
69
93
|
schenesort browse ~/wallpapers -r # recursive
|
|
94
|
+
schenesort browse # uses paths.wallpaper from config
|
|
95
|
+
schenesort get --mood peaceful -b # browse query results
|
|
70
96
|
```
|
|
71
97
|
|
|
98
|
+

|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
72
102
|
**Keyboard shortcuts:**
|
|
73
103
|
| Key | Action |
|
|
74
104
|
|--------------|----------------|
|
|
@@ -104,6 +134,10 @@ schenesort get --random -n 10 # 10 random wallpapers
|
|
|
104
134
|
schenesort get -1 # single random wallpaper
|
|
105
135
|
schenesort get -1 --mood dramatic # random with filter
|
|
106
136
|
|
|
137
|
+
# Browse results in TUI
|
|
138
|
+
schenesort get --mood peaceful --browse # open matches in browser
|
|
139
|
+
schenesort get --style photography -b # short form
|
|
140
|
+
|
|
107
141
|
# For scripting (paths only)
|
|
108
142
|
schenesort get -1 -p # just the path
|
|
109
143
|
feh $(schenesort get -1 -p) # set random wallpaper
|
|
@@ -115,6 +149,36 @@ schenesort stats
|
|
|
115
149
|
|
|
116
150
|
The database is stored at `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`).
|
|
117
151
|
|
|
152
|
+
## Collage Generation
|
|
153
|
+
|
|
154
|
+
Create a collage grid (up to 4x4) from wallpapers matching query criteria:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Create a 2x2 collage (default)
|
|
158
|
+
schenesort collage output.png --mood peaceful
|
|
159
|
+
|
|
160
|
+
# Create a 4x4 collage of landscape images
|
|
161
|
+
schenesort collage wall.png --subject landscape --cols 4 --rows 4
|
|
162
|
+
|
|
163
|
+
# Custom tile size (default: 480x270)
|
|
164
|
+
schenesort collage collage.png --tile-width 640 --tile-height 360
|
|
165
|
+
|
|
166
|
+
# Combine filters
|
|
167
|
+
schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+

|
|
171
|
+
|
|
172
|
+
| Option | Description | Default |
|
|
173
|
+
|-----------------|--------------------------------------|---------|
|
|
174
|
+
| `--cols` | Number of columns (1-4) | 2 |
|
|
175
|
+
| `--rows` | Number of rows (1-4) | 2 |
|
|
176
|
+
| `--tile-width` | Width of each tile in pixels | 480 |
|
|
177
|
+
| `--tile-height` | Height of each tile in pixels | 270 |
|
|
178
|
+
| `--random` | Select images randomly | True |
|
|
179
|
+
|
|
180
|
+
All `get` query filters are supported: `--tag`, `--mood`, `--color`, `--style`, `--subject`, `--time`, `--screen`, `--min-width`, `--min-height`, `--search`.
|
|
181
|
+
|
|
118
182
|
## Metadata Management
|
|
119
183
|
|
|
120
184
|
Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying the original files.
|
|
@@ -136,7 +200,7 @@ Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying
|
|
|
136
200
|
| `source` | Source URL or info |
|
|
137
201
|
| `ai_model` | Model used for metadata generation |
|
|
138
202
|
|
|
139
|
-
### Generate Metadata
|
|
203
|
+
### Generate Metadata using ollama
|
|
140
204
|
|
|
141
205
|
```bash
|
|
142
206
|
# Preview what would be generated
|
|
@@ -304,15 +368,15 @@ Metadata is stored in standard XMP format, compatible with digiKam, darktable, a
|
|
|
304
368
|
|
|
305
369
|
Images are tagged with recommended screen sizes based on resolution:
|
|
306
370
|
|
|
307
|
-
| Screen
|
|
308
|
-
|
|
309
|
-
| 8K
|
|
310
|
-
| 5K
|
|
311
|
-
| 4K
|
|
312
|
-
| Ultrawide 4K
|
|
313
|
-
| Ultrawide 1440p | 3440x1440
|
|
314
|
-
| 1440p
|
|
315
|
-
| 1080p
|
|
316
|
-
| 720p
|
|
371
|
+
| Screen | Resolution |
|
|
372
|
+
|-----------------|------------|
|
|
373
|
+
| 8K | 7680x4320 |
|
|
374
|
+
| 5K | 5120x2880 |
|
|
375
|
+
| 4K | 3840x2160 |
|
|
376
|
+
| Ultrawide 4K | 5120x2160 |
|
|
377
|
+
| Ultrawide 1440p | 3440x1440 |
|
|
378
|
+
| 1440p | 2560x1440 |
|
|
379
|
+
| 1080p | 1920x1080 |
|
|
380
|
+
| 720p | 1280x720 |
|
|
317
381
|
|
|
318
382
|
An image is recommended for a screen size if it can cover the screen without upscaling.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
schenesort/__init__.py,sha256=Z7bmXIWiFUCKj1j4JRlPZvmfzc4RC10Lh1WCKNsJn20,61
|
|
2
|
+
schenesort/cli.py,sha256=LU_s4c7Eg_uozxP5I599TV9Y-WirQxx0h-8LvFNYlqM,51550
|
|
3
|
+
schenesort/config.py,sha256=8EuYv2nma3QKnLPSAgAsaOHZlORzinHjy-Bj_LKPfJA,2739
|
|
4
|
+
schenesort/db.py,sha256=RbfZN6d5O5MkTRPbu51fA7tQLOJf1huIjxFdoT4sESk,11398
|
|
5
|
+
schenesort/xmp.py,sha256=1VS_I4akY8Dv_KLPOdzPgBCFy0280oSCsMmo-_A9cNE,9749
|
|
6
|
+
schenesort/tui/__init__.py,sha256=bqSyRlefPfsYhUxsub4Rltz7yjCQVPXvhzj9n2bn370,141
|
|
7
|
+
schenesort/tui/app.py,sha256=HGKBnszTJXtLu9Mo7eJY4wiO2qkB7dFftBoM4GC_js4,6202
|
|
8
|
+
schenesort/tui/widgets/__init__.py,sha256=3cm7vfXG5-xC_UhIbgEtuMxq5I5tXg6okokJ4ecjGIE,202
|
|
9
|
+
schenesort/tui/widgets/image_preview.py,sha256=j_KTQBJk0vbO18FzTo-GYkRlgerBe1K3VGQwMPbEuUw,2846
|
|
10
|
+
schenesort/tui/widgets/metadata_panel.py,sha256=8DnsvDdKf3jzqHOo7dDmxJxL0Mjx5mX7HNwwcMUcJn0,4860
|
|
11
|
+
schenesort-2.3.0.dist-info/METADATA,sha256=F8AMExvB_0nU7CgGkW2CM1az2XJizdQafy0EBJkwnPw,12592
|
|
12
|
+
schenesort-2.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
+
schenesort-2.3.0.dist-info/entry_points.txt,sha256=J5lS-N6KgmzjutFi5bG1jv-4Wszbz3MfcOHcbznBVcw,50
|
|
14
|
+
schenesort-2.3.0.dist-info/licenses/LICENSE,sha256=sMw3SMb9ec9dbM2twEMVeunsGwuljza-9kEXg4kSJpg,1070
|
|
15
|
+
schenesort-2.3.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
schenesort/__init__.py,sha256=Z7bmXIWiFUCKj1j4JRlPZvmfzc4RC10Lh1WCKNsJn20,61
|
|
2
|
-
schenesort/cli.py,sha256=L7TISfb_ebXI6Do0ncpWsYOj--OZtxfTnVqWjgTg8-I,45659
|
|
3
|
-
schenesort/config.py,sha256=n8rf2htxwYuyRqBXnfEZ0pJIwK5DTTQws82MPbDQ4zQ,2476
|
|
4
|
-
schenesort/db.py,sha256=RbfZN6d5O5MkTRPbu51fA7tQLOJf1huIjxFdoT4sESk,11398
|
|
5
|
-
schenesort/xmp.py,sha256=1VS_I4akY8Dv_KLPOdzPgBCFy0280oSCsMmo-_A9cNE,9749
|
|
6
|
-
schenesort/tui/__init__.py,sha256=bqSyRlefPfsYhUxsub4Rltz7yjCQVPXvhzj9n2bn370,141
|
|
7
|
-
schenesort/tui/app.py,sha256=nyEUBuffGuIrXJvzNjq0pVFujhJ2Z-1NAjy3bdK_kuU,5736
|
|
8
|
-
schenesort/tui/widgets/__init__.py,sha256=3cm7vfXG5-xC_UhIbgEtuMxq5I5tXg6okokJ4ecjGIE,202
|
|
9
|
-
schenesort/tui/widgets/image_preview.py,sha256=FhFwaIPzNkwcMlL9aqKV7yvuI3SVe6ATYWgY__9ySo4,2795
|
|
10
|
-
schenesort/tui/widgets/metadata_panel.py,sha256=8DnsvDdKf3jzqHOo7dDmxJxL0Mjx5mX7HNwwcMUcJn0,4860
|
|
11
|
-
schenesort-2.1.1.dist-info/METADATA,sha256=a2kMj55og_OmHo_zJ1_OOzwQr_5PecBY2oIqrOcf51I,9928
|
|
12
|
-
schenesort-2.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
-
schenesort-2.1.1.dist-info/entry_points.txt,sha256=J5lS-N6KgmzjutFi5bG1jv-4Wszbz3MfcOHcbznBVcw,50
|
|
14
|
-
schenesort-2.1.1.dist-info/licenses/LICENSE,sha256=sMw3SMb9ec9dbM2twEMVeunsGwuljza-9kEXg4kSJpg,1070
|
|
15
|
-
schenesort-2.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|