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
37 changes: 36 additions & 1 deletion .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'Publish macOS beta 4 preview with voice commands') && github.sha || 'checks' }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -101,3 +101,38 @@ jobs:
path: build/macos-qa/${{ matrix.arch }}/
if-no-files-found: warn
retention-days: 30

publish-voice-preview:
name: Publish the authorized macOS hands-free preview
needs: macos
# Only the explicit owner-authorized publication commit can write a release.
if: >-
github.event_name == 'push' && github.repository == 'aaamosh/DotDial' &&
github.ref == 'refs/heads/main' && github.event.head_commit.id == github.sha &&
(github.event.head_commit.message == 'Publish macOS beta 4 preview with voice commands' ||
startsWith(github.event.head_commit.message, format('{0}{1}', 'Publish macOS beta 4 preview with voice commands', fromJSON('"\n"'))))
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22.12.0
- uses: actions/download-artifact@v4
with:
name: dotdial-macos-arm64-preview-${{ github.sha }}
path: build/macos-release/arm64
- uses: actions/download-artifact@v4
with:
name: dotdial-macos-x64-preview-${{ github.sha }}
path: build/macos-release/x64
- name: Publish only the exact native artifacts after all acceptance gates
run: node scripts/publish-macos-preview.cjs
env:
GH_TOKEN: ${{ github.token }}
71 changes: 66 additions & 5 deletions docs/MACOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Login Items, local audio cues and application bundles for both Mac architectures
hotkeys, sounds and saved replies do not need a system Node or Python install.

