Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/tflite-cross-platform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: TFLite cross-platform

# The optimized/tflite release must run identically on macOS, Linux, and
# Windows. This checks, on every OS ai-edge-litert ships wheels for:
# byte-compilation of the scripts, LiteRT importability, the weights
# link-or-copy fallback (no symlink privilege needed on Windows), console
# encoding under the runner's native code page, and --play dispatch.
# NO model downloads happen in CI.

on:
push:
paths:
- "optimized/tflite/**"
- ".github/workflows/tflite-cross-platform.yml"
pull_request:
paths:
- "optimized/tflite/**"
- ".github/workflows/tflite-cross-platform.yml"

jobs:
test:
strategy:
fail-fast: false
# Exactly the set ai-edge-litert has wheels for: manylinux x86_64 +
# aarch64, macosx_12_0_arm64, win_amd64. No macOS-Intel (macos-13) and
# no Windows-ARM runners — those combos have no wheel.
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install runtime requirements
run: python -m pip install -r optimized/tflite/requirements.txt

- name: Byte-compile all scripts
run: |
python -m py_compile \
optimized/tflite/scripts/sa3_tflite.py \
optimized/tflite/scripts/weights.py \
optimized/tflite/scripts/install.py \
optimized/tflite/scripts/examples.py \
optimized/tflite/scripts/test_windows_compat.py \
optimized/tflite/models/defs/tflite_pipeline.py

- name: LiteRT interpreter imports
run: python -c "from ai_edge_litert.interpreter import Interpreter"

