blazen_llm_core/compute/traits.rs
1//! Compute platform traits for media generation providers.
2//!
3//! Implement [`ComputeProvider`] as the base, then add media-specific
4//! traits ([`ImageGeneration`], [`VideoGeneration`], etc.) for the
5//! capabilities your provider supports.
6//!
7//! ```rust,ignore
8//! use blazen_llm_core::compute::*;
9//! use blazen_llm_core::BlazenError;
10//!
11//! struct MyImageProvider { /* ... */ }
12//!
13//! #[async_trait::async_trait]
14//! impl ComputeProvider for MyImageProvider {
15//! fn provider_id(&self) -> &str { "my-provider" }
16//! async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError> { todo!() }
17//! async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError> { todo!() }
18//! async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError> { todo!() }
19//! async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError> { todo!() }
20//! }
21//!
22//! #[async_trait::async_trait]
23//! impl ImageGeneration for MyImageProvider {
24//! async fn generate_image(&self, request: ImageRequest) -> Result<ImageResult, BlazenError> {
25//! // Your implementation
26//! todo!()
27//! }
28//! async fn upscale_image(&self, request: UpscaleRequest) -> Result<ImageResult, BlazenError> {
29//! // Your implementation
30//! todo!()
31//! }
32//! }
33//! ```
34
35use std::sync::Arc;
36
37use async_trait::async_trait;
38
39use super::job::{ComputeRequest, ComputeResult, JobHandle, JobStatus};
40use super::requests::{
41 BackgroundRemovalRequest, CadReconstructionRequest, CadRequest, CaptionRequest, DepthRequest,
42 DetectionRequest, EmbeddingRequest, ImageEditRequest, ImageRequest, MusicRequest,
43 ReconstructionRequest, RetopologyRequest, SegmentationRequest, SpeechRequest, TextureRequest,
44 ThreeDRequest, TranscriptionRequest, UpscaleRequest, VideoEditRequest, VideoRequest,
45 VoiceCloneRequest,
46};
47use super::results::{
48 AudioResult, CadReconstructionResult, CadResult, CaptionResult, DepthResult, DetectionResult,
49 EmbeddingResult, ImageResult, ReconstructionResult, RetopologyResult, SegmentationResult,
50 TextureResult, ThreeDResult, TranscriptionResult, VideoResult, VoiceHandle,
51};
52use crate::error::BlazenError;
53
54// ---------------------------------------------------------------------------
55// Core compute trait
56// ---------------------------------------------------------------------------
57
58/// A compute platform that supports async job submission and polling.
59///
60/// This is the base trait for compute providers like fal.ai, Replicate,
61/// `RunPod`, etc. It models the common pattern of: submit a job, poll for
62/// status, retrieve the result.
63#[async_trait]
64pub trait ComputeProvider: Send + Sync {
65 /// Provider identifier (e.g., "fal", "replicate", "runpod").
66 fn provider_id(&self) -> &str;
67
68 /// Submit a compute job and get a handle to track it.
69 async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError>;
70
71 /// Poll the current status of a submitted job.
72 async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError>;
73
74 /// Wait for a job to complete and return the result.
75 ///
76 /// The default implementation delegates to the provider. Providers may
77 /// override this with long-polling, `WebSockets`, or SSE.
78 async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError>;
79
80 /// Cancel a running or queued job.
81 async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError>;
82
83 /// Submit a job and wait for the result (convenience method).
84 ///
85 /// This is equivalent to calling [`ComputeProvider::submit`] followed
86 /// by [`ComputeProvider::result`].
87 async fn run(&self, request: ComputeRequest) -> Result<ComputeResult, BlazenError> {
88 let job = self.submit(request).await?;
89 self.result(job).await
90 }
91
92 /// Optional configuration metadata for this provider.
93 fn provider_config(&self) -> Option<&crate::traits::ProviderConfig> {
94 None
95 }
96}
97
98// Arc passthrough for the base compute trait so capability sub-traits
99// (e.g. `VideoEdit`) can also be forwarded through `Arc<P>` — their
100// `impl ... for Arc<P>` requires `Arc<P>: ComputeProvider`.
101#[async_trait]
102impl<P: ComputeProvider + ?Sized> ComputeProvider for Arc<P> {
103 fn provider_id(&self) -> &str {
104 (**self).provider_id()
105 }
106 async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError> {
107 (**self).submit(request).await
108 }
109 async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError> {
110 (**self).status(job).await
111 }
112 async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError> {
113 (**self).result(job).await
114 }
115 async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError> {
116 (**self).cancel(job).await
117 }
118 async fn run(&self, request: ComputeRequest) -> Result<ComputeResult, BlazenError> {
119 (**self).run(request).await
120 }
121 fn provider_config(&self) -> Option<&crate::traits::ProviderConfig> {
122 (**self).provider_config()
123 }
124}
125
126// ---------------------------------------------------------------------------
127// Media-specific traits
128// ---------------------------------------------------------------------------
129
130/// Image generation and upscaling capability.
131///
132/// Providers that support image generation (fal.ai, Replicate, etc.)
133/// implement this trait to provide a typed interface for image operations.
134#[async_trait]
135pub trait ImageGeneration: ComputeProvider {
136 /// Generate images from a text prompt.
137 async fn generate_image(&self, request: ImageRequest) -> Result<ImageResult, BlazenError>;
138
139 /// Upscale an existing image.
140 async fn upscale_image(&self, request: UpscaleRequest) -> Result<ImageResult, BlazenError>;
141
142 /// Edit an existing image (img2img, inpaint, outpaint, etc.).
143 ///
144 /// Returns [`BlazenError::Unsupported`] by default so existing image
145 /// providers keep compiling unchanged; providers that support editing
146 /// (fal.ai, etc.) override this.
147 async fn edit_image(&self, _request: ImageEditRequest) -> Result<ImageResult, BlazenError> {
148 Err(BlazenError::unsupported(
149 "image editing not supported by this provider",
150 ))
151 }
152}
153
154/// Video generation capability.
155///
156/// Providers that support video synthesis implement this trait.
157#[async_trait]
158pub trait VideoGeneration: ComputeProvider {
159 /// Generate a video from a text prompt.
160 async fn text_to_video(&self, request: VideoRequest) -> Result<VideoResult, BlazenError>;
161
162 /// Generate a video from a source image and prompt.
163 async fn image_to_video(&self, request: VideoRequest) -> Result<VideoResult, BlazenError>;
164}
165
166/// Video editing / conditioning capability (VACE-style: reference / control /
167/// inpaint / outpaint / extend → video). Beyond plain text→video.
168#[async_trait]
169pub trait VideoEdit: ComputeProvider {
170 /// Edit/condition a video from references, a control video, and/or a mask.
171 ///
172 /// Returns [`BlazenError::Unsupported`] by default so existing video
173 /// providers keep compiling unchanged; providers that support editing
174 /// (native VACE, fal, etc.) override this.
175 async fn edit_video(&self, _request: VideoEditRequest) -> Result<VideoResult, BlazenError> {
176 Err(BlazenError::unsupported(
177 "video editing not supported by this provider",
178 ))
179 }
180}
181
182/// Audio generation capability (TTS, music, sound effects).
183///
184/// Providers that support audio synthesis implement this trait.
185#[async_trait]
186pub trait AudioGeneration: ComputeProvider {
187 /// Synthesize speech from text.
188 async fn text_to_speech(&self, request: SpeechRequest) -> Result<AudioResult, BlazenError>;
189
190 /// Generate music from a prompt.
191 ///
192 /// Returns [`BlazenError::Unsupported`] by default.
193 async fn generate_music(&self, _request: MusicRequest) -> Result<AudioResult, BlazenError> {
194 Err(BlazenError::unsupported(
195 "music generation not supported by this provider",
196 ))
197 }
198
199 /// Generate sound effects from a prompt.
200 ///
201 /// Returns [`BlazenError::Unsupported`] by default.
202 async fn generate_sfx(&self, _request: MusicRequest) -> Result<AudioResult, BlazenError> {
203 Err(BlazenError::unsupported(
204 "sound effect generation not supported by this provider",
205 ))
206 }
207}
208
209/// Audio transcription capability (speech-to-text).
210///
211/// Providers that support transcription implement this trait.
212#[async_trait]
213pub trait Transcription: ComputeProvider {
214 /// Transcribe audio to text with optional diarization.
215 async fn transcribe(
216 &self,
217 request: TranscriptionRequest,
218 ) -> Result<TranscriptionResult, BlazenError>;
219}
220
221/// 3D model generation capability.
222///
223/// Providers that support 3D generation implement this trait.
224#[async_trait]
225pub trait ThreeDGeneration: ComputeProvider {
226 /// Generate a 3D model from a text prompt or source image.
227 async fn generate_3d(&self, request: ThreeDRequest) -> Result<ThreeDResult, BlazenError>;
228}
229
230/// Neural audio codec capability (PCM ↔ discrete tokens).
231///
232/// Providers that wrap a neural audio codec (`EnCodec`, `DAC`, `SNAC`, …)
233/// implement this trait to expose deterministic encode / decode passes
234/// outside the generative job API.
235///
236/// Both methods default to [`BlazenError::Unsupported`] so existing
237/// non-codec providers do not need to override them.
238#[async_trait]
239pub trait Codec: ComputeProvider {
240 /// Encode mono PCM samples (`f32` in `[-1.0, 1.0]`) into discrete
241 /// codebook tokens.
242 ///
243 /// Returns [`BlazenError::Unsupported`] by default.
244 async fn encode_audio(&self, _pcm: &[f32], _sample_rate: u32) -> Result<Vec<u32>, BlazenError> {
245 Err(BlazenError::unsupported(
246 "audio codec encode_audio not supported by this provider",
247 ))
248 }
249
250 /// Decode flat row-major codebook tokens back into mono PCM samples.
251 ///
252 /// Returns [`BlazenError::Unsupported`] by default.
253 async fn decode_audio(
254 &self,
255 _tokens: &[u32],
256 _num_codebooks: usize,
257 ) -> Result<Vec<f32>, BlazenError> {
258 Err(BlazenError::unsupported(
259 "audio codec decode_audio not supported by this provider",
260 ))
261 }
262}
263
264/// A compute provider that supports background removal on existing images.
265#[async_trait]
266pub trait BackgroundRemoval: ComputeProvider {
267 /// Remove the background from an image and return the result.
268 async fn remove_background(
269 &self,
270 request: BackgroundRemovalRequest,
271 ) -> Result<ImageResult, BlazenError>;
272}
273
274/// Image-analysis capability: segmentation, depth estimation, and captioning.
275///
276/// This is the public seam for image-understanding models. Blazen ships the
277/// fal proxy implementation; private native depth / segmentation / caption
278/// models (in the zreg-only tier) implement this same trait so they can be
279/// injected behind the public interface.
280///
281/// All three methods default to [`BlazenError::Unsupported`] so providers
282/// opt in only to the capabilities they actually offer.
283#[async_trait]
284pub trait ImageAnalysis: ComputeProvider {
285 /// Segment an image into one or more masks (automatic or promptable).
286 ///
287 /// Returns [`BlazenError::Unsupported`] by default.
288 async fn segment_image(
289 &self,
290 _request: SegmentationRequest,
291 ) -> Result<SegmentationResult, BlazenError> {
292 Err(BlazenError::unsupported(
293 "image segmentation not supported by this provider",
294 ))
295 }
296
297 /// Estimate a depth map from a single image.
298 ///
299 /// Returns [`BlazenError::Unsupported`] by default.
300 async fn estimate_depth(&self, _request: DepthRequest) -> Result<DepthResult, BlazenError> {
301 Err(BlazenError::unsupported(
302 "depth estimation not supported by this provider",
303 ))
304 }
305
306 /// Caption an image, or answer a question about it (VQA).
307 ///
308 /// Returns [`BlazenError::Unsupported`] by default.
309 async fn caption_image(&self, _request: CaptionRequest) -> Result<CaptionResult, BlazenError> {
310 Err(BlazenError::unsupported(
311 "image captioning not supported by this provider",
312 ))
313 }
314}
315
316/// Open-vocabulary object-detection capability: image + class prompts → boxes.
317///
318/// This is the public seam for open-vocabulary detectors (Grounding-DINO,
319/// YOLO-World, OWL-ViT, …). Blazen ships fal-proxied detection; private native
320/// detectors (in the zreg-only tier) implement this same trait so they can be
321/// injected behind the public interface, exactly like [`ImageAnalysis`].
322///
323/// The single method defaults to [`BlazenError::Unsupported`] so a provider
324/// opts in only when it actually offers detection.
325#[async_trait]
326pub trait ObjectDetection: ComputeProvider {
327 /// Detect objects in an image, constrained to the request's open-vocabulary
328 /// class prompts (`text_prompt` and/or `classes`).
329 ///
330 /// Returns [`BlazenError::Unsupported`] by default.
331 async fn detect_objects(
332 &self,
333 _request: DetectionRequest,
334 ) -> Result<DetectionResult, BlazenError> {
335 Err(BlazenError::unsupported(
336 "object detection not supported by this provider",
337 ))
338 }
339}
340
341/// Multi-view 3D reconstruction capability: N images / video → mesh + cloud.
342///
343/// This is the public seam for MASt3R-class reconstructors that recover scene
344/// geometry and camera poses from multiple overlapping views. Blazen ships no
345/// reconstruction backend today (fal has no `MASt3R` endpoint); private native
346/// reconstructors (in the zreg-only tier) implement this same trait so they can
347/// be injected behind the public interface, exactly like [`ObjectDetection`].
348///
349/// The single method defaults to [`BlazenError::Unsupported`] so a provider
350/// opts in only when it actually offers reconstruction.
351#[async_trait]
352pub trait Reconstruction: ComputeProvider {
353 /// Reconstruct 3D geometry (mesh + optional point cloud + camera poses)
354 /// from the request's multiple views (`image_urls` and/or `video_url`).
355 ///
356 /// Returns [`BlazenError::Unsupported`] by default.
357 async fn reconstruct(
358 &self,
359 _request: ReconstructionRequest,
360 ) -> Result<ReconstructionResult, BlazenError> {
361 Err(BlazenError::unsupported(
362 "3D reconstruction not supported by this provider",
363 ))
364 }
365}
366
367/// Text-to-CAD capability: natural-language instruction → CAD script → mesh.
368///
369/// This is the public seam for instruction-driven CAD code generators
370/// (BlenderLLM-class): an LLM emits an executable CAD program (e.g. Blender
371/// `bpy`) from a textual part description, which is then run headless to
372/// produce a mesh. The result carries **both** the generated mesh and the
373/// script source. Blazen ships no CAD backend today; private native CAD models
374/// (in the zreg-only tier) implement this same trait so they can be injected
375/// behind the public interface, exactly like [`Reconstruction`].
376///
377/// The single method defaults to [`BlazenError::Unsupported`] so a provider
378/// opts in only when it actually offers CAD generation.
379#[async_trait]
380pub trait TextToCad: ComputeProvider {
381 /// Generate a CAD model (mesh + the script that produced it) from the
382 /// request's natural-language `instruction`.
383 ///
384 /// Returns [`BlazenError::Unsupported`] by default.
385 async fn generate_cad(&self, _request: CadRequest) -> Result<CadResult, BlazenError> {
386 Err(BlazenError::unsupported(
387 "text-to-CAD generation not supported by this provider",
388 ))
389 }
390}
391
392/// Retopology capability: dense/scanned mesh → clean quad-dominant mesh.
393///
394/// This is the public seam for automatic retopology backends (`QuadriFlow` /
395/// `QuadGPT` / Quad-Remesher-class) that rebuild a (typically triangulated or
396/// scanned) mesh as an even-density, quad-dominant surface fit for
397/// animation/subdivision. Blazen ships no retopology backend today; private
398/// native retopologizers (in the zreg-only tier) implement this same trait so
399/// they can be injected behind the public interface, exactly like
400/// [`Reconstruction`].
401///
402/// The single method defaults to [`BlazenError::Unsupported`] so a provider
403/// opts in only when it actually offers retopology.
404#[async_trait]
405pub trait Retopology: ComputeProvider {
406 /// Retopologize the request's `mesh_url` into a clean, quad-dominant mesh.
407 ///
408 /// Returns [`BlazenError::Unsupported`] by default.
409 async fn retopologize(
410 &self,
411 _request: RetopologyRequest,
412 ) -> Result<RetopologyResult, BlazenError> {
413 Err(BlazenError::unsupported(
414 "retopology not supported by this provider",
415 ))
416 }
417}
418
419/// CAD-reconstruction capability: point cloud → parametric CAD model + program.
420///
421/// This is the public seam for point-cloud-to-CAD reverse-engineering backends
422/// (`CAD-Recode`-class): given a sampled point cloud, a model recovers an
423/// editable parametric model plus the CAD program (e.g. a `CadQuery` Python
424/// script) that constructs it. It is the point-cloud sibling of [`TextToCad`]'s
425/// instruction→CAD seam, and the result carries **both** the model and the
426/// script source. Blazen ships no CAD-reconstruction backend today; private
427/// native models (in the zreg-only tier) implement this same trait so they can
428/// be injected behind the public interface, exactly like [`Reconstruction`].
429///
430/// The single method defaults to [`BlazenError::Unsupported`] so a provider
431/// opts in only when it actually offers CAD reconstruction.
432#[async_trait]
433pub trait CadReconstruction: ComputeProvider {
434 /// Reconstruct a parametric CAD model (model + the program that produced
435 /// it) from the request's `point_cloud_url`.
436 ///
437 /// Returns [`BlazenError::Unsupported`] by default.
438 async fn reconstruct_cad(
439 &self,
440 _request: CadReconstructionRequest,
441 ) -> Result<CadReconstructionResult, BlazenError> {
442 Err(BlazenError::unsupported(
443 "CAD reconstruction not supported by this provider",
444 ))
445 }
446}
447
448/// Mesh-texturing capability: mesh + prompt → PBR texture set.
449///
450/// This is the public seam for text-conditioned mesh texturers (`Paint3D` /
451/// `TEXTure` / `MVDream`-class) that paint a physically-based material set onto an
452/// existing mesh. Blazen ships no texturing backend today; private native
453/// texturers (in the zreg-only tier) implement this same trait so they can be
454/// injected behind the public interface, exactly like [`Reconstruction`].
455///
456/// The single method defaults to [`BlazenError::Unsupported`] so a provider
457/// opts in only when it actually offers texturing.
458///
459/// # Why there is no `FalProvider` implementation (abstraction-only)
460///
461/// Unlike the other compute traits, `TextureGeneration` ships **no fal proxy**:
462///
463/// - **Native is blocked.** A native texturer needs ControlNet-style residual
464/// injection into the candle `UNet` (still missing — candle's `UNet` has no
465/// residual-injection seam) *and* a differentiable mesh rasteriser to bake
466/// the multi-view paint back onto UV space (no candle equivalent). Both gaps
467/// are tracked in the private `blazen-models-3d` tier.
468/// - **No fal endpoint fits.** As of 2026-06 fal.ai exposes *no* mesh-texturing
469/// endpoint. Every fal 3D app (`hunyuan3d/v2`, `trellis`, `hyper3d/rodin`,
470/// `triposr`) takes **text or an image** and emits a *new* mesh — the mesh is
471/// the output, never the input — so the texture set is baked during
472/// generation and there is no `(mesh + prompt) -> textures` API to proxy.
473/// The only mesh-in endpoint, `triposr/remeshing`, only re-topologises
474/// geometry and produces no textures. Forcing a [`TextureRequest::mesh_url`]
475/// into an `image_url` field would be semantically wrong (a mesh is not an
476/// image), so no proxy is wired.
477///
478/// `TextureGeneration` therefore stays **abstraction-only**: the public seam
479/// exists so a private native texturer (or a future fal texturing endpoint) can
480/// be injected behind it, but every shipped provider inherits the default
481/// `Unsupported` body until then.
482#[async_trait]
483pub trait TextureGeneration: ComputeProvider {
484 /// Paint a PBR texture set (albedo + optional normal/roughness/metallic)
485 /// onto the request's `mesh_url`, guided by the request's `prompt`.
486 ///
487 /// Returns [`BlazenError::Unsupported`] by default.
488 async fn generate_texture(
489 &self,
490 _request: TextureRequest,
491 ) -> Result<TextureResult, BlazenError> {
492 Err(BlazenError::unsupported(
493 "texture generation not supported by this provider",
494 ))
495 }
496}
497
498/// Multimodal embedding capability: image / text / audio → vector.
499///
500/// This is the public seam for multimodal embedders (CLIP / `SigLIP` / CLAP /
501/// sentence-transformer-class) that encode an input into a dense vector in a
502/// shared embedding space. Blazen ships no embedding backend today; private
503/// native embedders (in the zreg-only tier) implement this same trait so they
504/// can be injected behind the public interface, exactly like [`Reconstruction`].
505///
506/// The single method defaults to [`BlazenError::Unsupported`] so a provider
507/// opts in only when it actually offers embedding.
508#[async_trait]
509pub trait Embedding: ComputeProvider {
510 /// Embed the request's `input` (text, image URL, or audio URL) into a
511 /// dense vector.
512 ///
513 /// Returns [`BlazenError::Unsupported`] by default.
514 async fn embed(&self, _request: EmbeddingRequest) -> Result<EmbeddingResult, BlazenError> {
515 Err(BlazenError::unsupported(
516 "embedding not supported by this provider",
517 ))
518 }
519}
520
521// ---------------------------------------------------------------------------
522// Voice cloning
523// ---------------------------------------------------------------------------
524
525/// Voice cloning capability.
526///
527/// Distinct from `AudioGeneration::text_to_speech` because cloning creates
528/// a persisted voice that can be reused across later TTS calls. No
529/// Blazen-shipped provider implements this trait -- it exists so users
530/// building their own providers (via `CustomProvider`) can wire up
531/// services like `ElevenLabs` or `zvoice` into Blazen's capability system.
532#[async_trait]
533pub trait VoiceCloning: ComputeProvider {
534 /// Clone a voice from one or more reference audio clips and return
535 /// a persistent handle that can be passed as `SpeechRequest.voice`
536 /// on subsequent TTS calls.
537 async fn clone_voice(&self, request: VoiceCloneRequest) -> Result<VoiceHandle, BlazenError>;
538
539 /// List all voices known to this provider (presets + cloned).
540 ///
541 /// Returns `BlazenError::Unsupported` by default.
542 async fn list_voices(&self) -> Result<Vec<VoiceHandle>, BlazenError> {
543 Err(BlazenError::unsupported(
544 "list_voices not supported by this provider",
545 ))
546 }
547
548 /// Delete a previously cloned voice.
549 ///
550 /// Returns `BlazenError::Unsupported` by default.
551 async fn delete_voice(&self, voice: &VoiceHandle) -> Result<(), BlazenError> {
552 let _ = voice;
553 Err(BlazenError::unsupported(
554 "delete_voice not supported by this provider",
555 ))
556 }
557}
558
559// Backwards-compatible type alias for the old trait name.
560/// Alias for [`ImageGeneration`] -- the old name before the multi-modal
561/// expansion.
562pub trait ImageModel: ImageGeneration {}
563impl<T: ImageGeneration> ImageModel for T {}
564
565// ---------------------------------------------------------------------------
566// Tests
567// ---------------------------------------------------------------------------
568
569#[cfg(test)]
570mod tests {
571 use super::super::requests::{ImageEditRequest, ImageEditType};
572 use super::*;
573
574 /// A minimal non-fal [`ImageGeneration`] provider that implements only the
575 /// required methods and inherits the defaulted `edit_image`.
576 struct NoEditProvider;
577
578 #[async_trait]
579 impl ComputeProvider for NoEditProvider {
580 fn provider_id(&self) -> &'static str {
581 "no-edit"
582 }
583 async fn submit(&self, _request: ComputeRequest) -> Result<JobHandle, BlazenError> {
584 Err(BlazenError::unsupported("submit"))
585 }
586 async fn status(&self, _job: &JobHandle) -> Result<JobStatus, BlazenError> {
587 Err(BlazenError::unsupported("status"))
588 }
589 async fn result(&self, _job: JobHandle) -> Result<ComputeResult, BlazenError> {
590 Err(BlazenError::unsupported("result"))
591 }
592 async fn cancel(&self, _job: &JobHandle) -> Result<(), BlazenError> {
593 Err(BlazenError::unsupported("cancel"))
594 }
595 }
596
597 #[async_trait]
598 impl ImageGeneration for NoEditProvider {
599 async fn generate_image(&self, _request: ImageRequest) -> Result<ImageResult, BlazenError> {
600 Err(BlazenError::unsupported("generate"))
601 }
602 async fn upscale_image(
603 &self,
604 _request: UpscaleRequest,
605 ) -> Result<ImageResult, BlazenError> {
606 Err(BlazenError::unsupported("upscale"))
607 }
608 // `edit_image` intentionally NOT overridden -> uses the trait default.
609 }
610
611 #[tokio::test]
612 async fn default_edit_image_returns_unsupported() {
613 let provider = NoEditProvider;
614 let request = ImageEditRequest::new(
615 "https://example.com/src.png",
616 "make it pop",
617 ImageEditType::Img2Img,
618 );
619 let err = provider.edit_image(request).await.unwrap_err();
620 assert!(
621 matches!(err, BlazenError::Unsupported { .. }),
622 "expected Unsupported, got: {err:?}"
623 );
624 }
625
626 /// A provider that opts into [`ImageAnalysis`] but overrides none of its
627 /// methods, so all three inherit the defaulted `Unsupported` bodies.
628 struct NoAnalysisProvider;
629
630 #[async_trait]
631 impl ComputeProvider for NoAnalysisProvider {
632 fn provider_id(&self) -> &'static str {
633 "no-analysis"
634 }
635 async fn submit(&self, _request: ComputeRequest) -> Result<JobHandle, BlazenError> {
636 Err(BlazenError::unsupported("submit"))
637 }
638 async fn status(&self, _job: &JobHandle) -> Result<JobStatus, BlazenError> {
639 Err(BlazenError::unsupported("status"))
640 }
641 async fn result(&self, _job: JobHandle) -> Result<ComputeResult, BlazenError> {
642 Err(BlazenError::unsupported("result"))
643 }
644 async fn cancel(&self, _job: &JobHandle) -> Result<(), BlazenError> {
645 Err(BlazenError::unsupported("cancel"))
646 }
647 }
648
649 // Opts into the capability without overriding any method.
650 impl ImageAnalysis for NoAnalysisProvider {}
651
652 #[tokio::test]
653 async fn default_image_analysis_methods_return_unsupported() {
654 let provider = NoAnalysisProvider;
655
656 let seg_err = provider
657 .segment_image(SegmentationRequest::new("https://example.com/img.png"))
658 .await
659 .unwrap_err();
660 assert!(
661 matches!(seg_err, BlazenError::Unsupported { .. }),
662 "expected Unsupported, got: {seg_err:?}"
663 );
664
665 let depth_err = provider
666 .estimate_depth(DepthRequest::new("https://example.com/img.png"))
667 .await
668 .unwrap_err();
669 assert!(
670 matches!(depth_err, BlazenError::Unsupported { .. }),
671 "expected Unsupported, got: {depth_err:?}"
672 );
673
674 let cap_err = provider
675 .caption_image(CaptionRequest::new("https://example.com/img.png"))
676 .await
677 .unwrap_err();
678 assert!(
679 matches!(cap_err, BlazenError::Unsupported { .. }),
680 "expected Unsupported, got: {cap_err:?}"
681 );
682 }
683
684 // Opts into detection without overriding the method -> defaulted body.
685 impl ObjectDetection for NoAnalysisProvider {}
686
687 #[tokio::test]
688 async fn default_detect_objects_returns_unsupported() {
689 let provider = NoAnalysisProvider;
690 let err = provider
691 .detect_objects(
692 DetectionRequest::new("https://example.com/img.png").with_prompt("a dog. a cat."),
693 )
694 .await
695 .unwrap_err();
696 assert!(
697 matches!(err, BlazenError::Unsupported { .. }),
698 "expected Unsupported, got: {err:?}"
699 );
700 }
701
702 // Opts into reconstruction without overriding the method -> defaulted body.
703 impl Reconstruction for NoAnalysisProvider {}
704
705 #[tokio::test]
706 async fn default_reconstruct_returns_unsupported() {
707 let provider = NoAnalysisProvider;
708 let err = provider
709 .reconstruct(ReconstructionRequest::from_images([
710 "https://example.com/view1.png",
711 "https://example.com/view2.png",
712 ]))
713 .await
714 .unwrap_err();
715 assert!(
716 matches!(err, BlazenError::Unsupported { .. }),
717 "expected Unsupported, got: {err:?}"
718 );
719 }
720
721 // Opts into text-to-CAD without overriding the method -> defaulted body.
722 impl TextToCad for NoAnalysisProvider {}
723
724 #[tokio::test]
725 async fn default_generate_cad_returns_unsupported() {
726 let provider = NoAnalysisProvider;
727 let err = provider
728 .generate_cad(CadRequest::new("a hexagonal nut with a central hole"))
729 .await
730 .unwrap_err();
731 assert!(
732 matches!(err, BlazenError::Unsupported { .. }),
733 "expected Unsupported, got: {err:?}"
734 );
735 }
736
737 // Opts into retopology without overriding the method -> defaulted body.
738 impl Retopology for NoAnalysisProvider {}
739
740 #[tokio::test]
741 async fn default_retopologize_returns_unsupported() {
742 let provider = NoAnalysisProvider;
743 let err = provider
744 .retopologize(RetopologyRequest::from_mesh(
745 "https://example.com/dense.glb",
746 ))
747 .await
748 .unwrap_err();
749 assert!(
750 matches!(err, BlazenError::Unsupported { .. }),
751 "expected Unsupported, got: {err:?}"
752 );
753 }
754
755 // Opts into CAD reconstruction without overriding the method -> defaulted body.
756 impl CadReconstruction for NoAnalysisProvider {}
757
758 #[tokio::test]
759 async fn default_reconstruct_cad_returns_unsupported() {
760 let provider = NoAnalysisProvider;
761 let err = provider
762 .reconstruct_cad(CadReconstructionRequest::from_point_cloud(
763 "https://example.com/scan.ply",
764 ))
765 .await
766 .unwrap_err();
767 assert!(
768 matches!(err, BlazenError::Unsupported { .. }),
769 "expected Unsupported, got: {err:?}"
770 );
771 }
772
773 // Opts into texture generation without overriding the method -> defaulted body.
774 impl TextureGeneration for NoAnalysisProvider {}
775
776 #[tokio::test]
777 async fn default_generate_texture_returns_unsupported() {
778 let provider = NoAnalysisProvider;
779 let err = provider
780 .generate_texture(TextureRequest::from_mesh_and_prompt(
781 "https://example.com/mesh.glb",
782 "weathered bronze",
783 ))
784 .await
785 .unwrap_err();
786 assert!(
787 matches!(err, BlazenError::Unsupported { .. }),
788 "expected Unsupported, got: {err:?}"
789 );
790 }
791
792 // Opts into embedding without overriding the method -> defaulted body.
793 impl Embedding for NoAnalysisProvider {}
794
795 #[tokio::test]
796 async fn default_embed_returns_unsupported() {
797 let provider = NoAnalysisProvider;
798 let err = provider
799 .embed(EmbeddingRequest::from_text("a photo of a dragon"))
800 .await
801 .unwrap_err();
802 assert!(
803 matches!(err, BlazenError::Unsupported { .. }),
804 "expected Unsupported, got: {err:?}"
805 );
806 }
807}