feat(bin): track canonical user launchers
This commit is contained in:
parent
7da6cec15c
commit
8551db9e35
10 changed files with 1391 additions and 2 deletions
10
bin/.local/bin/colmap-amd
Executable file
10
bin/.local/bin/colmap-amd
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Évite un rendu logiciel Mesa accidentel.
|
||||
unset LIBGL_ALWAYS_SOFTWARE
|
||||
unset MESA_LOADER_DRIVER_OVERRIDE
|
||||
|
||||
# Utilise Xwayland/GLX pour le contexte OpenGL de SIFTGPU.
|
||||
export QT_QPA_PLATFORM=xcb
|
||||
|
||||
exec colmap "$@"
|
||||
33
bin/.local/bin/firefox-scribd
Executable file
33
bin/.local/bin/firefox-scribd
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly PROFILE="$HOME/.mozilla/firefox-scribd"
|
||||
readonly URL="${1:-https://fr.scribd.com/}"
|
||||
|
||||
case "$URL" in
|
||||
http://scribd.com/*|https://scribd.com/*|\
|
||||
http://*.scribd.com/*|https://*.scribd.com/*)
|
||||
;;
|
||||
*)
|
||||
notify-send \
|
||||
"Firefox Scribd" \
|
||||
"URL refusée : ce lanceur accepte uniquement scribd.com"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$PROFILE"
|
||||
|
||||
exec systemd-run \
|
||||
--user \
|
||||
--scope \
|
||||
--quiet \
|
||||
--unit="firefox-scribd-$(date +%s)-$$" \
|
||||
--slice=app-graphical.slice \
|
||||
--property=MemoryMax=8G \
|
||||
--property=MemorySwapMax=0 \
|
||||
/usr/bin/firefox \
|
||||
--no-remote \
|
||||
--profile "$PROFILE" \
|
||||
--new-window "$URL"
|
||||
|
|
@ -1 +0,0 @@
|
|||
/home/fy59/.local/src/llama.cpp/build/bin/llama-cli
|
||||
|
|
@ -1 +0,0 @@
|
|||
/home/fy59/.local/src/llama.cpp/build/bin/llama-server
|
||||
108
bin/.local/bin/opencode-away
Executable file
108
bin/.local/bin/opencode-away
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
printf '%s\n' 'usage: opencode-away [--check] [opencode arguments...]'
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
printf 'opencode-away: required command not found: %s\n' "$1" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if [[ ${1-} == --help ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_only=0
|
||||
if [[ ${1-} == --check ]]; then
|
||||
check_only=1
|
||||
shift
|
||||
fi
|
||||
|
||||
require_command opencode || exit 127
|
||||
require_command systemd-inhibit || exit 127
|
||||
require_command git || exit 127
|
||||
require_command ps || exit 127
|
||||
require_command kill || exit 127
|
||||
|
||||
if [[ $(systemd-inhibit --help 2>&1) != *handle-lid-switch* ]]; then
|
||||
printf '%s\n' 'opencode-away: systemd-inhibit lacks handle-lid-switch' >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
printf '%s\n' 'opencode-away: run this command inside a Git worktree' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ((check_only)); then
|
||||
printf '%s\n' 'OpenCode away mode check: OK'
|
||||
printf 'repository: %s\n' "$(git rev-parse --show-toplevel)"
|
||||
printf 'opencode: %s\n' "$(command -v opencode)"
|
||||
printf 'systemd-inhibit: %s\n' "$(command -v systemd-inhibit)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -a suspended_pids=()
|
||||
cleanup_done=0
|
||||
|
||||
restore_swayidle() {
|
||||
local pid
|
||||
((cleanup_done)) && return 0
|
||||
cleanup_done=1
|
||||
for pid in "${suspended_pids[@]}"; do
|
||||
if kill -0 "$pid" 2>/dev/null &&
|
||||
[[ $(ps -o comm= -p "$pid" 2>/dev/null) == swayidle ]]; then
|
||||
kill -CONT "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
if ((${#suspended_pids[@]})); then
|
||||
printf '%s\n' 'swayidle restored'
|
||||
fi
|
||||
}
|
||||
|
||||
trap restore_swayidle EXIT
|
||||
|
||||
while read -r pid state; do
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
[[ $state == T* ]] && continue
|
||||
if kill -STOP "$pid" 2>/dev/null; then
|
||||
suspended_pids+=("$pid")
|
||||
fi
|
||||
done < <(ps -C swayidle -o pid=,stat= 2>/dev/null)
|
||||
|
||||
printf '%s\n' 'OpenCode away mode'
|
||||
printf '%s\n' 'sleep inhibited'
|
||||
printf '%s\n' 'lid switch inhibited'
|
||||
if ((${#suspended_pids[@]})); then
|
||||
printf 'swayidle suspended:'
|
||||
printf ' %s' "${suspended_pids[@]}"
|
||||
printf '\n'
|
||||
else
|
||||
printf '%s\n' 'swayidle suspended: none active'
|
||||
fi
|
||||
|
||||
export LARDON_OPENCODE_LONG_RUN=1
|
||||
|
||||
if [[ -n ${OPENCODE_AWAY_COMMAND-} ]]; then
|
||||
child_command=(bash -c "$OPENCODE_AWAY_COMMAND")
|
||||
else
|
||||
child_command=(opencode --auto "$@")
|
||||
fi
|
||||
|
||||
systemd-inhibit \
|
||||
--what=sleep:idle:handle-lid-switch \
|
||||
--who='OpenCode Long Run' \
|
||||
--why='Autonomous Lardon3D development ticket' \
|
||||
--mode=block \
|
||||
"${child_command[@]}"
|
||||
|
||||
child_status=$?
|
||||
|
||||
restore_swayidle
|
||||
|
||||
exit "$child_status"
|
||||
91
bin/.local/bin/pi-dev
Executable file
91
bin/.local/bin/pi-dev
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SWAYIDLE_MODE="none"
|
||||
declare -a SWAYIDLE_CMD=()
|
||||
|
||||
cleanup() {
|
||||
rc=$?
|
||||
|
||||
trap - EXIT INT TERM HUP
|
||||
|
||||
printf '\n[pi-dev] Arrêt de la session...\n'
|
||||
|
||||
# Restaurer swayidle exactement selon son mode d'origine.
|
||||
case "$SWAYIDLE_MODE" in
|
||||
systemd)
|
||||
printf '[pi-dev] Redémarrage de swayidle.service...\n'
|
||||
systemctl --user start swayidle.service || true
|
||||
;;
|
||||
|
||||
process)
|
||||
if ((${#SWAYIDLE_CMD[@]})); then
|
||||
printf '[pi-dev] Redémarrage de swayidle...\n'
|
||||
nohup "${SWAYIDLE_CMD[@]}" \
|
||||
>/dev/null 2>&1 &
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '[pi-dev] Session restaurée.\n'
|
||||
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM HUP
|
||||
|
||||
|
||||
#
|
||||
# 1. Neutraliser swayidle
|
||||
#
|
||||
|
||||
if systemctl --user is-active --quiet swayidle.service 2>/dev/null; then
|
||||
|
||||
SWAYIDLE_MODE="systemd"
|
||||
|
||||
printf '[pi-dev] Neutralisation de swayidle.service...\n'
|
||||
systemctl --user stop swayidle.service
|
||||
|
||||
else
|
||||
|
||||
SWAYIDLE_PID="$(pgrep -xo swayidle || true)"
|
||||
|
||||
if [[ -n "$SWAYIDLE_PID" ]]; then
|
||||
|
||||
SWAYIDLE_MODE="process"
|
||||
|
||||
# Sauvegarder exactement argv avant d'arrêter swayidle.
|
||||
mapfile -d '' SWAYIDLE_CMD \
|
||||
< "/proc/$SWAYIDLE_PID/cmdline"
|
||||
|
||||
printf '[pi-dev] Neutralisation de swayidle (PID %s)...\n' \
|
||||
"$SWAYIDLE_PID"
|
||||
|
||||
kill "$SWAYIDLE_PID"
|
||||
|
||||
# Attendre sa disparition.
|
||||
for _ in {1..20}; do
|
||||
kill -0 "$SWAYIDLE_PID" 2>/dev/null || break
|
||||
sleep 0.05
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
#
|
||||
# 2. Lancer Pi avec inhibition de la fermeture du capot
|
||||
#
|
||||
# L'inhibitor existe uniquement pendant que Pi tourne.
|
||||
#
|
||||
|
||||
printf '[pi-dev] swayidle neutralisé.\n'
|
||||
printf '[pi-dev] Veille fermeture du capot neutralisée.\n'
|
||||
printf '[pi-dev] Lancement de Pi / OpenRouter.\n\n'
|
||||
|
||||
systemd-inhibit \
|
||||
--what=handle-lid-switch \
|
||||
--who="pi-dev" \
|
||||
--why="Session de développement Pi / Lardon3D" \
|
||||
--mode=block \
|
||||
pi "$@"
|
||||
697
bin/.local/bin/scan3d
Executable file
697
bin/.local/bin/scan3d
Executable file
|
|
@ -0,0 +1,697 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# scan3d
|
||||
# Pipeline interactif :
|
||||
# photos -> pré-tri -> COLMAP -> OpenMVS -> maillage texturé
|
||||
#
|
||||
# Les photos originales ne sont jamais déplacées ni supprimées.
|
||||
# Le script s'arrête avant l'ouverture de MeshLab.
|
||||
|
||||
OPENMVS_BIN="${OPENMVS_BIN:-$HOME/Applications/openmvs-2.4.0/openmvs}"
|
||||
OPENMVS_IMAGE="${OPENMVS_IMAGE:-localhost/openmvs:2.4.0}"
|
||||
|
||||
THREADS="${SCAN3D_THREADS:-4}"
|
||||
SEQUENTIAL_OVERLAP="${SCAN3D_OVERLAP:-20}"
|
||||
RESOLUTION_LEVEL="${SCAN3D_RESOLUTION_LEVEL:-3}"
|
||||
NUMBER_VIEWS="${SCAN3D_NUMBER_VIEWS:-4}"
|
||||
MIN_FREE_GIB="${SCAN3D_MIN_FREE_GIB:-45}"
|
||||
|
||||
USE_GPU=1
|
||||
KEEP_DEPTHMAPS=0
|
||||
|
||||
RED=$'\e[31m'
|
||||
GREEN=$'\e[32m'
|
||||
YELLOW=$'\e[33m'
|
||||
BLUE=$'\e[34m'
|
||||
BOLD=$'\e[1m'
|
||||
RESET=$'\e[0m'
|
||||
|
||||
log() {
|
||||
printf '\n%s%s==>%s %s\n' "$BOLD" "$BLUE" "$RESET" "$*"
|
||||
}
|
||||
|
||||
ok() {
|
||||
printf '%s✓%s %s\n' "$GREEN" "$RESET" "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '%s⚠%s %s\n' "$YELLOW" "$RESET" "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '%sERREUR:%s %s\n' "$RED" "$RESET" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup_on_error() {
|
||||
local code=$?
|
||||
if (( code != 0 )); then
|
||||
printf '\n%sLe pipeline s’est arrêté avec le code %d.%s\n' \
|
||||
"$RED" "$code" "$RESET" >&2
|
||||
printf 'Relance simplement %sscan3d%s pour reprendre à la dernière étape validée.\n' \
|
||||
"$BOLD" "$RESET" >&2
|
||||
fi
|
||||
}
|
||||
trap cleanup_on_error EXIT
|
||||
|
||||
prompt_default() {
|
||||
local prompt="$1"
|
||||
local default="$2"
|
||||
local answer
|
||||
read -r -e -p "$prompt [$default] : " answer
|
||||
printf '%s' "${answer:-$default}"
|
||||
}
|
||||
|
||||
yes_no() {
|
||||
local prompt="$1"
|
||||
local default="${2:-n}"
|
||||
local answer suffix
|
||||
|
||||
if [[ "$default" == "o" ]]; then
|
||||
suffix="[O/n]"
|
||||
else
|
||||
suffix="[o/N]"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt $suffix : " answer
|
||||
answer="${answer:-$default}"
|
||||
case "${answer,,}" in
|
||||
o|oui|y|yes) return 0 ;;
|
||||
n|non|no) return 1 ;;
|
||||
*) printf 'Réponds par o ou n.\n' ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "commande introuvable : $1"
|
||||
}
|
||||
|
||||
run_logged() {
|
||||
local logfile="$1"
|
||||
shift
|
||||
mkdir -p "$(dirname "$logfile")"
|
||||
"$@" 2>&1 | tee "$logfile"
|
||||
}
|
||||
|
||||
natural_list() {
|
||||
find "$@" -print0 | sort -zV
|
||||
}
|
||||
|
||||
choose_source_dir() {
|
||||
local default_dir="$PWD"
|
||||
local entered
|
||||
|
||||
while true; do
|
||||
entered="$(prompt_default 'Dossier contenant les photos' "$default_dir")"
|
||||
entered="${entered/#\~/$HOME}"
|
||||
entered="$(realpath -m "$entered")"
|
||||
|
||||
if [[ -d "$entered" ]]; then
|
||||
RAW_DIR="$entered"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Le dossier n’existe pas : $entered"
|
||||
done
|
||||
}
|
||||
|
||||
choose_categories() {
|
||||
printf '\n%sQuelles photos utiliser pour la reconstruction ?%s\n' "$BOLD" "$RESET"
|
||||
printf ' 1) Bonnes uniquement — recommandé\n'
|
||||
printf ' 2) Bonnes + suspectes — plus complet, plus risqué\n'
|
||||
printf ' 3) Toutes, y compris mauvaises — déconseillé\n'
|
||||
|
||||
local choice
|
||||
while true; do
|
||||
read -r -p 'Choix [1] : ' choice
|
||||
choice="${choice:-1}"
|
||||
case "$choice" in
|
||||
1)
|
||||
SELECTED_CATEGORIES=("bonnes")
|
||||
CATEGORY_LABEL="bonnes"
|
||||
return
|
||||
;;
|
||||
2)
|
||||
SELECTED_CATEGORIES=("bonnes" "suspectes")
|
||||
CATEGORY_LABEL="bonnes-suspectes"
|
||||
return
|
||||
;;
|
||||
3)
|
||||
SELECTED_CATEGORIES=("bonnes" "suspectes" "mauvaises")
|
||||
CATEGORY_LABEL="toutes"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
printf 'Choisis 1, 2 ou 3.\n'
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
write_tri_script() {
|
||||
cat > "$TRI_PY" <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
path: Path
|
||||
category: str
|
||||
sharpness: float
|
||||
white: float
|
||||
black: float
|
||||
brightness: float
|
||||
reason: str
|
||||
|
||||
|
||||
def natural_key(path: Path) -> list[object]:
|
||||
return [
|
||||
int(part) if part.isdigit() else part.lower()
|
||||
for part in re.split(r"(\d+)", path.name)
|
||||
]
|
||||
|
||||
|
||||
def load_gray(path: Path) -> np.ndarray:
|
||||
with Image.open(path) as source:
|
||||
source = source.convert("L")
|
||||
gray = np.asarray(source)
|
||||
|
||||
height, width = gray.shape
|
||||
maximum_dimension = 1600
|
||||
|
||||
if max(width, height) > maximum_dimension:
|
||||
scale = maximum_dimension / max(width, height)
|
||||
gray = cv2.resize(
|
||||
gray,
|
||||
(
|
||||
max(1, round(width * scale)),
|
||||
max(1, round(height * scale)),
|
||||
),
|
||||
interpolation=cv2.INTER_AREA,
|
||||
)
|
||||
|
||||
return gray
|
||||
|
||||
|
||||
def analyze(path: Path) -> tuple[dict[str, float] | None, str]:
|
||||
try:
|
||||
gray = load_gray(path)
|
||||
except Exception as error:
|
||||
return None, f"illisible: {error}"
|
||||
|
||||
return {
|
||||
"sharpness": float(cv2.Laplacian(gray, cv2.CV_64F).var()),
|
||||
"white": float(np.count_nonzero(gray >= 250) / gray.size * 100),
|
||||
"black": float(np.count_nonzero(gray <= 5) / gray.size * 100),
|
||||
"brightness": float(gray.mean()),
|
||||
}, ""
|
||||
|
||||
|
||||
def classify(path: Path, metrics: dict[str, float] | None, error: str) -> Result:
|
||||
if metrics is None:
|
||||
return Result(path, "mauvaises", 0.0, 0.0, 0.0, 0.0, error)
|
||||
|
||||
sharpness = metrics["sharpness"]
|
||||
white = metrics["white"]
|
||||
black = metrics["black"]
|
||||
brightness = metrics["brightness"]
|
||||
|
||||
reasons: list[str] = []
|
||||
|
||||
if sharpness < 100:
|
||||
reasons.append("très floue")
|
||||
elif sharpness < 170:
|
||||
reasons.append("floue possible")
|
||||
|
||||
if white > 20:
|
||||
reasons.append("très surexposée")
|
||||
elif white > 8:
|
||||
reasons.append("surexposition possible")
|
||||
|
||||
if black > 35:
|
||||
reasons.append("très sombre")
|
||||
elif black > 15:
|
||||
reasons.append("ombres bouchées possibles")
|
||||
|
||||
if brightness > 225 or brightness < 25:
|
||||
reasons.append("luminosité extrême")
|
||||
|
||||
severe = (
|
||||
sharpness < 100
|
||||
or white > 20
|
||||
or black > 35
|
||||
or brightness > 225
|
||||
or brightness < 25
|
||||
)
|
||||
|
||||
if severe:
|
||||
category = "mauvaises"
|
||||
elif reasons:
|
||||
category = "suspectes"
|
||||
else:
|
||||
category = "bonnes"
|
||||
|
||||
return Result(
|
||||
path,
|
||||
category,
|
||||
sharpness,
|
||||
white,
|
||||
black,
|
||||
brightness,
|
||||
"; ".join(reasons) or "ok",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("images", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
images_dir = args.images.expanduser().resolve()
|
||||
output_dir = args.output.expanduser().resolve()
|
||||
|
||||
excluded_roots = {
|
||||
output_dir,
|
||||
images_dir / "scan3d",
|
||||
images_dir / "tri_resultat",
|
||||
images_dir / "test_colmap",
|
||||
images_dir / "colmap_complet",
|
||||
}
|
||||
|
||||
images: list[Path] = []
|
||||
for path in images_dir.rglob("*"):
|
||||
if not path.is_file() or path.is_symlink():
|
||||
continue
|
||||
if path.suffix.lower() not in EXTENSIONS:
|
||||
continue
|
||||
if any(root == path or root in path.parents for root in excluded_roots):
|
||||
continue
|
||||
images.append(path)
|
||||
|
||||
images.sort(key=natural_key)
|
||||
|
||||
if not images:
|
||||
print("Aucune image trouvée.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"{len(images)} images trouvées.")
|
||||
|
||||
results: list[Result] = []
|
||||
for index, path in enumerate(images, 1):
|
||||
metrics, error = analyze(path)
|
||||
results.append(classify(path, metrics, error))
|
||||
print(
|
||||
f"\rAnalyse {index}/{len(images)} : {path.name[:55]:55s}",
|
||||
end="",
|
||||
flush=True,
|
||||
)
|
||||
print()
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
for category in ("bonnes", "suspectes", "mauvaises"):
|
||||
category_dir = output_dir / category
|
||||
if category_dir.exists():
|
||||
for child in category_dir.iterdir():
|
||||
if child.is_symlink() or child.is_file():
|
||||
child.unlink()
|
||||
category_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with (output_dir / "resultats.csv").open(
|
||||
"w", encoding="utf-8", newline=""
|
||||
) as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(
|
||||
[
|
||||
"fichier",
|
||||
"categorie",
|
||||
"netteté",
|
||||
"blanc_pct",
|
||||
"noir_pct",
|
||||
"luminosité",
|
||||
"raison",
|
||||
]
|
||||
)
|
||||
|
||||
counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0}
|
||||
for result in results:
|
||||
counters[result.category] += 1
|
||||
link_name = f"{counters[result.category]:06d}_{result.path.name}"
|
||||
link_path = output_dir / result.category / link_name
|
||||
link_path.symlink_to(result.path.resolve())
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
str(result.path),
|
||||
result.category,
|
||||
f"{result.sharpness:.2f}",
|
||||
f"{result.white:.2f}",
|
||||
f"{result.black:.2f}",
|
||||
f"{result.brightness:.2f}",
|
||||
result.reason,
|
||||
]
|
||||
)
|
||||
|
||||
for category in ("bonnes", "suspectes", "mauvaises"):
|
||||
count = sum(result.category == category for result in results)
|
||||
print(f"{category}: {count}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
PY
|
||||
}
|
||||
|
||||
podman_openmvs() {
|
||||
podman run --rm \
|
||||
-v "$OPENMVS_BIN:/openmvs:ro,Z" \
|
||||
-v "$WORK_DIR:/data:Z" \
|
||||
-w /data/openmvs \
|
||||
"$OPENMVS_IMAGE" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
printf '%s\n' "${BOLD}scan3d — pipeline photogrammétrique COLMAP + OpenMVS${RESET}"
|
||||
|
||||
require_command colmap
|
||||
require_command podman
|
||||
require_command /usr/bin/python
|
||||
require_command realpath
|
||||
require_command sort
|
||||
require_command find
|
||||
require_command sqlite3
|
||||
|
||||
/usr/bin/python - <<'PY' >/dev/null 2>&1 || die \
|
||||
"Modules Python manquants. Installe : sudo pacman -S python-opencv python-numpy python-pillow"
|
||||
import cv2
|
||||
import numpy
|
||||
from PIL import Image
|
||||
PY
|
||||
|
||||
[[ -x "$OPENMVS_BIN/InterfaceCOLMAP" ]] ||
|
||||
die "OpenMVS introuvable dans : $OPENMVS_BIN"
|
||||
[[ -x "$OPENMVS_BIN/DensifyPointCloud" ]] ||
|
||||
die "DensifyPointCloud introuvable dans : $OPENMVS_BIN"
|
||||
|
||||
podman image exists "$OPENMVS_IMAGE" ||
|
||||
die "Image Podman absente : $OPENMVS_IMAGE"
|
||||
|
||||
choose_source_dir
|
||||
|
||||
PROJECT_ROOT="$RAW_DIR/scan3d"
|
||||
TRI_DIR="$PROJECT_ROOT/tri"
|
||||
TRI_PY="$PROJECT_ROOT/tri_photos.py"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT"
|
||||
write_tri_script
|
||||
|
||||
if [[ -f "$TRI_DIR/resultats.csv" ]]; then
|
||||
if yes_no "Un tri existe déjà. Le réutiliser ?" o; then
|
||||
ok "Tri existant conservé."
|
||||
else
|
||||
log "Nouveau tri des photos"
|
||||
rm -rf "$TRI_DIR"
|
||||
/usr/bin/python "$TRI_PY" "$RAW_DIR" --output "$TRI_DIR"
|
||||
fi
|
||||
else
|
||||
log "Tri des photos"
|
||||
/usr/bin/python "$TRI_PY" "$RAW_DIR" --output "$TRI_DIR"
|
||||
fi
|
||||
|
||||
printf '\nRésultat du tri :\n'
|
||||
for category in bonnes suspectes mauvaises; do
|
||||
count="$(find "$TRI_DIR/$category" -maxdepth 1 -type l 2>/dev/null | wc -l)"
|
||||
printf ' %-11s : %s\n' "$category" "$count"
|
||||
done
|
||||
|
||||
choose_categories
|
||||
|
||||
WORK_DIR="$PROJECT_ROOT/projet-$CATEGORY_LABEL"
|
||||
|
||||
if [[ -d "$WORK_DIR" ]] && find "$WORK_DIR" -mindepth 1 -print -quit | grep -q .; then
|
||||
printf '\nUn projet existe déjà : %s\n' "$WORK_DIR"
|
||||
printf ' 1) Reprendre là où il s’est arrêté\n'
|
||||
printf ' 2) Tout effacer et recommencer\n'
|
||||
printf ' 3) Annuler\n'
|
||||
|
||||
while true; do
|
||||
read -r -p 'Choix [1] : ' resume_choice
|
||||
resume_choice="${resume_choice:-1}"
|
||||
case "$resume_choice" in
|
||||
1) break ;;
|
||||
2)
|
||||
rm -rf "$WORK_DIR"
|
||||
break
|
||||
;;
|
||||
3) exit 0 ;;
|
||||
*) printf 'Choisis 1, 2 ou 3.\n' ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK_DIR"/{images,sparse,logs,openmvs}
|
||||
|
||||
free_gib="$(df -Pk "$WORK_DIR" | awk 'NR==2 {printf "%d", $4/1024/1024}')"
|
||||
printf '\nEspace libre : %s Gio\n' "$free_gib"
|
||||
if (( free_gib < MIN_FREE_GIB )); then
|
||||
warn "Moins de ${MIN_FREE_GIB} Gio libres."
|
||||
yes_no "Continuer malgré le risque de remplir le disque ?" n ||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if yes_no "Conserver les cartes de profondeur après le résultat final ?" n; then
|
||||
KEEP_DEPTHMAPS=1
|
||||
fi
|
||||
|
||||
if yes_no "Utiliser la Radeon via OpenGL pour l’extraction et le matching ?" o; then
|
||||
USE_GPU=1
|
||||
else
|
||||
USE_GPU=0
|
||||
fi
|
||||
|
||||
log "Préparation des images sélectionnées"
|
||||
rm -rf "$WORK_DIR/images"
|
||||
mkdir -p "$WORK_DIR/images"
|
||||
|
||||
index=1
|
||||
for category in "${SELECTED_CATEGORIES[@]}"; do
|
||||
while IFS= read -r -d '' image; do
|
||||
target="$(readlink -f "$image")"
|
||||
[[ -f "$target" ]] || continue
|
||||
|
||||
extension="${target##*.}"
|
||||
extension="${extension,,}"
|
||||
printf -v output_name 'img_%07d.%s' "$index" "$extension"
|
||||
ln -s "$target" "$WORK_DIR/images/$output_name"
|
||||
((index++))
|
||||
done < <(
|
||||
find "$TRI_DIR/$category" -maxdepth 1 -type l -print0 | sort -zV
|
||||
)
|
||||
done
|
||||
|
||||
IMAGE_COUNT="$(find "$WORK_DIR/images" -maxdepth 1 -type l | wc -l)"
|
||||
(( IMAGE_COUNT > 0 )) || die "Aucune image sélectionnée."
|
||||
ok "$IMAGE_COUNT images préparées."
|
||||
|
||||
DATABASE="$WORK_DIR/database.db"
|
||||
|
||||
if [[ ! -f "$WORK_DIR/.features.done" ]]; then
|
||||
log "COLMAP — extraction des caractéristiques"
|
||||
rm -f "$DATABASE"
|
||||
run_logged "$WORK_DIR/logs/01-features.log" \
|
||||
colmap feature_extractor \
|
||||
--database_path "$DATABASE" \
|
||||
--image_path "$WORK_DIR/images" \
|
||||
--ImageReader.single_camera 1 \
|
||||
--ImageReader.camera_model SIMPLE_RADIAL \
|
||||
--FeatureExtraction.use_gpu "$USE_GPU"
|
||||
touch "$WORK_DIR/.features.done"
|
||||
else
|
||||
ok "Extraction déjà terminée."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/.matching.done" ]]; then
|
||||
log "COLMAP — matching séquentiel"
|
||||
run_logged "$WORK_DIR/logs/02-matching.log" \
|
||||
colmap sequential_matcher \
|
||||
--database_path "$DATABASE" \
|
||||
--FeatureMatching.use_gpu "$USE_GPU" \
|
||||
--SequentialMatching.overlap "$SEQUENTIAL_OVERLAP"
|
||||
touch "$WORK_DIR/.matching.done"
|
||||
else
|
||||
ok "Matching déjà terminé."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/.mapper.done" ]]; then
|
||||
log "COLMAP — reconstruction clairsemée"
|
||||
rm -rf "$WORK_DIR/sparse"
|
||||
mkdir -p "$WORK_DIR/sparse"
|
||||
run_logged "$WORK_DIR/logs/03-mapper.log" \
|
||||
colmap mapper \
|
||||
--database_path "$DATABASE" \
|
||||
--image_path "$WORK_DIR/images" \
|
||||
--output_path "$WORK_DIR/sparse"
|
||||
touch "$WORK_DIR/.mapper.done"
|
||||
else
|
||||
ok "Reconstruction clairsemée déjà terminée."
|
||||
fi
|
||||
|
||||
BEST_MODEL=""
|
||||
BEST_REGISTERED=-1
|
||||
|
||||
while IFS= read -r model; do
|
||||
analysis="$(colmap model_analyzer --path "$model" 2>&1 || true)"
|
||||
registered="$(
|
||||
awk -F': ' '/Registered images:/ {print $NF; exit}' <<<"$analysis" |
|
||||
tr -dc '0-9'
|
||||
)"
|
||||
[[ -n "$registered" ]] || registered=0
|
||||
printf '%s\n' "$analysis" > "$model/model-analysis.txt"
|
||||
|
||||
printf ' Modèle %-5s : %s images enregistrées\n' \
|
||||
"$(basename "$model")" "$registered"
|
||||
|
||||
if (( registered > BEST_REGISTERED )); then
|
||||
BEST_REGISTERED="$registered"
|
||||
BEST_MODEL="$model"
|
||||
fi
|
||||
done < <(
|
||||
find "$WORK_DIR/sparse" -mindepth 1 -maxdepth 1 -type d | sort -V
|
||||
)
|
||||
|
||||
[[ -n "$BEST_MODEL" ]] || die "COLMAP n’a produit aucun modèle."
|
||||
printf '%s\n' "$BEST_MODEL" > "$WORK_DIR/best-model.txt"
|
||||
|
||||
ok "Meilleur modèle : $BEST_REGISTERED/$IMAGE_COUNT images."
|
||||
|
||||
DENSE_DIR="$WORK_DIR/dense"
|
||||
|
||||
if [[ ! -f "$WORK_DIR/.undistort.done" ]]; then
|
||||
log "COLMAP — correction des images"
|
||||
rm -rf "$DENSE_DIR"
|
||||
run_logged "$WORK_DIR/logs/04-undistort.log" \
|
||||
colmap image_undistorter \
|
||||
--image_path "$WORK_DIR/images" \
|
||||
--input_path "$BEST_MODEL" \
|
||||
--output_path "$DENSE_DIR" \
|
||||
--output_type COLMAP
|
||||
touch "$WORK_DIR/.undistort.done"
|
||||
else
|
||||
ok "Images déjà corrigées."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/openmvs/scene.mvs" ]]; then
|
||||
log "OpenMVS — import du modèle COLMAP"
|
||||
run_logged "$WORK_DIR/logs/05-interface-colmap.log" \
|
||||
podman_openmvs \
|
||||
/openmvs/InterfaceCOLMAP \
|
||||
--input-file /data/dense \
|
||||
--image-folder /data/dense/images \
|
||||
--output-file /data/openmvs/scene.mvs \
|
||||
--working-folder /data/openmvs \
|
||||
--verbosity 1
|
||||
else
|
||||
ok "Projet OpenMVS déjà importé."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/openmvs/scene_dense.ply" ]]; then
|
||||
log "OpenMVS — densification"
|
||||
run_logged "$WORK_DIR/logs/06-densify.log" \
|
||||
podman_openmvs \
|
||||
/openmvs/DensifyPointCloud \
|
||||
--input-file /data/openmvs/scene.mvs \
|
||||
--output-file /data/openmvs/scene_dense.mvs \
|
||||
--working-folder /data/openmvs \
|
||||
--resolution-level "$RESOLUTION_LEVEL" \
|
||||
--number-views "$NUMBER_VIEWS" \
|
||||
--max-threads "$THREADS" \
|
||||
--verbosity 1
|
||||
else
|
||||
ok "Nuage dense déjà généré."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/openmvs/scene_dense_mesh.ply" ]]; then
|
||||
log "OpenMVS — reconstruction du maillage"
|
||||
run_logged "$WORK_DIR/logs/07-reconstruct-mesh.log" \
|
||||
podman_openmvs \
|
||||
/openmvs/ReconstructMesh \
|
||||
--input-file /data/openmvs/scene_dense.mvs \
|
||||
--output-file /data/openmvs/scene_dense_mesh.mvs \
|
||||
--working-folder /data/openmvs \
|
||||
--max-threads "$THREADS" \
|
||||
--min-point-distance 4 \
|
||||
--verbosity 1
|
||||
else
|
||||
ok "Maillage déjà reconstruit."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/openmvs/scene_dense_mesh_refine.ply" ]]; then
|
||||
log "OpenMVS — raffinement du maillage"
|
||||
run_logged "$WORK_DIR/logs/08-refine-mesh.log" \
|
||||
podman_openmvs \
|
||||
/openmvs/RefineMesh \
|
||||
--input-file /data/openmvs/scene_dense.mvs \
|
||||
--mesh-file /data/openmvs/scene_dense_mesh.ply \
|
||||
--output-file /data/openmvs/scene_dense_mesh_refine.mvs \
|
||||
--working-folder /data/openmvs \
|
||||
--resolution-level "$RESOLUTION_LEVEL" \
|
||||
--max-threads "$THREADS" \
|
||||
--verbosity 1
|
||||
else
|
||||
ok "Maillage déjà raffiné."
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WORK_DIR/openmvs/scene_dense_mesh_refine_texture.ply" ]]; then
|
||||
log "OpenMVS — texturage"
|
||||
run_logged "$WORK_DIR/logs/09-texture-mesh.log" \
|
||||
podman_openmvs \
|
||||
/openmvs/TextureMesh \
|
||||
--input-file /data/openmvs/scene_dense.mvs \
|
||||
--mesh-file /data/openmvs/scene_dense_mesh_refine.ply \
|
||||
--output-file /data/openmvs/scene_dense_mesh_refine_texture.mvs \
|
||||
--working-folder /data/openmvs \
|
||||
--max-threads "$THREADS" \
|
||||
--verbosity 1
|
||||
else
|
||||
ok "Maillage déjà texturé."
|
||||
fi
|
||||
|
||||
if (( !KEEP_DEPTHMAPS )); then
|
||||
log "Nettoyage des cartes de profondeur intermédiaires"
|
||||
find "$WORK_DIR/openmvs" -maxdepth 1 -type f \
|
||||
\( -name 'depth*.png' -o -name 'depth*.conf.png' \) \
|
||||
-delete
|
||||
fi
|
||||
|
||||
FINAL="$WORK_DIR/openmvs/scene_dense_mesh_refine_texture.ply"
|
||||
[[ -f "$FINAL" ]] || die "Le modèle final est absent."
|
||||
|
||||
trap - EXIT
|
||||
|
||||
printf '\n%s%sSCAN 3D TERMINÉ%s\n' "$BOLD" "$GREEN" "$RESET"
|
||||
printf 'Modèle texturé :\n %s\n' "$FINAL"
|
||||
printf 'Images enregistrées : %s/%s\n' "$BEST_REGISTERED" "$IMAGE_COUNT"
|
||||
printf 'Espace occupé : '
|
||||
du -sh "$WORK_DIR" | awk '{print $1}'
|
||||
printf '\nPour l’ouvrir manuellement :\n'
|
||||
printf ' meshlab %q\n' "$FINAL"
|
||||
430
bin/.local/bin/scan3d-watch
Executable file
430
bin/.local/bin/scan3d-watch
Executable file
|
|
@ -0,0 +1,430 @@
|
|||
#!/usr/bin/env python3
|
||||
"""scan3d-watch — TUI Textual en lecture seule pour scan3d."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
import psutil
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widgets import Footer, Label, ProgressBar, Static
|
||||
|
||||
|
||||
PROCESS_NAMES = {
|
||||
"colmap",
|
||||
"DensifyPointCloud",
|
||||
"ReconstructMesh",
|
||||
"RefineMesh",
|
||||
"TextureMesh",
|
||||
"InterfaceCOLMAP",
|
||||
}
|
||||
|
||||
PROGRESS_PATTERNS = (
|
||||
re.compile(r"Processing image \[(\d+)/(\d+)\]"),
|
||||
re.compile(r"Estimated depth-maps (\d+) \((\d+)%"),
|
||||
re.compile(r"Geometric-consistent estimated depth-maps (\d+) \((\d+)%"),
|
||||
re.compile(r"Dense fused depth-maps (\d+) \((\d+)%"),
|
||||
)
|
||||
|
||||
PHASES = (
|
||||
("features", "Extraction"),
|
||||
("sequential", "Matching séquentiel"),
|
||||
("vocab", "Matching global"),
|
||||
("transitive", "Matching transitif"),
|
||||
("matching", "Matching"),
|
||||
("mapper", "Mapper"),
|
||||
("undistort", "Undistortion"),
|
||||
("interface", "Import OpenMVS"),
|
||||
("densify", "Densification"),
|
||||
("reconstruct", "Maillage"),
|
||||
("refine", "Raffinement"),
|
||||
("texture", "Texturage"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Progress:
|
||||
phase: str = "En attente"
|
||||
current: int = 0
|
||||
total: int = 0
|
||||
percent: float = 0.0
|
||||
message: str = ""
|
||||
|
||||
|
||||
def human_bytes(value: int | float) -> str:
|
||||
units = ("o", "Kio", "Mio", "Gio", "Tio")
|
||||
number = float(value)
|
||||
for unit in units:
|
||||
if abs(number) < 1024 or unit == units[-1]:
|
||||
return f"{number:.1f} {unit}"
|
||||
number /= 1024
|
||||
return f"{number:.1f} Tio"
|
||||
|
||||
|
||||
def directory_size(path: Path) -> int:
|
||||
total = 0
|
||||
try:
|
||||
for root, _dirs, files in os.walk(path):
|
||||
for name in files:
|
||||
try:
|
||||
total += (Path(root) / name).stat().st_size
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def find_project(root: Path) -> Path:
|
||||
scan3d = root / "scan3d"
|
||||
return scan3d if scan3d.is_dir() else root
|
||||
|
||||
|
||||
def latest_log(project: Path) -> Path | None:
|
||||
logs: list[Path] = []
|
||||
try:
|
||||
logs = [path for path in project.rglob("*.log") if path.is_file()]
|
||||
except OSError:
|
||||
return None
|
||||
return max(logs, key=lambda path: path.stat().st_mtime, default=None)
|
||||
|
||||
|
||||
def read_tail(path: Path, lines: int = 12) -> list[str]:
|
||||
try:
|
||||
with path.open("rb") as file:
|
||||
file.seek(0, os.SEEK_END)
|
||||
size = file.tell()
|
||||
block = 8192
|
||||
data = b""
|
||||
while size > 0 and data.count(b"\n") <= lines:
|
||||
read_size = min(block, size)
|
||||
size -= read_size
|
||||
file.seek(size)
|
||||
data = file.read(read_size) + data
|
||||
return data.decode("utf-8", errors="replace").splitlines()[-lines:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
def detect_progress(project: Path) -> Progress:
|
||||
log = latest_log(project)
|
||||
if log is None:
|
||||
return Progress()
|
||||
|
||||
lower_name = log.name.lower()
|
||||
phase = next(
|
||||
(label for token, label in PHASES if token in lower_name),
|
||||
log.stem,
|
||||
)
|
||||
|
||||
result = Progress(phase=phase)
|
||||
tail = read_tail(log, 120)
|
||||
|
||||
# COLMAP : extraction ou matching.
|
||||
for line in reversed(tail):
|
||||
match = re.search(r"Processing image \[(\d+)/(\d+)\]", line)
|
||||
if match:
|
||||
current = int(match.group(1))
|
||||
total = int(match.group(2))
|
||||
|
||||
result.current = current
|
||||
result.total = total
|
||||
result.percent = current * 100 / total if total else 0
|
||||
result.message = line.strip()
|
||||
return result
|
||||
|
||||
# COLMAP Mapper : nombre d’images réellement enregistrées.
|
||||
for line in reversed(tail):
|
||||
match = re.search(r"num_reg_frames=(\d+)", line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
current = int(match.group(1))
|
||||
|
||||
# Le log est normalement dans projet-*/logs/.
|
||||
work_dir = log.parent.parent
|
||||
images_dir = work_dir / "images"
|
||||
|
||||
try:
|
||||
total = sum(
|
||||
1
|
||||
for path in images_dir.iterdir()
|
||||
if path.is_file() or path.is_symlink()
|
||||
)
|
||||
except OSError:
|
||||
total = 0
|
||||
|
||||
result.current = current
|
||||
result.total = total
|
||||
result.percent = current * 100 / total if total else 0
|
||||
result.message = line.strip()
|
||||
return result
|
||||
|
||||
# OpenMVS : cartes de profondeur.
|
||||
for line in reversed(tail):
|
||||
match = re.search(
|
||||
r"(?:Estimated|Geometric-consistent estimated|Dense fused) "
|
||||
r"depth-maps (\d+) \((\d+)%",
|
||||
line,
|
||||
)
|
||||
if match:
|
||||
result.current = int(match.group(1))
|
||||
result.percent = float(match.group(2))
|
||||
result.message = line.strip()
|
||||
return result
|
||||
|
||||
if tail:
|
||||
result.message = tail[-1].strip()
|
||||
|
||||
return result
|
||||
|
||||
def amd_gpu() -> tuple[str, str, str]:
|
||||
for card in sorted(Path("/sys/class/drm").glob("card[0-9]*")):
|
||||
device = card / "device"
|
||||
try:
|
||||
if (device / "vendor").read_text().strip() != "0x1002":
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
busy = "?"
|
||||
used = total = None
|
||||
temperature = "?"
|
||||
|
||||
try:
|
||||
busy = (device / "gpu_busy_percent").read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
used = int((device / "mem_info_vram_used").read_text())
|
||||
total = int((device / "mem_info_vram_total").read_text())
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
for hwmon in (device / "hwmon").glob("hwmon*"):
|
||||
try:
|
||||
temperature = f"{int((hwmon / 'temp1_input').read_text()) / 1000:.0f}°C"
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
|
||||
vram = (
|
||||
f"{human_bytes(used)}/{human_bytes(total)}"
|
||||
if used is not None and total is not None
|
||||
else "indisponible"
|
||||
)
|
||||
return f"{busy}%", vram, temperature
|
||||
|
||||
return "absent", "—", "—"
|
||||
|
||||
def active_process() -> str:
|
||||
cpu_count = psutil.cpu_count(logical=True) or 1
|
||||
rows: list[tuple[float, str]] = []
|
||||
|
||||
for process in psutil.process_iter(
|
||||
["name", "cpu_percent", "memory_percent"]
|
||||
):
|
||||
try:
|
||||
name = process.info["name"] or ""
|
||||
if name not in PROCESS_NAMES:
|
||||
continue
|
||||
|
||||
raw_cpu = float(process.info["cpu_percent"] or 0)
|
||||
global_cpu = raw_cpu / cpu_count
|
||||
memory = float(process.info["memory_percent"] or 0)
|
||||
|
||||
rows.append(
|
||||
(
|
||||
raw_cpu,
|
||||
f"{name} CPU {global_cpu:.0f}% RAM {memory:.1f}%",
|
||||
)
|
||||
)
|
||||
|
||||
except (
|
||||
psutil.NoSuchProcess,
|
||||
psutil.AccessDenied,
|
||||
TypeError,
|
||||
):
|
||||
continue
|
||||
|
||||
if not rows:
|
||||
return "Aucun processus COLMAP/OpenMVS"
|
||||
|
||||
rows.sort(reverse=True)
|
||||
return rows[0][1]
|
||||
|
||||
class Scan3DWatch(App[None]):
|
||||
TITLE = "scan3d-watch"
|
||||
SUB_TITLE = "lecture seule"
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit", "Quitter"),
|
||||
Binding("r", "refresh_now", "Actualiser"),
|
||||
Binding("l", "toggle_logs", "Logs"),
|
||||
]
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
background: #11111b;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
#title {
|
||||
height: 1;
|
||||
padding: 0 1;
|
||||
background: #313244;
|
||||
color: #cba6f7;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#phase {
|
||||
height: 3;
|
||||
padding: 0 1;
|
||||
border: round #89b4fa;
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
height: 1;
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
#stats {
|
||||
height: 7;
|
||||
padding: 0 1;
|
||||
border: round #a6e3a1;
|
||||
}
|
||||
|
||||
#project {
|
||||
height: 5;
|
||||
padding: 0 1;
|
||||
border: round #f9e2af;
|
||||
}
|
||||
|
||||
#logs {
|
||||
height: 1fr;
|
||||
min-height: 4;
|
||||
padding: 0 1;
|
||||
border: round #fab387;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
Footer {
|
||||
height: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
super().__init__()
|
||||
self.root = root
|
||||
self.project = find_project(root)
|
||||
self.logs_visible = True
|
||||
psutil.cpu_percent(interval=None)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("", id="title")
|
||||
yield Static("", id="phase")
|
||||
yield ProgressBar(total=100, show_eta=False, id="progress")
|
||||
yield Static("", id="stats")
|
||||
yield Static("", id="project")
|
||||
yield Static("", id="logs")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.refresh_data()
|
||||
self.set_interval(1.0, self.refresh_data)
|
||||
|
||||
def action_refresh_now(self) -> None:
|
||||
self.refresh_data()
|
||||
|
||||
def action_toggle_logs(self) -> None:
|
||||
self.logs_visible = not self.logs_visible
|
||||
logs = self.query_one("#logs")
|
||||
logs.set_class(not self.logs_visible, "hidden")
|
||||
|
||||
def refresh_data(self) -> None:
|
||||
progress = detect_progress(self.project)
|
||||
memory = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
disk = psutil.disk_usage(self.root)
|
||||
cpu = psutil.cpu_percent(interval=None)
|
||||
gpu, vram, gpu_temp = amd_gpu()
|
||||
|
||||
self.query_one("#title", Label).update(
|
||||
f"SCAN3D {self.root.name} • {self.project}"
|
||||
)
|
||||
|
||||
progress_text = progress.phase
|
||||
if progress.total:
|
||||
progress_text += (
|
||||
f"\n{progress.current}/{progress.total} "
|
||||
f"({progress.percent:.1f}%)"
|
||||
)
|
||||
elif progress.percent:
|
||||
progress_text += f"\n{progress.current} ({progress.percent:.1f}%)"
|
||||
elif progress.message:
|
||||
progress_text += "\nactivité détectée"
|
||||
|
||||
self.query_one("#phase", Static).update(progress_text)
|
||||
self.query_one("#progress", ProgressBar).update(
|
||||
total=100,
|
||||
progress=max(0, min(100, progress.percent)),
|
||||
)
|
||||
|
||||
self.query_one("#stats", Static).update(
|
||||
"[b]Système[/b]\n"
|
||||
f"CPU {cpu:.0f}% GPU {gpu} Temp GPU {gpu_temp}\n"
|
||||
f"RAM {human_bytes(memory.used)}/{human_bytes(memory.total)} "
|
||||
f"dispo {human_bytes(memory.available)}\n"
|
||||
f"Swap {human_bytes(swap.used)}/{human_bytes(swap.total)} "
|
||||
f"VRAM {vram}\n"
|
||||
f"{active_process()}"
|
||||
)
|
||||
|
||||
size = directory_size(self.project)
|
||||
self.query_one("#project", Static).update(
|
||||
"[b]Projet[/b]\n"
|
||||
f"Taille {human_bytes(size)}\n"
|
||||
f"Disque {human_bytes(disk.used)}/{human_bytes(disk.total)} "
|
||||
f"libre {human_bytes(disk.free)}"
|
||||
)
|
||||
|
||||
log = latest_log(self.project)
|
||||
if log is None:
|
||||
log_text = "[b]Logs[/b]\nAucun fichier .log trouvé"
|
||||
else:
|
||||
lines = read_tail(log, 8)
|
||||
cleaned = [
|
||||
re.sub(r"^[IWEF]\d+\s+\S+\s+\d+\s+", "", line)
|
||||
for line in lines
|
||||
]
|
||||
log_text = f"[b]Logs — {log.name}[/b]\n" + "\n".join(cleaned)
|
||||
|
||||
self.query_one("#logs", Static).update(log_text)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else Path.cwd())
|
||||
root = root.expanduser().resolve()
|
||||
|
||||
if not root.is_dir():
|
||||
print(f"Dossier introuvable : {root}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
Scan3DWatch(root).run()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
16
bin/.local/bin/sway-close
Executable file
16
bin/.local/bin/sway-close
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
app_id="$(
|
||||
swaymsg -t get_tree |
|
||||
jq -r '.. | objects | select(.focused? == true) |
|
||||
(.app_id // .window_properties.class // "")'
|
||||
)"
|
||||
|
||||
case "$app_id" in
|
||||
limusic-app|LiMusic|limusic)
|
||||
pkill -TERM -x limusic-app
|
||||
;;
|
||||
*)
|
||||
swaymsg kill
|
||||
;;
|
||||
esac
|
||||
6
ytmusic-tui/.config/ytmusic-tui/config.toml
Normal file
6
ytmusic-tui/.config/ytmusic-tui/config.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[ui]
|
||||
theme = "catppuccin"
|
||||
|
||||
[player]
|
||||
volume = 80
|
||||
audio_quality = "high"
|
||||
Loading…
Reference in a new issue