Skip to main content

HoraVox - a multi-language speaking clock using local AI voice models

Project description

HoraVox logotype: a simplistic analog clock and text HORAVOX

pip CI PyPI Downloads horavox GitHub repo Coverage Status LICENSE GPLv3

A multi-language speaking clock that announces the time using Piper text-to-speech. It runs entirely offline using local AI voice models -- no API key or internet connection required (except for the initial voice download). It speaks the current hour on the hour using natural language idioms (e.g., "quarter past two", "wpół do czwartej") and supports any language through JSON data files.

Features

  • Natural time idioms -- not just "it is 14:30" but "half past two" (English) or "wpół do trzeciej" (Polish)
  • Classic & modern modes -- idiomatic ("quarter past five") or digital ("five fifteen")
  • Multi-language -- add a new language by creating a JSON file in data/lang/
  • Fully offline -- uses local AI voice models, no API key or cloud service needed
  • Voice management -- browse, download, and auto-detect Piper voices from Hugging Face
  • Bluetooth audio fix -- plays a silent MP3 before speech to prevent clipping on Bluetooth speakers
  • Flexible scheduling -- restrict announcements to a time range (e.g., 7:00--22:00)
  • Configurable interval -- announce every N minutes with --freq (e.g., every 30 min)
  • Volume control -- set volume 0--100% with --volume
  • Scheduled announcements -- speak the time (or a custom message) at specific times with vox at
  • Background mode -- run as a daemon with --background, stop with --stop
  • Time-based messages -- attach custom messages to specific times with recurring schedules via vox config mapping.add
  • Sleep / wake -- temporarily mute all running daemons with vox sleep, auto-wakes when the time range restarts
  • Autostart service -- add as a system service with vox service add, runs on login
  • Command hooks -- run a shell command after each announcement with --exec (e.g., desktop notifications)
  • Hour beeps -- 2 beeps on the full hour, 1 beep on the half hour
  • Simulated time -- debug with --time HH:MM to set a fake starting time
  • Silent by default -- no terminal output unless --verbose is passed

Requirements

  • Python 3.10+ and pip
  • aplay (ALSA utils, for WAV playback) -- sudo apt install alsa-utils
  • mpg123 (for MP3 playback) -- sudo apt install mpg123

Installation

From PyPI

pip install horavox

This installs the vox command.

From source

git clone https://github.com/jcubic/horavox.git
cd horavox
pip install .

This installs the vox command from the local source, including all dependencies.

Usage

HoraVox uses git-style subcommands:

vox <command> [options]
Command Description
vox clock Run the speaking clock
vox now Speak the current time once
vox list List running background instances
vox stop Stop running background instances
vox sleep Mute all running daemons
vox wakeup Resume all sleeping daemons
vox voice Manage Piper voice models
vox at Speak the time or a custom message at specified times
vox config Get or set default configuration
vox service Manage autostart service (add/delete/list/start/restart/status)
vox completion Generate shell completion scripts

Run vox <command> --help for command-specific options.

vox clock

Run the speaking clock in foreground or as a background daemon:

vox clock                                          # announce every hour
vox clock --freq 30                                # every 30 minutes
vox clock --start 7 --end 22                       # only between 7:00-22:00
vox clock --mode modern                            # digital style ("siedemnasta piętnaście")
vox clock --background                             # run as a daemon
vox clock --lang pl --voice pl_PL-darkman-medium   # specific language and voice
vox clock --volume 50                              # 50% volume
vox clock --exec 'notify-send "HoraVox" "$TEXT"'   # desktop notification on each announcement

Time range accepts H, HH, H:MM, or HH:MM. Supports midnight wrap (e.g., --start 22 --end 6).

Valid --freq values must divide 60 evenly: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60.

Classic mode (default) uses idiomatic expressions -- "quarter past five", "wpół do szóstej". Modern mode reads the time digitally -- "five fifteen", "siedemnasta piętnaście".

vox now

Speak the current time once and exit:

vox now                        # speak current time
vox now --time 16:00           # speak a specific time
vox now --mode modern          # digital style
vox now --volume 30            # quiet

vox stop / vox list

Stop and list running background instances:

vox list                       # print PIDs of running instances
vox list --verbose             # include command lines
vox stop                       # interactive selection if multiple instances
vox stop --pid 12345           # stop a specific instance

When multiple instances are running, vox stop shows an interactive menu with arrow-key selection.

vox sleep / vox wakeup

Temporarily mute all running daemons without stopping them:

