diff --git a/.agents/skills/lineup-image-to-csv/SKILL.md b/.agents/skills/lineup-image-to-csv/SKILL.md new file mode 100644 index 00000000..c3c96ba3 --- /dev/null +++ b/.agents/skills/lineup-image-to-csv/SKILL.md @@ -0,0 +1,67 @@ +--- +name: lineup-image-to-csv +description: Convert a festival lineup poster/flyer image into a schedule-import CSV. Use when asked to turn a lineup poster image into a CSV, digitize a festival schedule graphic, or prepare an image for the schedule import wizard. +allowed-tools: Read, Write, Bash(uv run --with pillow python .agents/skills/lineup-image-to-csv/crop.py *) +--- + +# Lineup image to CSV + +Turn a poster into a CSV matching [`docs/schedule-import.md`](../../../docs/schedule-import.md)'s columns and semantics — read that doc for the column contract, matching rules, and type semantics; this skill only covers the image-reading process, not the CSV format itself. + +## 0. Identify the poster's format + +Posters are not one shape. Read the full image (or, for a lineup split across several images, one representative image) before picking a flow: + +- **Grid poster** — boxed rows against a shared time ruler, one or more stage columns. Go to _1. Set up cropping_. Example: a Boom-style stage/day timetable. +- **Text-list poster** — a day header followed by plain `time — title — artist` lines, no boxes or ruler. Often one image per stage. Skip cropping entirely: read times and titles directly off the full image (re-crop only if resolution is too low to read text). Go straight to _5. Compile the CSV_, applying the midnight rule (_4_) where a day's lines cross `00:00`. Example: `ownspirit/*.jpeg`, one image per stage. +- **Name-only poster** — an artist list with no times or stages (e.g. a phase-announcement flyer). There is no schedule to extract. Leave `Stage`, `Date`, `Start Time`, `End Time` blank; only `Artists`, `Set Name`, `Type`, and `Description` apply. Capture groupings the poster itself marks — joint billings (`X | Y`), "2 sets", "Live", "B2B" — the same way step 5's multi-artist rule does. Example: `desert-base/2026/lineup-phase1.jpg`. + +Steps 1-3 (cropping, grid mapping, border reads) apply only to grid posters. Step 4 (the midnight rule) and step 5 apply to every poster that has times. + +## 1. Set up cropping + +The Read tool shows the whole image but can't crop, and a full poster is too low-resolution to resolve individual grid lines. Crop with `.agents/skills/lineup-image-to-csv/crop.py` rather than ad hoc Pillow snippets, run from the repo root: + +``` +uv run --with pillow python .agents/skills/lineup-image-to-csv/crop.py --image --box L,T,R,B --out /crop.png --scale 6 +``` + +`uv run --with pillow` needs no venv, so nothing lands in the repo; write crop output to your scratchpad via `--out`. Its `--box`/`--scale`/`--out` args cover every crop this skill needs; see the script's docstring for usage. + +## 2. Map the grid once + +Read the full image first for an overview: stage names (poster columns), the days covered, and roughly where each act sits. Then find the exact x-ranges of the ruler column(s) and each stage column by cropping the header strip (where stage-name bands meet the ruler) and reading pixel offsets off the crop's reported scale factor. + +Composite a ruler + stage-column crop (`crop.py --box --box ` pastes them side by side) before reading times — reading a stage column in isolation gives you box edges with no times to anchor them to. + +## 3. Read times by border, not by label + +**A box's border is its boundary**: a set's top border sits exactly on the gridline of its start time; its bottom border sits exactly on the gridline of its end time. The time label printed next to a row is that row's _start_ — never eyeball which label a box's text looks centered on, always find the row line the border itself touches. + +Crop each stage (with its ruler) in overlapping chunks tall enough to show ~8-12 rows, with `--scale 5` to `--scale 8` — that's the zoom level where a border-to-label read is unambiguous. Move top to bottom through the full poster; re-crop tighter around any boundary that lands ambiguously between two labels in a first pass. + +Do not try to locate borders by scanning pixel colors along a fixed x/y line — text glyphs produce the same light pixels as border strokes and give false positives. Visual crop-and-read is slower but is the only reliable method. + +## 4. Apply the midnight rule + +The date rail (e.g. "24.9 THURSDAY") flips to the next calendar date exactly at the `00:00` gridline, regardless of how late the festival's evening "feels" like it's still going. A set starting at `23:00` and ending at `01:00` keeps the _start_ row's date — the import pipeline's own end-before-start rollover (see `docs/schedule-import.md`) advances the end date automatically, so don't do it yourself in the CSV. + +Confirm this against the poster itself (crop the date-rail text column near a day transition) rather than assuming — don't extrapolate from one confirmed transition to the rest of the poster without a spot-check, since posters are hand-laid and can be inconsistent. + +## 5. Compile the CSV + +One row per act (a grid box, a text-list line, or a name in a name-only lineup). Column contract is in `docs/schedule-import.md`; the image-reading-specific parts: + +- **Multi-artist billing** (`X ft. Y`, `X B2B Y`, `X & Y`, `X | Y`): split into pipe-separated `Artists`, and keep the poster's exact original billing text as `Set Name`. +- **Subtitles** under a title (Live, Downtempo, a remix note, Opening Ceremony) go in `Description`. +- **Stage** column is the poster's stage header text verbatim (don't abbreviate or normalize it — fuzzy stage matching happens at import time, not here). Blank on a name-only lineup. +- Save the CSV alongside the source image, same base name (`festivals/.jpg` → `festivals/.csv`). + +## Done when + +- **Grid poster**: every act on every stage and day has a start and end time each confirmed by a border read (not inferred from a neighboring act's spacing or pattern). +- **Text-list poster**: every line under every day header on every stage image has a row, in the order it appears. +- **Name-only poster**: every name on the poster has a row. +- Every multi-artist row has both a split `Artists` field and a `Set Name`. + +Before importing: dry-run the CSV through the schedule import wizard and check the diff review step — stage names fuzzy-match against existing DB stages, and a near-miss surfaces there, not here. diff --git a/.agents/skills/lineup-image-to-csv/crop.py b/.agents/skills/lineup-image-to-csv/crop.py new file mode 100644 index 00000000..a984d32e --- /dev/null +++ b/.agents/skills/lineup-image-to-csv/crop.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Crop region(s) out of a poster image, optionally compositing them +side by side, for close reading at zoom. + +Usage: + crop.py --image IMG --box L,T,R,B [--box L,T,R,B ...] --out OUT [--scale N] + +Multiple --box args are cropped independently, then pasted left to right +into one output image (e.g. pairing a ruler crop with a stage-column crop +so times and boxes read together). --scale resizes the result by an +integer factor with nearest-neighbor sampling, which keeps gridlines and +borders sharp instead of blurring them. +""" +import argparse + +from PIL import Image + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--image", required=True) + parser.add_argument( + "--box", action="append", required=True, help="left,top,right,bottom (pixels)" + ) + parser.add_argument("--out", required=True) + parser.add_argument("--scale", type=int, default=1) + args = parser.parse_args() + + src = Image.open(args.image) + crops = [src.crop(tuple(int(v) for v in box.split(","))) for box in args.box] + + width = sum(c.width for c in crops) + height = max(c.height for c in crops) + composite = Image.new(crops[0].mode, (width, height)) + x = 0 + for c in crops: + composite.paste(c, (x, 0)) + x += c.width + + if args.scale > 1: + composite = composite.resize( + (composite.width * args.scale, composite.height * args.scale), + Image.NEAREST, + ) + + composite.save(args.out) + print(f"{args.out}: {composite.width}x{composite.height}") + + +if __name__ == "__main__": + main() diff --git a/.candlekeep/.gitignore b/.candlekeep/.gitignore deleted file mode 100644 index fe381aef..00000000 --- a/.candlekeep/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!shelf.json diff --git a/.claude/skills/lineup-image-to-csv b/.claude/skills/lineup-image-to-csv new file mode 120000 index 00000000..82c2e929 --- /dev/null +++ b/.claude/skills/lineup-image-to-csv @@ -0,0 +1 @@ +../../.agents/skills/lineup-image-to-csv \ No newline at end of file diff --git a/desert-base/2026/desert-base-2026-lineup-phase1-import.csv b/desert-base/2026/desert-base-2026-lineup-import.csv similarity index 71% rename from desert-base/2026/desert-base-2026-lineup-phase1-import.csv rename to desert-base/2026/desert-base-2026-lineup-import.csv index 8332a58c..40e715b7 100644 --- a/desert-base/2026/desert-base-2026-lineup-phase1-import.csv +++ b/desert-base/2026/desert-base-2026-lineup-import.csv @@ -54,3 +54,23 @@ Ulien,,performance,,,,,Live Art Borix,,performance,,,,,Live Art Boaz Philosoph,,performance,,,,,Live Art Maya Sierra,,performance,,,,,Live Art +Grouch,,music,,,,, +Grouch in Dub,,music,,,,,Solo +Persona,,music,,,,, +Melachim Alef Umelachim Bet,מלכים א' ומלכים ב',music,,,,, +Master Minded,,music,,,,, +Kojo On Acid,,music,,,,, +Oran Ray,Oran Ray Live,music,,,,,Live +Adi-J,,music,,,,, +Obsidian,,music,,,,, +Ovesh,,music,,,,, +DDAARR,DDAARR Live,music,,,,,Live +Aurora,Aurora Live,music,,,,,Live +Ty De La Luna,,music,,,,, +Yair Freiman,Yair Freiman Live,music,,,,,Live +Sesame,,music,,,,, +Hadar,,music,,,,, +Jooraffe,,music,,,,, +Buca Nonsense,,music,,,,, +Saadis,,music,,,,, +Kōrd|A-La-Rass,Vinyl Radio by Kōrd & A-La-Rass,music,,,,, diff --git a/desert-base/2026/lineup-phase2.jpg b/desert-base/2026/lineup-phase2.jpg new file mode 100644 index 00000000..93f21af2 Binary files /dev/null and b/desert-base/2026/lineup-phase2.jpg differ diff --git a/festivals/infected.csv b/festivals/infected.csv new file mode 100644 index 00000000..e3281036 --- /dev/null +++ b/festivals/infected.csv @@ -0,0 +1,68 @@ +Artists,Set Name,Stage,Date,Start Time,End Time,Description,Type +Nadav Dagon | Amana Women,Nadav Dagon ft. Amana Women,Main Stage,2026-09-24,19:00,20:00,,music +Daskal | Assaf Amdurski,Daskal ft. Assaf Amdurski,Main Stage,2026-09-24,20:00,21:00,,music +Knesiyat Hasechel | Eitan Reiter,כנסיית השכל & איתן רייטר,Main Stage,2026-09-24,21:00,22:00,,music +Infected Mushroom,,Main Stage,2026-09-24,22:00,01:00,3 Hour Live Band Show,music +Amigdala,,Main Stage,2026-09-25,12:00,14:00,,music +Shtuby,,Main Stage,2026-09-25,14:00,16:00,,music +Dekel,,Main Stage,2026-09-25,16:00,18:00,,music +Eitan Reiter,,Main Stage,2026-09-25,18:00,20:00,Live,music +Freedom Fighters,,Main Stage,2026-09-25,20:00,21:30,,music +Pettra,,Main Stage,2026-09-25,21:30,23:00,,music +Blastoyz,,Main Stage,2026-09-25,23:00,00:30,,music +Bliss,,Main Stage,2026-09-26,00:30,02:00,,music +Infected Mushroom,,Main Stage,2026-09-26,05:00,08:00,3 Hours Live Retro Set,music +Oi,,Ursul Records Stage,2026-09-24,22:00,23:30,(Omer Inbar) Opening Ceremony,music +K.O,,Ursul Records Stage,2026-09-24,23:30,01:00,,music +Millero,,Ursul Records Stage,2026-09-25,01:00,02:30,,music +Rebrn,,Ursul Records Stage,2026-09-25,02:30,04:00,,music +Dodi,,Ursul Records Stage,2026-09-25,10:00,11:00,,music +Ben Lipshitz,,Ursul Records Stage,2026-09-25,11:00,12:00,,music +Maayan Rahamim | Miss Albi,Maayan Rahamim B2B Miss Albi,Ursul Records Stage,2026-09-25,12:00,13:30,,music +Itayga,,Ursul Records Stage,2026-09-25,13:30,15:00,,music +Noya,,Ursul Records Stage,2026-09-25,15:00,16:30,,music +Sharon Attar,,Ursul Records Stage,2026-09-25,16:30,18:00,,music +T-Puse,,Ursul Records Stage,2026-09-25,18:00,19:30,,music +Mishell,,Ursul Records Stage,2026-09-25,19:30,21:00,,music +Clüb De Combat,,Ursul Records Stage,2026-09-25,21:00,22:30,,music +Omer Keinan,,Ursul Records Stage,2026-09-25,22:30,00:30,,music +Yannik Müller,,Ursul Records Stage,2026-09-26,00:30,02:00,,music +T-Puse | Omer Keinan,T-Puse B2B Omer Keinan,Ursul Records Stage,2026-09-26,02:00,04:00,Ursul Records,music +Argaman,,Fusion Culture Stage,2026-09-24,17:00,19:00,,music +Cutterpiller,,Fusion Culture Stage,2026-09-25,01:00,03:00,Israeli Remixes,music +Mendi,,Fusion Culture Stage,2026-09-25,08:00,09:30,Live,music +Shell-E,,Fusion Culture Stage,2026-09-25,09:30,11:00,,music +Essence Project,,Fusion Culture Stage,2026-09-25,11:00,12:30,,music +Doobo | Sushi,Doobo B2B Sushi,Fusion Culture Stage,2026-09-25,12:30,14:00,,music +Tomik | Leto CC,Tomik B2B Leto CC,Fusion Culture Stage,2026-09-25,14:00,15:30,,music +Yūgen,,Fusion Culture Stage,2026-09-25,15:30,17:00,,music +Dosh,,Fusion Culture Stage,2026-09-25,17:00,18:30,Live,music +Shtuby,,Fusion Culture Stage,2026-09-25,18:30,20:00,Downtempo,music +Mantismash,,Fusion Culture Stage,2026-09-25,20:00,21:30,,music +Bayawaka,,Fusion Culture Stage,2026-09-25,21:30,23:00,,music +Nueq,,Fusion Culture Stage,2026-09-25,23:00,00:30,,music +Symbolico,,Fusion Culture Stage,2026-09-26,00:30,02:00,,music +Cutterpiller,,Fusion Culture Stage,2026-09-26,02:00,03:30,,music +Stulip | Katnoa,Stulip B2B Katnoa,Fusion Culture Stage,2026-09-26,03:30,05:00,,music +Sunset Riders,,Pulse Stage by Tuborg,2026-09-24,18:00,19:30,,music +Duskii,,Pulse Stage by Tuborg,2026-09-24,19:30,20:30,,music +Mementomor,,Pulse Stage by Tuborg,2026-09-25,00:00,01:00,,music +Broosh,,Pulse Stage by Tuborg,2026-09-25,01:00,02:00,,music +Libaax,,Pulse Stage by Tuborg,2026-09-25,02:00,03:00,,music +Gigi,,Pulse Stage by Tuborg,2026-09-25,03:00,04:00,,music +Ranyo,,Pulse Stage by Tuborg,2026-09-25,10:00,11:00,,music +Nevo,,Pulse Stage by Tuborg,2026-09-25,11:00,12:00,,music +Eukai,,Pulse Stage by Tuborg,2026-09-25,12:00,13:00,,music +Chuka,,Pulse Stage by Tuborg,2026-09-25,13:00,14:00,,music +Adiros,,Pulse Stage by Tuborg,2026-09-25,14:00,15:00,,music +Yoti,,Pulse Stage by Tuborg,2026-09-25,15:00,16:00,,music +Chemical Drive,,Pulse Stage by Tuborg,2026-09-25,16:00,17:00,,music +Artifex,,Pulse Stage by Tuborg,2026-09-25,17:00,18:00,,music +Moosh On,,Pulse Stage by Tuborg,2026-09-25,18:00,19:30,,music +Haymus,,Pulse Stage by Tuborg,2026-09-25,19:30,21:00,,music +Tetrakørd,,Pulse Stage by Tuborg,2026-09-25,21:00,23:00,,music +Indepth,,Pulse Stage by Tuborg,2026-09-25,23:00,01:00,,music +Fvmestic,,Pulse Stage by Tuborg,2026-09-26,01:00,02:00,,music +Detune,,Pulse Stage by Tuborg,2026-09-26,02:00,03:00,,music +Multiverse,,Pulse Stage by Tuborg,2026-09-26,03:00,04:00,,music +The Rabbit Hole,,Pulse Stage by Tuborg,2026-09-26,04:00,05:00,,music diff --git a/festivals/infected.jpg b/festivals/infected.jpg new file mode 100644 index 00000000..cd044866 Binary files /dev/null and b/festivals/infected.jpg differ