mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-14 17:00:52 +02:00
voice/ft-voiced (was frame_voice.py) runs in Frametop's container as frametop-voice.service. It keeps the mic, the wake word and live text, and hands the keyboard side to the input relay: - push-to-talk keys are key combinations (dictate, dictate_hold); the relay sends "toggle|start|stop" to @frametop_voice, taken only from this user - text goes to the relay as "type TEXT" (ASCII, in pieces of 2000 bytes), so it lands where typing goes; the relay holds it back while a modifier is held - controller buttons are read from vrserver's web socket with input/vrws.py, in a thread, so the websockets package goes No own uinput keyboard, fd store, evdev, or controller service any more. whisper-server runs only while the wake word is on, started and stopped by ft-voiced. Voice commands and hooks run on the host through distrobox-host-exec (env passed through env(1)); their FRAME_VOICE_* variables keep frame-voice's names. Settings: ~/.config/frametop-voice.json (voice_settings.py, standard library), wake word off by default, "advanced" for the old config.toml tunables. voice/run.sh install carries frame-voice's settings over once (its keys become key combinations) and turns the standalone units off. Install: install.sh step 8/11 asks (yes by default; --no-voice through get.sh, install-release.sh and a FrameDrop checkbox). voice/build.sh builds whisper-server (whisper.cpp d09f61a, Vulkan, static) and build/venv; voice/models.sh fetches the 322 MB of models into ~/.local/share/frametop/voice by fixed URL and sha256. The image gets glslc, spirv-headers-devel and pipewire-utils, uv.lock a voice group (sherpa-onnx with its core pinned: uv dropped it), CI smoke whisper-server and the venv. uninstall.sh asks about the models; update-check and report.sh know the service (report.sh leaves out the lines with what was said). Tests: voice/test (settings, carrying over, typing through a stand-in relay, push-to-talk, controller buttons from a stand-in web socket) in just test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
11 KiB
Bash
Executable File
247 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Frametop's one-line installer. In a terminal on the Steam Frame (Konsole in the desktop, or
|
|
# over SSH):
|
|
#
|
|
# curl -fsSL https://frametop.github.io/frametop/get.sh | bash
|
|
#
|
|
# It asks which version to install, clones the repo into ~/frametop (or updates the clone
|
|
# that's there), and runs its install.sh. Run it again to update, or to switch versions.
|
|
# Its third and fourth choices, or --release, install a release instead: Frametop built, in one file. It downloads the
|
|
# release's Frametop.zip from GitHub (about 1.1 GB: the newest stable release, or with
|
|
# --experimental the newest of any), unpacks it in ~/.cache/frametop/release, and runs its
|
|
# install-release.sh, which checks this SteamOS build against the releases' SteamOS table and
|
|
# installs without building anything (pack/README.md, Releases). The same zip installs with
|
|
# FrameDrop from a PC, or unpacked by hand on the headset.
|
|
# Options (piped, they go after "bash -s --"):
|
|
# --stable the main branch: tested releases (the default for a new install)
|
|
# --experimental the experimental branch: the newest features, less tested
|
|
# --branch NAME another branch, such as a fix to test before it's released
|
|
# --dir DIR where the repo goes (default ~/frametop; for --release, where the
|
|
# releases go, default ~/.local/share/frametop/releases)
|
|
# --clone-only get or update the repo (or unpack the release), but don't run install.sh
|
|
# --release install a release from GitHub instead of cloning the repo
|
|
# --version V with --release: that release (tag vV), not the newest
|
|
# --zip FILE|URL with --release: this Frametop.zip instead of GitHub's newest
|
|
# --any-steamos with --release: install even on a SteamOS build the release breaks on
|
|
# --yes, --no-voice, --no-eye-tracker, --no-bluetooth, --bluetooth passed to install.sh (--yes also
|
|
# answers this script's questions: the version already there, or stable)
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: get.sh [--stable | --experimental | --branch NAME] [--dir DIR] [--clone-only] [--yes]
|
|
[--no-voice] [--no-eye-tracker] [--no-bluetooth | --bluetooth]
|
|
get.sh --release [--stable | --experimental | --version V | --zip FILE|URL]
|
|
[--any-steamos] [--dir DIR] [--clone-only] [--yes] [--no-voice] [--no-eye-tracker]
|
|
[--no-bluetooth | --bluetooth]
|
|
piped: curl -fsSL https://frametop.github.io/frametop/get.sh | bash -s -- [options]
|
|
EOF
|
|
}
|
|
|
|
# Frametop lives in the Frametop organization's repo: the branches clone from it, and its CI
|
|
# runners (Depot, which need an organization) build the releases. It moved there from
|
|
# DeeJanuz/frametop on 2026-10-09; GitHub redirects clones made from the old name.
|
|
SLUG=${FRAMETOP_REPO:-Frametop/frametop} # FRAMETOP_REPO: another repo's releases, such as a fork's
|
|
|
|
# release_zip CHANNEL VERSION: the URL of a release's Frametop.zip on GitHub.
|
|
release_zip() {
|
|
if [ -n "$2" ]; then
|
|
echo "https://github.com/$SLUG/releases/download/v$2/Frametop.zip"
|
|
elif [ "$1" = stable ]; then
|
|
echo "https://github.com/$SLUG/releases/latest/download/Frametop.zip" # newest non-prerelease
|
|
else
|
|
curl -fsSL "https://api.github.com/repos/$SLUG/releases?per_page=30" | python3 -c '
|
|
import json, sys
|
|
for r in json.load(sys.stdin): # newest first
|
|
for a in r.get("assets", []):
|
|
if a.get("name") == "Frametop.zip" and not r.get("draft"):
|
|
print(a["browser_download_url"])
|
|
sys.exit(0)
|
|
sys.exit(1)'
|
|
fi
|
|
}
|
|
|
|
# install_release CHANNEL VERSION ZIP DIR CLONE_ONLY ANY_STEAMOS -- INSTALL_ARGS...
|
|
install_release() {
|
|
local channel=$1 version=$2 zip=$3 dir=$4 clone_only=$5 any=$6 cache=$HOME/.cache/frametop/release
|
|
shift 7
|
|
local args=("$@")
|
|
[ -n "$dir" ] && args+=(--dir "$dir")
|
|
[ "$clone_only" = 1 ] && args+=(--unpack-only)
|
|
[ "$any" = 1 ] && args+=(--any-steamos)
|
|
mkdir -p "$cache"
|
|
local url= file loc key status=0
|
|
case $zip in
|
|
https://*|'')
|
|
url=$zip
|
|
if [ -z "$url" ]; then
|
|
url=$(release_zip "$channel" "$version") ||
|
|
{ echo "couldn't find a Frametop release on GitHub (or GitHub can't be reached)" >&2; return 1; }
|
|
fi
|
|
# "latest" names a different file after each release: resume only the same release's.
|
|
if [[ $url == */releases/latest/download/* ]]; then
|
|
loc=$(curl -fsSI --proto '=https' "$url" | tr -d '\r' | sed -n 's/^[Ll]ocation: //p' | head -1)
|
|
[[ $loc == https://github.com/* ]] && url=$loc
|
|
fi
|
|
key=$(printf %s "$url" | sha256sum | cut -c1-16)
|
|
file=$cache/Frametop-$key.zip
|
|
find "$cache" -maxdepth 1 -name 'Frametop-*.zip*' ! -name "Frametop-$key.zip*" -delete
|
|
if [ ! -f "$file" ]; then
|
|
echo "Downloading $url (about 1.1 GB; if it stops, run this again to resume)"
|
|
curl -fL --proto '=https' -C - -o "$file.part" "$url" ||
|
|
{ echo "the download didn't finish: run this again to resume it" >&2; return 1; }
|
|
mv "$file.part" "$file"
|
|
fi ;;
|
|
*://*) echo "the zip has to come over https: $zip" >&2; return 1 ;;
|
|
*) file=$zip; [ -f "$file" ] || { echo "no file $file" >&2; return 1; } ;;
|
|
esac
|
|
echo "Unpacking $file"
|
|
rm -rf "$cache/unpacked"
|
|
if ! unzip -q "$file" -d "$cache/unpacked"; then
|
|
[ -n "$url" ] && rm -f "$file"
|
|
echo "$file didn't unpack: run this again to download it again" >&2
|
|
return 1
|
|
fi
|
|
"$cache/unpacked/Frametop/install-release.sh" "${args[@]}" || status=$?
|
|
rm -rf "$cache/unpacked"
|
|
if [ "$status" = 3 ] && [ -n "$url" ]; then
|
|
rm -f "$file" # damaged: the next run downloads it again
|
|
fi
|
|
[ "$status" = 0 ] || return "$status"
|
|
# Loaded into podman and copied out: the download isn't needed any more.
|
|
[ "$clone_only" = 1 ] || rm -rf "$cache"
|
|
}
|
|
|
|
# Everything happens in main, called on the last line, so a download cut short runs nothing.
|
|
main() {
|
|
local repo=https://github.com/Frametop/frametop.git dir= branch= clone_only=0
|
|
local yes=0 tty=0 current= def answer release=0 zip= want= any=0
|
|
local pass=()
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--stable) branch=main ;;
|
|
--experimental) branch=experimental ;;
|
|
--branch) branch=${2:?--branch needs a branch name}; shift ;;
|
|
--dir) dir=${2:?--dir needs a folder}; shift ;;
|
|
--clone-only) clone_only=1 ;;
|
|
--release) release=1 ;;
|
|
--zip) zip=${2:?--zip needs a file or URL}; shift ;;
|
|
--version) want=${2:?--version needs a version}; shift ;;
|
|
--any-steamos) any=1 ;;
|
|
--yes) yes=1; pass+=("$1") ;;
|
|
--no-bluetooth|--bluetooth|--no-eye-tracker|--no-voice) pass+=("$1") ;;
|
|
-h|--help) usage; return 0 ;;
|
|
*) echo "unknown option: $1" >&2; usage >&2; return 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
if [ "$release" = 0 ] && { [ -n "$zip" ] || [ -n "$want" ] || [ "$any" = 1 ]; }; then
|
|
echo "--zip, --version, and --any-steamos go with --release" >&2
|
|
return 2
|
|
fi
|
|
if [ "$release" = 1 ] && [ -n "$branch" ] && [ "$branch" != main ] && [ "$branch" != experimental ]; then
|
|
echo "releases come from the stable or experimental list, not a branch" >&2
|
|
return 2
|
|
fi
|
|
local dir_arg=$dir # the menu's release choice puts releases in their own default place
|
|
if [ -z "$dir" ] && [ "$release" = 0 ]; then
|
|
dir=$HOME/frametop
|
|
fi
|
|
|
|
if ! { grep -qx 'ID=steamos' /etc/os-release && grep -qE '^VARIANT_ID="?vr"?$' /etc/os-release; } 2>/dev/null; then
|
|
echo "Frametop installs on a Steam Frame (SteamOS, VR variant). Run this in a terminal on the headset." >&2
|
|
return 1
|
|
fi
|
|
# Piped into bash, stdin is this script: the questions (here and install.sh's) read the terminal.
|
|
{ : </dev/tty; } 2>/dev/null && tty=1
|
|
if [ "$tty" = 0 ] && [ "$yes" = 0 ]; then
|
|
echo "This asks questions, and there's no terminal to ask in: run it in one, or add --yes." >&2
|
|
return 1
|
|
fi
|
|
|
|
if [ "$release" = 0 ] && [ -e "$dir/.git" ]; then
|
|
git -C "$dir" remote get-url origin 2>/dev/null | grep -qi 'frametop' ||
|
|
{ echo "$dir is a git repo, but not Frametop's. Pick another folder with --dir." >&2; return 1; }
|
|
current=$(git -C "$dir" branch --show-current)
|
|
elif [ "$release" = 0 ] && [ -e "$dir" ]; then
|
|
echo "$dir is there and isn't Frametop's repo. Move it, or pick another folder with --dir." >&2
|
|
return 1
|
|
elif [ "$release" = 1 ] && [ -f "${dir:-$HOME/.local/share/frametop/releases}/current/.frametop-release" ]; then
|
|
current=$(sed -n 's/^CHANNEL=//p' "${dir:-$HOME/.local/share/frametop/releases}/current/.frametop-release")
|
|
[ "$current" = stable ] && current=main
|
|
fi
|
|
|
|
if [ -z "$branch" ] && { [ "$release" = 0 ] || { [ -z "$zip" ] && [ -z "$want" ]; }; }; then
|
|
def=main
|
|
[ "$current" = experimental ] && def=experimental
|
|
if [ "$yes" = 1 ]; then
|
|
branch=$def
|
|
else
|
|
echo "Which version of Frametop?"
|
|
echo " 1) stable: the main branch, tested releases"
|
|
echo " 2) experimental: the newest features, less tested"
|
|
if [ "$release" = 0 ]; then
|
|
echo " 3) stable release: built, nothing to compile (a 1.1 GB download)"
|
|
echo " 4) experimental release: built, nothing to compile (a 1.1 GB download)"
|
|
fi
|
|
[ -n "$current" ] && echo "(installed now: $current)"
|
|
read -r -p "Choose 1$([ "$release" = 0 ] && echo ", 2, 3, or 4" || echo " or 2") [$([ "$def" = main ] && echo 1 || echo 2)]: " \
|
|
answer </dev/tty || answer=
|
|
case ${answer:-$def} in
|
|
1|main|s*) branch=main ;;
|
|
2|experimental|e*) branch=experimental ;;
|
|
3|4) [ "$release" = 0 ] || { echo "not 1 or 2: $answer" >&2; return 2; }
|
|
release=1 dir=$dir_arg
|
|
branch=$([ "$answer" = 3 ] && echo main || echo experimental) ;;
|
|
*) echo "not one of the choices: $answer" >&2; return 2 ;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
if [ "$release" = 1 ]; then
|
|
install_release "$([ "$branch" = experimental ] && echo experimental || echo stable)" "$want" "$zip" "$dir" \
|
|
"$clone_only" "$any" -- ${pass[@]+"${pass[@]}"}
|
|
return
|
|
fi
|
|
|
|
if ! git ls-remote --exit-code --heads "$repo" "$branch" >/dev/null; then
|
|
echo "Frametop has no branch called $branch (or GitHub can't be reached)." >&2
|
|
return 1
|
|
fi
|
|
|
|
if [ ! -e "$dir" ]; then
|
|
echo "Cloning Frametop ($branch) into $dir"
|
|
git clone --branch "$branch" "$repo" "$dir"
|
|
else
|
|
if ! git -C "$dir" diff --quiet || ! git -C "$dir" diff --cached --quiet; then
|
|
echo "$dir has changes of its own. Commit or stash them first (git -C $dir status)." >&2
|
|
return 1
|
|
fi
|
|
echo "Updating $dir to the latest $branch"
|
|
git -C "$dir" fetch --quiet origin
|
|
if [ "$current" != "$branch" ]; then
|
|
if git -C "$dir" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
git -C "$dir" switch --quiet "$branch"
|
|
else
|
|
git -C "$dir" switch --quiet --track -c "$branch" "origin/$branch"
|
|
fi
|
|
fi
|
|
git -C "$dir" merge --ff-only --quiet "origin/$branch" ||
|
|
{ echo "$dir has commits of its own on $branch, so it can't just move to the latest. Update it by hand." >&2; return 1; }
|
|
fi
|
|
echo "Frametop $branch: $(git -C "$dir" log -1 --format='%h %s')"
|
|
|
|
if [ "$clone_only" = 1 ]; then
|
|
echo "Install with: cd $dir && ./install.sh"
|
|
return 0
|
|
fi
|
|
cd "$dir"
|
|
if [ "$tty" = 1 ]; then
|
|
./install.sh "${pass[@]}" </dev/tty
|
|
else
|
|
./install.sh "${pass[@]}" </dev/null
|
|
fi
|
|
}
|
|
|
|
main "$@"
|