vox sleep                      # mute everything until range restarts
vox sleep --until 08:00        # mute until 8:00 AM
vox sleep --for 2h             # mute for 2 hours
vox sleep --for 1h30m          # mute for 1 hour 30 minutes
vox wakeup                     # resume all daemons immediately

When used with vox clock that has a --start/--end range, sleep auto-wakes when the range restarts. For example, if the clock runs 8:00--22:00 and you sleep at 15:00, it stays muted until 8:00 the next day. Cross-midnight ranges work too -- a clock running 22:00--2:00 that sleeps at 23:00 auto-wakes at 22:00 the next evening.

A clock without an explicit range (--start/--end) requires --until or --for since there is no range boundary to auto-wake on.

vox at instances respect sleep but have no range concept, so they only resume on vox wakeup or when --until/--for expires.

vox list shows a [sleeping] marker next to muted instances.

vox voice

Interactive voice browser -- navigate with arrow keys, press i to install, u to uninstall, q to quit:

vox voice                      # interactive voice browser
vox voice --lang en            # for a specific language
vox voice --list               # non-interactive list (for scripting)
vox voice --list --lang pl     # non-interactive for a specific language

Installed voices are marked with [*]. The default voice (the one that would be used by vox clock or vox now) is marked with [D]. Press Enter to test a voice -- it speaks the current time. If the voice isn't installed, it downloads it first. Downloads show a progress bar below the list.

Volume and sound

--nosound is equivalent to --volume 0 -- both skip voice loading and audio playback entirely. Available on vox clock and vox now.

vox at

Speak the time at specific times — one-shot or recurring like Google Calendar:

# One-shot (waits, speaks, exits)
vox at 12:55                                  # speak at 12:55 today
vox at 12:55 --date 2026-05-10               # speak at 12:55 on a specific date
vox at 12:55 --date friday                   # speak at 12:55 next Friday (never today)
vox at 12:55 --date friday,2026-12-25        # multiple dates (day names + exact)
vox at 9:00,12:00,18:00                       # multiple times today

# Recurring (persistent loop)
vox at 12:55 --repeat everyday                # every day at 12:55
vox at 12:55 --repeat sunday,wednesday        # specific days of the week
vox at 9:00,18:00 --repeat weekdays           # weekdays only
vox at 8:00 --repeat weekends --lang pl       # Polish, weekends only

# Custom message (speak text instead of the time)
vox at 12:00 --repeat weekdays -m "Time for lunch"
vox at 9:00 --message "Stand-up meeting in 5 minutes"

# Common flags
vox at 9:00 --repeat everyday --background    # run as a daemon
vox at 9:00 --repeat everyday --volume 30     # quiet

# Run a command after each announcement
vox at 9:00 --repeat weekdays --exec 'notify-send "HoraVox" "$TEXT"'

Times are comma-separated in HH:MM format. The --date flag accepts day names (mondaysunday) or exact dates (YYYY-MM-DD), comma-separated; day names always resolve to the next occurrence (never today). The --repeat flag accepts day keywords: mondaysunday, everyday, weekdays, weekends. --date and --repeat are mutually exclusive. Without either, the process runs for today and exits after the last scheduled time.

Use --message / -m to speak custom text instead of the current time — useful for reminders. Beeps still play as usual.

Supports the same --lang, --voice, --mode, --volume, --background, and --debug flags as vox clock.

Works with vox service add too:

vox service add "at 12:55 --repeat sunday,wednesday --volume 50"
vox service add "at 9:00 --repeat weekdays -m 'Stand-up meeting'"

--exec (run a command after announcements)

Both vox clock and vox at support --exec CMD to run a shell command after each announcement. The following environment variables are available in the command:

Variable Description
$TEXT The full spoken text
$TIME Announced time in HH:MM format
$DATE Current date in YYYY-MM-DD format
$MESSAGE Custom message (from --message or mapping), empty if none

Desktop notification examples for each platform:

# Linux (notify-send)
vox clock --exec 'notify-send "HoraVox" "$TEXT"'

# macOS (osascript)
vox clock --exec 'osascript -e "display notification \"$TEXT\" with title \"HoraVox\""'

# Windows (PowerShell + BurntToast)
vox clock --exec 'powershell -Command "New-BurntToastNotification -Text \"HoraVox\",\"$TEXT\""'

The command runs asynchronously (fire-and-forget) so it won't block the next announcement.

vox config

Set default values and aliases so you don't have to repeat common flags:

