#!/bin/bash # # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # One sweep of the local AI stack for the drawer's AI page. Always prints # these six lines, tab separated, and exits 0: # # enginellamaup|downidle|busymodel # enginesdup|downidle|busymodel # appassistantrunning|stopped- # appchatrunning|stopped- # appimggenrunning|stoppedmodel # appfanficrunning|stoppedpid # # An empty value is "-". A probe that fails reads as down or stopped, not as # an error: every row is something that may simply not be running. # # The test stubs curl, pgrep, llamachat and imggen on PATH and points # AI_ASSISTANT and AI_SD_LOG at fixtures; see test-ai-state.sh. set -u LLAMA="${AI_LLAMA_URL:-http://127.0.0.1:8181}" SD_LOG="${AI_SD_LOG:-${XDG_CACHE_HOME:-$HOME/.cache}/sd-server.log}" ASSISTANT="${AI_ASSISTANT:-$HOME/Programming/GIT/desktop-assistant/assistant.sh}" row() { local IFS=$'\t'; printf '%s\n' "$*"; } llama() { local models id busy=idle models="$(curl -sf -m 2 "$LLAMA/models")" || { row engine llama down idle -; return; } id="$(jq -r 'first(.data[] | select(.status.value == "loaded") | .id) // empty' \ <<<"$models" 2>/dev/null)" # autoload=false is not optional: without it this probe loads an unloaded # model into VRAM, and the model can unload between the two requests. if [[ -n "$id" ]] && curl -sf -m 2 -G --data-urlencode "model=$id" -d autoload=false "$LLAMA/slots" | jq -e 'any(.[]; .is_processing)' >/dev/null 2>&1; then busy=busy fi row engine llama up "$busy" "${id:--}" } sd() { local line words i model=- busy=idle mtime line="$(pgrep -axo sd-server)" || { row engine sd down idle -; return; } read -ra words <<<"$line" for ((i = 1; i < ${#words[@]} - 1; i++)); do case "${words[i]}" in --diffusion-model|-m) model="${words[i + 1]##*/}" ;; esac done # ponytail: sd-server has no progress endpoint, but its log streams # progress bars while generating, so a log written in the last 5s means # busy. A step slower than 5s reads as idle. mtime="$(stat -c %Y "$SD_LOG" 2>/dev/null)" || mtime=0 (( $(date +%s) - mtime < 5 )) && busy=busy row engine sd up "$busy" "$model" } apps() { local out pid if "$ASSISTANT" status >/dev/null 2>&1; then row app assistant running - else row app assistant stopped - fi if llamachat --ping >/dev/null 2>&1; then row app chat running - else row app chat stopped - fi # imggen status exits 0 either way: the daemon's JSON when it is up, # "daemon down" when it is not. out="$(imggen status 2>/dev/null)" if [[ "$out" == "{"* ]]; then row app imggen running "$(jq -r '.model // "-"' <<<"$out" 2>/dev/null || echo -)" else row app imggen stopped - fi # It runs through env, so the command line is python3 then the script, # by path from ~/bin or by ./fanfictioner from the repo. if pid="$(pgrep -u "$USER" -fo '^[^ ]*python3[^ ]* [^ ]*fanfictioner( |$)')"; then row app fanfic running "$pid" else row app fanfic stopped - fi } llama sd apps exit 0