- name: Windows-compat test suite
run: python optimized/tflite/scripts/test_windows_compat.py
2 changes: 2 additions & 0 deletions optimized/tflite/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Batch files must keep CRLF line endings on every checkout (cmd.exe quirk-proofing).
*.bat text eol=crlf
36 changes: 33 additions & 3 deletions optimized/tflite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Portable CPU inference for **Stable Audio 3** — the LiteRT/TFLite sibling of t
[MLX](../mlx) (Apple Silicon) and [TensorRT](../tensorRT) (NVIDIA GPU) releases.
No PyTorch, transformers, or stable-audio-tools at runtime — just `ai_edge_litert`
(LiteRT) driving fully self-contained `.tflite` graphs through the XNNPACK CPU
delegate. Runs anywhere LiteRT runs: **macOS / Linux, x86 / ARM**.
delegate. Runs anywhere LiteRT runs: **macOS / Linux / Windows, x86 / ARM**
(Windows is x64-only — see [Windows](#windows)).

## Quick Install

Expand Down Expand Up @@ -133,6 +134,34 @@ End-to-end on a fresh machine: **~10 seconds** + weight downloads.
Portable CPU (no GPU required). Python 3.9+. `./install.sh --python 3.12` to
pin a different Python.

### Windows

The stack runs natively on Windows x64 — no WSL needed. `ai-edge-litert`
ships `win_amd64` wheels for Python **3.10–3.13** (x64 only; no Windows-ARM
wheels), so install a Python in that range from
[python.org](https://www.python.org/downloads/) (check *"Add python.exe to
PATH"*), then from `optimized\tflite\`:

```bat
install.bat :: one-time setup (python -m venv + pip)
sa3.bat --prompt "lofi house loop" --dit sm-music --decoder same-s
```

`install.bat` / `sa3.bat` are the Windows twins of `./install.sh` / `./sa3`
(plain venv + pip; uv not required). Notes:

- **ffmpeg is optional** — only needed for mp3 / 24-bit / non-44.1 kHz
`--init-audio` inputs: `winget install ffmpeg` (or `choco install ffmpeg`).
- **Symlinks**: downloaded weights are exposed via symlinks where possible;
without Developer Mode, Windows disallows creating them, so the downloader
automatically falls back to a hardlink (zero-copy) or a plain copy. No
action needed either way.
- **Long paths**: the HuggingFace cache nests deeply — if downloads fail with
path-length errors, enable Windows long paths once (admin PowerShell):
`Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 1`
(the `LongPathsEnabled` registry switch), then restart the terminal.
- `--play` uses the stdlib `winsound` player (macOS uses `afplay`).

## Run

`./sa3` is a thin shell wrapper around `.venv/bin/python scripts/sa3_tflite.py
Expand Down Expand Up @@ -161,7 +190,7 @@ pin a different Python.
./sa3 --prompt "ambient drone" --cfg 3.0 --negative-prompt "drums, vocals" \
--dit sm-music --decoder same-s --out drone.wav

# Generate + play immediately (afplay; Ctrl-C stops both)
# Generate + play immediately (afplay/winsound/aplay; Ctrl-C stops both)
./sa3 --prompt "rainforest" --dit sm-sfx --decoder same-s --play

# All options + categorised examples
Expand Down Expand Up @@ -215,7 +244,7 @@ For sub-realtime latency on a supported device, prefer the GPU siblings:
| `--threads` | 8 | XNNPACK CPU threads (all TFLite models run on CPU) |
| `--free-models` | on | Free each model after its last use; `--no-free-models` keeps them resident |
| `--out` / `-o` | (auto) | Relative → `output/<file>`; absolute → as-is. 16-bit PCM stereo @ 44.1 kHz, trimmed to exactly `--seconds` |
| `--play` | off | After writing, play via `afplay` (macOS); Ctrl-C stops both |
| `--play` | off | After writing, play the WAV: `afplay` (macOS) / `winsound` (Windows) / `aplay` (Linux); Ctrl-C stops both |

All `.tflite` models are **fp32** except T5Gemma, which is **fp16** (numerically
lossless there). There is no dtype knob: on CPU, int8/fp16 weights buy size, not
Expand All @@ -228,6 +257,7 @@ speed (XNNPACK dequantizes to fp32 to matmul), and int8 costs quality on the DiT
sa3_tflite/
├── sa3 ← shell wrapper (use this)
├── install.sh ← uv bootstrap (run once)
├── sa3.bat / install.bat ← Windows twins of sa3 / install.sh
├── bootstrap.sh ← one-line curl installer
├── README.md
├── requirements.txt ← ai_edge_litert, numpy, sentencepiece, soundfile, huggingface_hub
Expand Down
43 changes: 43 additions & 0 deletions optimized/tflite/install.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@echo off
rem SA3 TFLite installer for Windows — the .bat twin of install.sh.
rem
rem Mirrors install.sh's steps with plain stdlib tooling (no uv required):
rem 1. create a project-local .venv\ (python -m venv)
rem 2. pip install -r requirements.txt into it
rem 3. hand off to scripts\install.py for the weight-download prompt
rem
rem Extra args are forwarded to install.py, e.g.:
rem install.bat --download sm-music
rem
rem Note: ai-edge-litert ships win_amd64 wheels for Python 3.10-3.13 —
rem use a Python in that range (3.11 recommended).
setlocal
set "SCRIPT_DIR=%~dp0"

where python >nul 2>nul
if errorlevel 1 (
echo error: python not found on PATH. 1>&2
echo Install Python 3.10-3.13 from https://www.python.org/downloads/ 1>&2
echo ^(check "Add python.exe to PATH" in the installer^) and re-run install.bat. 1>&2
exit /b 1
)

if exist "%SCRIPT_DIR%.venv\Scripts\python.exe" (
echo Reusing existing .venv\
) else (
echo Creating virtual environment at .venv\ ...
python -m venv "%SCRIPT_DIR%.venv"
if errorlevel 1 exit /b 1
)

echo Installing dependencies ^(pip install -r requirements.txt^) ...
"%SCRIPT_DIR%.venv\Scripts\python.exe" -m pip install --upgrade pip
if errorlevel 1 exit /b 1
"%SCRIPT_DIR%.venv\Scripts\python.exe" -m pip install -r "%SCRIPT_DIR%requirements.txt"
if errorlevel 1 exit /b 1

rem install.py's pip step is skipped — deps were just installed above
rem (same contract install.sh uses).
set "INSTALL_SKIP_PIP=1"
"%SCRIPT_DIR%.venv\Scripts\python.exe" "%SCRIPT_DIR%scripts\install.py" %*
exit /b %ERRORLEVEL%
22 changes: 22 additions & 0 deletions optimized/tflite/sa3.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@echo off
rem Windows twin of the `sa3` bash wrapper — runs scripts\sa3_tflite.py via the
rem project-local .venv (created by install.bat) so no activation is needed.
rem Falls back to whatever `python` is on PATH if .venv\ doesn't exist yet.
rem
rem Usage: sa3.bat --prompt "lofi house" --dit sm-music --decoder same-s --out a.wav
setlocal
set "SCRIPT_DIR=%~dp0"

if not exist "%SCRIPT_DIR%scripts\sa3_tflite.py" (
echo error: scripts\sa3_tflite.py not found - repo files are missing or moved. 1>&2
exit /b 1
)

set "PY=%SCRIPT_DIR%.venv\Scripts\python.exe"
if not exist "%PY%" (
echo warning: .venv\ not found - run install.bat for one-time setup. Trying `python` from PATH. 1>&2
set "PY=python"
)

"%PY%" "%SCRIPT_DIR%scripts\sa3_tflite.py" %*
exit /b %ERRORLEVEL%
27 changes: 24 additions & 3 deletions optimized/tflite/scripts/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
import sys
from pathlib import Path

# Windows consoles default to a legacy code page (cp1252/cp437) that can't encode
# the emoji/box-drawing characters below. Force UTF-8 with replacement; no-op on
# macOS/Linux. (sa3_tflite.py/install.py do this too — reconfigure is idempotent.)
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except AttributeError:
pass # exotic stream without reconfigure() — leave as-is

# Enable ANSI/VT escape processing in legacy Windows consoles (cmd.exe);
# harmless elsewhere and on modern Windows Terminal.
if os.name == "nt":
os.system("")

# This file lives in <project>/scripts/. SCRIPT_DIR points at the project
# root (where ./sa3, ./install.sh, models/, .venv/ live).
SCRIPT_DIR = Path(__file__).resolve().parent.parent
Expand All @@ -41,9 +55,16 @@ def _have(binary: str) -> bool:
def _py_invocation() -> tuple[str, str, bool]:
"""Return (command-to-print, tip-or-empty, is_wrapper).

Prefer the ./sa3 wrapper if present, else `.venv/bin/python scripts/sa3_tflite.py`,
else fall back to a bare `python scripts/sa3_tflite.py`.
Prefer the sa3 wrapper if present (./sa3 on POSIX, sa3.bat on Windows), else
the .venv python + scripts/sa3_tflite.py, else a bare `python scripts/sa3_tflite.py`.
"""
if os.name == "nt":
if (SCRIPT_DIR / "sa3.bat").exists():
return "sa3.bat", "the sa3.bat wrapper uses .venv automatically", True
venv_py = SCRIPT_DIR / ".venv" / "Scripts" / "python.exe"
if venv_py.exists():
return str(venv_py.relative_to(SCRIPT_DIR)), "", False
return "python", "", False
wrapper = SCRIPT_DIR / "sa3"
if wrapper.exists() and os.access(wrapper, os.X_OK):
return "./sa3", "the ./sa3 wrapper uses .venv automatically", True
Expand Down Expand Up @@ -108,7 +129,7 @@ def cmd(args: str, comment: str = "") -> None:
one_dec = "same-l" if one_dit == "medium" else "same-s"
cmd(f'--prompt "ambient drone" --dit {one_dit} --decoder {one_dec} \\\n'
f' --seconds 20 --out drone.wav --play',
"writes WAV + plays via afplay (Ctrl-C stops both)")
"writes WAV + plays it (afplay/winsound/aplay; Ctrl-C stops both)")

# ── Audio-to-audio + inpaint ─────────────────────────────────────
hdr("🎚️ Audio-to-audio & inpainting (requires an input WAV)")
Expand Down
19 changes: 17 additions & 2 deletions optimized/tflite/scripts/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@
import sys
from pathlib import Path

# Windows consoles default to a legacy code page (cp1252/cp437) that can't encode
# the ✓/→ status glyphs. Force UTF-8 with replacement; no-op on macOS/Linux.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except AttributeError:
pass # exotic stream without reconfigure() — leave as-is

# Enable ANSI/VT escape processing in legacy Windows consoles (cmd.exe);
# harmless elsewhere and on modern Windows Terminal.
if os.name == "nt":
os.system("")

# This file lives in <project>/scripts/. SCRIPT_DIR points at the project
# root (where models/, requirements.txt, .venv/, sa3 wrapper live).
SCRIPT_DIR = Path(__file__).resolve().parent.parent
Expand All @@ -42,7 +55,9 @@ def check_environment() -> None:
f"Re-run install.py with a Python {MIN_PY[0]}.{MIN_PY[1]}+ interpreter, e.g.:\n"
f" /path/to/python3.11 install.py\n\n"
f"On macOS: brew install python@3.11\n"
f"On Debian/Ubuntu: apt install python3.11\n",
f"On Debian/Ubuntu: apt install python3.11\n"
f"On Windows: install from https://www.python.org/downloads/ "
f"(check 'Add python.exe to PATH')\n",
file=sys.stderr,
)
sys.exit(1)
Expand Down Expand Up @@ -136,7 +151,7 @@ def main() -> None:
ensure_local(rel)
else:
step("No --download set — weights will auto-download on first ./sa3 use")
print(f" To pre-download instead, pass: {sys.executable.split('/')[-1]} install.py --download sm-music")
print(f" To pre-download instead, pass: {Path(sys.executable).name} install.py --download sm-music")
print(f" or: ./install.sh --download sm-music,medium")

from examples import print_example_commands, BOLD, GREEN, RESET
Expand Down
Loading
Loading