Skip to Content
Docs

Actions

One decorated Python function per job. Its parameters are the form, and what it returns goes to the Library or the page.

View as Markdown

An action

app.py
import ox1audio as ox


@ox.action("Convert")
def convert(files: ox.Files("audio"), format: ox.Choice("wav", "flac", "mp3") = "wav"):
    made = []
    for i, path in enumerate(files):
        ox.progress(i / len(files), path.name)
        out = ox.audio.convert(path, format)
        made.append(ox.sound(out, title=path.stem))
    return made
  • @ox.action("Title") turns a plain function into an action. The title is the button people press; the function's name is how a page calls it.
  • The parameters are the controls, and Python defaults are their defaults. Mixel draws the form, checks the input before your code sees it, and runs the action in the background.
  • Write plain functions: no async, no yield, no threads, no server, and no if __name__ == "__main__". Mixel runs them.
  • One file can hold several actions. app.json › entry names the file.

Controls

app.py
import ox1audio as ox


@ox.action("Export stems")
def export(
    songs: ox.Files("audio"),
    destination: ox.Folder() | None = None,
    format: ox.Choice("wav", "flac", "mp3") = "wav",
    name: ox.Text(placeholder="Song name") = "",
    gain: ox.Slider(-12, 12, 0.5, unit="dB") = 0.0,
    takes: ox.Number(1, 8, 1) = 1,
    normalize: ox.Toggle(label="Normalize") = False,
):
    return f"Would export {len(songs)} songs as {format}."
ControlThe person seesYour code gets
ox.Files("audio" | "midi" | "any", multiple=True)A file picker that also takes drops and Library itemslist[Path], or one Path with multiple=False
ox.Folder()A folder pickerPath
ox.Choice("a", "b", multiple=False)A segmented choicestr, or list[str] with multiple=True
ox.Text(placeholder="")A text fieldstr
ox.Number(min, max, step)A number fieldint when min, max and step are whole, else float
ox.Slider(min, max, step)A slider. One that runs from below 0 to above fills from 0, and starts there; origin= sets another no-change valuethe same as Number
ox.Range(min, max, step)One slider with a start and an enda (low, high) tuple
ox.Toggle()A switchbool

Plain str, int, float and bool work too. Every control takes label=. Number, Slider and Range take unit=, shown after the value, like "dB", "Hz" or "%"; a Range in Hz across decades moves on a log scale. ox.Folder() | None = None makes a control optional.

Progress and Stop

Call ox.progress(fraction, label) often as the work moves: a fraction from 0 to 1, or None while you can't tell.

When the person presses Stop, the next ox.progress raises ox.Cancelled. To clean up, catch it and raise it again:

app.py
import ox1audio as ox


@ox.action("Make copies")
def copies(files: ox.Files("audio")):
    made = []
    try:
        for i, path in enumerate(files):
            ox.progress(i / len(files), path.name)
            made.append(ox.sound(ox.audio.convert(path, "flac"), title=path.stem))
    except ox.Cancelled:
        print(f"Stopped after {len(made)} files")
        raise
    return made

Everything already made with ox.sound, ox.midi or ox.file is kept and reaches the Library, so Stop never throws away finished work. ox.Cancelled is a BaseException, so except Exception doesn't swallow it.

An action stops after 30 minutes unless you give it longer: @ox.action("Separate", timeout=3600).

Results

ReturnWhat happens
ox.sound(path, title), ox.midi(...), ox.file(...), or a list of themAdded to the Library, and listed under the form
a strShown to the person, as written
a dict or list of plain valuesHanded to your page
NoneNothing to show

Metadata goes after the title: ox.sound(out, title="Loop", bpm=120, key="A minor"). More in The Library.

For something the person can fix, raise ox.Problem. They see your words, and nothing goes to the log:

raise ox.Problem("That file has no audio.", hint="Try a WAV or MP3.")

Any other exception shows "The app ran into a problem", and the details go to the log, which mixel dev prints.

Events

A page can get updates while an action runs. ox.emit(name, body) sends one; the body is plain JSON, at most 256 KiB:

ox.emit("partial", {"done": 3, "of": 10})

The page receives it in ox.run(…, { onEvent }). See A custom page.

Helpers

Use these before reaching for a package.

NeedUse
File infoox.audio.probe(path) → .rate .channels .duration .codec
Another formatox.audio.convert(path, "wav") → Path
Samples as numpyox.audio.read(path) and ox.audio.write(samples, rate, "wav")
A folder for new filesox.temp()
Save next to the originalfolder=ox.beside(path) (None for a Library file)
The app's own filesox.data.read / write / list / remove

More in Working with audio.

Dependencies

Python packages go in pyproject.toml › [project].dependencies. Then:

uv sync

That installs them into the app's .venv, which mixel check, test and run use, and writes uv.lock. Mixel installs exactly the versions the lock pins, so commit it: the registry refuses an app with packages and no lock. mixel check --fix adds a package the code imports but doesn't list, then runs uv sync for you. numpy, scipy and soundfile are fine. The SDK already ships ffmpeg, so don't add ffmpeg wrappers or pydub. Never commit models, songs, or files over 5 MB; a short test tone is fine.

Testing

ox.test runs an action the way Mixel would and records what it made:

tests/test_app.py
import unittest
from pathlib import Path

import ox1audio as ox

import app

TONE = Path(__file__).with_name("tone.wav")


class ConvertTest(unittest.TestCase):
    def test_converts_to_flac(self):
        run = ox.test(app.convert, files=[TONE], format="flac")
        self.assertEqual(run["items"][0]["title"], "tone")
        self.assertTrue(run["items"][0]["path"].endswith(".flac"))

    def test_stop_keeps_finished_files(self):
        with self.assertRaises(ox.Cancelled):
            ox.test(app.convert, files=[TONE, TONE], stop_at=0.5)

It returns {"result", "items", "progress", "events"}. stop_at=0.5 presses Stop halfway. ox.Problem is raised as it is, so you can assert on it. Run the tests with mixel test.

Never

  • Touch files other than the ones the person chose, files next to them, ox.temp() and ox.data.
  • Run programs (subprocess, os.system), open sockets, or send telemetry.
  • Edit AGENTS.md or dist/ by hand, or commit .venv/ or node_modules/.

mixel check flags these, and review refuses them.

On this page