Compare commits

...

2 commits

Author SHA1 Message Date
DannyDannyDanny
05896f6d3b phantom-ship/shipyard: rename poppler_utils → poppler-utils
nixpkgs renamed it; the old attr is now an error alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 13:17:33 +02:00
DannyDannyDanny
cc8cc05a08 phantom-ship/shipyard: add media-processing tools for feedback
Feedback now accepts photos, voice notes, video, documents etc. Phase
A captures + stores raw files (Pillow for EXIF strip); Phase B derives
OCR text, speech transcripts, poster frames, PDF text — all via
subprocess so each tool degrades gracefully if absent. Wire the
following into the shipyard service:

  - python3Packages.pillow → EXIF strip on captured photos
  - ffmpeg                 → poster frames + audio→16kHz WAV for whisper
  - tesseract (eng + rus)  → OCR (vyscul writes in Russian)
  - whisper-cpp            → speech-to-text for voice / audio / video
  - poppler_utils          → pdftotext for document attachments

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 13:16:57 +02:00
5 changed files with 204 additions and 2 deletions

View file

@ -0,0 +1,14 @@
# Standalone nixosSystem registration for distant-shore.
# Temporary: clan integration (zerotier/data-mesher/dm-pull-deploy) needs
# vars generated via sops on the admin machine. Until that runs, this
# keeps the box buildable without clan deps. Delete this file when
# distant-shore moves into flake-modules/clan.nix.
{ inputs, ... }: {
flake.nixosConfigurations.distant-shore = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
inputs.disko.nixosModules.disko
../nixos/hosts/distant-shore.nix
];
};
}

View file

