From b1b9a4ad25636b7ff08c3c10a1ca75be84366e84 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 13 Sep 2026 15:15:08 -0400 Subject: [PATCH] oskiewar netplay: a sword nobody restored `saberPickups` was sim state that the rollback snapshot did not restore. It is mutated during the fight -- a blade goes inactive when it is taken, and a new one is pushed onto the map when an item arm comes off -- and it was missing from `netSimArrays`, so a rewind re-simulated frames in which a sword had already been picked up, or picked one up that the rewound timeline had not yet dropped. What made it so quiet is that `swordHeld` itself lives on the player and was always restored. The two seats agreed about every single number they hashed while disagreeing about whether somebody was holding a sword -- and a sword is a 1.5x reach multiplier in `meleeSpecFor`, so they went on agreeing for hundreds of frames until one seat's strike reached and the other's did not. The evidence was a `hitStunUntil` exactly 16,667us apart: one frame. So `swordHeld` is hashed now too. The fix is the snapshot line; the hash line is so that the next bug of this shape reports itself at the frame it happens instead of at the frame it finally changes an outcome. Found by keeping the JSON the hash was taken over -- behind `__oskiewarHashTrace`, off by default -- and diffing the two seats at the first frame they disagree. That is the tool this needed and did not have: a desync report names a frame and two numbers, and this turns those numbers back into fields. It is left in place. This is NOT a skatepark bug. It is on `main` at v114 and has been since the sabers landed; the park only made the fighters reach a blade during the netplay suite's choreography. The netplay suite is 18/0 here against 16/3 on main -- this also repairs the pre-existing "rounds roll over inside the fight" failure, which was the same fault wearing a different hat. Co-Authored-By: Claude Opus 5 (1M context) --- apple/PROGRESS.md | 53 +-- at/cli.mjs | 39 +- at/package-lock.json | 3 + easel/src/runtimes.mjs | 15 +- easel/src/tui.mjs | 3 +- easel/test/live.test.mjs | 9 +- marketing/podcast/SCORE.md | 5 + marketing/podcast/bin/produce.mjs | 35 +- pop/imab/README.md | 40 ++ pop/imab/bin/lyrictrack.mjs | 2 +- pop/imab/bin/syllawizard.mjs | 33 +- pop/imab/bin/vocalset.mjs | 226 ++++++++- pop/imab/lyric-overrides.json | 11 +- pop/imab/vocal-sets.json | 3 +- .../Sources/SlabMenubar/PromptPreview.swift | 266 +++++++---- .../SlabMenubar/PromptSigilOverlay.swift | 19 +- .../public/aesthetic.computer/disks/amail.mjs | 278 ++++++++--- .../public/aesthetic.computer/disks/chat.mjs | 87 ++-- .../aesthetic.computer/disks/laklok.mjs | 433 +++++------------- .../aesthetic.computer/disks/prompt.mjs | 8 +- system/public/prompt.ac/index.html | 14 +- toolchain/email/style-guide.md | 43 +- toolchain/laklok-sisters/parity.mjs | 8 +- xbox/live/oskiewar.js | 32 +- xbox/live/social/manifest.json | 2 +- xbox/live/social/oskiewar-title.jpg | Bin 57877 -> 60057 bytes xbox/live/social/oskiewar-title.mp4 | Bin 238013 -> 179993 bytes xbox/live/tests/oskiewar-netplay.test.mjs | 54 ++- 28 files changed, 1078 insertions(+), 643 deletions(-) diff --git a/apple/PROGRESS.md b/apple/PROGRESS.md index 2ce313c07e..56b0f5b5de 100644 --- a/apple/PROGRESS.md +++ b/apple/PROGRESS.md @@ -5,7 +5,7 @@ Append a new section on top for each App Store build. --- -## 1.1 (4) — 2026-09-11 (submitted) +## 1.1 (4) — 2026-09-11 (in progress) Builds 1.1 (2) and 1.1 (3) were uploaded in April and May but never attached to an App Store version, and TestFlight let them expire. This build carries @@ -37,55 +37,6 @@ lane instead of by hand. Xcode 15+ needs the platform even for device archives. Fix is `xcodebuild -downloadPlatform iOS` (~8 GB on disk); the build lane now preflights this. -- **Archive host = blueberry** (Xcode 26.6 + iOS 26.5 runtime already installed, - 39 GiB free) — neo would have to download the 8.5 GB platform first. Sync the - lane files with `rsync --relative apple/fastlane apple/PROGRESS.md - apple/aesthetic-computer-Info.plist apple/aesthetic.computer.xcodeproj/project.pbxproj - blueberry:~/aesthetic-computer/`, then `ssh blueberry bash -c "cd - ~/aesthetic-computer/apple && fastlane ios build"` (login shell is fish). First - run died at CodeSign with `errSecInternalComponent` — the ssh session cannot - reach the login keychain; fix is the blueberry recipe: `security - unlock-keychain` + `security set-key-partition-list -S apple-tool:,apple:,codesign: - -s` (interactive, @jeffrey's password), then rerun. - **Unlock state is per audit session**: an unlock in one `ssh -t` does nothing - for the next ssh connection, so unlock and archive in the SAME session: - `ssh -t blueberry 'security unlock-keychain ~/Library/Keychains/login.keychain-db; cd ~/aesthetic-computer/apple && fastlane ios build'`. -- **Export ran on neo, not blueberry.** Blueberry's archive succeeded but its export - produced nothing; the `.xcarchive` was tar-piped to `apple/build/` on neo and - exported with `xcodebuild -exportArchive … -exportOptionsPlist - ExportOptions-AppStore.plist -allowProvisioningUpdates` + the S4TQKG6U99 key. - Signing is a **Cloud Managed Apple Distribution** certificate (SHA1 D25B0254…, - expires 2027-04-23) — cloud-managed, so it never appears in - `security find-identity`, and `/v1/certificates` does not list it either. - Verified in the ipa: aps-environment=production, get-task-allow=false, - ITSAppUsesNonExemptEncryption=false, app + appex both build 4. -- **deliver gotcha:** it validates `./fastlane/metadata` and `./fastlane/screenshots` - on every call even with `skip_metadata`/`skip_screenshots`, and rejects the - `ios/` platform subfolder as an unknown locale — so `metadata_path` and - `screenshots_path` live in `deliver_defaults`, not per lane. -- **Uploaded 2026-09-11 13:04** via `fastlane ios upload` ("Successfully uploaded - package to App Store Connect"). Poll `asc.mjs status --app aestheticcomputer - --platform IOS` until build 4 (dated today) reads VALID, then `fastlane ios ship`. -- **Build 4 VALID on ASC 2026-09-11 10:05 PT** (id `09702f94-172a-46ac-b4bd-9c55cee7910d`, - usesNonExemptEncryption=false from the plist key, minOS 15.0). Waiting on - `fastlane ios ship`. -- **SUBMITTED 2026-09-11 16:00 ET via `fastlane ios ship`** — version 1.1 and - reviewSubmission `3645eb9f-9254-4d77-9f5c-db9210da77e2` both WAITING_FOR_REVIEW. - Three 409s on the way, all fixed in `slab/menuband/bin/asc.mjs`: - (1) `submit` picked the highest build NUMBER across every train — the 2023 - 1.0 train reached build 5, so it tried to attach an EXPIRED build; now filters - `preReleaseVersion.version` + `expired=false`, sorted by upload date. - (2) Apple's expanded age-rating questionnaire had ten unanswered fields - (advertising, ageAssurance, parentalControls, gunsOrOtherWeapons, - healthOrWellnessTopics, lootBox, messagingAndChat, userGeneratedContent, - socialMedia, socialMediaAgeRestricted). They live on the **editable appInfo** - (`556bc291…`, created with version 1.1); the live 1.0 appInfo's declaration is - locked. Declared honestly per the operator: chat, UGC, social = yes (not - age-restricted); ads, loot boxes, health, parental controls, age assurance = - no; guns = none. Rating stayed 4+. - (3) `submit` opened a fresh reviewSubmission on every retry; it now reuses an - open READY_FOR_REVIEW shell and skips an item already present. One empty - shell (`f60158d8…`) is orphaned — Apple refuses to cancel an empty one. - `CURRENT_PROJECT_VERSION` bumped 3 → 4 on both targets (3 was consumed by the expired upload). - Order: `fastlane ios meta` → `fastlane ios build` → `fastlane ios upload` @@ -96,7 +47,7 @@ lane instead of by hand. --- -## 1.1 (3) — 2026-04-30 (uploaded, never submitted; build expired) +## 1.1 (3) — 2026-04-30 (in progress) Reliability pass before this build ships. Field reports of the app sitting on the boot.mjs animation indefinitely (5,000s+) with no way to recover diff --git a/at/cli.mjs b/at/cli.mjs index 5551827c06..4152dbb216 100755 --- a/at/cli.mjs +++ b/at/cli.mjs @@ -9,8 +9,11 @@ import { config } from "dotenv"; config(); // Load .env from at/ directory const PDS_URL = process.env.PDS_URL || "https://at.aesthetic.computer"; -const BSKY_SERVICE = - process.env.BSKY_SERVICE || "https://public.api.bsky.app"; +// Two endpoints, not one. Reads go to the unauthenticated AppView; writes go +// to the PDS we hold an app password for. BSKY_SERVICE steers writes only — +// vault/at/.env points it at bsky.social, and pointing reads there too makes +// every getProfile 401. +const BSKY_PUBLIC = process.env.BSKY_PUBLIC || "https://public.api.bsky.app"; // --------------------------------------------------------------------------- // Argument parser (same style as memory/cli.mjs) @@ -119,7 +122,7 @@ async function commandResolve(args) { if (!input.startsWith("did:")) { console.log(`Resolving handle: ${input}`); const profile = await fetchJSON( - `${BSKY_SERVICE}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(input)}`, + `${BSKY_PUBLIC}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(input)}`, ); did = profile.did; console.log(` @${input} -> ${did}\n`); @@ -168,7 +171,7 @@ async function commandResolve(args) { async function commandProfile(args) { const actor = requireArg(args, 1, "handle-or-did"); const profile = await fetchJSON( - `${BSKY_SERVICE}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`, + `${BSKY_PUBLIC}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`, ); const p = profile; @@ -188,7 +191,7 @@ async function commandPosts(args) { const limit = parseInt(args.limit) || 10; const data = await fetchJSON( - `${BSKY_SERVICE}/xrpc/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(actor)}&limit=${limit}`, + `${BSKY_PUBLIC}/xrpc/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(actor)}&limit=${limit}`, ); if (!data.feed?.length) { @@ -246,9 +249,24 @@ async function commandPost(args) { if (args.image) { const { readFileSync } = await import("fs"); const imageData = readFileSync(args.image); - const { data } = await agent.uploadBlob(imageData, { - encoding: "image/png", - }); + // The blob's encoding has to match the bytes, or the post renders broken. + // Bluesky also caps a blob near a megabyte, and its error for going over + // is not obvious, so say so here instead. + const kinds = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", + gif: "image/gif", webp: "image/webp" }; + const encoding = kinds[args.image.split(".").pop().toLowerCase()]; + if (!encoding) { + console.error(`Unsupported image type: ${args.image}`); + process.exit(1); + } + if (imageData.length > 976_560) { + console.error( + `${args.image} is ${Math.round(imageData.length / 1024)}KB; Bluesky ` + + `accepts about 953KB. Downscale or re-encode it as JPEG first.`, + ); + process.exit(1); + } + const { data } = await agent.uploadBlob(imageData, { encoding }); postRecord.embed = { $type: "app.bsky.embed.images", images: [ @@ -388,7 +406,7 @@ async function commandAccountCheck(args) { let did = input; if (!input.startsWith("did:")) { const profile = await fetchJSON( - `${BSKY_SERVICE}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(input)}`, + `${BSKY_PUBLIC}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(input)}`, ); did = profile.did; } @@ -594,7 +612,8 @@ Environment: PDS_ADMIN_PASSWORD Admin password for PDS operations BSKY_IDENTIFIER Bluesky handle for posting BSKY_APP_PASSWORD Bluesky app password for posting - BSKY_SERVICE Bluesky API (default: https://public.api.bsky.app) + BSKY_SERVICE Bluesky PDS for posting (default: https://bsky.social) + BSKY_PUBLIC Bluesky AppView for reads (default: https://public.api.bsky.app) Examples: ac-at health diff --git a/at/package-lock.json b/at/package-lock.json index 9625c1a55f..90e4b25d24 100644 --- a/at/package-lock.json +++ b/at/package-lock.json @@ -19,6 +19,9 @@ }, "devDependencies": { "@types/node": "^22.10.2" + }, + "engines": { + "node": ">=24.18.1 <25" } }, "node_modules/@atproto/api": { diff --git a/easel/src/runtimes.mjs b/easel/src/runtimes.mjs index 76f28a175f..b0e50790a1 100644 --- a/easel/src/runtimes.mjs +++ b/easel/src/runtimes.mjs @@ -1,9 +1,11 @@ // runtimes.mjs — the piece languages Aesthetic Computer can run. // // A piece is JavaScript, KidLisp, or Lua, chosen by file extension. The blank -// templates are the starting point for a session's piece: each one paints -// something on its own so a phone pointed at the QR code has an image -// immediately, before the agent has written anything. +// templates are the starting point for a session's piece: each one paints a +// flat color on its own so a phone pointed at the QR code has an image +// immediately, before the agent has written anything. The blank does not write +// its own name — the system's corner label already shows it, and a name left +// in the source tends to survive the agent's first edit as clutter. export const RUNTIMES = { mjs: { @@ -16,9 +18,8 @@ export const RUNTIMES = { blank: (name) => `// ${name}, ${stamp()} // A blank Aesthetic Computer piece. -function paint({ wipe, ink, screen }) { +function paint({ wipe }) { wipe(70, 50, 100); - ink(255, 100, 255).write("${name}", { center: "xy" }); return false; // Painted once; return true to keep painting. } @@ -33,8 +34,6 @@ export { paint }; routable: true, blank: (name) => `; ${name}, ${stamp()} (wipe "purple") -(ink "pink") -(write "${name}" 6 6) `, }, lua: { @@ -62,8 +61,6 @@ end function draw() background(70, 50, 100) - fill(255, 100, 255) - text("${name}", 6, 16) end `, }, diff --git a/easel/src/tui.mjs b/easel/src/tui.mjs index 8cd9784182..e4593c8f79 100755 --- a/easel/src/tui.mjs +++ b/easel/src/tui.mjs @@ -207,7 +207,8 @@ function developerInstructions() { return [ "You are running inside Easel, a terminal interface for Aesthetic Computer (AC) work.", account, - `This session's piece is ${live.file} (${live.runtime.label}). It already exists as a blank piece. Edit that file unless the user asks for something else.`, + `This session's piece is ${live.file} (${live.runtime.label}). It already exists as a blank piece that paints a flat color and nothing else. Edit that file unless the user asks for something else.`, + "Do not write the piece's name onto the screen: the system already shows it in the corner label. If the file still carries a placeholder that writes its own name, remove it in your first edit.", ...dialect, ...styleInstructions(), "Every save of that file is pushed live to a phone that scanned the interface's QR code, so small frequent edits are better than one big rewrite.", diff --git a/easel/test/live.test.mjs b/easel/test/live.test.mjs index ebf17a7744..72be48d160 100644 --- a/easel/test/live.test.mjs +++ b/easel/test/live.test.mjs @@ -114,11 +114,16 @@ test("the rendered code matches the encoder module for module", async () => { assert.equal(isFinder(last, last), false, "no finder in the bottom-right"); }); -test("every runtime carries a blank that names itself", () => { +// The name lives in the blank's header comment only. The system's corner label +// already shows it on screen, so a blank that also wrote it drew it twice — and +// the agent's first edit tended to keep the centered placeholder around. +test("every runtime carries a blank that names itself in a comment, not on screen", () => { assert.deepEqual(runtimeIds(), ["mjs", "lisp", "lua"]); for (const id of runtimeIds()) { const runtime = runtimeFor(id); - assert.match(runtime.blank("movika"), /movika/); + const blank = runtime.blank("movika"); + assert.match(blank, /movika/); + assert.doesNotMatch(blank, /(?:write|text)\s*\(?\s*"movika"/, `${id} blank writes its name on screen`); assert.equal(runtimeForExtension(runtime.extension), runtime); } assert.equal(runtimeFor(".LISP").id, "lisp"); diff --git a/marketing/podcast/SCORE.md b/marketing/podcast/SCORE.md index 7264be8f6f..cc06fd1383 100644 --- a/marketing/podcast/SCORE.md +++ b/marketing/podcast/SCORE.md @@ -120,6 +120,11 @@ Flags: `--open` (slab-afplay the result), `--force` (bypass say cache), `--stability 0.55 --similarity 0.8 --speed 0.98` (voice tuning), `--base ` on `feed.mjs` (override the asset host). +For an addressed audio reply, use `--frame reply --nobed`. This retains the +voice, mastering, and speech QA, omits podcast framing and jingles, and marks +the audio metadata as a synthesized private reply. Keep its source in the +private paper directory and its slug outside the publish allowlist. + ## Reused from /pop - `/api/say` invocation pattern + content-hash caching — lifted from `pop/bin/say.mjs`. diff --git a/marketing/podcast/bin/produce.mjs b/marketing/podcast/bin/produce.mjs index be0b7dd8c3..53e5c66ee6 100644 --- a/marketing/podcast/bin/produce.mjs +++ b/marketing/podcast/bin/produce.mjs @@ -14,7 +14,7 @@ // node bin/produce.mjs ../../opinion/lotus-notes.md --open // flags: --open --force --stability 0.5 --similarity 0.8 --speed 1.0 // --bedstyle sosoft|lofi|club | --bedfile --bedbpm N [--bedbeats 3] -// --frame reading|daily|log +// --frame reading|daily|log|reply import { writeFileSync, readFileSync, mkdirSync, existsSync, rmSync } from "node:fs"; import { resolve, dirname } from "node:path"; @@ -261,6 +261,7 @@ console.log(`\n${units.length} utterances · body ${bodySec.toFixed(1)}s${BEAT_A // 3. Intro + outro voice-over, with the measured length. Two frames: // "reading" (the liturgical essay framing) and "daily" (the short-update show). const FRAME = flags.frame || "reading"; +const IS_REPLY = FRAME === "reply"; const introText = FRAME === "daily" ? `The daily, from Aesthetic Dot Computer. ${script.title}. Approximately ${lengthText}.` : FRAME === "log" @@ -271,12 +272,13 @@ const outroText = FRAME === "daily" : FRAME === "log" ? `That's the log so far. Letters to mail at aesthetic dot computer.` : `Here ends the reading. Questions and feedback are welcome at mail at aesthetic dot computer. Unless you ask us not to, your letter may be read or mentioned on a future episode.`; -console.log("Narrating frame…"); -const introVo = await say(introText, "intro"); -const outroVo = await say(outroText, "outro"); +if (!IS_REPLY) console.log("Narrating frame…"); +const introVo = IS_REPLY ? null : await say(introText, "intro"); +const outroVo = IS_REPLY ? null : await say(outroText, "outro"); // 4. Jingles. -const { intro: introJingle, outro: outroJingle } = renderJingles(resolve(ROOT, "assets")); +const { intro: introJingle, outro: outroJingle } = IS_REPLY + ? {} : renderJingles(resolve(ROOT, "assets")); // 5. Assemble. console.log("\nAssembling…"); @@ -298,9 +300,11 @@ const add = (src) => { }; const gap = (s) => { if (s <= 0) return; const d = resolve(build, `seg_${String(n++).padStart(3, "0")}.wav`); silence(d, s); seq.push(d); clock += s; }; -add(introJingle); -gap(0.35); -add(introVo); +if (!IS_REPLY) { + add(introJingle); + gap(0.35); + add(introVo); +} // Push the body to the next bar boundary so the first VOICED onset lands on // the downbeat (its lead consonant just ahead of it), and every later // sentence onset lands on a beat. @@ -315,10 +319,12 @@ for (let i = 0; i < units.length; i++) { add(units[i].wav); cues.push({ start: startSec, end: clock, text: units[i].text }); } -gap((BEAT_ALIGN ? snapUp(clock, BAR, 0.5) : clock + 0.8) - clock); -add(outroVo); -gap(0.3); -add(outroJingle); +if (!IS_REPLY) { + gap((BEAT_ALIGN ? snapUp(clock, BAR, 0.5) : clock + 0.8) - clock); + add(outroVo); + gap(0.3); + add(outroJingle); +} // Write an SRT of the reading (sentence-level, exact from the timeline). const srtTime = (s) => { @@ -471,10 +477,10 @@ execFileSync("ffmpeg", [ "-disposition:v", "attached_pic", "-metadata", `title=${script.title}`, "-metadata", `artist=${speaker}`, - "-metadata", `album=Aesthetic Computer — Readings`, + "-metadata", `album=${IS_REPLY ? "Aesthetic Computer — Private Replies" : "Aesthetic Computer — Readings"}`, "-metadata", `genre=Spoken Word`, "-metadata", `date=${script.date}`, - "-metadata", `comment=A reading of the essay "${script.title}". ${lengthText}.`, + "-metadata", `comment=${IS_REPLY ? `Private reply draft. Synthesized with ${VOICE.provider}/${VOICE.voice}.` : `A reading of the essay "${script.title}". ${lengthText}.`}`, outMp3, ], { stdio: "ignore" }); @@ -513,6 +519,7 @@ if (existsSync(sidecarPath)) { } writeFileSync(sidecarPath, JSON.stringify({ slug: script.slug, title: script.title, author: speaker, date: script.date, + frame: FRAME, voice: VOICE, status: hosted(script.slug) ? "publish-cleared" : "draft-unpublished", description: script.paragraphs[0], lengthText, durationSec: Math.round(total), bytes: readFileSync(outMp3).length, diff --git a/pop/imab/README.md b/pop/imab/README.md index 61d444cf65..6810a59724 100644 --- a/pop/imab/README.md +++ b/pop/imab/README.md @@ -54,8 +54,48 @@ pop/.venv/bin/python pop/imab/bin/lyricscroll.py # the scrolling review clip: alignment-video # chassis over the real floor mix, lyric # ribbon underneath (needs lyrictrack first) +pop/.venv/bin/python pop/imab/bin/melodyproof.py [take…] + # DOES HE SING WHAT WE WROTE? every syllable's + # sung pitch vs melody.json, per take, two + # stages: SET (vocalset's note-locked render — + # proves the render) and RAW (the demucs stem — + # the verdict). Register fitted per take in + # cents; boundaries from drawn / dtw / notealign. + # → out/melodyproof.html (?only=) + # also writes boundaries-dtw-.json (the + # lead's hand warped onto each take) for the wizard +node pop/imab/bin/syllawizard.mjs # draw syllable rectangles on THIS take's + # spectrogram (takes// in the cache; seeds + # from processed → drawn → dtw boundaries) +pop/.venv/bin/python pop/imab/bin/notealign.py + # forced alignment: the written melody DTW'd onto + # the f0 track → syllable spans (a proposal) +node pop/imab/bin/setalign.mjs # every set retimed onto the lead's syllable + # onsets → out/imab-set--aligned.wav + + # imab-sets-successive.mp3 (no layering) +open pop/imab/review.html # scroll-through of every demo, take and spine ``` +### boundary files — which timebase + +- `processed-boundaries-.json` — @jeffrey's hand on the **SET** (vocalset's + stretched, note-locked wav; the wizard's `takes//spec.png`). Feeds vocalset. +- `boundaries-drawn-.json` — @jeffrey's hand on the **RAW** stem. The lead's + (Sep 1) carries the a/but mislabel that `lyric-overrides.json` drawnFix corrects; + 7427's (Sep 9, drawn on neo) is clean. +- `boundaries-dtw-.json` — melodyproof's warp of the lead's SET hand onto + another take's SET, by MFCC+RMS DTW. A seed, not a truth. +- `melody.json` — the notepat ground truth, one note per syllable. Edit here, not + in the scripts. + +### where the work lives + +The raw takes (`toolchain/whistlegraph/downloads/whistlegraph-.wav`), +their demucs stems and the full `~/.cache/ac/imab` work dir live on **neo**; +blueberry holds copies of the six stems + wavs as of 2026-09-13. Uncommitted +lane work has stranded on both machines before — `git status pop/imab` on +each before assuming a fix "already happened". + - **out/imab-click-124** — 2-bar count-in, then 64 bars. Beat 1 accented, brighter door tick at every 8-bar phrase start. - **out/imab-guide-124** — the same click with the hook melody diff --git a/pop/imab/bin/lyrictrack.mjs b/pop/imab/bin/lyrictrack.mjs index 4de174fc56..1c738fa97d 100644 --- a/pop/imab/bin/lyrictrack.mjs +++ b/pop/imab/bin/lyrictrack.mjs @@ -194,7 +194,7 @@ writeFileSync(`${OUT}/imab-gt-targets.json`, JSON.stringify(sylls.map((s, i) => console.log("→ aesthetivox note-lock (targets from current boundaries)"); sh(PY, [`${REPO}/pop/bin/autotune.py`, `${OUT}/imab-sacredvox.wav`, AVOX, "--targets", `${OUT}/imab-gt-targets.json`, "--register-fit", - "--strength", "1.0", "--preserve", "0.25", "--glide-ms", "30"]); // flutish: dead-on, little ride + "--strength", "1.0", "--preserve", "0.0", "--glide-ms", "0"]); // EXTREME: stepped, no scoop, no ride let stemFile = "out/imab-sacredvox.wav"; const gridOvr = Object.entries(ovr.overrides ?? {}); if (gridOvr.length && existsSync(AVOX)) { diff --git a/pop/imab/bin/syllawizard.mjs b/pop/imab/bin/syllawizard.mjs index d27a69f581..8c3afeec61 100644 --- a/pop/imab/bin/syllawizard.mjs +++ b/pop/imab/bin/syllawizard.mjs @@ -23,22 +23,45 @@ const WORK = `${process.env.HOME}/.cache/ac/imab`; const TAKE = process.argv[2] || "7311159624588070175"; const PXS = 260; -const spec = readFileSync(`${WORK}/wizard-spec.png`).toString("base64"); -const audio = readFileSync(`${WORK}/wizard-audio.mp3`).toString("base64"); +// per-take inputs (takes//) — the shared wizard-* files are a fallback +// from before the wizard was parametric; reading them for every take put +// one take's audio under five other takes' rectangles. +const TAKE_DIR = `${WORK}/takes/${TAKE}`; +const specPath = existsSync(`${TAKE_DIR}/spec.png`) ? `${TAKE_DIR}/spec.png` : `${WORK}/wizard-spec.png`; +const audioPath = existsSync(`${TAKE_DIR}/audio.mp3`) ? `${TAKE_DIR}/audio.mp3` : `${WORK}/wizard-audio.mp3`; +if (!specPath.startsWith(TAKE_DIR)) console.warn(`! no takes/${TAKE}/ in the cache — falling back to the shared wizard-* files`); +const spec = readFileSync(specPath).toString("base64"); +const audio = readFileSync(audioPath).toString("base64"); const SYLS = [ ["i'm", 0], ["a", 1], ["but", 2], ["ter", 2], ["fly", 2], ["flap", 3], ["ping", 3], ["for", 4], ["you", 5], ["guys", 6], ["just", 7], ["a", 8], ["cos", 9], ["tume", 9], ["i", 10], ["put", 11], ["on", 12], ["in", 13], ["my", 14], ["room", 15]]; -let seed = []; +// seed rectangles: the best boundaries we already have for this take, +// so drawing starts from a proposal instead of a blank strip. +// processed-boundaries / boundaries-drawn = @jeffrey's hand (lane root) +// boundaries-dtw = melodyproof's warp of the lead's hand +// bounds-.json = the old cache seed +let seed = [], seedFrom = null; +for (const name of [`processed-boundaries-${TAKE}.json`, `boundaries-drawn-${TAKE}.json`, `boundaries-dtw-${TAKE}.json`]) { + const p = resolve(HERE, `../${name}`); + if (!existsSync(p)) continue; + const by = new Map(); + for (const s of JSON.parse(readFileSync(p, "utf8")).sylls) if (!by.has(`${s.label}/${s.wi}`)) by.set(`${s.label}/${s.wi}`, s); + seed = SYLS.map(([lab, wi]) => { const s = by.get(`${lab}/${wi}`); + return s ? { fromMs: s.fromMs, toMs: s.toMs, fLo: s.fLo ?? 0.15, fHi: s.fHi ?? 0.9 } : null; }); + seedFrom = name; break; +} const bp = `${WORK}/bounds-${TAKE}.json`; -if (existsSync(bp)) { +if (!seedFrom && existsSync(bp)) { const B = JSON.parse(readFileSync(bp, "utf8")); const flat = B.words.flatMap((w) => w.sylls || []); seed = SYLS.map(([lab], i) => { const s = flat[i]; return s ? { fromMs: s.fromMs, toMs: s.toMs, fLo: 0.15, fHi: 0.9 } : null; }); + seedFrom = `cache bounds-${TAKE}.json`; } +if (seedFrom) console.log(`→ seeded from ${seedFrom}`); const html = `syllawizard · ${TAKE}