mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 05:45:01 -04:00
Add Gaussian Splat generation functionality
- Introduced a new `GaussianSplat` variant in the `JobOutput` enum to handle output from Gaussian splat jobs. - Implemented `GenerateSplatAction` for initiating Gaussian splat generation, including input and output structures. - Created `GaussianSplatJob` and its associated processing logic for batch image processing and splat generation. - Added `GaussianSplatProcessor` to manage the processing of images into 3D Gaussian splats. - Updated the media module to include new splat-related functionalities and integrated them into the existing workflow. - Enhanced the interface components to support visualization and interaction with Gaussian splats, including a toggle for displaying splat overlays. - Updated TypeScript types to accommodate new splat generation inputs and outputs, ensuring type safety across the application.
This commit is contained in:
@@ -82,6 +82,13 @@ pub enum JobOutput {
|
||||
error_count: usize,
|
||||
},
|
||||
|
||||
/// Gaussian splat generation output
|
||||
GaussianSplat {
|
||||
total_processed: usize,
|
||||
success_count: usize,
|
||||
error_count: usize,
|
||||
},
|
||||
|
||||
/// Generic output with custom data
|
||||
#[specta(skip)]
|
||||
Custom(serde_json::Value),
|
||||
@@ -269,6 +276,17 @@ impl fmt::Display for JobOutput {
|
||||
total_processed, success_count, error_count
|
||||
)
|
||||
}
|
||||
Self::GaussianSplat {
|
||||
total_processed,
|
||||
success_count,
|
||||
error_count,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Gaussian splat: {} processed ({} success, {} errors)",
|
||||
total_processed, success_count, error_count
|
||||
)
|
||||
}
|
||||
Self::Custom(_) => write!(f, "Custom output"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! - Thumbnail generation
|
||||
//! - OCR (text extraction from images/PDFs)
|
||||
//! - Speech-to-text (audio/video transcription)
|
||||
//! - Gaussian splat generation (3D view synthesis from images)
|
||||
//! - Video transcoding
|
||||
//! - Audio metadata extraction
|
||||
//! - Image optimization
|
||||
@@ -13,6 +14,7 @@ pub mod blurhash;
|
||||
pub mod metadata_extractor;
|
||||
pub mod ocr;
|
||||
pub mod proxy;
|
||||
pub mod splat;
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
pub mod speech;
|
||||
@@ -29,6 +31,7 @@ pub use metadata_extractor::{
|
||||
};
|
||||
pub use ocr::{OcrJob, OcrProcessor};
|
||||
pub use proxy::{ProxyJob, ProxyProcessor};
|
||||
pub use splat::{GaussianSplatJob, GaussianSplatProcessor};
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
pub use speech::{SpeechToTextJob, SpeechToTextProcessor};
|
||||
|
||||
95
core/src/ops/media/splat/action.rs
Normal file
95
core/src/ops/media/splat/action.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! Gaussian splat action handlers
|
||||
|
||||
use super::{
|
||||
job::{GaussianSplatJob, GaussianSplatJobConfig},
|
||||
processor::GaussianSplatProcessor,
|
||||
};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
ops::indexing::{path_resolver::PathResolver, processor::ProcessorEntry},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GenerateSplatInput {
|
||||
pub entry_uuid: Uuid,
|
||||
pub model_path: Option<String>, // Path to SHARP model checkpoint or None for auto-download
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GenerateSplatOutput {
|
||||
/// Job ID for tracking splat generation progress
|
||||
pub job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GenerateSplatAction {
|
||||
input: GenerateSplatInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GaussianSplatJobOutput {
|
||||
pub total_processed: usize,
|
||||
pub success_count: usize,
|
||||
pub error_count: usize,
|
||||
}
|
||||
|
||||
impl GenerateSplatAction {
|
||||
pub fn new(input: GenerateSplatInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryAction for GenerateSplatAction {
|
||||
type Input = GenerateSplatInput;
|
||||
type Output = GenerateSplatOutput;
|
||||
|
||||
fn from_input(input: GenerateSplatInput) -> Result<Self, String> {
|
||||
Ok(Self::new(input))
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: Arc<crate::library::Library>,
|
||||
_context: Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
tracing::info!(
|
||||
"Dispatching Gaussian splat job for entry: {}",
|
||||
self.input.entry_uuid
|
||||
);
|
||||
|
||||
// Create job config for single file
|
||||
let job_config = super::job::GaussianSplatJobConfig {
|
||||
location_id: None,
|
||||
entry_uuid: Some(self.input.entry_uuid), // Single file mode
|
||||
model_path: self.input.model_path,
|
||||
reprocess: false,
|
||||
};
|
||||
|
||||
// Create job
|
||||
let job = super::job::GaussianSplatJob::new(job_config);
|
||||
|
||||
// Dispatch job
|
||||
let job_handle = library
|
||||
.jobs()
|
||||
.dispatch(job)
|
||||
.await
|
||||
.map_err(|e| ActionError::Internal(format!("Failed to dispatch job: {}", e)))?;
|
||||
|
||||
tracing::info!("Gaussian splat job dispatched: {}", job_handle.id());
|
||||
|
||||
Ok(GenerateSplatOutput {
|
||||
job_id: job_handle.id().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"media.splat.generate"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(GenerateSplatAction, "media.splat.generate");
|
||||
346
core/src/ops/media/splat/job.rs
Normal file
346
core/src/ops/media/splat/job.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
//! Gaussian splat generation job for batch image processing
|
||||
|
||||
use super::processor::GaussianSplatProcessor;
|
||||
use crate::{
|
||||
infra::{
|
||||
db::entities::entry,
|
||||
job::{prelude::*, traits::DynJob},
|
||||
},
|
||||
ops::indexing::processor::ProcessorEntry,
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GaussianSplatJobConfig {
|
||||
/// Location ID to process (None = all entries in library)
|
||||
pub location_id: Option<Uuid>,
|
||||
/// Single entry UUID to process (for UI-triggered single file)
|
||||
pub entry_uuid: Option<Uuid>,
|
||||
/// Path to SHARP model checkpoint (None = auto-download)
|
||||
pub model_path: Option<String>,
|
||||
/// Reprocess files that already have splats
|
||||
pub reprocess: bool,
|
||||
}
|
||||
|
||||
impl Default for GaussianSplatJobConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
location_id: None,
|
||||
entry_uuid: None,
|
||||
model_path: None,
|
||||
reprocess: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SplatJobState {
|
||||
phase: SplatPhase,
|
||||
entries: Vec<(i32, std::path::PathBuf, Option<String>)>,
|
||||
processed: usize,
|
||||
success_count: usize,
|
||||
error_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
enum SplatPhase {
|
||||
Discovery,
|
||||
Processing,
|
||||
Complete,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GaussianSplatJob {
|
||||
config: GaussianSplatJobConfig,
|
||||
state: SplatJobState,
|
||||
}
|
||||
|
||||
impl GaussianSplatJob {
|
||||
pub fn new(config: GaussianSplatJobConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
state: SplatJobState {
|
||||
phase: SplatPhase::Discovery,
|
||||
entries: Vec::new(),
|
||||
processed: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_location(location_id: Uuid) -> Self {
|
||||
Self::new(GaussianSplatJobConfig {
|
||||
location_id: Some(location_id),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Job for GaussianSplatJob {
|
||||
const NAME: &'static str = "gaussian_splat";
|
||||
const RESUMABLE: bool = true;
|
||||
const DESCRIPTION: Option<&'static str> =
|
||||
Some("Generate 3D Gaussian splats from images for view synthesis");
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl JobHandler for GaussianSplatJob {
|
||||
type Output = GaussianSplatJobOutput;
|
||||
|
||||
async fn run(&mut self, ctx: JobContext<'_>) -> JobResult<Self::Output> {
|
||||
match self.state.phase {
|
||||
SplatPhase::Discovery => {
|
||||
ctx.log("Starting Gaussian splat discovery phase");
|
||||
self.run_discovery(&ctx).await?;
|
||||
self.state.phase = SplatPhase::Processing;
|
||||
}
|
||||
SplatPhase::Processing => {}
|
||||
SplatPhase::Complete => {
|
||||
return Ok(GaussianSplatJobOutput {
|
||||
total_processed: self.state.processed,
|
||||
success_count: self.state.success_count,
|
||||
error_count: self.state.error_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.log(format!(
|
||||
"Gaussian splat processing {} images",
|
||||
self.state.entries.len()
|
||||
));
|
||||
|
||||
let processor = GaussianSplatProcessor::new(ctx.library_arc());
|
||||
let processor = if let Some(ref model_path) = self.config.model_path {
|
||||
processor.with_model_path(model_path.clone())
|
||||
} else {
|
||||
processor
|
||||
};
|
||||
|
||||
let total = self.state.entries.len();
|
||||
|
||||
while self.state.processed < total {
|
||||
ctx.check_interrupt().await?;
|
||||
|
||||
let (entry_id, path, mime_type) = &self.state.entries[self.state.processed];
|
||||
|
||||
// Load entry to get content_id
|
||||
let entry_model = entry::Entity::find_by_id(*entry_id)
|
||||
.one(ctx.library_db())
|
||||
.await?
|
||||
.ok_or_else(|| JobError::execution("Entry not found"))?;
|
||||
|
||||
let proc_entry = ProcessorEntry {
|
||||
id: *entry_id,
|
||||
uuid: entry_model.uuid,
|
||||
path: path.clone(),
|
||||
kind: crate::ops::indexing::state::EntryKind::File,
|
||||
size: entry_model.size as u64,
|
||||
content_id: entry_model.content_id,
|
||||
mime_type: mime_type.clone(),
|
||||
};
|
||||
|
||||
if !processor.should_process(&proc_entry) {
|
||||
self.state.processed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Report progress
|
||||
ctx.progress(Progress::Indeterminate(format!(
|
||||
"Generating splat for {}...",
|
||||
path.file_name().and_then(|n| n.to_str()).unwrap_or("file")
|
||||
)));
|
||||
|
||||
let result = processor.process(ctx.library_db(), &proc_entry).await;
|
||||
|
||||
match result {
|
||||
Ok(result) if result.success => {
|
||||
ctx.log(format!(
|
||||
"Generated splat for {}: {} bytes",
|
||||
path.display(),
|
||||
result.bytes_processed
|
||||
));
|
||||
self.state.success_count += 1;
|
||||
}
|
||||
Ok(_) => {
|
||||
warn!("Splat generation failed for {}", path.display());
|
||||
self.state.error_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.log(format!(
|
||||
"ERROR: Splat generation error for {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
));
|
||||
self.state.error_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.processed += 1;
|
||||
|
||||
// Report progress with count
|
||||
ctx.progress(Progress::Count {
|
||||
current: self.state.processed,
|
||||
total,
|
||||
});
|
||||
|
||||
if self.state.processed % 5 == 0 {
|
||||
ctx.checkpoint().await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.phase = SplatPhase::Complete;
|
||||
ctx.log(format!(
|
||||
"Gaussian splat generation complete: {} success, {} errors",
|
||||
self.state.success_count, self.state.error_count
|
||||
));
|
||||
|
||||
Ok(GaussianSplatJobOutput {
|
||||
total_processed: self.state.processed,
|
||||
success_count: self.state.success_count,
|
||||
error_count: self.state.error_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GaussianSplatJob {
|
||||
async fn run_discovery(&mut self, ctx: &JobContext<'_>) -> JobResult<()> {
|
||||
use crate::infra::db::entities::{content_identity, entry, mime_type};
|
||||
|
||||
ctx.log("Starting Gaussian splat discovery");
|
||||
|
||||
// Check if SHARP CLI is available
|
||||
if !super::check_sharp_available().await.unwrap_or(false) {
|
||||
return Err(JobError::execution(
|
||||
"SHARP CLI not found. Please install ml-sharp (pip install -e /path/to/ml-sharp)",
|
||||
));
|
||||
}
|
||||
|
||||
ctx.log("SHARP CLI available");
|
||||
|
||||
let db = ctx.library_db();
|
||||
|
||||
// Check if this is single-file mode (from UI action)
|
||||
if let Some(entry_uuid) = self.config.entry_uuid {
|
||||
ctx.log(format!("Single file mode: processing entry {}", entry_uuid));
|
||||
|
||||
// Load the specific entry
|
||||
let entry_model = entry::Entity::find()
|
||||
.filter(entry::Column::Uuid.eq(entry_uuid))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| JobError::execution("Entry not found"))?;
|
||||
|
||||
if let Some(content_id) = entry_model.content_id {
|
||||
if let Ok(Some(ci)) = content_identity::Entity::find_by_id(content_id)
|
||||
.one(db)
|
||||
.await
|
||||
{
|
||||
if let Some(mime_id) = ci.mime_type_id {
|
||||
if let Ok(Some(mime)) = mime_type::Entity::find_by_id(mime_id).one(db).await
|
||||
{
|
||||
if super::is_splat_supported(&mime.mime_type) {
|
||||
if let Ok(path) = crate::ops::indexing::PathResolver::get_full_path(
|
||||
db,
|
||||
entry_model.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.state.entries.push((
|
||||
entry_model.id,
|
||||
path,
|
||||
Some(mime.mime_type),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.log(format!(
|
||||
"Single file discovered: {} entries",
|
||||
self.state.entries.len()
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Batch mode - discover all eligible entries
|
||||
let entries = entry::Entity::find()
|
||||
.filter(entry::Column::ContentId.is_not_null())
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
ctx.log(format!("Found {} entries with content", entries.len()));
|
||||
|
||||
for entry_model in entries {
|
||||
if let Some(content_id) = entry_model.content_id {
|
||||
if let Ok(Some(ci)) = content_identity::Entity::find_by_id(content_id)
|
||||
.one(db)
|
||||
.await
|
||||
{
|
||||
if let Some(mime_id) = ci.mime_type_id {
|
||||
if let Ok(Some(mime)) = mime_type::Entity::find_by_id(mime_id).one(db).await
|
||||
{
|
||||
if super::is_splat_supported(&mime.mime_type) {
|
||||
if let Ok(path) = crate::ops::indexing::PathResolver::get_full_path(
|
||||
db,
|
||||
entry_model.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.state.entries.push((
|
||||
entry_model.id,
|
||||
path,
|
||||
Some(mime.mime_type),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.log(format!(
|
||||
"Discovery complete: {} image files",
|
||||
self.state.entries.len()
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GaussianSplatJobOutput {
|
||||
pub total_processed: usize,
|
||||
pub success_count: usize,
|
||||
pub error_count: usize,
|
||||
}
|
||||
|
||||
impl From<GaussianSplatJobOutput> for JobOutput {
|
||||
fn from(output: GaussianSplatJobOutput) -> Self {
|
||||
JobOutput::GaussianSplat {
|
||||
total_processed: output.total_processed,
|
||||
success_count: output.success_count,
|
||||
error_count: output.error_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DynJob for GaussianSplatJob {
|
||||
fn job_name(&self) -> &'static str {
|
||||
"Gaussian Splat Generation"
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GaussianSplatJob> for Box<dyn DynJob> {
|
||||
fn from(job: GaussianSplatJob) -> Self {
|
||||
Box::new(job)
|
||||
}
|
||||
}
|
||||
99
core/src/ops/media/splat/mod.rs
Normal file
99
core/src/ops/media/splat/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
//! Gaussian Splat generation system
|
||||
//!
|
||||
//! Generates 3D Gaussian splats from images using Apple's SHARP model.
|
||||
//! Generates .ply sidecar files for photorealistic view synthesis.
|
||||
|
||||
pub mod action;
|
||||
pub mod job;
|
||||
pub mod processor;
|
||||
|
||||
pub use action::{GenerateSplatAction, GenerateSplatInput, GenerateSplatOutput};
|
||||
pub use job::{GaussianSplatJob, GaussianSplatJobConfig};
|
||||
pub use processor::GaussianSplatProcessor;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Generate a 3D Gaussian splat from an image using SHARP
|
||||
///
|
||||
/// This calls the `sharp` CLI tool as a subprocess.
|
||||
/// The sharp tool must be installed (e.g., via `pip install -r requirements.txt` in ml-sharp repo)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source_path` - Path to the input image
|
||||
/// * `output_dir` - Directory where the .ply file will be generated
|
||||
/// * `model_path` - Optional path to the SHARP model checkpoint
|
||||
///
|
||||
/// # Returns
|
||||
/// Path to the generated .ply file
|
||||
pub async fn generate_splat_from_image(
|
||||
source_path: &Path,
|
||||
output_dir: &Path,
|
||||
model_path: Option<&Path>,
|
||||
) -> Result<PathBuf> {
|
||||
use tokio::process::Command;
|
||||
|
||||
// Ensure output directory exists
|
||||
tokio::fs::create_dir_all(output_dir).await?;
|
||||
|
||||
// Build command
|
||||
let mut cmd = Command::new("sharp");
|
||||
cmd.arg("predict")
|
||||
.arg("-i")
|
||||
.arg(source_path)
|
||||
.arg("-o")
|
||||
.arg(output_dir);
|
||||
|
||||
// Add model path if provided
|
||||
if let Some(model) = model_path {
|
||||
cmd.arg("-c").arg(model);
|
||||
}
|
||||
|
||||
// Execute
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to execute 'sharp' command. Is it installed?")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("SHARP failed: {}", stderr);
|
||||
}
|
||||
|
||||
// The output file will be named based on input filename with .ply extension
|
||||
let ply_filename = source_path
|
||||
.file_stem()
|
||||
.context("Invalid source filename")?
|
||||
.to_str()
|
||||
.context("Non-UTF8 filename")?;
|
||||
|
||||
let ply_path = output_dir.join(format!("{}.ply", ply_filename));
|
||||
|
||||
if !ply_path.exists() {
|
||||
anyhow::bail!(
|
||||
"SHARP did not generate expected output file: {:?}",
|
||||
ply_path
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ply_path)
|
||||
}
|
||||
|
||||
/// Check if SHARP CLI is available in PATH
|
||||
pub async fn check_sharp_available() -> Result<bool> {
|
||||
let output = tokio::process::Command::new("sharp")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
Ok(output.is_ok())
|
||||
}
|
||||
|
||||
/// Check if an image type is supported for splat generation
|
||||
pub fn is_splat_supported(mime_type: &str) -> bool {
|
||||
// SHARP supports common image formats
|
||||
matches!(
|
||||
mime_type,
|
||||
"image/jpeg" | "image/png" | "image/webp" | "image/bmp" | "image/tiff"
|
||||
)
|
||||
}
|
||||
167
core/src/ops/media/splat/processor.rs
Normal file
167
core/src/ops/media/splat/processor.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
//! Gaussian splat processor - generates 3D splats from images
|
||||
|
||||
use crate::library::Library;
|
||||
use crate::ops::indexing::processor::{ProcessorEntry, ProcessorResult};
|
||||
use crate::ops::indexing::state::EntryKind;
|
||||
use crate::ops::sidecar::types::{SidecarFormat, SidecarKind, SidecarVariant};
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub struct GaussianSplatProcessor {
|
||||
library: Arc<Library>,
|
||||
model_path: Option<String>,
|
||||
}
|
||||
|
||||
impl GaussianSplatProcessor {
|
||||
pub fn new(library: Arc<Library>) -> Self {
|
||||
Self {
|
||||
library,
|
||||
model_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_model_path(mut self, path: String) -> Self {
|
||||
self.model_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_settings(mut self, settings: &Value) -> Result<Self> {
|
||||
if let Some(path) = settings.get("model_path").and_then(|v| v.as_str()) {
|
||||
self.model_path = Some(path.to_string());
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn should_process(&self, entry: &ProcessorEntry) -> bool {
|
||||
if !matches!(entry.kind, EntryKind::File) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if entry.content_id.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry
|
||||
.mime_type
|
||||
.as_ref()
|
||||
.map_or(false, |m| super::is_splat_supported(m))
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
&self,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
entry: &ProcessorEntry,
|
||||
) -> Result<ProcessorResult> {
|
||||
let content_uuid = if let Some(content_id) = entry.content_id {
|
||||
use crate::infra::db::entities::content_identity;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
let ci = content_identity::Entity::find()
|
||||
.filter(content_identity::Column::Id.eq(content_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("ContentIdentity not found"))?;
|
||||
|
||||
ci.uuid
|
||||
.ok_or_else(|| anyhow::anyhow!("ContentIdentity missing UUID"))?
|
||||
} else {
|
||||
return Ok(ProcessorResult::failure(
|
||||
"Entry has no content_id".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
debug!("→ Generating Gaussian splat for: {}", entry.path.display());
|
||||
|
||||
// Get sidecar manager
|
||||
let sidecar_manager = self
|
||||
.library
|
||||
.core_context()
|
||||
.get_sidecar_manager()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("SidecarManager not available"))?;
|
||||
|
||||
// Check if splat already exists
|
||||
if sidecar_manager
|
||||
.exists(
|
||||
&self.library.id(),
|
||||
&content_uuid,
|
||||
&SidecarKind::GaussianSplat,
|
||||
&SidecarVariant::new("ply"),
|
||||
&SidecarFormat::Ply,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
debug!("Gaussian splat already exists for {}", content_uuid);
|
||||
return Ok(ProcessorResult::success(0, 0));
|
||||
}
|
||||
|
||||
// Compute sidecar path
|
||||
let sidecar_path = sidecar_manager
|
||||
.compute_path(
|
||||
&self.library.id(),
|
||||
&content_uuid,
|
||||
&SidecarKind::GaussianSplat,
|
||||
&SidecarVariant::new("ply"),
|
||||
&SidecarFormat::Ply,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to compute path: {}", e))?;
|
||||
|
||||
// Create temporary output directory
|
||||
let temp_dir = std::env::temp_dir().join(format!("sd_splat_{}", content_uuid));
|
||||
tokio::fs::create_dir_all(&temp_dir).await?;
|
||||
|
||||
// Generate splat using SHARP
|
||||
let model_path_ref = self.model_path.as_ref().map(|s| std::path::Path::new(s));
|
||||
let ply_path = super::generate_splat_from_image(&entry.path, &temp_dir, model_path_ref)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate Gaussian splat: {}", e))?;
|
||||
|
||||
// Read generated PLY file
|
||||
let ply_data = tokio::fs::read(&ply_path).await?;
|
||||
let ply_size = ply_data.len();
|
||||
|
||||
debug!("Generated splat: {} bytes", ply_size);
|
||||
|
||||
// Ensure sidecar directory exists
|
||||
if let Some(parent) = sidecar_path.absolute_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
// Copy PLY to sidecar location
|
||||
tokio::fs::copy(&ply_path, &sidecar_path.absolute_path).await?;
|
||||
|
||||
// Clean up temp directory
|
||||
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
|
||||
|
||||
debug!(
|
||||
"✓ Generated Gaussian splat: {} ({} bytes)",
|
||||
sidecar_path.relative_path.display(),
|
||||
ply_size
|
||||
);
|
||||
|
||||
// Register sidecar in database
|
||||
sidecar_manager
|
||||
.record_sidecar(
|
||||
&self.library,
|
||||
&content_uuid,
|
||||
&SidecarKind::GaussianSplat,
|
||||
&SidecarVariant::new("ply"),
|
||||
&SidecarFormat::Ply,
|
||||
ply_size as u64,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to record sidecar: {}", e))?;
|
||||
|
||||
Ok(ProcessorResult::success(1, ply_size as u64))
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
"gaussian_splat"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub enum SidecarKind {
|
||||
Embeddings,
|
||||
Ocr,
|
||||
Transcript,
|
||||
GaussianSplat,
|
||||
}
|
||||
|
||||
impl SidecarKind {
|
||||
@@ -22,6 +23,7 @@ impl SidecarKind {
|
||||
Self::Embeddings => "embeddings",
|
||||
Self::Ocr => "ocr",
|
||||
Self::Transcript => "transcript",
|
||||
Self::GaussianSplat => "gaussian_splat",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +35,7 @@ impl SidecarKind {
|
||||
Self::Embeddings => "embeddings",
|
||||
Self::Ocr => "ocr",
|
||||
Self::Transcript => "transcript",
|
||||
Self::GaussianSplat => "gaussian_splats",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +57,7 @@ impl TryFrom<&str> for SidecarKind {
|
||||
"embeddings" => Ok(Self::Embeddings),
|
||||
"ocr" => Ok(Self::Ocr),
|
||||
"transcript" => Ok(Self::Transcript),
|
||||
"gaussian_splat" => Ok(Self::GaussianSplat),
|
||||
_ => Err(format!("Invalid sidecar kind: {}", value)),
|
||||
}
|
||||
}
|
||||
@@ -98,6 +102,7 @@ impl From<String> for SidecarVariant {
|
||||
/// - Json: Text-based structured data (OCR, transcripts)
|
||||
/// - MessagePack: Binary structured data (embeddings, vectors)
|
||||
/// - Text: Plain text extractions
|
||||
/// - Ply: 3D model format for Gaussian splats
|
||||
///
|
||||
/// MessagePack is preferred for embeddings because:
|
||||
/// - 6x smaller than JSON (1.7KB vs 10KB per 384-dim vector)
|
||||
@@ -112,6 +117,7 @@ pub enum SidecarFormat {
|
||||
Json,
|
||||
MessagePack,
|
||||
Text,
|
||||
Ply,
|
||||
}
|
||||
|
||||
impl SidecarFormat {
|
||||
@@ -122,6 +128,7 @@ impl SidecarFormat {
|
||||
Self::Json => "json",
|
||||
Self::MessagePack => "msgpack",
|
||||
Self::Text => "txt",
|
||||
Self::Ply => "ply",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +139,7 @@ impl SidecarFormat {
|
||||
Self::Json => "json",
|
||||
Self::MessagePack => "messagepack",
|
||||
Self::Text => "text",
|
||||
Self::Ply => "ply",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,6 +160,7 @@ impl TryFrom<&str> for SidecarFormat {
|
||||
"json" => Ok(Self::Json),
|
||||
"msgpack" | "messagepack" => Ok(Self::MessagePack),
|
||||
"text" | "txt" => Ok(Self::Text),
|
||||
"ply" => Ok(Self::Ply),
|
||||
_ => Err(format!("Invalid sidecar format: {}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MagnifyingGlassPlus,
|
||||
MagnifyingGlassMinus,
|
||||
ArrowCounterClockwise,
|
||||
Cube,
|
||||
} from "@phosphor-icons/react";
|
||||
import { VideoPlayer } from "./VideoPlayer";
|
||||
import { AudioPlayer } from "./AudioPlayer";
|
||||
@@ -28,12 +29,29 @@ function ImageRenderer({ file, onZoomChange }: ContentRendererProps) {
|
||||
const [originalLoaded, setOriginalLoaded] = useState(false);
|
||||
const [originalUrl, setOriginalUrl] = useState<string | null>(null);
|
||||
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
|
||||
const [showSplat, setShowSplat] = useState(false);
|
||||
const { zoom, zoomIn, zoomOut, reset, isZoomed, transform } =
|
||||
useZoomPan(containerRef);
|
||||
|
||||
// Get a stable identifier for the image file itself
|
||||
const imageFileId = file.content_identity?.uuid || file.id;
|
||||
|
||||
// Check if Gaussian splat sidecar exists and get URL
|
||||
const splatSidecar = file.sidecars?.find(
|
||||
(s) => s.kind === "gaussian_splat" && s.format === "ply"
|
||||
);
|
||||
const hasSplat = !!splatSidecar;
|
||||
|
||||
// Build sidecar URL for the splat
|
||||
const splatUrl = hasSplat && file.content_identity?.uuid
|
||||
? buildSidecarUrl(
|
||||
file.content_identity.uuid,
|
||||
splatSidecar!.kind,
|
||||
splatSidecar!.variant,
|
||||
splatSidecar!.format,
|
||||
)
|
||||
: null;
|
||||
|
||||
// Notify parent of zoom state changes
|
||||
useEffect(() => {
|
||||
onZoomChange?.(isZoomed);
|
||||
@@ -44,6 +62,7 @@ function ImageRenderer({ file, onZoomChange }: ContentRendererProps) {
|
||||
setShouldLoadOriginal(false);
|
||||
setOriginalLoaded(false);
|
||||
setOriginalUrl(null);
|
||||
setShowSplat(false); // Reset splat view when file changes
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setShouldLoadOriginal(true);
|
||||
@@ -107,11 +126,54 @@ function ImageRenderer({ file, onZoomChange }: ContentRendererProps) {
|
||||
|
||||
const thumbnailUrl = getHighestResThumbnail();
|
||||
|
||||
// Render splat view separately (not overlayed)
|
||||
if (showSplat && hasSplat && splatUrl) {
|
||||
return (
|
||||
<>
|
||||
{/* Splat Toggle */}
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<button
|
||||
onClick={() => setShowSplat(false)}
|
||||
className="rounded-lg p-2 bg-accent text-white backdrop-blur-xl transition-colors hover:bg-accent/90"
|
||||
title="Show Image"
|
||||
>
|
||||
<Cube size={20} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Splat Viewer - matches direct .ply rendering structure */}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<FileComponent.Thumb file={file} size={200} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MeshViewer file={file} splatUrl={splatUrl} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Render image view with zoom/pan
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`relative w-full h-full flex items-center justify-center ${isZoomed ? "overflow-visible" : "overflow-hidden"}`}
|
||||
>
|
||||
{/* Splat Toggle (top-left) */}
|
||||
{hasSplat && (
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<button
|
||||
onClick={() => setShowSplat(true)}
|
||||
className="rounded-lg p-2 bg-app-box/80 text-ink backdrop-blur-xl transition-colors hover:bg-app-hover"
|
||||
title="Show 3D Splat"
|
||||
>
|
||||
<Cube size={20} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoom Controls */}
|
||||
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2">
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as THREE from "three";
|
||||
interface MeshViewerProps {
|
||||
file: File;
|
||||
onZoomChange?: (isZoomed: boolean) => void;
|
||||
splatUrl?: string | null; // Optional URL to Gaussian splat sidecar
|
||||
}
|
||||
|
||||
interface MeshSceneProps {
|
||||
@@ -172,7 +173,7 @@ function GaussianSplatViewer({
|
||||
);
|
||||
}
|
||||
|
||||
export function MeshViewer({ file, onZoomChange }: MeshViewerProps) {
|
||||
export function MeshViewer({ file, onZoomChange, splatUrl }: MeshViewerProps) {
|
||||
const platform = usePlatform();
|
||||
const [meshUrl, setMeshUrl] = useState<string | null>(null);
|
||||
const [isGaussianSplat, setIsGaussianSplat] = useState(false);
|
||||
@@ -192,9 +193,17 @@ export function MeshViewer({ file, onZoomChange }: MeshViewerProps) {
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [fileId]);
|
||||
}, [fileId, splatUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// If splatUrl is provided, use it directly (it's a Gaussian splat sidecar)
|
||||
if (splatUrl) {
|
||||
setMeshUrl(splatUrl);
|
||||
setIsGaussianSplat(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldLoad || !platform.convertFileSrc) {
|
||||
return;
|
||||
}
|
||||
@@ -211,6 +220,11 @@ export function MeshViewer({ file, onZoomChange }: MeshViewerProps) {
|
||||
const url = platform.convertFileSrc(physicalPath);
|
||||
setMeshUrl(url);
|
||||
|
||||
// Only run detection if not using splatUrl (splatUrl is already known to be a Gaussian splat)
|
||||
if (splatUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an AbortController to cancel the detection fetch if component unmounts
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -254,7 +268,7 @@ export function MeshViewer({ file, onZoomChange }: MeshViewerProps) {
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [shouldLoad, fileId, file.sd_path, platform]);
|
||||
}, [shouldLoad, fileId, file.sd_path, platform, splatUrl]);
|
||||
|
||||
if (!meshUrl || loading) {
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Trash,
|
||||
FilmStrip,
|
||||
VideoCamera,
|
||||
Cube,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { getContentKind } from "../components/Explorer/utils";
|
||||
@@ -120,6 +121,7 @@ function OverviewTab({ file }: { file: File }) {
|
||||
// AI Processing mutations
|
||||
const extractText = useLibraryMutation("media.ocr.extract");
|
||||
const transcribeAudio = useLibraryMutation("media.speech.transcribe");
|
||||
const generateSplat = useLibraryMutation("media.splat.generate");
|
||||
const regenerateThumbnail = useLibraryMutation(
|
||||
"media.thumbnail.regenerate",
|
||||
);
|
||||
@@ -463,6 +465,52 @@ function OverviewTab({ file }: { file: File }) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Gaussian Splat for images */}
|
||||
{isImage && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"Generate splat clicked for file:",
|
||||
file.id,
|
||||
);
|
||||
generateSplat.mutate(
|
||||
{
|
||||
entry_uuid: file.id,
|
||||
model_path: null,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log(
|
||||
"Splat generation success:",
|
||||
data,
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(
|
||||
"Splat generation error:",
|
||||
error,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
disabled={generateSplat.isPending}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
"bg-app-box hover:bg-app-hover border border-app-line",
|
||||
generateSplat.isPending &&
|
||||
"opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<Cube size={4} weight="bold" />
|
||||
<span>
|
||||
{generateSplat.isPending
|
||||
? "Generating..."
|
||||
: "Generate 3D Splat"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Speech-to-text for audio/video */}
|
||||
{(isVideo || isAudio) && (
|
||||
<button
|
||||
|
||||
@@ -1072,6 +1072,14 @@ variants: string[];
|
||||
*/
|
||||
encoding_time_secs: number };
|
||||
|
||||
export type GenerateSplatInput = { entry_uuid: string; model_path: string | null };
|
||||
|
||||
export type GenerateSplatOutput = {
|
||||
/**
|
||||
* Job ID for tracking splat generation progress
|
||||
*/
|
||||
job_id: string };
|
||||
|
||||
/**
|
||||
* Generate thumbstrip for a single video file
|
||||
*/
|
||||
@@ -1633,7 +1641,11 @@ export type JobOutput =
|
||||
/**
|
||||
* Speech-to-text transcription output
|
||||
*/
|
||||
{ type: "SpeechToText"; data: { total_processed: number; success_count: number; error_count: number } };
|
||||
{ type: "SpeechToText"; data: { total_processed: number; success_count: number; error_count: number } } |
|
||||
/**
|
||||
* Gaussian splat generation output
|
||||
*/
|
||||
{ type: "GaussianSplat"; data: { total_processed: number; success_count: number; error_count: number } };
|
||||
|
||||
export type JobPauseInput = { job_id: string };
|
||||
|
||||
@@ -3050,6 +3062,7 @@ export type Sidecar = { id: number; content_uuid: string; kind: string; variant:
|
||||
* - Json: Text-based structured data (OCR, transcripts)
|
||||
* - MessagePack: Binary structured data (embeddings, vectors)
|
||||
* - Text: Plain text extractions
|
||||
* - Ply: 3D model format for Gaussian splats
|
||||
*
|
||||
* MessagePack is preferred for embeddings because:
|
||||
* - 6x smaller than JSON (1.7KB vs 10KB per 384-dim vector)
|
||||
@@ -3057,9 +3070,9 @@ export type Sidecar = { id: number; content_uuid: string; kind: string; variant:
|
||||
* - Already used in Spacedrive (job serialization)
|
||||
* - Enables sub-30ms semantic search on 1M+ files
|
||||
*/
|
||||
export type SidecarFormat = "webp" | "mp_4" | "json" | "message_pack" | "text";
|
||||
export type SidecarFormat = "webp" | "mp_4" | "json" | "message_pack" | "text" | "ply";
|
||||
|
||||
export type SidecarKind = "thumb" | "thumbstrip" | "proxy" | "embeddings" | "ocr" | "transcript";
|
||||
export type SidecarKind = "thumb" | "thumbstrip" | "proxy" | "embeddings" | "ocr" | "transcript" | "gaussian_splat";
|
||||
|
||||
export type SidecarVariant = string;
|
||||
|
||||
@@ -4000,209 +4013,211 @@ success: boolean };
|
||||
// ===== API Type Unions =====
|
||||
|
||||
export type CoreAction =
|
||||
{ type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput }
|
||||
{ type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput }
|
||||
| { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput }
|
||||
| { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput }
|
||||
| { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput }
|
||||
| { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput }
|
||||
| { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput }
|
||||
| { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput }
|
||||
| { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput }
|
||||
| { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput }
|
||||
| { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput }
|
||||
| { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput }
|
||||
| { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput }
|
||||
| { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput }
|
||||
| { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput }
|
||||
| { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput }
|
||||
| { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput }
|
||||
| { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput }
|
||||
| { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput }
|
||||
| { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput }
|
||||
| { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput }
|
||||
| { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput }
|
||||
| { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput }
|
||||
| { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput }
|
||||
;
|
||||
|
||||
export type LibraryAction =
|
||||
{ type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput }
|
||||
| { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput }
|
||||
| { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput }
|
||||
| { type: 'files.copy'; input: FileCopyInput; output: JobReceipt }
|
||||
| { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput }
|
||||
| { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput }
|
||||
| { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput }
|
||||
| { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput }
|
||||
| { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput }
|
||||
| { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput }
|
||||
| { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput }
|
||||
| { type: 'indexing.start'; input: IndexInput; output: JobReceipt }
|
||||
| { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput }
|
||||
| { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput }
|
||||
{ type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput }
|
||||
| { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput }
|
||||
| { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt }
|
||||
| { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput }
|
||||
| { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput }
|
||||
| { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput }
|
||||
| { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput }
|
||||
| { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput }
|
||||
| { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput }
|
||||
| { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput }
|
||||
| { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput }
|
||||
| { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput }
|
||||
| { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput }
|
||||
| { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput }
|
||||
| { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput }
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
| { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput }
|
||||
| { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput }
|
||||
| { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput }
|
||||
| { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput }
|
||||
| { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput }
|
||||
| { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput }
|
||||
| { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput }
|
||||
| { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput }
|
||||
| { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput }
|
||||
| { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput }
|
||||
| { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput }
|
||||
| { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput }
|
||||
| { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput }
|
||||
| { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput }
|
||||
| { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput }
|
||||
| { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput }
|
||||
| { type: 'indexing.start'; input: IndexInput; output: JobReceipt }
|
||||
| { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput }
|
||||
| { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput }
|
||||
| { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput }
|
||||
| { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput }
|
||||
| { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput }
|
||||
| { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput }
|
||||
| { type: 'files.copy'; input: FileCopyInput; output: JobReceipt }
|
||||
| { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput }
|
||||
| { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput }
|
||||
| { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput }
|
||||
| { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput }
|
||||
| { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput }
|
||||
| { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput }
|
||||
| { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput }
|
||||
| { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput }
|
||||
| { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput }
|
||||
| { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput }
|
||||
| { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput }
|
||||
| { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput }
|
||||
| { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput }
|
||||
| { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput }
|
||||
| { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput }
|
||||
| { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput }
|
||||
| { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput }
|
||||
| { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput }
|
||||
| { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput }
|
||||
| { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput }
|
||||
| { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput }
|
||||
| { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput }
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
;
|
||||
|
||||
export type CoreQuery =
|
||||
{ type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput }
|
||||
{ type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus }
|
||||
| { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput }
|
||||
| { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput }
|
||||
| { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput }
|
||||
| { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput }
|
||||
| { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus }
|
||||
| { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput }
|
||||
| { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput }
|
||||
| { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput }
|
||||
| { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput }
|
||||
| { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus }
|
||||
| { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput }
|
||||
| { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus }
|
||||
| { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput }
|
||||
| { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] }
|
||||
| { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput }
|
||||
| { type: 'core.status'; input: Empty; output: CoreStatus }
|
||||
| { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] }
|
||||
;
|
||||
|
||||
export type LibraryQuery =
|
||||
{ type: 'jobs.list'; input: JobListInput; output: JobListOutput }
|
||||
| { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput }
|
||||
| { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput }
|
||||
| { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput }
|
||||
| { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'test.ping'; input: PingInput; output: PingOutput }
|
||||
| { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput }
|
||||
| { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput }
|
||||
{ type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput }
|
||||
| { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput }
|
||||
| { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput }
|
||||
| { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput }
|
||||
| { type: 'files.by_id'; input: FileByIdQuery; output: File }
|
||||
| { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput }
|
||||
| { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library }
|
||||
| { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput }
|
||||
| { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput }
|
||||
| { type: 'test.ping'; input: PingInput; output: PingOutput }
|
||||
| { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput }
|
||||
| { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput }
|
||||
| { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library }
|
||||
| { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput }
|
||||
| { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout }
|
||||
| { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput }
|
||||
| { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput }
|
||||
| { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] }
|
||||
| { type: 'files.by_path'; input: FileByPathQuery; output: File }
|
||||
| { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput }
|
||||
| { type: 'jobs.list'; input: JobListInput; output: JobListOutput }
|
||||
| { type: 'files.by_path'; input: FileByPathQuery; output: File }
|
||||
;
|
||||
|
||||
// ===== Wire Method Mappings =====
|
||||
|
||||
export const WIRE_METHODS = {
|
||||
coreActions: {
|
||||
'network.pair.cancel': 'action:network.pair.cancel.input',
|
||||
'libraries.create': 'action:libraries.create.input',
|
||||
'network.pair.join': 'action:network.pair.join.input',
|
||||
'libraries.delete': 'action:libraries.delete.input',
|
||||
'network.spacedrop.send': 'action:network.spacedrop.send.input',
|
||||
'network.sync_setup': 'action:network.sync_setup.input',
|
||||
'models.whisper.delete': 'action:models.whisper.delete.input',
|
||||
'models.whisper.download': 'action:models.whisper.download.input',
|
||||
'network.start': 'action:network.start.input',
|
||||
'libraries.create': 'action:libraries.create.input',
|
||||
'network.pair.generate': 'action:network.pair.generate.input',
|
||||
'network.device.revoke': 'action:network.device.revoke.input',
|
||||
'core.reset': 'action:core.reset.input',
|
||||
'network.pair.cancel': 'action:network.pair.cancel.input',
|
||||
'network.pair.join': 'action:network.pair.join.input',
|
||||
'libraries.open': 'action:libraries.open.input',
|
||||
'network.spacedrop.send': 'action:network.spacedrop.send.input',
|
||||
'core.reset': 'action:core.reset.input',
|
||||
'network.pair.generate': 'action:network.pair.generate.input',
|
||||
'network.start': 'action:network.start.input',
|
||||
'network.stop': 'action:network.stop.input',
|
||||
'network.sync_setup': 'action:network.sync_setup.input',
|
||||
'network.device.revoke': 'action:network.device.revoke.input',
|
||||
},
|
||||
|
||||
libraryActions: {
|
||||
'jobs.cancel': 'action:jobs.cancel.input',
|
||||
'media.ocr.extract': 'action:media.ocr.extract.input',
|
||||
'locations.rescan': 'action:locations.rescan.input',
|
||||
'files.copy': 'action:files.copy.input',
|
||||
'spaces.delete_group': 'action:spaces.delete_group.input',
|
||||
'jobs.pause': 'action:jobs.pause.input',
|
||||
'indexing.verify': 'action:indexing.verify.input',
|
||||
'spaces.update': 'action:spaces.update.input',
|
||||
'volumes.add_cloud': 'action:volumes.add_cloud.input',
|
||||
'spaces.add_group': 'action:spaces.add_group.input',
|
||||
'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input',
|
||||
'indexing.start': 'action:indexing.start.input',
|
||||
'locations.remove': 'action:locations.remove.input',
|
||||
'media.proxy.generate': 'action:media.proxy.generate.input',
|
||||
'tags.apply': 'action:tags.apply.input',
|
||||
'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input',
|
||||
'media.thumbnail': 'action:media.thumbnail.input',
|
||||
'volumes.untrack': 'action:volumes.untrack.input',
|
||||
'spaces.add_item': 'action:spaces.add_item.input',
|
||||
'spaces.delete_item': 'action:spaces.delete_item.input',
|
||||
'volumes.speed_test': 'action:volumes.speed_test.input',
|
||||
'libraries.export': 'action:libraries.export.input',
|
||||
'spaces.update_group': 'action:spaces.update_group.input',
|
||||
'jobs.resume': 'action:jobs.resume.input',
|
||||
'volumes.track': 'action:volumes.track.input',
|
||||
'volumes.index': 'action:volumes.index.input',
|
||||
'volumes.remove_cloud': 'action:volumes.remove_cloud.input',
|
||||
'media.speech.transcribe': 'action:media.speech.transcribe.input',
|
||||
'libraries.rename': 'action:libraries.rename.input',
|
||||
'files.delete': 'action:files.delete.input',
|
||||
'volumes.refresh': 'action:volumes.refresh.input',
|
||||
'locations.triggerJob': 'action:locations.triggerJob.input',
|
||||
'locations.enable_indexing': 'action:locations.enable_indexing.input',
|
||||
'locations.add': 'action:locations.add.input',
|
||||
'locations.import': 'action:locations.import.input',
|
||||
'locations.update': 'action:locations.update.input',
|
||||
'tags.apply': 'action:tags.apply.input',
|
||||
'media.proxy.generate': 'action:media.proxy.generate.input',
|
||||
'spaces.create': 'action:spaces.create.input',
|
||||
'volumes.speed_test': 'action:volumes.speed_test.input',
|
||||
'libraries.rename': 'action:libraries.rename.input',
|
||||
'locations.export': 'action:locations.export.input',
|
||||
'spaces.update': 'action:spaces.update.input',
|
||||
'spaces.delete_group': 'action:spaces.delete_group.input',
|
||||
'spaces.add_item': 'action:spaces.add_item.input',
|
||||
'locations.triggerJob': 'action:locations.triggerJob.input',
|
||||
'indexing.start': 'action:indexing.start.input',
|
||||
'spaces.reorder_items': 'action:spaces.reorder_items.input',
|
||||
'spaces.reorder_groups': 'action:spaces.reorder_groups.input',
|
||||
'volumes.index': 'action:volumes.index.input',
|
||||
'jobs.pause': 'action:jobs.pause.input',
|
||||
'locations.update': 'action:locations.update.input',
|
||||
'spaces.delete_item': 'action:spaces.delete_item.input',
|
||||
'files.copy': 'action:files.copy.input',
|
||||
'tags.create': 'action:tags.create.input',
|
||||
'media.splat.generate': 'action:media.splat.generate.input',
|
||||
'indexing.verify': 'action:indexing.verify.input',
|
||||
'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input',
|
||||
'volumes.refresh': 'action:volumes.refresh.input',
|
||||
'volumes.add_cloud': 'action:volumes.add_cloud.input',
|
||||
'libraries.export': 'action:libraries.export.input',
|
||||
'locations.import': 'action:locations.import.input',
|
||||
'locations.rescan': 'action:locations.rescan.input',
|
||||
'spaces.update_group': 'action:spaces.update_group.input',
|
||||
'media.speech.transcribe': 'action:media.speech.transcribe.input',
|
||||
'volumes.track': 'action:volumes.track.input',
|
||||
'locations.enable_indexing': 'action:locations.enable_indexing.input',
|
||||
'locations.remove': 'action:locations.remove.input',
|
||||
'locations.add': 'action:locations.add.input',
|
||||
'jobs.resume': 'action:jobs.resume.input',
|
||||
'media.ocr.extract': 'action:media.ocr.extract.input',
|
||||
'jobs.cancel': 'action:jobs.cancel.input',
|
||||
'spaces.add_group': 'action:spaces.add_group.input',
|
||||
'volumes.remove_cloud': 'action:volumes.remove_cloud.input',
|
||||
'spaces.delete': 'action:spaces.delete.input',
|
||||
'locations.export': 'action:locations.export.input',
|
||||
'files.delete': 'action:files.delete.input',
|
||||
},
|
||||
|
||||
coreQueries: {
|
||||
'models.whisper.list': 'query:models.whisper.list',
|
||||
'network.status': 'query:network.status',
|
||||
'network.sync_setup.discover': 'query:network.sync_setup.discover',
|
||||
'core.events.list': 'query:core.events.list',
|
||||
'network.devices.list': 'query:network.devices.list',
|
||||
'network.pair.status': 'query:network.pair.status',
|
||||
'core.ephemeral_status': 'query:core.ephemeral_status',
|
||||
'jobs.remote.all_devices': 'query:jobs.remote.all_devices',
|
||||
'jobs.remote.for_device': 'query:jobs.remote.for_device',
|
||||
'core.events.list': 'query:core.events.list',
|
||||
'network.sync_setup.discover': 'query:network.sync_setup.discover',
|
||||
'core.ephemeral_status': 'query:core.ephemeral_status',
|
||||
'network.pair.status': 'query:network.pair.status',
|
||||
'network.status': 'query:network.status',
|
||||
'network.devices.list': 'query:network.devices.list',
|
||||
'libraries.list': 'query:libraries.list',
|
||||
'models.whisper.list': 'query:models.whisper.list',
|
||||
'core.status': 'query:core.status',
|
||||
'libraries.list': 'query:libraries.list',
|
||||
},
|
||||
|
||||
libraryQueries: {
|
||||
'jobs.list': 'query:jobs.list',
|
||||
'files.directory_listing': 'query:files.directory_listing',
|
||||
'sync.eventLog': 'query:sync.eventLog',
|
||||
'sync.activity': 'query:sync.activity',
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'search.files': 'query:search.files',
|
||||
'test.ping': 'query:test.ping',
|
||||
'spaces.list': 'query:spaces.list',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'files.media_listing': 'query:files.media_listing',
|
||||
'volumes.list': 'query:volumes.list',
|
||||
'spaces.get': 'query:spaces.get',
|
||||
'sync.activity': 'query:sync.activity',
|
||||
'jobs.info': 'query:jobs.info',
|
||||
'files.by_id': 'query:files.by_id',
|
||||
'tags.search': 'query:tags.search',
|
||||
'libraries.info': 'query:libraries.info',
|
||||
'jobs.active': 'query:jobs.active',
|
||||
'sync.eventLog': 'query:sync.eventLog',
|
||||
'test.ping': 'query:test.ping',
|
||||
'locations.list': 'query:locations.list',
|
||||
'files.directory_listing': 'query:files.directory_listing',
|
||||
'search.files': 'query:search.files',
|
||||
'files.media_listing': 'query:files.media_listing',
|
||||
'libraries.info': 'query:libraries.info',
|
||||
'sync.metrics': 'query:sync.metrics',
|
||||
'spaces.get_layout': 'query:spaces.get_layout',
|
||||
'jobs.info': 'query:jobs.info',
|
||||
'locations.list': 'query:locations.list',
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'devices.list': 'query:devices.list',
|
||||
'files.by_path': 'query:files.by_path',
|
||||
'spaces.list': 'query:spaces.list',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'files.unique_to_location': 'query:files.unique_to_location',
|
||||
'jobs.list': 'query:jobs.list',
|
||||
'files.by_path': 'query:files.by_path',
|
||||
},
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user