249 lines
5.4 KiB
Bash
Executable File
249 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
tools/watch_prompt_image_folder.sh --folder DIR --target TMUX_TARGET [options]
|
|
|
|
Watch a folder for prompt/image pairs and notify a selected Byobu/tmux Codex
|
|
pane using tmux send-keys. Prompt and image files are paired by basename:
|
|
atlas_case_001.txt + atlas_case_001.png
|
|
atlas_case_002.prompt + atlas_case_002.jpg
|
|
|
|
Required:
|
|
--folder DIR Folder to watch.
|
|
--target TARGET tmux target pane, for example session:1.0.
|
|
|
|
Options:
|
|
--notes FILE Notes/output file to mention in the Codex message.
|
|
Default: DIR/prompt-learning.md
|
|
--state FILE Seen-state file. Default: DIR/.sxcp_watch_seen
|
|
--once Scan once and exit.
|
|
--dry-run Print tmux send-keys commands instead of sending them.
|
|
--poll-interval SEC Poll/fallback interval in seconds. Default: 2
|
|
--stable-delay SEC Seconds to wait before accepting a new image. Default: 1
|
|
--prompt-exts CSV Prompt extensions. Default: txt,prompt
|
|
--image-exts CSV Image extensions. Default: png,jpg,jpeg,webp
|
|
-h, --help Show this help.
|
|
|
|
Inside Byobu/tmux, get the current target with:
|
|
tmux display-message -p '#S:#I.#P'
|
|
EOF
|
|
}
|
|
|
|
folder=""
|
|
target=""
|
|
notes=""
|
|
state=""
|
|
once=0
|
|
dry_run=0
|
|
poll_interval=2
|
|
stable_delay=1
|
|
prompt_exts_csv="txt,prompt"
|
|
image_exts_csv="png,jpg,jpeg,webp"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--folder)
|
|
folder="${2:-}"
|
|
shift 2
|
|
;;
|
|
--target)
|
|
target="${2:-}"
|
|
shift 2
|
|
;;
|
|
--notes)
|
|
notes="${2:-}"
|
|
shift 2
|
|
;;
|
|
--state)
|
|
state="${2:-}"
|
|
shift 2
|
|
;;
|
|
--once)
|
|
once=1
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
dry_run=1
|
|
shift
|
|
;;
|
|
--poll-interval)
|
|
poll_interval="${2:-}"
|
|
shift 2
|
|
;;
|
|
--stable-delay)
|
|
stable_delay="${2:-}"
|
|
shift 2
|
|
;;
|
|
--prompt-exts)
|
|
prompt_exts_csv="${2:-}"
|
|
shift 2
|
|
;;
|
|
--image-exts)
|
|
image_exts_csv="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$folder" || -z "$target" ]]; then
|
|
echo "--folder and --target are required" >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ ! -d "$folder" ]]; then
|
|
echo "folder does not exist: $folder" >&2
|
|
exit 2
|
|
fi
|
|
|
|
folder="$(cd "$folder" && pwd -P)"
|
|
if [[ -z "$notes" ]]; then
|
|
notes="$folder/prompt-learning.md"
|
|
fi
|
|
if [[ -z "$state" ]]; then
|
|
state="$folder/.sxcp_watch_seen"
|
|
fi
|
|
if [[ "$notes" != /* ]]; then
|
|
notes="$PWD/$notes"
|
|
fi
|
|
if [[ "$state" != /* ]]; then
|
|
state="$PWD/$state"
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$state")"
|
|
touch "$state"
|
|
|
|
IFS=',' read -r -a prompt_exts <<< "$prompt_exts_csv"
|
|
IFS=',' read -r -a image_exts <<< "$image_exts_csv"
|
|
|
|
shell_quote() {
|
|
printf '%q' "$1"
|
|
}
|
|
|
|
state_key() {
|
|
local prompt_path="$1"
|
|
local image_path="$2"
|
|
printf '%s | %s\n' "$prompt_path" "$image_path"
|
|
}
|
|
|
|
is_seen() {
|
|
local key="$1"
|
|
grep -Fxq -- "$key" "$state"
|
|
}
|
|
|
|
mark_seen() {
|
|
local key="$1"
|
|
printf '%s\n' "$key" >> "$state"
|
|
}
|
|
|
|
file_size() {
|
|
wc -c < "$1" | tr -d '[:space:]'
|
|
}
|
|
|
|
wait_for_stable_image() {
|
|
local image_path="$1"
|
|
if [[ "$dry_run" -eq 1 || "$stable_delay" == "0" ]]; then
|
|
return 0
|
|
fi
|
|
local before
|
|
local after
|
|
before="$(file_size "$image_path")"
|
|
sleep "$stable_delay"
|
|
after="$(file_size "$image_path")"
|
|
[[ "$before" == "$after" ]]
|
|
}
|
|
|
|
find_image_for_prompt() {
|
|
local prompt_path="$1"
|
|
local filename
|
|
local stem
|
|
local ext
|
|
local candidate
|
|
filename="$(basename "$prompt_path")"
|
|
stem="${filename%.*}"
|
|
for ext in "${image_exts[@]}"; do
|
|
candidate="$folder/$stem.$ext"
|
|
if [[ -f "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
candidate="$folder/$stem.${ext^^}"
|
|
if [[ -f "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
notify_codex() {
|
|
local prompt_path="$1"
|
|
local image_path="$2"
|
|
local message
|
|
message="New atlas sample ready: prompt=$prompt_path image=$image_path. Analyze it and append prompt-learning notes to $notes."
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
printf 'tmux send-keys -t %s %s Enter\n' "$(shell_quote "$target")" "$(shell_quote "$message")"
|
|
return 0
|
|
fi
|
|
tmux send-keys -t "$target" "$message" Enter
|
|
}
|
|
|
|
scan_once() {
|
|
local notified=0
|
|
local ext
|
|
local prompt_path
|
|
local image_path
|
|
local key
|
|
for ext in "${prompt_exts[@]}"; do
|
|
while IFS= read -r -d '' prompt_path; do
|
|
if ! image_path="$(find_image_for_prompt "$prompt_path")"; then
|
|
continue
|
|
fi
|
|
key="$(state_key "$prompt_path" "$image_path")"
|
|
if is_seen "$key"; then
|
|
continue
|
|
fi
|
|
if ! wait_for_stable_image "$image_path"; then
|
|
continue
|
|
fi
|
|
notify_codex "$prompt_path" "$image_path"
|
|
mark_seen "$key"
|
|
notified=$((notified + 1))
|
|
done < <(find "$folder" -maxdepth 1 -type f \( -iname "*.$ext" \) -print0)
|
|
done
|
|
if [[ "$notified" -eq 0 && "$once" -eq 1 ]]; then
|
|
echo "no new prompt/image pairs in $folder"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
if [[ "$once" -eq 1 ]]; then
|
|
scan_once
|
|
exit 0
|
|
fi
|
|
|
|
scan_once
|
|
if command -v inotifywait >/dev/null 2>&1; then
|
|
while inotifywait -qq -e close_write,create,moved_to "$folder"; do
|
|
scan_once
|
|
done
|
|
else
|
|
echo "inotifywait not found; polling $folder every $poll_interval seconds" >&2
|
|
while true; do
|
|
sleep "$poll_interval"
|
|
scan_once
|
|
done
|
|
fi
|