DeveloperThumbnail API

Thumbnail API

The sb-thumbnails crate provides a unified thumbnail generation pipeline shared by desktop, mobile (Tauri), and cloud worker pods. All outputs are capped at 512×512 — AI tagging and vision embeddings consume the 512 thumbnail, never the full-resolution source.

Canonical crate: crates/ai/crates/sb-thumbnails/ in studiobrain-cloud (package sb-thumbnails, license UNLICENSED). This page documents that crate, including the pdf and video feature flags.

SBAI-2149 moved the crate from studiobrain-core into studiobrain-ai. studiobrain-ai is archived (SBAI-10112). Do not add a git dependency on it. The commercial crate now lives in this repo. A smaller Apache-2.0 subset lives in studiobrain-core again, at crates/sb-thumbnails (image, svg, and model3d only — no pdf or video modules).

CopyPathWho depends on it today
Commercial (this page)studiobrain-cloud/crates/ai/crates/sb-thumbnailscrates/sb-thumbnail-worker path-depends with feature full. crates/sb-enterprise path-depends with feature image only.
Open-core subsetstudiobrain-core/crates/sb-thumbnailscrates/sb-server path-depends on ../sb-thumbnails with feature image, behind the thumbnails feature.
Desktop and mobile manifestspath = "../../../crates/ai/crates/sb-thumbnails" (SBAI-12187, #2359), features image and svgclients/desktop/src-tauri/Cargo.toml and clients/mobile/src-tauri/Cargo.toml still declare that dependency. It points at the archived repo. It is not the canonical path.

3D Model Upload Auto-Enqueue

When a 3D model file is uploaded via POST /api/assets/upload, the server automatically enqueues it into the AI tag queue for thumbnail generation. Supported formats: gltf, glb, obj, fbx, dae, blend, 3ds, ply — the set defined at crates/sb-server/src/services/asset_types.rs:34. The enqueue is best-effort: the upload always succeeds even if the queue is unavailable.

See Asset Tag Queue for the internal queue flow.

Feature Flags

The crate uses feature-gated backends. The default feature set is image only. Which features each package turns on is in Edition Differences. pdf and video exist on the commercial crate, but their native backends are not linked yet (pdf = [] and video = [] in crates/ai/crates/sb-thumbnails/Cargo.toml).

FeatureBackendSystem dependencies
imageimage cratenone (pure Rust)
svgresvg + usvgnone (pure Rust)
pdfstub (planned: pdfium-render)none linked today
videostub (planned: ffmpeg-next)none linked today
model3dgltf (parser)none for parsing; render stub

Supported Formats

Format kindFormatsBackend feature
Rasterjpg, jpeg, png, webp, gif, bmp, heic, tiffimage
SVGsvgsvg
PDFpdfpdf
Videomp4, mov, mkv, webm, avivideo
3D Modelgltf, glb, obj, fbxmodel3d

Public API

Core Types

pub use sb_thumbnails::{
    Pipeline, ThumbnailRequest, ThumbnailResult,
    CacheLayout, Error, Result,
    MAX_DIM, STANDARD_SIZES, THUMBNAIL_CONTENT_TYPE, THUMBNAIL_EXTENSION,
    detect_format, clamp_size, FormatKind,
};

Constants

ConstantValueDescription
MAX_DIM512Maximum thumbnail dimension (hard cap)
STANDARD_SIZES[64, 256, 512]Default thumbnail sizes produced by the pipeline
THUMBNAIL_CONTENT_TYPE"image/webp"Media type for still thumbnails
THUMBNAIL_EXTENSION"webp"File extension for still thumbnails

Pipeline

The top-level pipeline orchestrator.

pub struct Pipeline {
    cache: CacheLayout,
}
 
impl Pipeline {
    pub fn new(cache: CacheLayout) -> Self;
    pub fn cache(&self) -> &CacheLayout;
    pub fn supported_formats(&self) -> Vec<&'static str>;
    pub async fn generate(&self, req: ThumbnailRequest) -> Result<ThumbnailResult>;
}

ThumbnailRequest

A request to generate thumbnails for a single source asset.

pub struct ThumbnailRequest {
    pub source_path: PathBuf,           // Path to the source file
    pub tenant_id: String,              // Tenant scope for cache layout
    pub asset_id: String,               // Stable asset ID for output keys
    pub format_hint: Option<String>,    // Optional mime type or extension hint
    pub sizes: Vec<u32>,                // Sizes to generate (default: 64, 256, 512)
    pub generate_gif_preview: bool,     // Refused by the Cloudflare isolate; see below
    pub generate_3d_views: bool,        // Refused by the Cloudflare isolate; see below
}

Cloudflare isolate rejection

The SaaS thumbnail isolate (packages/cloudflare-worker/sb-thumbnail-worker) renders still lossless WebP for raster and SVG only. It does not produce {asset}_gif.gif or {asset}_3d_v1..v4.webp, and it does not ignore the flags.

If generate_gif_preview or generate_3d_views is true, validate_job rejects the whole job before any still is written. The Durable Object stores a permanent failure. Branch on code and features. Do not match reason; that string is human text.

{
  "status": "failed",
  "code": "unsupported_feature",
  "features": ["generate_gif_preview", "generate_3d_views"],
  "reason": "generate_gif_preview and generate_3d_views are not implemented by the isolate codec",
  "permanent": true,
  "exhausted": false,
  "attempt": 0
}

features contains only the flags that were set, in the order generate_gif_preview, then generate_3d_views. GET / on the worker advertises the same names:

{
  "codecs": ["raster", "svg"],
  "unsupported_features": ["generate_gif_preview", "generate_3d_views"],
  "unsupported_feature_code": "unsupported_feature"
}

To get still WebP from the isolate, submit the job again with both flags false. CacheLayout::gif_preview_key and CacheLayout::model3d_view_key stay reserved for a native renderer. The isolate never writes those keys. The native GIF backend (video.rs) and 3D renderer (model3d.rs) are stubs today, so those keys are not produced there either.

There is no with_generate_gif_preview or with_generate_3d_views builder. Set the public fields.

ThumbnailResult

pub struct ThumbnailResult {
    pub asset_id: String,           // The asset this result is for
    pub format: Option<String>,     // Detected format slug
    pub generated: Vec<PathBuf>,    // List of generated thumbnail file paths
    pub errors: Vec<String>,        // Per-size errors (does not short-circuit)
}

CacheLayout

Computes key paths for the per-tenant cache. Keys are forward-slash-delimited (suitable for S3/Garage). local_path() converts to OS-native separators.

pub struct CacheLayout {
    // Private fields
}
 
impl CacheLayout {
    pub fn new(root: impl Into<PathBuf>, tenant_id: impl Into<String>) -> Result<Self>;
    pub fn root(&self) -> &Path;
    pub fn tenant_id(&self) -> &str;
    pub fn entity_key(&self, entity_type: &str, id: &str) -> String;
    pub fn thumbnail_key(&self, asset_id: &str, size: u32) -> String;
    pub fn gif_preview_key(&self, asset_id: &str) -> String;
    pub fn model3d_view_key(&self, asset_id: &str, view: Model3dView) -> String;
    pub fn local_path(&self, key: &str) -> PathBuf;
}

Format Detection

pub fn detect_format(path: &Path, hint: Option<&str>) -> Option<String>;
pub fn clamp_size(size: u32) -> u32;
 
pub enum FormatKind {
    Raster,
    Svg,
    Pdf,
    Video,
    Model3d,
}
 
impl FormatKind {
    pub fn from_slug(slug: &str) -> Option<Self>;
}

Cache Layout

Generated thumbnails are written to a per-tenant local cache directory and mirrored to the sb-tenant-cache Garage S3 bucket so devices can pull each other’s thumbnails:

~/.studiobrain/cache/{tenant_id}/
  thumbnails/{asset_id}_64.webp
  thumbnails/{asset_id}_256.webp
  thumbnails/{asset_id}_512.webp
  thumbnails/{asset_id}_gif.gif       // animated preview (video/gif)
  thumbnails/{asset_id}_3d_v1.webp    // front
  thumbnails/{asset_id}_3d_v2.webp    // side
  thumbnails/{asset_id}_3d_v3.webp    // top
  thumbnails/{asset_id}_3d_v4.webp    // iso

Still thumbnails from the commercial crate use .webp (THUMBNAIL_EXTENSION in crates/ai/crates/sb-thumbnails/src/lib.rs). The open-core subset in studiobrain-core still writes .jpg.

The _gif.gif and _3d_v* names are reserved. The Cloudflare isolate does not write them. A job that sets generate_gif_preview or generate_3d_views fails with code: "unsupported_feature". See Cloudflare isolate rejection.

3D Model Views

Rust cache-key contract

The _3d_v{1..4} key scheme is defined by this crate’s Model3dView enum — Front = 1, Side = 2, Top = 3, Iso = 4 — in crates/ai/crates/sb-thumbnails/src/cache.rs. CacheLayout::model3d_view_key builds {tenant}/thumbnails/{asset_id}_3d_v{N}.{THUMBNAIL_EXTENSION} from it:

Model3dView variantIndexKey suffix
Front1_3d_v1
Side2_3d_v2
Top3_3d_v3
Iso4_3d_v4

Example: model3d_view_key("m1", Model3dView::Top) → tenant-abc/thumbnails/m1_3d_v3.webp.

Rendering is stubbed. model3d.rs parses glTF/GLB, but render_view / render_views return Error::Model3dError("3d render backend not yet implemented; bevy/wgpu system deps pending"). The SaaS isolate rejects generate_3d_views (see above). These keys are the cache-layout contract, not files produced today. See the SBAI-1443 note below.

Historical Python renderer (retired)

The retired Python renderer used the following four views. This table is historical reference, not the current Rust contract:

ViewRenderer view keyKey suffixDescription
Frontfront_3d_v1.jpgDefault orientation, used as primary thumbnail
Sideside_3d_v2.jpg90° Y-axis rotation
Backback_3d_v3.jpg180° Y-axis rotation
Three-quarterthree_quarter_3d_v4.jpg45° Y + 30° X rotation (isometric)

Historical source for this table: archived studiobrain-ai processors/model3d/renderer.py, _VIEW_ORDER = ("front", "side", "back", "three_quarter"). That file is not in studiobrain-cloud or studiobrain-core. Nothing in the active trees renders these four Python views.

⚠️

The historical Python views differ from the Rust API. Model3dView in crates/ai/crates/sb-thumbnails/src/cache.rs is Front / Side / Top / Iso, and it is the source for the _3d_v{N} keys above. The Python table’s view order (front/side/back/three-quarter) is not. crates/ai/crates/sb-thumbnails/src/model3d.rs:1 says “glTF/GLB parsing ships; rendering is stubbed”, and :59 / :66 return "3d render backend not yet implemented; bevy/wgpu system deps pending".

SBAI-1443 — 3D view rendering is not live. Rechecked 2026-10-10 against studiobrain-cloud and studiobrain-core. The commercial pipeline still returns a stub error, so it does not produce the four views.

pathstateevidence
Commercial Rust sb-thumbnailsrender stubbedcrates/ai/crates/sb-thumbnails/src/model3d.rs:1, :59, and :66. pipeline.rs:318 honours generate_3d_views by recording that the renderer is not implemented, then returns Model3dError.
Open-core Rust copysame stubstudiobrain-core crates/sb-thumbnails/src/pipeline.rs:264
Python trimesh renderernot in the active reposNo renderer.py under studiobrain-cloud or studiobrain-core. studiobrain-core crates/sb-server/src/services/asset_tag_worker.rs:8 and :443 say the Python /api/asset/analyze/queue path was retired in SBAI-5714. The old routes/assets.rs:3396 quote is no longer in that file.
Core request flagproduction passes falsecrates/sb-server/src/routes/assets.rs:4642. Another false at :6912 is inside the test module that starts at :6850.
Cloud thumbnail workerforwards the job flagcrates/sb-thumbnail-worker/src/worker.rs:278 copies job.generate_3d_views into the pipeline request. The only true in that crate is a serde test at src/queue.rs:323. The pipeline still errors.

The renderer description below is the old studiobrain-ai Python implementation. It is not in the active trees. Treat the rest of this section as history, not current behaviour.

SBAI-3371 (historical — this path has no live caller): The AI-side asset_processor.py handled 3D model rendering directly via the processors/model3d/ module. The Model3DRenderer class produces deterministic front/side/back/three-quarter views using trimesh.save_image when pyrender is available, falling back to a PIL wireframe projector (no GPU needed), and finally a blank placeholder. All four rendered PNGs are handed through AssetProcessor.process_image() — the same path as normal image uploads — so 3D assets receive identical RAM tagging, captioning, and embedding vectors as 2D images. The result dict includes thumbnails (all four views as base64 PNGs), thumbnail (front view), tags (deduplicated across all views), and ai_analysis (per-view analysis keyed by thumbnail_front, thumbnail_side, etc.).

Thumbnails are stored in the _thumbnails subfolder alongside the asset (e.g. Characters/hero/_thumbnails/front.png). Supported formats: .glb, .gltf, .fbx, .obj.

Usage Example

use sb_thumbnails::{CacheLayout, Pipeline, ThumbnailRequest, STANDARD_SIZES};
 
// Set up cache for a tenant
let cache = CacheLayout::new("/home/user/.studiobrain/cache", "tenant-abc")?;
let pipeline = Pipeline::new(cache);
 
// Generate thumbnails for a source asset
let req = ThumbnailRequest::new(
    "/path/to/source/image.png",
    "tenant-abc",
    "asset-123",
);
 
let result = pipeline.generate(req).await?;
println!("Generated {} thumbnails", result.generated.len());

GIF preview and 3D views

Do not expect these calls to write files. The Cloudflare isolate rejects either flag with code: "unsupported_feature" and writes nothing. The native pipeline’s GIF and 3D backends are stubs.

let mut req = ThumbnailRequest::new(
    "/path/to/video.mp4",
    "tenant-abc",
    "asset-456",
);
req.generate_gif_preview = true;
// Isolate: permanent failure, features: ["generate_gif_preview"].
// Native video backend: Error::VideoError (ffmpeg stub). No asset-456_gif.gif.
 
let mut model = ThumbnailRequest::new(
    "/path/to/model.glb",
    "tenant-abc",
    "asset-789",
);
model.generate_3d_views = true;
// Isolate: permanent failure, features: ["generate_3d_views"].
// Native model3d backend: Error::Model3dError. No asset-789_3d_v{1..4} keys.

Error Types

#[derive(Debug, thiserror::Error)]
pub enum Error {
    UnsupportedFormat(String),      // Format not recognized
    Io(io::Error),                  // Filesystem / IO error
    ImageError(String),             // image crate backend error
    SvgError(String),               // SVG backend error
    PdfError(String),               // PDF backend error
    VideoError(String),             // Video backend error
    Model3dError(String),           // 3D model backend error
    CacheError(String),             // Cache layout / write error
    MissingFeature(&'static str),   // Feature flag not enabled in this build
    InvalidInput(String),           // Malformed request
}

Edition Differences

The commercial crate is studiobrain-cloud/crates/ai/crates/sb-thumbnails. studiobrain-ai is archived. Do not cargo-depend on it. Each package below enables a different feature set. pdf and video are stubs even when full is on, and they are absent from the open-core subset.

PackageDependency todayFeatures
Desktop clients/desktop/src-tauripath ../../../crates/ai/crates/sb-thumbnails (SBAI-12187)image, svg declared.
Mobile clients/mobile/src-tauripath ../../../crates/ai/crates/sb-thumbnails (SBAI-12187)image, svg
Cloud crates/sb-thumbnail-workerpath ../ai/crates/sb-thumbnailsfull (image, svg, pdf, video, model3d)
Cloud crates/sb-enterprisepath ../ai/crates/sb-thumbnailsimage only
Self-hosted studiobrain-core sb-serverpath ../sb-thumbnails (Apache-2.0 subset)image only, behind the thumbnails feature