vox config lang=pl                     # default language
vox config voice=pl_PL-mc_speech-medium # default voice
vox config mode=classic                # default time style
vox config volume=30                   # default volume (0-100)
vox config                             # list all settings and aliases
vox config lang                        # show a single setting
vox config --unset voice               # remove a setting

Settings are stored in ~/.horavox/config.json and apply to vox clock, vox now, vox at, and vox voice. Command-line flags always override config values.

Aliases

Aliases work like git aliases -- define default arguments for any subcommand:

vox config alias.clock '--start 9 --end 1 --background --freq 30 --volume 30'
vox config alias.now '--mode modern'

Now vox clock expands to vox clock --start 9 --end 1 --background --freq 30 --volume 30. Explicit arguments override alias defaults:

vox clock --volume 50    # overrides --volume 30 from the alias

Manage aliases the same way as settings:

vox config alias.clock                 # show an alias
vox config --unset alias.clock         # remove an alias

Time-based messages (mapping)

Attach custom messages to specific clock times. When vox clock fires at a mapped time, it speaks the time followed by a short pause and the message:

vox config mapping.add 17:00 'feed the cat'                           # every day at 17:00
vox config mapping.add 9:00 'stand-up meeting' --date weekdays        # weekdays only
vox config mapping.add 8:00 'weekend run' --date saturday,sunday      # specific days
vox config mapping.add 12:00 'lunch time' --date monday,wednesday,friday
vox config mapping                                                     # list all entries
vox config --unset mapping.0                                           # remove by index

The --date flag accepts the same values as vox at --repeat: day names (monday--sunday), everyday, weekdays, weekends, comma-separated. Without --date, the message plays every day. Entries without a message text still match (useful for future extensions) but produce no extra speech.

To speak only the message without the time, set:

vox config settings.mapping.time=false

vox service

Manage autostart service instances that run on login:

vox service add "clock --lang pl --voice pl_PL-mc_speech-medium --start 9 --end 1 --freq 30 --volume 30"
vox service list                       # list installed instances
vox service delete <id>                # delete a specific instance
vox service delete --all               # delete all instances
vox service delete                     # interactive selection if multiple
vox service start                      # start the service manually
vox service restart                    # restart the service (e.g. after editing instances)
vox service status                     # show service and instance status

The quoted argument is any valid vox subcommand with its flags. The --background flag and vox prefix are stripped automatically. Unknown commands are rejected at add time.

On the first install, a platform-specific service is registered and started:

Platform Mechanism
Linux systemd user service (~/.config/systemd/user/horavox.service)
macOS launchd user agent (~/Library/LaunchAgents/com.horavox.service.plist)
Windows Startup folder script (%APPDATA%\...\Startup\horavox.vbs)

Subsequent installs add instances to the registry and signal the running service to reload. When the last instance is removed, the service is automatically unregistered.

Shell completion

HoraVox supports tab completion for bash, zsh, and fish via argcomplete. Generate and activate the completion script for your shell:

# Bash — add to ~/.bashrc
eval "$(vox completion --bash)"

# Zsh — add to ~/.zshrc
eval "$(vox completion --zsh)"

# Fish — add to ~/.config/fish/config.fish
vox completion --fish | source

Once activated, pressing Tab will complete command names, option flags, and values (e.g., --mode classic|modern).

Custom commands

Like git, any executable named vox-<name> in your $PATH can be invoked as vox <name>. This lets you extend HoraVox with your own commands or scripts:

# Create a custom command
cat > ~/bin/vox-greet << 'EOF'
#!/bin/bash
vox now --lang en --voice en_US-lessac-medium
EOF
chmod +x ~/bin/vox-greet

# Use it
vox greet

Adding a new language

Create a JSON file in data/lang/<code>.json (e.g., de.json for German). The file contains two mode sections:

{
  "classic": {
    "hours": ["midnight", "one o'clock", "...", "eleven o'clock"],
    "hours_alt": ["midnight", "one", "...", "eleven"],
    "minutes": {
      "1": "one", "2": "two", "...": "...", "29": "twenty nine"
    },
    "patterns": {
      "full_hour": "{hour}",
      "quarter_past": "quarter past {hour_alt}",
      "half_past": "half past {hour_alt}",
      "quarter_to": "quarter to {next_hour_alt}",
      "minutes_past": "{minutes} past {hour_alt}",
      "minutes_to": "{minutes} to {next_hour_alt}"
    }
  },
  "modern": {
    "hours": ["midnight", "one o'clock", "..."],
    "hours_alt": ["twelve", "one", "..."],
    "minutes": {
      "1": "oh one", "...": "...", "59": "fifty nine"
    },
    "patterns": {
      "full_hour": "{hour}",
      "time": "{hour_alt} {minutes}"
    }
  }
}

