schenesort 2.2.0__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 CHANGED
@@ -1225,6 +1225,138 @@ def metadata_update_dimensions(
1225
1225
  typer.echo(f"\n{action} {updated_count} sidecar(s), skipped {skipped_count}.")
1226
1226
 
1227
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
+
1228
1360
  @metadata_app.command("embed")
1229
1361
  def metadata_embed(
1230
1362
  path: Annotated[Path, typer.Argument(help="Image file or directory")],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schenesort
3
- Version: 2.2.0
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,7 +12,7 @@ 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.2.0
15
+ # Schenesort v2.3.0
16
16
 
17
17
  A CLI tool for managing wallpaper collections with model generated metadata, terminal UI browsing, and SQLite-based
18
18
  querying.
@@ -26,7 +26,7 @@ Once you have a collection of wallpapers re-named with metadata sidecars, run th
26
26
  that can be queried to retrieve suggestions based on tags, colours, names and the like. Then use `get` to get a
27
27
  wallpaper path `feh $(schenesort get -1 -p)` or `hyprctl hyprpaper wallpaper "eDP-1,$(schenesort get -1 -p)"`
28
28
 
29
- schenesort also provides a bunch of utility commands to satisfy a gooner collection.
29
+ ![Browse example - Autumn](docs/browse-destruction.png)
30
30
 
31
31
  ## Installation
32
32
 
@@ -44,6 +44,9 @@ uv sync
44
44
 
45
45
  ## Quick Start
46
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
+
47
50
  ```bash
48
51
  # Generate metadata for images
49
52
  schenesort metadata generate ~/wallpapers -r
@@ -74,6 +77,7 @@ schenesort get -1 -p | xargs feh # random wallpaper
74
77
  | `info` | Show collection file statistics |
75
78
  | `describe` | AI-rename images based on content (Ollama) |
76
79
  | `models` | List available Ollama models |
80
+ | `collage` | Create a collage grid from matching wallpapers |
77
81
  | `metadata show` | Display XMP sidecar metadata |
78
82
  | `metadata set` | Manually set metadata fields |
79
83
  | `metadata generate` | Generate metadata with AI (Ollama) |
@@ -91,8 +95,6 @@ schenesort browse # uses paths.wallpaper from config
91
95
  schenesort get --mood peaceful -b # browse query results
92
96
  ```
93
97
 
94
- ![Browse example - Greek](docs/browse-greek.png)
95
-
96
98
  ![Browse example - Autumn](docs/browse-autumn.png)
97
99
 
98
100
  ![Browse example - Stallman](docs/browse-stallman.png)
@@ -147,6 +149,36 @@ schenesort stats
147
149
 
148
150
  The database is stored at `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`).
149
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
+ ![Collage example](docs/collage.png)
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
+
150
182
  ## Metadata Management
151
183
 
152
184
  Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying the original files.
@@ -168,7 +200,7 @@ Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying
168
200
  | `source` | Source URL or info |
169
201
  | `ai_model` | Model used for metadata generation |
170
202
 
171
- ### Generate Metadata with AI
203
+ ### Generate Metadata using ollama
172
204
 
173
205
  ```bash
174
206
  # Preview what would be generated
@@ -1,5 +1,5 @@
1
1
  schenesort/__init__.py,sha256=Z7bmXIWiFUCKj1j4JRlPZvmfzc4RC10Lh1WCKNsJn20,61
2
- schenesort/cli.py,sha256=lGbQ5nJ5TjusG7XlZfOVmdaCeef9eRO_a9JL1I-V_Fc,46291
2
+ schenesort/cli.py,sha256=LU_s4c7Eg_uozxP5I599TV9Y-WirQxx0h-8LvFNYlqM,51550
3
3
  schenesort/config.py,sha256=8EuYv2nma3QKnLPSAgAsaOHZlORzinHjy-Bj_LKPfJA,2739
4
4
  schenesort/db.py,sha256=RbfZN6d5O5MkTRPbu51fA7tQLOJf1huIjxFdoT4sESk,11398
5
5
  schenesort/xmp.py,sha256=1VS_I4akY8Dv_KLPOdzPgBCFy0280oSCsMmo-_A9cNE,9749
@@ -8,8 +8,8 @@ schenesort/tui/app.py,sha256=HGKBnszTJXtLu9Mo7eJY4wiO2qkB7dFftBoM4GC_js4,6202
8
8
  schenesort/tui/widgets/__init__.py,sha256=3cm7vfXG5-xC_UhIbgEtuMxq5I5tXg6okokJ4ecjGIE,202
9
9
  schenesort/tui/widgets/image_preview.py,sha256=j_KTQBJk0vbO18FzTo-GYkRlgerBe1K3VGQwMPbEuUw,2846
10
10
  schenesort/tui/widgets/metadata_panel.py,sha256=8DnsvDdKf3jzqHOo7dDmxJxL0Mjx5mX7HNwwcMUcJn0,4860
11
- schenesort-2.2.0.dist-info/METADATA,sha256=FOfxFWXroyg2RtZxMCrHL1Bf5qC5d8OPeMb_3cHmK7M,11251
12
- schenesort-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- schenesort-2.2.0.dist-info/entry_points.txt,sha256=J5lS-N6KgmzjutFi5bG1jv-4Wszbz3MfcOHcbznBVcw,50
14
- schenesort-2.2.0.dist-info/licenses/LICENSE,sha256=sMw3SMb9ec9dbM2twEMVeunsGwuljza-9kEXg4kSJpg,1070
15
- schenesort-2.2.0.dist-info/RECORD,,
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,,