Download the published **Apple Silicon or Intel DMG** from the
[macOS Preview 2 release](https://github.com/aaamosh/DotDial/releases/tag/v0.1.0-beta.3-macos-preview.2).
[macOS beta.4 hands-free preview](https://github.com/aaamosh/DotDial/releases/tag/v0.1.0-beta.4-macos-preview.1).
The release also includes application ZIPs, source manifests and one combined
`SHA256SUMS` file. Its notes identify the exact source revision and native CI run.

Expand Down Expand Up @@ -136,6 +136,46 @@ When enabled, wake continues listening locally during calls even if transmission
to the dot is muted. The wake phrase can re-enable microphone and speakers. Turn
wake off and save if you do not want local listening. See [Privacy](PRIVACY.md).

## Hands-free call commands (beta.4)

The beta.4 Mac preview contains the same seven configurable commands as Linux
beta.4. After local wake setup, enable **Hands-free call commands** in
**Settings → Voice** and save. This option is off by default, including upgrades.

| Action | Default English phrase |
| --- | --- |
| Microphone off | Microphone off |
| Microphone on | Microphone on |
| Speakers off | Radio silence |
| Speakers on | Sound on please |
| Hang up | Hang up |
| Play saved replies | Replay messages |
| Stop playback | Stop the replay |

Edit the phrases in Voice settings or `wakeWord.commands`. Save applies them
while a call is active: only the local recognizer is replaced, not the call.
Each phrase has two to six English words. Overlap with the wake phrase or another
command is rejected. The normal wake phrase still starts or reactivates a call.
The selected Mac microphone is shared with the existing Electron capture path;
Python receives audio on stdin and does not open another microphone.

Microphone and speaker choices are independent. Speaker mute also stops replay;
voice hangup cancels unfinished actions. The listener stays available during
idle saved-reply playback so **Stop the replay** works. A bare wake detection is
suppressed during that playback and its short decoding tail to avoid placing an
accidental call. Disabling commands leaves ordinary wake behavior available.

Call microphone mute stops transmission, not local wake listening. Turn off wake
and save to stop local listening. Speech before mute takes effect may reach the
dot, and loudspeaker audio may trigger commands through the microphone. A headset
and distinct phrases reduce that risk. No new cloud recognition or audio storage
is added by these commands.

The older [beta.3 Preview 2](https://github.com/aaamosh/DotDial/releases/tag/v0.1.0-beta.3-macos-preview.2)
does not contain these commands. The new preview retains its silent-sink capture
fix and all original native acceptance gates, and adds a twelfth required gate
for command recognition and routing.

## Start at login

Enable **Start at login** in the installed application. DotDial uses macOS Login
Expand Down Expand Up @@ -188,9 +228,13 @@ Config reports and doctor do not open a call or capture audio.

Quit DotDial before replacing the `.app`. Keep the last working installer until
you have checked the new version. Replace only the application, then reopen it;
settings and saved replies remain in Application Support. This port does not
introduce a configuration-schema migration. If a later release introduces one,
follow that release's migration and rollback instructions before downgrading.
settings and saved replies remain in Application Support. Beta.4 adds
`wakeWord.commandsEnabled` and `wakeWord.commands` within configuration version 1.
Missing fields receive opt-out defaults on upgrade. Before upgrading, preserve a
copy of `config.json`: after saving beta.4 settings, beta.3's strict parser rejects
these new fields. To roll back, restore the pre-upgrade config along with the old
app, without replacing or deleting `data/` or `recordings/`. Alternatively remove
only those two keys from a backed-up copy of the config before using beta.3.

To uninstall, turn off Start at login, quit DotDial and move the application to the
Trash. User data remains. If you also want to delete the browser session, model,
Expand Down Expand Up @@ -247,11 +291,28 @@ valid signed entitlements do not establish a TCC permission grant, Developer ID
signing or notarization.
Synthetic checks do not establish that real ChatGPT authentication, a microphone
permission prompt, physical audio hardware or Login Items will work on every Mac.
Preview 2 retains these remaining acceptance gaps.
The beta.4 hands-free preview retains these remaining acceptance gaps.
Before promoting it beyond preview, test those with an actual user session on the
minimum supported macOS and a current version. Do not put account data or
recordings in CI.

The application icon is derived from `src/assets/dotdial-macos.svg`, using the
existing DotDial mark. The reviewed ICNS file is included in source and pinned by
the public-source scanner; icon generation tools are not needed to build the app.

## Native voice-command acceptance

The required `native_voice_commands` gate runs the packaged listener, native
managed Python, `WakeManager` and `VoiceCommands` through twelve speech cases.
All seven defaults must produce exactly the expected command and dispatch,
without extra wakes. Controls also cover the wake phrase with commands enabled,
unrelated speech, disabled commands, an edited phrase and the old phrase after
replacement. Speech is generated by the already-built, pinned Flite SLT voice.
The report and WAV provenance are preserved as `voice-commands.json` and
`voice-fixtures/` in the native QA artifact.

This gate injects synthetic PCM at the capture callback and uses an isolated call
state and dispatcher. The separate `native_wake_pipeline` gate exercises actual
Electron capture and the bounded PCM pipe. Neither is an authenticated account
call or physical-device test. Additional regressions check live phrase edits,
stale-event rejection, selected-input preservation and command opt-out on Mac.
22 changes: 19 additions & 3 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ The build jobs had read-only repository permissions; only the promotion job coul
write release contents. That completed job has been removed from active CI.

Regular workflows build and verify packages for pull requests, `main` pushes,
`v*` tags and manual runs, then preserve artifacts and reports. There is no active
publication path in normal CI: both completed macOS promotion jobs have been
removed. The original workflows, publishers and regression tests remain available
`v*` tags and manual runs, then preserve artifacts and reports. Both completed beta.3 macOS promotion jobs have been removed. The separately
gated beta.4 hands-free publication is described below; normal CI runs cannot
invoke it. The original workflows, publishers and regression tests remain available
at their respective released source commits for provenance.

The first promotion downloaded the distribution artifacts from that same workflow
Expand Down Expand Up @@ -157,3 +157,19 @@ Complete the physical microphone, real account, TCC, Login Items and minimum-mac
session checks before promoting this preview to a more general release. Preserve
the explicit preview classification until that acceptance and the planned Apple
signing/notarization work are complete.

## macOS beta.4 hands-free preview

The next explicit publication uses the separate tag
`v0.1.0-beta.4-macos-preview.1` and application version `0.1.0-beta.4`. The
reviewed main commit's first line must be exactly
`Publish macOS beta 4 preview with voice commands`. The one-time workflow job
waits for both architectures and all twelve required package gates, then promotes
only those same-run artifacts with matching manifests and SHA-256. It does not
rebuild packages or download Release assets. Earlier previews and Linux beta.4
are outside its mutation target.

The publisher retains strict draft identity, fixed-tag and no-overwrite checks.
After successful readback, retire this one-time job and link the new downloads.
The archived beta.3 publisher remains accessible at its original source; ordinary
pushes, pull requests and workflow dispatches do not authorize publication.
27 changes: 25 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ copy through the push event. For a branch without a pull request, open a draft
pull request or request a manual run. Newer runs cancel older runs only for the
same workflow, event and ref; a manual run does not cancel an automatic run.

These workflows preserve build artifacts and reports. They do not publish
GitHub releases. The completed one-time publisher for the first macOS preview
Ordinary workflow runs preserve build artifacts and reports without publishing
GitHub releases. A separate one-time beta.4 publication requires the exact main
commit subject documented in [Releasing](RELEASING.md#macos-beta4-hands-free-preview). The completed one-time publisher for the first macOS preview
is retained in the [released source](https://github.com/aaamosh/DotDial/blob/54889c5b04fe3dc32b79084d56bbec6e187a53de/.github/workflows/macos.yml),
and its script and regression tests remain in the repository. Removing that job
from active CI does not change the public tag or released files.
Expand Down Expand Up @@ -225,6 +226,7 @@ The verifier requires every stage to pass:
| `native_wake_decoder` | The packaged installer repairs a deliberately removed required file in isolated test data; its restored hash matches the original. Native Python dependencies then load the real model and the listener consumes synthetic PCM with a clean EOF. |
| `native_wake_pipeline` | Electron capture, IPC and bounded PCM delivery reach the native Python listener, including four pause/resume cycles, stopped-reader cleanup and restart. |
| `native_wake_speech` | Generated speech produces the required positive and two negative recognition results described above. |
| `native_voice_commands` | Packaged native recognition and routing pass all twelve command, wake, custom-phrase and negative-control cases described below. |
| `disk_image` | The DMG is readable and contains the verified application and Applications shortcut. |

The packaged GUI smoke waits for the application to show its settings window
Expand Down Expand Up @@ -290,3 +292,24 @@ microphone unplug/reconnect, login startup, a spoken wake phrase and a real acco
call. Run that session on macOS 13 Ventura as well as a current supported version;
deployment metadata alone cannot substitute for a minimum-version runtime test.
Keep private account data and recordings out of test artifacts.

## macOS beta.4 hands-free acceptance

Native macOS packages retain the previous eleven gates and require a twelfth:
`native_voice_commands`. It invokes `scripts/smoke-macos-voice.cjs` through the
bundled Electron Node runtime with the extracted app, private wake environment,
pinned Flite fixture directory and a fresh report path. No Release asset is
downloaded by this check.

Twelve cases exercise the actual packaged Python listener and JavaScript routing:
seven default commands; ordinary wake with commands enabled; unrelated speech;
commands disabled; a custom microphone-off phrase; and rejection of the replaced
phrase. Every case requires the exact ordered event sequence, matching dispatch,
successful decoder EOF and clean capture/process teardown. Synthetic PCM replaces
the capture callback and dispatch uses an isolated call state. This complements,
not replaces, real Electron PCM pipeline, cancellation and hardware acceptance.

`voice-commands.json` preserves results even on a failing case; `voice-fixtures/`
contains generated WAVs linked by hashes to the report and pinned Flite binary.
The CLI gate also checks seven opt-out command defaults, a saved phrase change
and the native Command-Shift-Space shortcut without system Node.
68 changes: 68 additions & 0 deletions docs/releases/macos-beta4-preview-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# DotDial for macOS 0.1.0-beta.4

**Your voice has the controls.** This native Mac preview includes the hands-free
commands already published in Linux beta.4, using the same configuration,
recognizer and call-control implementation.

## Turn it on

In **Settings → Voice**, set up local wake support, enable **Hands-free call
commands**, and save. Commands are off by default and use short English phrases.
Each phrase can be edited without restarting DotDial or reconnecting a call.

| Action | Default phrase |
| --- | --- |
| Microphone off | Microphone off |
| Microphone on | Microphone on |
| Speakers off | Radio silence |
| Speakers on | Sound on please |
| End the call | Hang up |
| Play saved replies | Replay messages |
| Stop saved-reply playback | Stop the replay |

Your existing wake phrase still starts a call. Overlapping command/wake phrases
are rejected. Microphone and speakers remain independent; hangup cancels pending
audio actions, and playback can be stopped by voice even after a call has ended.
On macOS the selected Electron microphone feeds the local Python recognizer over
its existing bounded audio pipe. No second Python microphone or cloud recognition
service is introduced. Muting the call stops transmission, not the local listener.
Speech spoken before mute takes effect may reach the dot. Speaker output can be
picked up by the microphone; use distinct phrases or a headset.

## Downloads and provenance

Use the `arm64` download on Apple Silicon and `x64` on Intel. Names contain
`preview-{{SOURCE_SHORT}}`; each architecture has a DMG, an app ZIP and a source
manifest. `SHA256SUMS` covers those six files.

Source: `{{SOURCE_COMMIT}}`.
[Native build and acceptance]({{CI_RUN_URL}}).

These packages are published only after both native jobs pass all **12 required
package gates**. The new command gate uses the real packaged listener,
WakeManager and command router with synthetic English speech: seven commands,
wake with commands enabled, unrelated speech, commands disabled, a custom phrase
and rejection of its replaced phrase. It checks exact events, dispatch and
process cleanup. Its microphone input and call dispatch are isolated fixtures,
not a hardware or real-account call test. Existing Electron PCM, GUI, four-resume,
backpressure, saved-reply, permission-policy and package checks remain required.

## Upgrade and limitations

Quit DotDial, replace the app in Applications and reopen it. Existing profiles,
shortcuts, sounds and saved replies stay in Application Support. Voice commands
remain off until explicitly enabled. Back up `config.json` before upgrading:
once beta.4 saves its new command fields, the older beta.3 strict parser will not
accept them. Restore the pre-upgrade config when rolling back; keep the data and
recordings directories intact. See [the Mac guide](https://github.com/aaamosh/DotDial/blob/{{SOURCE_COMMIT}}/docs/MACOS.md).

This is an **ad-hoc signed preview, without Apple Developer ID or notarization**.
Native tests run on macOS 15 for both architectures; macOS 13 is the binary
minimum, not a verified runtime result. Physical audio devices, real permission
prompts, sleep/unplug recovery, user-session Login Items and authenticated ChatGPT
calls still require hands-on acceptance. Synthetic speech is a regression check,
not an accuracy measurement across accents, speakers or rooms.

The application remains unofficial and uses internal web-call routes. An existing
ChatGPT dot is required. Earlier macOS previews and the Linux beta.4 release are
preserved unchanged.
41 changes: 41 additions & 0 deletions scripts/compare-native-voice.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

// Counterfactual native decoder measurements. Never modifies the app, model,
// user settings or twelve mandatory recognition-to-routing acceptance cases.
const assert = require('node:assert/strict');
const path = require('node:path');
const crypto = require('node:crypto');
const { spawnSync } = require('node:child_process');
const wrapper = `
import runpy, sys, sherpa_onnx
listener, paths, precision, *arguments = sys.argv[1:]
original = sherpa_onnx.KeywordSpotter
def diagnostic_spotter(**options):
options['max_active_paths'] = int(paths)
for part in ('encoder', 'decoder', 'joiner'):
if precision == 'all' or precision == part:
options[part] = options[part].replace('.int8.onnx', '.onnx')
return original(**options)
sherpa_onnx.KeywordSpotter = diagnostic_spotter
sys.argv = [listener, *arguments]
runpy.run_path(listener, run_name='__main__')
`;
function compareDecoder({ python, source, data, model, wake, commands, pcm, report }) {
report.recognitionComparison = [];
for (const [paths, precision] of [[4, 'none'], [8, 'none'], [16, 'none'], [4, 'encoder'], [4, 'all']]) {
const start = performance.now();
const result = spawnSync(python, ['-I', '-c', wrapper,
path.join(source, 'src/wake/listener.py'), String(paths), precision,
'--stdin-audio', '--model', path.join(data, 'models', model), '--phrase', wake,
'--sensitivity', '6', '--commands-json', JSON.stringify(commands)],
{ input: pcm, encoding: 'utf8', timeout: 15000, maxBuffer: 65536 });
const observation = { maxActivePaths: paths, fp32: precision,
pcmSha256: crypto.createHash('sha256').update(pcm).digest('hex'),
elapsedMs: performance.now() - start, exitCode: result.status,
stdout: result.stdout || '', error: result.error?.code || null };
report.recognitionComparison.push(observation);
assert.equal(result.error, undefined, 'counterfactual decoder exits within its bound');
assert.equal(result.status, 0, result.stderr);
}
}
module.exports = { compareDecoder };
Loading
Loading