@ -0,0 +1,37 @@
# Declarative disk layout for distant-shore (ThinkPad X13 Gen 2 — 256 GB
# SK Hynix NVMe). UEFI/systemd-boot, no encryption: it's a headless,
# WiFi-only server that must reboot unattended (clan dm-pull-deploy), so
# a LUKS passphrase prompt at boot would hang it. Mirrors sunken-ship's
# plain-ext4 choice. Device is wiped + repartitioned at install time by
# clan/nixos-anywhere.
{
disko.devices = {
disk.main = {
type = "disk";
device = "/dev/nvme0n1";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "fmask=0022" "dmask=0022" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

View file

@ -0,0 +1,18 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View file

@ -0,0 +1,114 @@
# NixOS server on a ThinkPad X13 Gen 2 (Intel i5-1145G7, 16 GB).
# WiFi-only, headless, unattended auto-rebuild via clan dm-pull-deploy.
# No LUKS (mirrors sunken-ship) so reboots don't block on a passphrase.
#
# Blank-slate server for now — no application services. Give it a purpose
# later (just add services here and let dm-pull-deploy roll it out).
{ config, lib, pkgs, ... }:
{
imports = [
./distant-shore-hardware.nix
../disko-distant-shore.nix
];
boot.loader.systemd-boot.enable = true;
# Secure Boot is enforced and the BIOS supervisor password is unknown, so
# we can't enrol our own SB keys. Instead, shim (MS-signed) is placed on
# the ESP and chain-loads systemd-boot; the NVRAM boot entry points at
# shim. We manage that entry imperatively via efibootmgr; letting bootctl
# touch EFI variables would replace it on every rebuild.
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.efi.efiSysMountPoint = "/boot"; # matches disko ESP mountpoint
# --- Secure Boot via shim + MOK (no firmware key enrolment possible) ------
# The firmware trusts Microsoft-signed shim; shim trusts our enrolled MOK.
# On every bootloader install we: (1) sign systemd-boot with the MOK and
# drop it where shim chain-loads it (grubx64.efi), (2) install shim as the
# firmware-booted binary (+ MokManager), (3) MOK-sign every kernel image
# systemd-boot will hand off to (shim verifies them via the shim-lock
# protocol). Re-runs on each nixos-rebuild so auto-deployed generations
# stay bootable. Keys + shim live in /etc/secrets (outside the repo).
boot.loader.systemd-boot.extraInstallCommands = ''
# NixOS's bootloader-install systemd unit runs with a minimal PATH that
# doesn't include coreutils, so use absolute paths for cp/mv.
KEY=/etc/secrets/MOK.key
CRT=/etc/secrets/MOK.crt
sb() { ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$2" "$1"; }
# systemd-boot -> shim's chain-load target
sb /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/BOOT/grubx64.efi
# shim (MS-signed) is what the firmware boots; MokManager beside it
${pkgs.coreutils}/bin/cp -f /etc/secrets/shimx64.efi /boot/EFI/BOOT/BOOTX64.EFI
${pkgs.coreutils}/bin/cp -f /etc/secrets/mmx64.efi /boot/EFI/BOOT/mmx64.efi
# MOK-sign each kernel (skip already-signed; never touch initrds)
for k in /boot/EFI/nixos/*bzImage.efi; do
[ -e "$k" ] || continue
if ! ${pkgs.sbsigntool}/bin/sbverify --cert "$CRT" "$k" >/dev/null 2>&1; then
${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$k.tmp" "$k" \
&& ${pkgs.coreutils}/bin/mv -f "$k.tmp" "$k"
fi
done
'';
networking.hostName = "distant-shore";
# WiFi via NetworkManager. The wpa_supplicant stack hit two issues on this
# box: (1) it strips CAP_CHOWN so wpa couldn't create its ctrl_interface,
# and (2) dhcpcd didn't grab a lease after the (late) association at boot,
# needing a manual restart — fatal for an unattended headless server. NM
# handles association + DHCP atomically and connected first-try here.
# The PSK stays out of the repo: it's substituted from /etc/secrets/nm.env
# ($PSK_INTENO) into the declared profile at activation.
networking.networkmanager.enable = true;
networking.networkmanager.ensureProfiles.environmentFiles = [ "/etc/secrets/nm.env" ];
networking.networkmanager.ensureProfiles.profiles."Inteno-89FE" = {
connection = { id = "Inteno-89FE"; type = "wifi"; autoconnect = true; };
wifi = { ssid = "Inteno-89FE"; mode = "infrastructure"; };
wifi-security = { key-mgmt = "wpa-psk"; psk = "$PSK_INTENO"; };
ipv4.method = "auto";
ipv6.method = "auto";
};
hardware.enableRedistributableFirmware = true; # iwlwifi for the Intel AX201 WiFi
time.timeZone = "Europe/Copenhagen";
# It's a laptop acting as a server: keep running with the lid shut.
services.logind.settings.Login.HandleLidSwitch = "ignore";
services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore";
# Reduce screen burn-in / power: blank the TTY after a minute.
boot.kernelParams = [ "consoleblank=60" ];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI)
nix.settings.trusted-users = [ "root" "danny" ];
system.stateVersion = "25.11";
users.users.danny = {
isNormalUser = true;
extraGroups = [ "wheel" "video" "audio" ];
openssh.authorizedKeys.keys = [
# Mac admin / fleet key (~/.ssh/id_ed25519_sunken_ship) — the key the
# Mac uses to reach the fleet; clan machines update relies on it.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin"
# Per-host key (~/.ssh/id_ed25519_distant_shore) — plain `ssh distant-shore`.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH61JOiOOPrAXekakAwTJg5yCSDfOIjlSvMYkpXrarAf distant-shore"
# sunken-ship (dm-pull-deploy push node) — reach distant-shore over ZT.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship"
];
};
users.users.root.openssh.authorizedKeys.keys =
config.users.users.danny.openssh.authorizedKeys.keys;
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
};
security.sudo.wheelNeedsPassword = false;
# mokutil — manage MOK enrolment for the shim chain; sbsigntool — inspect
# signatures on bootloader/kernel images when debugging.
environment.systemPackages = with pkgs; [ git mokutil sbsigntool ];
}

View file

@ -241,18 +241,37 @@ in
# Code deployed out-of-band via rsync to /home/danny/shipyard/
# (staying in-tree in ~/python-projects/26_shipyard/ until spun out to its own repo).
# Bot token (not in repo): ~danny/.secrets/telegram-bot-token-shipyard
# Data (feedback.jsonl, pointer cache): ~danny/.local/share/shipyard/
# Data (feedback.jsonl, feedback.db, pointer cache, feedback_media/):
# ~danny/.local/share/shipyard/
#
# Feedback now accepts photos / voice / video / docs / stickers etc.
# Phase A captures + stores raw files; Phase B derives OCR text
# (tesseract), speech transcripts (whisper-cpp), poster frames
# (ffmpeg) and PDF text (pdftotext) — all via subprocess, so each
# tool degrades gracefully if missing.
systemd.services.shipyard = let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
python-telegram-bot
httpx
pillow # EXIF strip on captured photos
]);
# tesseract with English + Russian tessdata — vyscul writes in
# Russian, screenshots can land in either language.
tesseractWithLangs = pkgs.tesseract.override {
enableLanguages = [ "eng" "rus" ];
};
in {
description = "Shipyard Telegram bot (mini-app launcher + feedback)";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pythonEnv ];
path = [
pythonEnv
pkgs.ffmpeg # video/animation posters, sticker decode
tesseractWithLangs # photo OCR
pkgs.whisper-cpp # voice/audio transcription
pkgs.poppler-utils # pdftotext (document handling)
];
environment = {
SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard";
# Owner-only commands (/admin, /grant, /revoke) — anyone else gets ignored.