diff --git a/bin/omarchy b/bin/omarchy index eda79a74..872907dc 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -32,6 +32,7 @@ GROUP_DESCRIPTIONS[branch]="Omarchy git branch management" GROUP_DESCRIPTIONS[brightness]="Display and keyboard brightness" GROUP_DESCRIPTIONS[capture]="Screenshots and screen recording" GROUP_DESCRIPTIONS[channel]="Omarchy release channel management" +GROUP_DESCRIPTIONS[ai]="AI prompt helpers" GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers" GROUP_DESCRIPTIONS[config]="System configuration helpers" GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs" diff --git a/bin/omarchy-ai-prompt b/bin/omarchy-ai-prompt new file mode 100755 index 00000000..10cce81e --- /dev/null +++ b/bin/omarchy-ai-prompt @@ -0,0 +1,70 @@ +#!/bin/bash + +# omarchy:summary=Ask Pi a question and show the answer as a notification +# omarchy:group=ai +# omarchy:name=prompt +# omarchy:args=[--speak] [prompt] +# omarchy:examples=omarchy ai prompt|omarchy-ai-prompt --speak "Summarize this" + +set -euo pipefail + +state_home=${XDG_STATE_HOME:-$HOME/.local/state}/omarchy +state_dir="$state_home/indicators" +session_file="$state_home/agent/prompt.jsonl" +state_file="$state_dir/ai" +mkdir -p "$state_dir" "$(dirname "$session_file")" + +markdown_to_pango() { + sed -E \ + -e 's/&/\&/g' \ + -e 's//\>/g' \ + -e 's/`([^`]+)`/\1<\/tt>/g' \ + -e 's/\*\*([^*]+)\*\*/\1<\/b>/g' \ + -e 's/__([^_]+)__/\1<\/b>/g' \ + -e 's/\*([^*]+)\*/\1<\/i>/g' \ + -e 's/_([^_]+)_/\1<\/i>/g' \ + -e 's/~~([^~]+)~~/\1<\/s>/g' +} + +speak=false +if (($# > 0)) && [[ ${1:-} == "--speak" ]]; then + speak=true + shift +fi + +if (($# > 0)); then + prompt="$*" +else + prompt=$(omarchy-menu-input "Ask AI" --width 800 || true) +fi + +prompt=$(sed 's/^[[:space:]]*//; s/[[:space:]]*$//' <<<"$prompt") +[[ -n $prompt ]] || exit 0 + +cleanup() { + rm -f "$state_file" + pkill -RTMIN+11 waybar 2>/dev/null || true +} +trap cleanup EXIT + +echo "thinking" >"$state_file" +pkill -RTMIN+11 waybar 2>/dev/null || true + +if answer=$(pi -p \ + --session "$session_file" \ + --model gemini-2.5-flash-lite \ + --skill "$HOME/.pi/agent/skills/omarchy" \ + --skill "$HOME/.pi/agent/git/github.com/badlogic/pi-skills/brave-search" \ + --append-system-prompt "For Omarchy desktop/system/config questions, load and follow the omarchy skill before answering. For current facts, historical facts, documentation, or questions that need web context, use the brave-search skill when BRAVE_API_KEY is available; otherwise answer from general knowledge and say when uncertain." \ + "$prompt" 2>&1); then + if [[ $speak == true ]]; then + omarchy-ai-speak "$answer" + else + formatted_answer=$(markdown_to_pango <<<"$answer") + omarchy-notification-send "󱚣" "Oma" "$formatted_answer" -t 10000 + fi +else + omarchy-notification-send "󱚡" "Oma failed" "$answer" -u critical -t 10000 + exit 1 +fi diff --git a/bin/omarchy-ai-prompt-transcribe b/bin/omarchy-ai-prompt-transcribe new file mode 100755 index 00000000..32afe998 --- /dev/null +++ b/bin/omarchy-ai-prompt-transcribe @@ -0,0 +1,96 @@ +#!/bin/bash + +# omarchy:summary=Record speech, transcribe it with Voxtype, and ask Pi +# omarchy:group=ai +# omarchy:name=prompt-transcribe +# omarchy:hidden=true + +set -euo pipefail + +runtime_dir=${XDG_RUNTIME_DIR:-/tmp}/omarchy +state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/indicators +pid_file="$runtime_dir/prompt-transcribe.pid" +wav_file="$runtime_dir/prompt-transcribe.wav" +state_file="$state_dir/ai" +log_file="$runtime_dir/prompt-transcribe.log" +mkdir -p "$runtime_dir" "$state_dir" + +log() { + printf '[%s] %s\n' "$(date '+%F %T')" "$*" >>"$log_file" +} + +show_indicator() { + echo "listening" >"$state_file" + pkill -RTMIN+11 waybar 2>/dev/null || true +} + +hide_indicator() { + rm -f "$state_file" + pkill -RTMIN+11 waybar 2>/dev/null || true +} + +log "started: ${1:-toggle}" + +case "${1:-toggle}" in + start) + if [[ -s $pid_file ]] && kill -0 "$(<"$pid_file")" 2>/dev/null; then + exit 0 + fi + + show_indicator + rm -f "$wav_file" + pw-record --target @DEFAULT_SOURCE@ --format s16 --rate 16000 --channels 1 "$wav_file" >>"$log_file" 2>&1 & + echo $! >"$pid_file" + log "recording started with pid $(<"$pid_file")" + ;; + stop) + if [[ -s $pid_file ]]; then + pid=$(<"$pid_file") + log "stopping recorder pid $pid" + kill "$pid" 2>/dev/null || true + + for _ in {1..20}; do + kill -0 "$pid" 2>/dev/null || break + sleep 0.05 + done + + rm -f "$pid_file" + else + log "no pid file found" + fi + + if [[ ! -s $wav_file ]]; then + log "no recording found, exiting" + hide_indicator + exit 0 + fi + + log "transcribing $(stat -c%s "$wav_file") bytes" + if transcript=$(voxtype transcribe "$wav_file" 2>>"$log_file"); then + log "transcription length: ${#transcript}" + else + status=$? + rm -f "$wav_file" + log "transcription failed with status $status" + hide_indicator + omarchy-notification-send "󱚣" "AI transcription failed" "Voxtype could not transcribe the prompt." -u critical + exit 1 + fi + + rm -f "$wav_file" + transcript=$(sed 's/^[[:space:]]*//; s/[[:space:]]*$//' <<<"$transcript") + if [[ -z $transcript ]]; then + log "empty transcription, exiting" + hide_indicator + exit 0 + fi + + log "sending transcript to omarchy-ai-prompt" + omarchy-ai-prompt --speak "$transcript" + log "omarchy-ai-prompt finished" + ;; + *) + echo "Usage: omarchy-ai-prompt-transcribe start|stop" + exit 1 + ;; +esac diff --git a/bin/omarchy-ai-speak b/bin/omarchy-ai-speak new file mode 100755 index 00000000..d070cd4b --- /dev/null +++ b/bin/omarchy-ai-speak @@ -0,0 +1,36 @@ +#!/bin/bash + +# omarchy:summary=Speak text using Piper TTS +# omarchy:group=ai +# omarchy:name=speak +# omarchy:args=[text] +# omarchy:examples=omarchy ai speak "Hello" + +set -euo pipefail + +voice_dir=${XDG_DATA_HOME:-$HOME/.local/share}/omarchy/piper +model="$voice_dir/en_US-amy-medium.onnx" +config="$voice_dir/en_US-amy-medium.onnx.json" +output=${XDG_RUNTIME_DIR:-/tmp}/omarchy-ai-speak.wav + +mkdir -p "$voice_dir" "$(dirname "$output")" + +if (($# > 0)); then + text="$*" +else + text=$(cat) +fi + +text=$(sed 's/^[[:space:]]*//; s/[[:space:]]*$//' <<<"$text") +[[ -n $text ]] || exit 0 + +if [[ ! -f $model ]]; then + curl -L --fail -o "$model" "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx" +fi + +if [[ ! -f $config ]]; then + curl -L --fail -o "$config" "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx.json" +fi + +uvx --from piper-tts piper --model "$model" --config "$config" --output-file "$output" <<<"$text" >/dev/null +pw-play "$output" diff --git a/config/waybar/config.jsonc b/config/waybar/config.jsonc index 61a11eca..535a357e 100644 --- a/config/waybar/config.jsonc +++ b/config/waybar/config.jsonc @@ -5,7 +5,7 @@ "spacing": 0, "height": 26, "modules-left": ["custom/omarchy", "hyprland/workspaces"], - "modules-center": ["clock", "custom/weather", "custom/update", "custom/voxtype", "custom/screenrecording-indicator", "custom/idle-indicator", "custom/notification-silencing-indicator"], + "modules-center": ["clock", "custom/weather", "custom/update", "custom/ai-indicator", "custom/voxtype", "custom/screenrecording-indicator", "custom/idle-indicator", "custom/notification-silencing-indicator"], "modules-right": [ "group/tray-expander", "bluetooth", @@ -144,6 +144,12 @@ "on-scroll-left": "", "on-scroll-right": "" }, + "custom/ai-indicator": { + "exec": "$OMARCHY_PATH/default/waybar/indicators/ai.sh", + "signal": 11, + "interval": 1, + "return-type": "json" + }, "custom/screenrecording-indicator": { "on-click": "omarchy-capture-screenrecording", "exec": "$OMARCHY_PATH/default/waybar/indicators/screen-recording.sh", diff --git a/config/waybar/style.css b/config/waybar/style.css index 63302efa..62b425ae 100644 --- a/config/waybar/style.css +++ b/config/waybar/style.css @@ -82,6 +82,7 @@ tooltip { opacity: 0; } +#custom-ai-indicator, #custom-screenrecording-indicator, #custom-idle-indicator, #custom-notification-silencing-indicator { @@ -92,6 +93,7 @@ tooltip { padding-bottom: 1px; } +#custom-ai-indicator.active, #custom-screenrecording-indicator.active { color: #a55555; } diff --git a/default/hypr/bindings/utilities.conf b/default/hypr/bindings/utilities.conf index ef8ef0d3..ef8c97a2 100644 --- a/default/hypr/bindings/utilities.conf +++ b/default/hypr/bindings/utilities.conf @@ -4,6 +4,7 @@ bindd = SUPER CTRL, E, Emoji picker, exec, omarchy-launch-walker -m symbols bindd = SUPER CTRL, C, Capture menu, exec, omarchy-menu capture bindd = SUPER CTRL, O, Toggle menu, exec, omarchy-menu toggle bindd = SUPER CTRL, H, Hardware menu, exec, omarchy-menu hardware +bindd = SUPER CTRL, P, AI prompt, exec, omarchy-ai-prompt bindd = SUPER ALT, SPACE, Omarchy menu, exec, omarchy-menu bindd = SUPER SHIFT, code:201, Omarchy menu, exec, omarchy-menu bindd = SUPER, ESCAPE, System menu, exec, omarchy-menu system @@ -67,6 +68,8 @@ bindd = SUPER CTRL, X, Toggle dictation, exec, voxtype record toggle bindd = , F9, Start dictation (push-to-talk), exec, voxtype record start binddr = , F9, Stop dictation (push-to-talk), exec, voxtype record stop +bindd = , F8, Start AI prompt dictation (push-to-talk), exec, omarchy-ai-prompt-transcribe start +binddr = , F8, Stop AI prompt dictation (push-to-talk), exec, omarchy-ai-prompt-transcribe stop # Zoom bindd = SUPER CTRL, Z, Zoom in, exec, hyprctl keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '.float + 1') diff --git a/default/waybar/indicators/ai.sh b/default/waybar/indicators/ai.sh new file mode 100755 index 00000000..7210c0ec --- /dev/null +++ b/default/waybar/indicators/ai.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +state_file=${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/indicators/ai + +if [[ -e $state_file ]]; then + state=$(<"$state_file") + + if [[ $state == "listening" ]]; then + echo '{"text":"󱚟","class":"active listening","tooltip":"AI is listening"}' + else + if (( $(date +%s) % 2 == 0 )); then + echo '{"text":"󱜙","class":"active thinking","tooltip":"AI is thinking"}' + else + echo '{"text":"󱚥","class":"active thinking","tooltip":"AI is thinking"}' + fi + fi +else + echo '{"text":"","class":"","tooltip":""}' +fi diff --git a/migrations/1778237131.sh b/migrations/1778237131.sh new file mode 100644 index 00000000..2442e9bd --- /dev/null +++ b/migrations/1778237131.sh @@ -0,0 +1,15 @@ +echo "Add AI prompt command bindings and Waybar indicator" + +WAYBAR_CONFIG=~/.config/waybar/config.jsonc +if [[ -f $WAYBAR_CONFIG ]] && ! grep -q "custom/ai-indicator" "$WAYBAR_CONFIG"; then + sed -i 's/"custom\/update", "custom\/voxtype"/"custom\/update", "custom\/ai-indicator", "custom\/voxtype"/' "$WAYBAR_CONFIG" + sed -i '/ "custom\/screenrecording-indicator": {/i\ "custom/ai-indicator": {\n "exec": "$OMARCHY_PATH/default/waybar/indicators/ai.sh",\n "signal": 11,\n "interval": 1,\n "return-type": "json"\n },' "$WAYBAR_CONFIG" +fi + +WAYBAR_STYLE=~/.config/waybar/style.css +if [[ -f $WAYBAR_STYLE ]] && ! grep -q "custom-ai-indicator" "$WAYBAR_STYLE"; then + sed -i 's/#custom-screenrecording-indicator,/#custom-ai-indicator,\n#custom-screenrecording-indicator,/' "$WAYBAR_STYLE" + sed -i 's/#custom-screenrecording-indicator.active {/#custom-ai-indicator.active,\n#custom-screenrecording-indicator.active {/' "$WAYBAR_STYLE" +fi + +omarchy-refresh-waybar diff --git a/piper/en_US-amy-medium.onnx b/piper/en_US-amy-medium.onnx new file mode 100644 index 00000000..ef4cc053 Binary files /dev/null and b/piper/en_US-amy-medium.onnx differ diff --git a/piper/en_US-amy-medium.onnx.json b/piper/en_US-amy-medium.onnx.json new file mode 100644 index 00000000..5a1e0a2d --- /dev/null +++ b/piper/en_US-amy-medium.onnx.json @@ -0,0 +1,493 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-us" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "dataset": "amy" +} \ No newline at end of file