Fields

Classic mode (idiomatic -- quarters, halves, past/to):

Field Required Description
hours Yes 24 entries (index 0 = midnight, 12 = noon, etc.) used in {hour} and {next_hour}
hours_alt No 24 entries for alternate forms (e.g., genitive case). Defaults to hours if omitted
minutes Yes Keys "1" through "29" -- spoken forms for minute counts
patterns Yes 6 patterns: full_hour, quarter_past, half_past, quarter_to, minutes_past, minutes_to

Modern mode (digital -- hour + minutes):

Field Required Description
hours Yes 24 entries for full-hour announcements (can include "midnight", "noon")
hours_alt No 24 entries for the hour in {hour_alt} {minutes} patterns. Defaults to hours
minutes Yes Keys "1" through "59" -- spoken forms for all minute values
patterns Yes 2 patterns: full_hour and time

Placeholders

Placeholder Meaning
{hour} Current hour from hours
{hour_alt} Current hour from hours_alt
{next_hour} Next hour from hours
{next_hour_alt} Next hour from hours_alt
{minutes} Minute count from minutes map
{remaining} Minutes remaining to next hour (same source as {minutes})

Pattern rules

Pattern When Example (English)
full_hour :00 "three o'clock"
quarter_past :15 "quarter past three"
half_past :30 "half past three"
quarter_to :45 "quarter to four"
minutes_past :01--:29 (not :15) "ten past three"
minutes_to :31--:59 (not :45) "ten to four"

Project structure

src/horavox/
  __init__.py         Package init
  main.py             CLI dispatcher (installed as `vox` via pip)
  core.py             Shared library — paths, logging, language, TTS, voice, sessions
  clock.py            vox clock — speaking clock loop + daemon
  now.py              vox now — speak once
  at.py               vox at — scheduled announcements (one-shot / recurring)
  stop.py             vox stop — stop daemons
  list.py             vox list — list running daemons
  sleep.py            vox sleep — mute running daemons
  wakeup.py           vox wakeup — resume sleeping daemons
  voice.py            vox voice — interactive voice browser
  config.py           vox config — get/set defaults and aliases
  service.py          vox service — autostart service management
  registry.py         CRUD for service instance registry
  completion.py       vox completion — shell completion scripts
  platforms/
    linux.py          systemd user service backend
    macos.py          launchd user agent backend
    windows.py        Windows startup folder backend
  data/
    lang/
      en.json         English time data
      pl.json         Polish time data
    blank.mp3         Silent MP3 for Bluetooth audio wake-up
    beep.mp3          Beep sound for hour/half-hour signals
pyproject.toml        Package configuration

~/.horavox/           Runtime data (created automatically)
  models/             Downloaded Piper voice models (.onnx)
  cache/              Voice catalog cache + PID file
  sessions/           Running daemon metadata (.json)
  config.json         Default settings, aliases, and time-based message mappings
  data.json           Installed service instances registry
  sleep.json          Sleep state file (created by vox sleep)
  horavox.log         Spoken words + error log

Development

See CONTRIBUTING.md for development setup, testing, and publishing instructions.

Name

The name of the project is takend from two words from Latin: Hora (hour) + Vox (voice) -- the voice of the hour.

Acknowledge

The logo use Clipart from OpenClipart and font Lovelo.

License

Copyright (C) 2026 Jakub T. Jankiewicz

Released under GNU GPL v3.0 or later

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

horavox-0.3.0.tar.gz (91.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

horavox-0.3.0-py3-none-any.whl (70.8 kB view details)

Uploaded Python 3

File details

Details for the file horavox-0.3.0.tar.gz.

File metadata

  • Download URL: horavox-0.3.0.tar.gz
  • Upload date:
  • Size: 91.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for horavox-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e345f3f291738cee8782a6ece30862f56fb288f199476f20b967bdb091bdf208
MD5 a1d078589b695a529eea214a0709934d
BLAKE2b-256 62b03064fd54d34d2200e9199f734c1de7fd0aeed0044769daaf9ffb08119880

See more details on using hashes here.

File details

Details for the file horavox-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: horavox-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 70.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for horavox-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3650e4861bb1589246c1f04c002efaf0d20d516cc6a76921e35d9efbf7c5a850
MD5 848106cc06a6387ed0ba5ac31db3956e
BLAKE2b-256 2026c13e2cdc6d51c44d062ffb77bda84943422d6ba11c04edb5ab5a00cec9c8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page