{"id":50907203,"url":"https://github.com/robthomson/imac-ethos-caller","last_synced_at":"2026-06-16T06:04:00.237Z","repository":{"id":363142950,"uuid":"1261432444","full_name":"robthomson/imac-ethos-caller","owner":"robthomson","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-14T18:51:17.000Z","size":64630,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T19:21:24.053Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/robthomson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-06T17:16:08.000Z","updated_at":"2026-06-14T18:50:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/robthomson/imac-ethos-caller","commit_stats":null,"previous_names":["robthomson/imac-ethos-caller"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/robthomson/imac-ethos-caller","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robthomson%2Fimac-ethos-caller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robthomson%2Fimac-ethos-caller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robthomson%2Fimac-ethos-caller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robthomson%2Fimac-ethos-caller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/robthomson","download_url":"https://codeload.github.com/robthomson/imac-ethos-caller/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robthomson%2Fimac-ethos-caller/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34393302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-16T06:04:00.108Z","updated_at":"2026-06-16T06:04:00.231Z","avatar_url":"https://github.com/robthomson.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"# IMAC Ethos Caller\n\nAn FrSky Ethos widget that calls IMAC aerobatic competition sequences via audio on the radio. Select a year and class, then use a switch to announce each maneuver in order as you fly.\n\n## Features\n\n- Supports all 8 IMAC classes: Basic, Sportsman, Intermediate, Intermediate ALT, Advanced, Advanced ALT, Unlimited, Unlimited ALT\n- Per-year sequence files — adding a new season never touches existing data\n- Shared audio catalog — repeated maneuvers (common across classes/years) reuse a single recording instead of duplicating it\n- Progress bar shows current maneuver position through the sequence\n- Three configurable switches: Trigger (next maneuver), Repeat (replay current), Reset (restart sequence)\n- Sequence data for 2026 included with real figure names from official IMAC PDFs\n\n## Radio Requirements\n\n- FrSky Ethos firmware (tested on X20S)\n- Sound files must be 16 kHz mono A-law WAV format\n\n## Installation\n\n1. Download the latest release ZIP from the [Releases](../../releases) page\n2. Extract to the root of your radio's SD card — the widget lands at `/scripts/imac-caller/`\n3. Add the **IMAC Caller** widget to a screen via the Ethos widget menu\n\n## Configuration\n\nLong-press the widget to open settings:\n\n| Field | Description |\n|---|---|\n| Routine | Year + class combination (e.g. \"2026 Advanced\") |\n| Trigger Switch | Advances to the next maneuver and announces it |\n| Repeat Switch | Re-announces the current maneuver |\n| Reset Switch | Resets to the start and announces the class reset |\n| Language | UI/audio language: Auto (follows the radio's locale), English, Français, Deutsch, Nederlands, Česky, Español, עברית, Italiano, Norsk, Polski, Português (BR), 中文 (简体) |\n\n## Languages\n\nThe widget UI is available in English, French, German, Dutch, Czech, Spanish,\nHebrew, Italian, Norwegian, Polish, Portuguese (Brazil), and Chinese\n(Simplified). By default (\"Auto\") it follows the radio's locale\n(`system.getLocale()`), falling back to English for any unsupported locale.\nYou can override this with the **Language** field in the widget settings.\n\nIMAC class names (Basic, Sportsman, Advanced, …) and figure/maneuver names\n(Loop, Hammerhead, Immelmann, …) are always shown in English — these are\nstandard FAI/IMAC competition terms pilots recognize from official sequence\nsheets, regardless of UI language.\n\nSpoken call-outs are available in all 12 supported languages. Any maneuver\nwithout translated audio falls back to the English recording — see\n[Adding Translated Audio](#adding-translated-audio) for how to add more.\n\nAudio is organized into per-locale voice-pack folders matching the official\nEthos audio packs (the same layout used by rfsuite's sound generator):\nEnglish ships `gb` (British, the default) and `us` voices, and French ships\n`femme` (female, the default) and `homme` (male) voices. The widget picks\nwhichever variant matches the radio's currently selected audio voice\n(`system.getAudioVoice()`), falling back to that locale's default variant if\nthe radio's voice isn't one of the available options. All other locales ship\na single `default` voice each, since Ethos doesn't offer multiple official\nvoice packs for those locales.\n\n## Generating Sound Files\n\nSound files are pre-built and included in releases. To regenerate or add new ones:\n\n**Prerequisites**\n\n```\npip install google-cloud-texttospeech sox\n```\n\nGoogle Cloud Application Default Credentials must be configured (run `gcloud auth application-default login` once).\n\n**Run**\n\n```cmd\nbin\\generate-sounds.cmd\n```\n\nThis scans the `soundlist.csv` files under `src/imac-caller/sounds/\u003clocale\u003e/\u003cvariant\u003e/` (one per locale/variant — see [Voice Variants](#voice-variants)) and generates any missing WAV files. To regenerate everything: delete the WAV files first, then run again.\n\nThe voice used is chosen automatically per CSV based on its locale/variant (`LOCALE_VOICES` in `bin/generate-sounds.py`). Pass `--voice \u003cname\u003e` to override the voice for all CSVs in the run.\n\n## Adding a New Year\n\nThe repo-root `seasons/` and `i18n/` folders are the editing surface — nothing in them is deployed to the radio. Everything under `src/imac-caller/` is generated.\n\n1. Create `seasons/\u003cyear\u003e/sequence.json` listing each class's `key`, `name`, `reset`, and ordered `figures` — each `reset`/figure value is a maneuver ID from [seasons/catalog.json](seasons/catalog.json).\n2. For any maneuver not already in the catalog, add it first:\n   ```cmd\n   python bin\\catalog-add.py \"Loop\" \"Loop. Outside entry, three-quarter opposite roll and quarter roll at the top.\"\n   python bin\\catalog-add.py --reset basic \"Basic sequence reset.\"\n   ```\n   Each command prints the maneuver ID to reference from `sequence.json`. New entries are added to `seasons/catalog.json` with English text only — every other locale (`fr`, `de`, `nl`, `cs`, `es`, `he`, `it`, `no`, `pl`, `pt-br`, `zh-cn`) starts as an English placeholder flagged `\"needs_translation\": true` (see [Adding Translated Audio](#adding-translated-audio)).\n3. Run `bin\\generate.cmd` — writes `sequences.lua` for that year and rebuilds the shared sound catalog (`src/imac-caller/sounds/\u003clocale\u003e/\u003cvariant\u003e/soundlist.csv`) from `seasons/catalog.json`.\n4. Run `bin\\generate-sounds.cmd` to produce any new WAV files.\n\nThe widget auto-discovers year folders at runtime — no code changes needed. Maneuvers reused from another class or year (same catalog ID) are not regenerated — no new audio or catalog entry is created for them.\n\nNever edit `sequences.lua`, `soundlist.csv`, or `src/imac-caller/i18n/*.lua` directly — they are generated from the JSON and will be overwritten.\n\n## Adding Translated Audio\n\nSpoken call-out translations live directly in\n[seasons/catalog.json](seasons/catalog.json), one entry per maneuver, with a\nblock for each of `fr`/`de`/`nl`/`cs`/`es`/`he`/`it`/`no`/`pl`/`pt-br`/`zh-cn`.\nNew maneuvers (added via `bin\\catalog-add.py`) start with each of these set to\nthe English placeholder and `\"needs_translation\": true`.\n\nTo translate:\n\n1. Find the maneuver's entry in `seasons/catalog.json` (search for its ID or English `en` text)\n2. Edit the `text` field under the target locale (e.g. `fr`, `pt-br`, `zh-cn`)\n3. Set `\"needs_translation\": false`\n4. Run `bin\\generate.cmd` — this adds an entry to\n   `src/imac-caller/sounds/\u003clang\u003e/\u003cvariant\u003e/soundlist.csv` (one CSV per\n   voice variant — see below), but only for entries with\n   `needs_translation: false`\n5. Run `bin\\generate-sounds.cmd` to generate the translated WAV files\n\nEntries still flagged `needs_translation: true` are skipped — no catalog entry\nand no audio is generated for them, so the widget keeps falling back to the\nEnglish audio for those maneuvers.\n\n### Sound Catalog\n\nAudio is not stored per year/class. [seasons/catalog.json](seasons/catalog.json)\nis the master catalog of every maneuver and reset announcement, keyed by a\n\"phrase ID\" — a slug of its label plus a short hash of its English TTS text,\ne.g. `loop-6c260463` (see `bin/catalog-add.py`). `seasons/\u003cyear\u003e/sequence.json`\nreferences maneuvers by phrase ID.\n\nEach entry also has a short `audio` id (6 hex characters, e.g. `ceaecb`,\nderived from the phrase ID) — `sequences.lua` uses this for `file`/`reset`\n(`file = \"ceaecb\"`, `reset = \"2f7cd5\"`), and the actual audio lives in a\nshared pool at `src/imac-caller/sounds/\u003clocale\u003e/\u003cvariant\u003e/\u003caudio id\u003e.wav`.\nThe on-radio audio path (`SCRIPTS:/imac-caller/sounds/\u003clocale\u003e/\u003cvariant\u003e/\u003caudio id\u003e.wav`)\nmust stay short, so the longer, descriptive phrase ID is only used for\nediting/reference in the JSON, never for filenames.\n\nBecause a maneuver's ID is derived from its label + English text, the same\nmaneuver referenced from multiple classes or years collapses to the same\nphrase ID (and `audio` id) and is only generated/stored once. The catalog\n(`soundlist.csv` per locale/variant) is rebuilt from `seasons/catalog.json` every time\n`bin/generate.py` runs, so it stays complete and deduped as years are added.\n\n### Voice Variants\n\nEach locale's catalog is split into one `soundlist.csv` per voice variant the\nradio supports (`LOCALE_VARIANTS` in `bin/generate.py`), matching the\nlayout of rfsuite's sound-generator soundpacks:\n\n| Locale | Variants | Default |\n|---|---|---|\n| English | `gb`, `us` | `gb` |\n| French | `femme`, `homme` | `femme` |\n| German | `default` | `default` |\n| Dutch | `default` | `default` |\n| Czech | `default` | `default` |\n| Spanish | `default` | `default` |\n| Hebrew | `default` | `default` |\n| Italian | `default` | `default` |\n| Norwegian | `default` | `default` |\n| Polish | `default` | `default` |\n| Portuguese (Brazil) | `default` | `default` |\n| Chinese (Simplified) | `default` | `default` |\n\nEnglish variants are generated from each catalog entry's `en` text; every\nother locale's variant is generated from the entry's text for that locale\n(e.g. `fr`, `de`, `cs`, `pt-br`, `zh-cn`), but only when `needs_translation`\nis `false`. Each variant is written to its own\n`src/imac-caller/sounds/\u003clocale\u003e/\u003cvariant\u003e/soundlist.csv`, with voices\nconfigured in `LOCALE_VOICES` in `bin/generate-sounds.py`:\n\n| Variant | Voice |\n|---|---|\n| `en/gb` | `en-GB-Neural2-A` |\n| `en/us` | `en-US-Wavenet-F` |\n| `fr/femme` | `fr-FR-Neural2-F` |\n| `fr/homme` | `fr-FR-Standard-B` |\n| `de/default` | `de-DE-Neural2-C` |\n| `nl/default` | `nl-NL-Wavenet-A` |\n| `cs/default` | `cs-CZ-Wavenet-A` |\n| `es/default` | `es-ES-Wavenet-C` |\n| `he/default` | `he-IL-Wavenet-C` |\n| `it/default` | `it-IT-Wavenet-B` |\n| `no/default` | `nb-NO-Standard-E` |\n| `pl/default` | `cs-CZ-Wavenet-A` |\n| `pt-br/default` | `pt-BR-Wavenet-A` |\n| `zh-cn/default` | `cmn-CN-Wavenet-A` |\n\nAt runtime, the widget picks the variant matching the radio's currently\nselected audio voice (`system.getAudioVoice()`), falling back to the locale's\ndefault variant.\n\n## Project Structure\n\nJSON under the repo root is the editing surface; everything under\n`src/imac-caller/` is generated by `bin/generate.py` and never hand-edited\n(except `widget_impl.lua`, `main.lua`, and `i18n/i18n.lua`, which are widget code).\n\n```\ni18n/                                # Editing surface — not deployed\n└── strings.json                    # UI chrome strings, all locales (year, routine, switch labels, ...)\n\nseasons/                            # Editing surface — not deployed\n├── catalog.json                    # Maneuver catalog: id -\u003e label + per-locale text + needs_translation\n└── 2026/\n    └── sequence.json               # Class/figure structure for this year — references catalog IDs\n\nsrc/imac-caller/                   # Deployed to radio (generated, except widget code below)\n├── main.lua                        # Widget entry point\n├── widget_impl.lua                 # Widget logic\n├── i18n/\n│   ├── i18n.lua                    # Runtime locale loader/translate helper (hand-written)\n│   ├── en.lua                      # Generated from i18n/strings.json\n│   ├── fr.lua\n│   ├── de.lua\n│   ├── nl.lua\n│   ├── cs.lua\n│   ├── es.lua\n│   ├── he.lua\n│   ├── it.lua\n│   ├── no.lua\n│   ├── pl.lua\n│   ├── pt-br.lua\n│   └── zh-cn.lua\n├── seasons/\n│   └── 2026/\n│       └── sequences.lua           # Generated from seasons/2026/sequence.json + seasons/catalog.json\n└── sounds/                          # Shared audio catalog (deduped across classes/years)\n    ├── en/\n    │   ├── gb/                      # Default — British English voice\n    │   │   ├── soundlist.csv\n    │   │   └── ceaecb.wav, 2f7cd5.wav, ... (named by each entry's short \"audio\" id)\n    │   └── us/                      # American English voice\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── fr/\n    │   ├── femme/                   # Default — translated audio (falls back to English if absent)\n    │   │   ├── soundlist.csv\n    │   │   └── ...\n    │   └── homme/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── de/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── nl/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── cs/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── es/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── he/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── it/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── no/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── pl/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    ├── pt-br/\n    │   └── default/\n    │       ├── soundlist.csv\n    │       └── ...\n    └── zh-cn/\n        └── default/\n            ├── soundlist.csv\n            └── ...\n\nbin/\n├── generate.py                     # JSON sources → i18n/*.lua + sequences.lua + sound catalog (soundlist.csv per locale/variant)\n├── generate.cmd                    # Windows wrapper\n├── catalog-add.py                  # Adds a new maneuver to seasons/catalog.json, prints its catalog ID\n├── generate-sounds.py              # soundlist.csv → WAV files via Google TTS (per-locale/variant voices)\n└── generate-sounds.cmd             # Windows wrapper (runs --only-missing)\n```\n\n## Development\n\nDeploy directly to a connected radio via the VS Code tasks (requires the radio connected via USB HID or serial).\n\n```\nCtrl+Shift+P → Tasks: Run Task → Deploy\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobthomson%2Fimac-ethos-caller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobthomson%2Fimac-ethos-caller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobthomson%2Fimac-ethos-caller/lists"}