Squashed from work; the fine-grained history is under tag archive/work-2026-08-29: - mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix - work: land the sources the last commits reference - kenney: catalogue all 50 free 3D kits; Modal dismissed() never fired - asset store: central vision-annotation queue; Kenney donate prompt - asset-ai: vision domain — image + prompt -> text on every fleet node - annotation runs the fleet's vision services through the normal job queue - vj: responsive DJ mixer + Windows drag-and-drop, cherry-picked from PR #1199 (vjroger) - store search: WordNet synonym expansion, query-side; per-term seeks - video scheduling: a cold model pin never downloads past a warm one, and a dedicated box only serves its role - an expansion can never lose a run — and `expand: true` finally means something - fleet panel: a slow box is not a missing box - h3: first+last keyframe conditioning (the weights were always FL2VA) - registry: the three H3 FL2VA tiers name their real conditioning - fleet scheduling: spread before stacking, and a stuck job moves - a download never steals a job from a box that has the weights - the faster GPU takes the tie: 6000 > 5090 > 4090 - an evicted flux model gives the card its VRAM back - a job now records what each of its stages was handed - open a stage in RUNS and read what it sent - every expanded song was an instrumental: the lyrics had nowhere to go - the writer never overwrites words the person wrote - store: a dependent job's body is spliced from its deps' results at claim - a fleet box's job row says what that job was asked for - the store runs a whole pipeline, and one record says how far it got - a client can declare a whole run, watch it, and stop it - 100% now means published, and the expander is a job you can queue - one card says what a spawned task is doing, everywhere - a run that ends says so, instead of being noticed later - when every box holding the model is busy, buy another copy
170 lines
6.6 KiB
Rust
170 lines
6.6 KiB
Rust
//! One asset's trip through the pass: turntable sheet in, model-ready image
|
|
//! and question out.
|
|
//!
|
|
//! Nothing here talks to a model or to the catalog. It prepares what the
|
|
//! fleet coordinator sends to a `vision` box — the framed, exposed,
|
|
//! downscaled sheet and the two halves of the question (the prompt and the
|
|
//! per-asset context line) — and [`crate::plan_upload`] turns the reply
|
|
//! into the record that is written back.
|
|
|
|
use crate::sheet;
|
|
use std::io::{Read, Write};
|
|
use std::net::{SocketAddr, TcpStream};
|
|
use std::time::Duration;
|
|
|
|
/// How a sheet is prepared for the vision tower.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct SheetPrep {
|
|
/// Square edge the 4x4 sheet is downscaled to before the model.
|
|
pub sheet_size: usize,
|
|
/// Gamma lift on subject pixels; 1.0 disables. Kenney renders sit on a
|
|
/// dark background and a 9B tower reads a dim subject as "black".
|
|
pub exposure: f32,
|
|
}
|
|
|
|
impl Default for SheetPrep {
|
|
fn default() -> Self {
|
|
Self { sheet_size: 512, exposure: 1.8 }
|
|
}
|
|
}
|
|
|
|
/// Decode a published turntable sheet into the pixels the vision tower is
|
|
/// shown.
|
|
///
|
|
/// `person` zooms each of the 16 cells onto its subject first: a character
|
|
/// is small in its cell, and the tower's patch budget was landing on empty
|
|
/// background instead of the face. Kit pieces keep their true in-cell size,
|
|
/// because that is what the `size` line reports.
|
|
///
|
|
/// The caller encodes the result for the wire (the fleet takes a base64
|
|
/// PNG/JPEG); this stays a plain RGB buffer so the framing decisions and
|
|
/// the transport encoding cannot drift into one function.
|
|
pub fn sheet_to_rgb(png: &[u8], person: bool, prep: &SheetPrep) -> Result<sheet::Rgb, String> {
|
|
Ok(frame_sheet(sheet::decode_png(png)?, person, prep))
|
|
}
|
|
|
|
/// The framing itself, for a caller that already decoded the sheet (a
|
|
/// generated asset publishes a JPEG thumbnail where an imported one
|
|
/// publishes the PNG turntable; both are pictures of the asset and both are
|
|
/// framed the same way).
|
|
pub fn frame_sheet(mut img: sheet::Rgb, person: bool, prep: &SheetPrep) -> sheet::Rgb {
|
|
if person {
|
|
img = sheet::zoom_to_subject(&img, 4, 0.15);
|
|
}
|
|
// Lift BEFORE downscaling: the box filter then averages corrected
|
|
// values rather than correcting an already-averaged shadow.
|
|
sheet::lift_exposure(&mut img, prep.exposure);
|
|
sheet::downscale(&img, prep.sheet_size)
|
|
}
|
|
|
|
/// The prompt asked about this asset.
|
|
pub fn prompt_for(person: bool) -> &'static str {
|
|
if person {
|
|
crate::PROMPT_PERSON
|
|
} else {
|
|
crate::PROMPT
|
|
}
|
|
}
|
|
|
|
/// The per-job context column: what the kit calls this piece, and which
|
|
/// kit it is.
|
|
///
|
|
/// Withholding the name only makes the model guess — an upright grey slab
|
|
/// reads as a book until you know the kit calls it "wall". The images still
|
|
/// decide everything the name cannot say (colour, orientation, how it
|
|
/// connects), which is the whole point of running a vision model instead of
|
|
/// parsing file names.
|
|
pub fn context_line(alias: &str, person: bool) -> String {
|
|
let (kit, name) = alias.rsplit_once('/').unwrap_or(("", alias));
|
|
let kit = kit.rsplit('/').next().unwrap_or("");
|
|
if person {
|
|
// Person framing: the SET name is context, but the questions are
|
|
// about the person. No construction-kit talk — a character does not
|
|
// snap onto a grid, and the grid frame biases the answers toward
|
|
// objects.
|
|
let set = if kit.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" It comes from the Kenney \"{kit}\" set.")
|
|
};
|
|
return format!(
|
|
"The set calls this character \"{name}\".{set} Describe the PERSON you see; \
|
|
trust the images over the name where they disagree."
|
|
);
|
|
}
|
|
let frame = if kit.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(
|
|
" It is a game asset from the Kenney \"{kit}\" set, a low-poly modular \
|
|
construction kit whose pieces snap together on a grid."
|
|
)
|
|
};
|
|
format!(
|
|
"The kit calls this piece \"{name}\".{frame} Trust the images over the name \
|
|
where they disagree."
|
|
)
|
|
}
|
|
|
|
/// Minimal GET against the store's data plane, which the asset client does
|
|
/// not wrap. Localhost or LAN, one request per connection, bounded read.
|
|
pub fn http_get(addr: SocketAddr, path: &str, token: &str) -> Result<Vec<u8>, String> {
|
|
let mut s = TcpStream::connect_timeout(&addr, Duration::from_secs(5))
|
|
.map_err(|e| format!("connect {addr}: {e}"))?;
|
|
s.set_read_timeout(Some(Duration::from_secs(30))).ok();
|
|
s.set_write_timeout(Some(Duration::from_secs(30))).ok();
|
|
let req = format!(
|
|
"GET {path} HTTP/1.1\r\nHost: {addr}\r\nAuthorization: Bearer {token}\r\n\
|
|
Accept: */*\r\nConnection: close\r\n\r\n"
|
|
);
|
|
s.write_all(req.as_bytes()).map_err(|e| format!("write: {e}"))?;
|
|
let mut buf = Vec::new();
|
|
s.read_to_end(&mut buf).map_err(|e| format!("read: {e}"))?;
|
|
let split = buf
|
|
.windows(4)
|
|
.position(|w| w == b"\r\n\r\n")
|
|
.ok_or("no header terminator")?;
|
|
let head = String::from_utf8_lossy(&buf[..split]).to_string();
|
|
let status = head
|
|
.lines()
|
|
.next()
|
|
.and_then(|l| l.split_whitespace().nth(1))
|
|
.and_then(|c| c.parse::<u16>().ok())
|
|
.ok_or("no status line")?;
|
|
if status != 200 {
|
|
return Err(format!("HTTP {status} for {path}"));
|
|
}
|
|
Ok(buf[split + 4..].to_vec())
|
|
}
|
|
|
|
/// The published turntable sheet for one alias.
|
|
pub fn thumbnail_sheet(data: SocketAddr, token: &str, alias: &str) -> Result<Vec<u8>, String> {
|
|
http_get(data, &format!("/v1/thumbnails/alias/{alias}"), token)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_context_names_the_kit_and_the_piece() {
|
|
let kit = context_line("kenney/nature-kit/tree-pine", false);
|
|
assert!(kit.contains("\"tree-pine\""), "{kit}");
|
|
assert!(kit.contains("\"nature-kit\""), "{kit}");
|
|
assert!(kit.contains("snap together on a grid"), "{kit}");
|
|
// A person is never introduced as a piece that snaps onto a grid.
|
|
let person = context_line("kenney/mini-dungeon/character-human", true);
|
|
assert!(person.contains("\"character-human\""), "{person}");
|
|
assert!(person.contains("\"mini-dungeon\""), "{person}");
|
|
assert!(person.contains("Describe the PERSON"), "{person}");
|
|
assert!(!person.contains("grid"), "{person}");
|
|
}
|
|
|
|
#[test]
|
|
fn an_alias_without_a_kit_still_names_the_piece() {
|
|
let line = context_line("tree", false);
|
|
assert!(line.contains("\"tree\""), "{line}");
|
|
assert!(!line.contains("Kenney"), "{line}");
|
|
}
|
|
|
|
}
|