Actions
One decorated Python function per job. Its parameters are the form, and what it returns goes to the Library or the page.
An action
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, noyield, no threads, no server, and noif __name__ == "__main__". Mixel runs them. - One file can hold several actions.
app.json›entrynames the file.
Controls
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}."| Control | The person sees | Your code gets |
|---|---|---|
ox.Files("audio" | "midi" | "any", multiple=True) | A file picker that also takes drops and Library items | list[Path], or one Path with multiple=False |
ox.Folder() | A folder picker | Path |
ox.Choice("a", "b", multiple=False) | A segmented choice | str, or list[str] with multiple=True |
ox.Text(placeholder="") | A text field | str |
ox.Number(min, max, step) | A number field | int 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 value | the same as Number |
ox.Range(min, max, step) | One slider with a start and an end | a (low, high) tuple |
ox.Toggle() | A switch | bool |
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:
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 madeEverything 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
| Return | What happens |
|---|---|
ox.sound(path, title), ox.midi(...), ox.file(...), or a list of them | Added to the Library, and listed under the form |
a str | Shown to the person, as written |
a dict or list of plain values | Handed to your page |
None | Nothing 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.
| Need | Use |
|---|---|
| File info | ox.audio.probe(path) → .rate .channels .duration .codec |
| Another format | ox.audio.convert(path, "wav") → Path |
| Samples as numpy | ox.audio.read(path) and ox.audio.write(samples, rate, "wav") |
| A folder for new files | ox.temp() |
| Save next to the original | folder=ox.beside(path) (None for a Library file) |
| The app's own files | ox.data.read / write / list / remove |
More in Working with audio.
Dependencies
Python packages go in pyproject.toml › [project].dependencies. Then:
uv syncThat 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:
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()andox.data. - Run programs (
subprocess,os.system), open sockets, or send telemetry. - Edit
AGENTS.mdordist/by hand, or commit.venv/ornode_modules/.
mixel check flags these, and review refuses them.