Automator watches the folder. Something smarter does the clicking. Every number below was measured on Jacks-iMac, not assumed.
Every geometry number changed between 2026-09-05 and 2026-09-07, with no deliberate action, just normal browsing.
| Sep 5 | Sep 7 | |
|---|---|---|
| Transform origin | 215, 202 | 0, 130 |
| Chrome window | 1778 × 1075 | 2048 × 1031 |
| Vertical chrome | 177 | 9 |
| Side panel | closed | open, 386 pt |
| Button | Sep 5 | Sep 7 | Drift |
|---|---|---|---|
| Play New Track | 609, 276 | 384, 204 | -225, -72 |
| Studio New project | 863, 276 | 619, 204 | -244, -72 |
| Mastering New Track | 1117, 276 | 853, 204 | -264, -72 |
| Voice Studio New Track | 1370, 276 | 1088, 204 | -282, -72 |
Read the middle two rows together. The Sep 5 point for Studio New project was x 863. On Sep 7, Mastering New Track sits at x 853, ten points away on a button 234 points wide. A stored coordinate would have started a mastering job, called it a stem split, and reported success.
[2048,1152]|2.0|1|[1778,1075]|177|0|2, live [2048,1152]|2.0|1|[2048,1031]|9|386|2.| Property | Value | Why it matters |
|---|---|---|
| Panel | 5120 × 2880 | 27" iMac Retina 5K, Radeon Pro 570, internal, single display |
| Logical size (points) | 2048 × 1152 | The space pyautogui clicks in. This is the number that matters. |
| Screenshot size (pixels) | 4096 × 2304 | What screencapture and Pillow return |
| Backing scale | 2.0 | Screenshot coords are exactly double the click coords |
| Menu bar | 24 pt | Top inset |
| Usable height | y 25 to 1101 | screen.availHeight 1076, so the Dock takes 51 pt at the bottom |
| Displays attached | 1 | No second monitor, so no origin offset problem today |
The retina trap, confirmed. pyautogui.size() returns 2048 × 1152. A screenshot of that same screen comes back 4096 × 2304. Anything found by image matching reports a position in the second space and clicks in the first, landing at double the intended coordinate, which on a 2048 wide screen means clicking off the edge of the display. The old moises_batch_splitter.py dodged this by refusing to use image matching, which was the right instinct.
The scaling mode is not the default. A 5K iMac defaults to a 2560 × 1440 logical space. This machine is set to 2048 × 1152, a Larger Text mode. Every coordinate on this page is valid only in that mode. Changing the Displays setting invalidates all of them silently, with no error, and the clicks land on whatever moved into that spot.
| Property | Value | Note |
|---|---|---|
| Window bounds | 215, 25, 1993, 1100 | left, top, right, bottom |
| Window size | 1778 × 1075 | Not maximised. 215 pt of empty desktop on the left. |
| Viewport, Studio home | 1778 × 898 | No side panel |
| Viewport, player2 tab | 1392 × 890 | Side panel open, 386 pt narrower |
| Vertical chrome | 177 or 185 | Varies by page in the same window |
| Horizontal chrome | 0 or 386 | Side panel is per tab, not per window |
| devicePixelRatio | 2 | CSS pixels map 1:1 to points here |
They are the same product. The desktop app is a ToDesktop Electron shell whose bundle loads https://studio.moises.ai from the network. Choosing the browser costs no features at all.
What the browser adds: Chrome answers AppleScript, so a script can set the window to a known rectangle before every run and read the live DOM for button positions. The Electron shell answers neither.
com.todesktop.220410vabqjud82, version 2.0.3. Strings in app.asar resolve to studio.moises.ai, studio-stage.moises.ai, plugins.moises.ai, and zero local player code. NSAppleScriptEnabled is absent from its Info.plist. System Events reports 0 windows for the Moises process. Chrome answered execute javascript "1+1" with 2, and bounds of window 1 with 215, 25, 1993, 1100.
The account is already on the new stack. The navigation reads Home, Play, Studio (badged New), Mastering, Voice Studio, Lyric Writer, and the account shows Pro. The old app.moises.ai player is out of scope.
The split flow lives behind Studio New project on studio.moises.ai/home. The runner should assert it landed on that host and that the button exists, then stop if either check fails, rather than click into a redesigned page.
studio.moises.ai/player2/e6a75b25… titled "01 - Inca Roads | Moises Play". Account label 7onething1 Pro. Home page exposes Play New Track, Studio New project, Mastering New Track, Voice Studio New Track.
studio.moises.ai/upload/split/3 looks like a shortcut into the split flow. Loading it cold renders the sidebar and nothing else. Three dialog nodes exist in the DOM with no text and no file input, so the upload modal is state pushed by the app rather than a routable page.
Consequence: the run has to pass through a real click on Studio New project, which is why solving that one button's position correctly is the load bearing step.
fileInputs: 0, dialogs: 3, and a main region containing only sidebar text.
A Folder Action is genuinely good at one job: noticing a file landed and handing the path to a script. It is native, survives reboots, needs no daemon, and costs nothing. That is the job it gets.
Automator's Watch Me Do is rejected for the clicking. It records against the Accessibility tree, and a Chromium render surface exposes almost none of one, so replay degrades to raw screen coordinates. Those coordinates break on all four variables measured above, and Watch Me Do has no way to notice it missed. It clicks the wrong place and reports success.
UI elements enabled returns true, and window enumeration still returns 0 windows for Moises, Chrome, and Firefox from this context. There is nothing stable for a recorder to anchor to.
Ask the page where the button is, convert to screen points, click, discard. Nothing is stored between runs except a guard string.
screen_x = window.screenX + rect.centerX - scrollX
screen_y = window.screenY + (outerHeight - innerHeight) + rect.centerY - scrollY
screenshot_px = screen_pt × backing_scale # ×2 on this Mac
Worked from today's live probe of studio.moises.ai/home, with origin (215, 202):
| Button | CSS centre | Screen point | Screenshot pixel |
|---|---|---|---|
| Play New Track | 394, 74 | 609, 276 | 1218, 552 |
| Studio New project | 648, 74 | 863, 276 | 1726, 552 |
| Mastering New Track | 902, 74 | 1117, 276 | 2234, 552 |
| Voice Studio New Track | 1155, 74 | 1370, 276 | 2740, 552 |
~/Projects/moises_api.py is a working Music.AI REST client, debugged as recently as today, including the Cloudflare 403 that reads like an auth failure and is not one. It uploads, creates jobs, polls, and downloads, with no browser involved.
Two honest blockers. There is no key on this machine, neither MUSIC_AI_KEY nor ~/.music_ai_key. And Music.AI API credits bill separately from the Moises Pro subscription, so a Pro plan does not unlock it. The browser route uses the subscription you already pay for.
The plan below keeps the queue and the filing layer identical for both, so swapping the driver later is a one file change rather than a rewrite.
test -f ~/.music_ai_key returned not found, MUSIC_AI_KEY unset. Client targets https://api.music.ai/v1.
probe_geometry.py written and run. It reads display scaling, screenshot scale, Chrome bounds, viewport, side panel width, chrome height, and live button rects, then solves the transform.Installed, attached, and exercised. The split in blast radius is the design point: queue maintenance runs on a timer forever, the mouse-driving pass stays a command you start on purpose.
failed/.Play New Track to screen (384, 204).Everything after the click is unverified. The native file panel, the separation options, completion detection, and stem download have not been exercised, so the runner dispatches the upload and then stops rather than marking anything done.
Clicking Studio New project creates an empty multitrack project. It does not open an upload dialog. The split-a-song entry point is Play New Track, and the runner targets that. Mapping this created one stray project in your account named Iron Station at studio.moises.ai/studio/2ifceRwlu9Kro. It is empty. I left it alone rather than deleting anything in your account.
Moises Studio has a hidden Studio MCP panel. If that is a real MCP server it would replace this whole clicking layer with tool calls. It does not render until the panel is opened, so it needs a look before more effort goes into the browser path.
| Approach | Breaks when | Does it notice? | Verdict |
|---|---|---|---|
| Automator Watch Me Do | Window moves, side panel toggles, scaling changes, Moises ships a redesign | No. Clicks the wrong spot, reports success | Rejected |
| Stored coordinates, the May script | Same four variables, plus the stale ~/Desktop paths | Only if a screenshot check is added | Retire |
| Image matching on screen | Immediately, from the 2.0 backing scale | Returns a wrong match, or nothing | Rejected |
| DOM solved coordinates | Moises renames or removes the button | Yes. Button missing means stop | Build this |
| Music.AI API | Never, for geometry reasons | Yes, real job status | Endgame, needs a key |
| Path | State | What it is |
|---|---|---|
enqueue.sh | live | Body of the Folder Action. Appends a path and exits in milliseconds. |
queue.py | tested | Durable queue: flock, debounce, retries, filing, attempt return |
install_folder_action.py | installed | Builds the .workflow bundle and attaches it. Reproducible. |
chrome_ctl.py | tested | AppleScript to Chrome: session check, live geometry, DOM rects, clicks |
moises_runner.py | dry run ok | Claims one item, asserts everything, drives the upload |
drain.sh + com.drwu.moises-watch | loaded | 60 s maintenance pass. Safe by default, --drive to upload. |
probe_geometry.py | built, run | Standalone measurement and drift guard |
geometry_2026-09-05.json, _home.json, 2026-09-07.json | written | Three fingerprints. The set is what proved the drift and the guard. |
Projects/moises_automation/moises_batch_splitter.py | stale | May 2026 calibrated clicker. Desktop paths, dead since the June offload. |
Projects/moises_api.py | no key | Working Music.AI client, waiting on credentials |
cd ~/Projects/_outputs/moises-watch-folder
# drop songs into inbox/ from anywhere, then
python3 queue.py status # what is waiting
python3 moises_runner.py --dry-run # show every click it would make, touch nothing
python3 moises_runner.py --run # upload one song, hands off the keyboard
python3 moises_runner.py --map # when Moises changes its layout
Abort a real run by slamming the mouse into the top left corner. The pyautogui failsafe raises immediately. Ctrl+C also works.
python3 install_folder_action.py --detach
launchctl unload ~/Library/LaunchAgents/com.drwu.moises-watch.plist
The probe needs a Moises tab open in Chrome and Chrome's View → Developer → Allow JavaScript from Apple Events enabled. It is already on, verified by the probe returning live rects.