START saves every song every week (old one parked z/zz), logs SAVED/PARKED on the key; SONG KEYS no longer warns ONLY <key>; install.sh and the location kit ship ableton_start.py (NA had the 10/22 copy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 11:57:14 -04:00
co-authored by Claude Opus 5.5
parent caeee9179b
commit e9c2ef0b6c
7 changed files with 30 additions and 37 deletions
+5 -4
View File
@@ -153,7 +153,7 @@ everything on the page at once.
| FOH MIX | **NOT MIXED** (amber): every track of a song is still at 0 dB. **TO MASTER**: a stem goes to Master instead of out to the console | looks again | | FOH MIX | **NOT MIXED** (amber): every track of a song is still at 0 dB. **TO MASTER**: a stem goes to Master instead of out to the console | looks again |
| LIGHTS | the `LIGHTING CUES` track has a clip under every song | looks again | | LIGHTS | the `LIGHTING CUES` track has a clip under every song | looks again |
| LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens around it (less than half a 16th note either way) for the strongest hit. **3 CUES OFF** (amber): a strong hit more than 25 ms before or after the cue while the cue itself sits on a quiet spot; the details say `JEHOVAH bar 329.2.1: Blinder Flash 2 is 37 ms before the hit` (bars as Live shows them, 4/4). Listened to once per saved set. | looks again (move the note in Live by hand: Live's control script cannot edit notes in the arrangement) | | LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens around it (less than half a 16th note either way) for the strongest hit. **3 CUES OFF** (amber): a strong hit more than 25 ms before or after the cue while the cue itself sits on a quiet spot; the details say `JEHOVAH bar 329.2.1: Blinder Flash 2 is 37 ms before the hit` (bars as Live shows them, 4/4). Listened to once per saved set. | looks again (move the note in Live by hand: Live's control script cannot edit notes in the arrangement) |
| SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. **JEHOVAH ONLY Db** (amber): Finished Songs has the song, but not in this key (save the new version there). A song that is not in Finished Songs at all is only noted. | looks again | | SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. Finished Songs having the song only in another key is fine (START transposes it). A song that is not in Finished Songs at all is only noted. | looks again |
| W TUNE | the `WAVES TUNE` clip that starts each song (a clip from `Ableton/Waves Tune - midi Program Change`, sent to Waves Tune as a program change) is the song's PCO key; a minor song is fine on its relative major. **JEHOVAH TUNE Db** names the song; the details say which clip to put at which bar. **TUNE PGM**: a renamed clip that sends another key's program. | looks again (Live's control script cannot change a clip in the arrangement: swap the clip by hand) | | W TUNE | the `WAVES TUNE` clip that starts each song (a clip from `Ableton/Waves Tune - midi Program Change`, sent to Waves Tune as a program change) is the song's PCO key; a minor song is fine on its relative major. **JEHOVAH TUNE Db** names the song; the details say which clip to put at which bar. **TUNE PGM**: a renamed clip that sends another key's program. | looks again (Live's control script cannot change a clip in the arrangement: swap the clip by hand) |
| ROUTING | read **live** from Ableton (AbletonOSC, so tracks just dragged in count before the set is saved): every stem goes where `ready.routing` says (Piano, Keys, Organ → `Ext. Out 5/6 Keys`; AG, EG, Bass → `7/8 GTRS`; Drums, Perc, FX, Synth Bass → `9/10 PERC / FX`; BGVS, Choir, Vox FX → `11/12 VOX`). The table was taken from 261025 NA Project and is edited on the settings page; "EG 13" goes where every other EG goes. **PIANO TO MASTER** (red) names the stem; **NEW STEM BANJO** (amber): a stem name the table does not know. | puts every stem back where the table says (and IN EARS' sends to 0 dB). Runs by itself in the auto-fix hours. | | ROUTING | read **live** from Ableton (AbletonOSC, so tracks just dragged in count before the set is saved): every stem goes where `ready.routing` says (Piano, Keys, Organ → `Ext. Out 5/6 Keys`; AG, EG, Bass → `7/8 GTRS`; Drums, Perc, FX, Synth Bass → `9/10 PERC / FX`; BGVS, Choir, Vox FX → `11/12 VOX`). The table was taken from 261025 NA Project and is edited on the settings page; "EG 13" goes where every other EG goes. **PIANO TO MASTER** (red) names the stem; **NEW STEM BANJO** (amber): a stem name the table does not know. | puts every stem back where the table says (and IN EARS' sends to 0 dB). Runs by itself in the auto-fix hours. |
| IN EARS | read live: no stem in a song group has its in-ear send (send A, to `A-IN EAR MIX`) at -inf, as songs dragged in from another set do. Levels someone set are left alone; `RESI TRACKS` and `MIDI GROUP` (`ready.ears_skip_groups`) are skipped. | sets those sends to 0 dB. Runs by itself in the auto-fix hours. | | IN EARS | read live: no stem in a song group has its in-ear send (send A, to `A-IN EAR MIX`) at -inf, as songs dragged in from another set do. Levels someone set are left alone; `RESI TRACKS` and `MIDI GROUP` (`ready.ears_skip_groups`) are skipped. | sets those sends to 0 dB. Runs by itself in the auto-fix hours. |
@@ -194,9 +194,10 @@ other special service `Projects/NA/<date> NA <plan title> Project`.
`ableton_start.py` then runs on the Tracks Mac (detached, at low priority), doing the workflow PDF's steps: `ableton_start.py` then runs on the Tracks Mac (detached, at low priority), doing the workflow PDF's steps:
1. **Last weekend's songs to Finished Songs.** Each song group of the newest weekend set goes to 1. **Last weekend's songs to Finished Songs.** Each song group of the newest weekend set goes to
`Finished Songs/TITLE [KEY]` (key from its locator), with its arrangement markers, lighting cues and guide inside `Finished Songs/TITLE [KEY]` (key from its locator), with its arrangement markers, lighting cues and guide inside
the group and its audio copied in. A song that matches a saved version (any key, parked or not) is left alone. the group and its audio copied in. Every song is saved again every week, changed or not: it replaces the version
A changed one replaces the version it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called `CAN'T HELP BUT PRAISE`), which is parked
`CAN'T HELP BUT PRAISE`), which is parked first as `zCAN'T HELP BUT PRAISE LM [Eb]`. first as `zCAN'T HELP BUT PRAISE LM [Eb]` (`zz...` when that name is taken), so the old ones sort to the bottom.
START's details list each `SAVED` and `PARKED` folder.
2. **A new set from the newest weekend set:** its songs, pads, Waves Tune cues and marker, guide, loop-guide and 2. **A new set from the newest weekend set:** its songs, pads, Waves Tune cues and marker, guide, loop-guide and
lighting clips go; the groups, the numbered locators (and their computer keys 1-9), the loop locators and the loop lighting clips go; the groups, the numbered locators (and their computer keys 1-9), the loop locators and the loop
cues stay. Empty loop cues are dropped. cues stay. Empty loop cues are dropped.
+7 -8
View File
@@ -5,8 +5,8 @@ does them by hand on Mondays. readiness.py sends this file to the Tracks Mac and
every special service (First / Together Tuesday, Morning Worship, ...). every special service (First / Together Tuesday, Morning Worship, ...).
1 the previous weekend set's songs go to Finished Songs as "TITLE [KEY]": the song group with its 1 the previous weekend set's songs go to Finished Songs as "TITLE [KEY]": the song group with its
arrangement markers, lighting cues and guide inside it, the audio copied in. A song that changed parks arrangement markers, lighting cues and guide inside it, the audio copied in. Every song, every week:
the old folder as "zTITLE [KEY]" first; an unchanged one is left as it is. the old folder is parked as "zTITLE [KEY]" ("zzTITLE [KEY]" when that is taken) first.
2 each new set ("261101 NA Project", "261103 NA First Tuesday Project") starts from the newest weekend 2 each new set ("261101 NA Project", "261103 NA First Tuesday Project") starts from the newest weekend
set: its songs, pads, Waves Tune cues and marker, guide and lighting clips go; the dedicated groups, set: its songs, pads, Waves Tune cues and marker, guide and lighting clips go; the dedicated groups,
the numbered locators (with their computer-key mappings), the loop locators and the loop cues stay. the numbered locators (with their computer-key mappings), the loop locators and the loop cues stay.
@@ -604,7 +604,7 @@ def helper_copy(src, gid, new_id, keep_from, keep_to, name):
def export_songs(prev_path, cfg, status, log): def export_songs(prev_path, cfg, status, log):
"""Step 1: every song of the previous weekend set to Finished Songs (new or changed ones only).""" """Step 1: every song of the previous weekend set to Finished Songs, the old version parked."""
root = load(prev_path) root = load(prev_path)
ls = root.find("LiveSet") ls = root.find("LiveSet")
proj = os.path.dirname(prev_path) proj = os.path.dirname(prev_path)
@@ -623,11 +623,9 @@ def export_songs(prev_path, cfg, status, log):
continue continue
mine = stems_signature(_wrap(fam), proj) mine = stems_signature(_wrap(fam), proj)
versions = list(saved_versions(cfg, dest_dir, title)) versions = list(saved_versions(cfg, dest_dir, title))
# Saved already, under this name or a near one ("CAN'T HELP BUT PRAISE LM [Eb]" for a group called # Every song is saved again every week (user, 2026-10-27): it replaces the version it was made from (most
# "CAN'T HELP BUT PRAISE"), parked or not: then it came from there and is left as it is. # of the same audio, same key: "CAN'T HELP BUT PRAISE LM [Eb]" for a group called "CAN'T HELP BUT PRAISE"),
if any(sig == mine for _, sig in versions): # else "TITLE [KEY]"; the old folder is parked as "zTITLE [KEY]" ("zz..." when that is taken).
continue
# Changed: it replaces the version it was made from (most of the same audio, same key), else a new name.
def overlap(sig): def overlap(sig):
a, b = set(sig), set(mine) a, b = set(sig), set(mine)
return len(a & b) / float(len(a | b) or 1) return len(a & b) / float(len(a | b) or 1)
@@ -684,6 +682,7 @@ def export_songs(prev_path, cfg, status, log):
os.rename(building, dest) os.rename(building, dest)
index[folder.upper()] = folder index[folder.upper()] = folder
done.append(folder) done.append(folder)
log.append("SAVED %s" % folder)
if files.missing: if files.missing:
log.append("%s: %d FILES MISSING" % (folder, len(files.missing))) log.append("%s: %d FILES MISSING" % (folder, len(files.missing)))
return done return done
+1 -1
View File
@@ -19,7 +19,7 @@ SETTINGS_LABEL=org.waterschurch.status-settings
DEST="$HOME/Library/Application Support/status-panel" DEST="$HOME/Library/Application Support/status-panel"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
DOMAIN="gui/$(id -u)" DOMAIN="gui/$(id -u)"
CODE=(status_panel.py readiness.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py) CODE=(status_panel.py readiness.py ableton_start.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py)
KEEP_CONFIG=0 KEEP_CONFIG=0
SITE="" SITE=""
SITE_NEXT=0 SITE_NEXT=0
+4 -4
View File
@@ -1182,9 +1182,8 @@ def eval_keys(rc, plan, look):
elif any(same_key(k, want) for _, k in versions): elif any(same_key(k, want) for _, k in versions):
checked += 1 checked += 1
else: else:
line.fault(WARN, "%s ONLY %s" % (name, "/".join(sorted({k for _, k in versions}))), # No warning (user, 2026-10-27): START transposes a song saved in another key.
"%s has %s, none in %s" % (" / ".join(folders), ", ".join(n for n, _ in versions), want)) checked += 1
bad = True
if not checked and not bad: if not checked and not bad:
line.fault(WARN, "KEY UNCHECKED " + short(title), "no locator with a key, no pad under it and nothing in Finished Songs " line.fault(WARN, "KEY UNCHECKED " + short(title), "no locator with a key, no pad under it and nothing in Finished Songs "
"for %s (%s in PCO)" % (title, want)) "for %s (%s in PCO)" % (title, want))
@@ -2202,7 +2201,8 @@ def eval_start(rc, services, look, now):
% (svc["name"], song, n, "" if n == 1 else "s")) % (svc["name"], song, n, "" if n == 1 else "s"))
if st.get("state") == "done" and now - (st.get("time") or 0) < 86400: if st.get("state") == "done" and now - (st.get("time") or 0) < 86400:
for note in st.get("log") or []: for note in st.get("log") or []:
if "CONSOLIDATE IN LIVE:" in note or note.startswith("OPEN ") or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note: if "CONSOLIDATE IN LIVE:" in note or note.startswith(("OPEN ", "SAVED ", "ALREADY SAVED ", "PARKED ", "NOT SAVED ", "RENAMED ")) \
or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note:
line.notes.append(note) line.notes.append(note)
line.info = " ".join(x["label"] for x in services) or "NO SERVICES" line.info = " ".join(x["label"] for x in services) or "NO SERVICES"
return line return line
+1 -1
View File
@@ -36,7 +36,7 @@ TRIGGER_NAME = "flash_tick_500"
LOOK_EVERY = 600 LOOK_EVERY = 600
SEEN, LAST = "discover-seen.json", "discover-last.json" SEEN, LAST = "discover-seen.json", "discover-last.json"
# What the program kit carries (no settings, keys or history of the location that serves it). # What the program kit carries (no settings, keys or history of the location that serves it).
KIT_FILES = ("status_panel.py", "readiness.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py", KIT_FILES = ("status_panel.py", "readiness.py", "ableton_start.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py",
"nav_keys.py", "atem_watch.js", "atem_state.js", "nav_templates.json", "prop_guard.py") "nav_keys.py", "atem_watch.js", "atem_state.js", "nav_templates.json", "prop_guard.py")
KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup") KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup")
+3 -4
View File
@@ -302,10 +302,9 @@ class TestKeys(unittest.TestCase):
def test_the_set_in_another_key(self): def test_the_set_in_another_key(self):
keys = dict(GOOD, **{"Holy Forever": "C"}) keys = dict(GOOD, **{"Holy Forever": "C"})
line = rd.eval_keys(RC, keyed(keys), with_pads_and_finished()) line = rd.eval_keys(RC, keyed(keys), with_pads_and_finished())
# Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb) # Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb): no warning, START transposes it
self.assertEqual(msgs(line), ["REJOICE AGAIN ONLY B", "HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C", self.assertEqual(msgs(line), ["HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C"])
"HOLY FOREVER ONLY B"]) self.assertEqual([lv for lv, _, _ in line.faults], [rd.ERR, rd.ERR])
self.assertEqual([lv for lv, _, _ in line.faults], [rd.WARN, rd.ERR, rd.ERR, rd.WARN])
def test_a_song_pco_has_no_key_for(self): def test_a_song_pco_has_no_key_for(self):
line = rd.eval_keys(RC, keyed(dict(GOOD, **{"Holy Forever": ""})), with_pads_and_finished()) line = rd.eval_keys(RC, keyed(dict(GOOD, **{"Holy Forever": ""})), with_pads_and_finished())
+7 -13
View File
@@ -349,22 +349,16 @@ class TestBuild(unittest.TestCase):
self.assertEqual(st["step"], "NOTHING TO DO") self.assertEqual(st["step"], "NOTHING TO DO")
self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), []) self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), [])
def test_an_unchanged_song_is_not_saved_again_and_a_changed_one_parks_the_old(self): def test_every_song_is_saved_every_week_and_the_old_ones_parked(self):
"""User, 2026-10-27: every song saved new every week; the old one goes to z, zz, ... (the bottom of the folder)."""
self.run_job() self.run_job()
for parked in ("zOLD SONG [Eb]", "zzOLD SONG [Eb]"):
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
st = self.run_job() st = self.run_job() # unchanged, saved again all the same
self.assertEqual(st["saved"], [])
self.assertFalse(os.path.exists(os.path.join(self.fin, "zOLD SONG [Eb]")))
# A re-consolidated stem (new file) is a new version: the old folder is parked as z.
base = os.path.join(self.proj, "NA", "261025 NA Project", "261025 NA Project.als")
xml = gzip.open(base).read().decode()
i = xml.index("21-Piano")
xml = xml[:i] + xml[i:].replace('Value="1000"', 'Value="2000"', 1)
write_als(base, xml)
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
st = self.run_job()
self.assertEqual(st["saved"], ["OLD SONG [Eb]"]) self.assertEqual(st["saved"], ["OLD SONG [Eb]"])
self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG [Eb]"))) self.assertIn("SAVED OLD SONG [Eb]", st["log"])
self.assertTrue(os.path.isdir(os.path.join(self.fin, parked)))
self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]", "OLD SONG.als")))
def test_a_changed_song_replaces_the_version_it_was_made_from(self): def test_a_changed_song_replaces_the_version_it_was_made_from(self):
# Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped). # Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped).