[READ-ONLY] Mirror of https://github.com/mrgnw/spatial-maker.
0

Configure Feed

Select the types of activity you want to include in your feed.

v0.3.1: Fix ffprobe parsing - use JSON output, calculate frames from duration*fps as fallback

+61 -51
+2 -1
Cargo.lock
··· 2218 2218 2219 2219 [[package]] 2220 2220 name = "spatial-maker" 2221 - version = "0.3.0" 2221 + version = "0.3.1" 2222 2222 dependencies = [ 2223 2223 "clap", 2224 2224 "dirs", ··· 2230 2230 "ort", 2231 2231 "reqwest", 2232 2232 "serde", 2233 + "serde_json", 2233 2234 "tempfile", 2234 2235 "thiserror 2.0.18", 2235 2236 "tokio",
+7 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "spatial-maker" 3 - version = "0.3.0" 3 + version = "0.3.1" 4 4 edition = "2021" 5 5 license = "MIT" 6 6 description = "Convert 2D images and videos to stereoscopic 3D spatial content for Apple Vision Pro using AI depth estimation" ··· 55 55 ndarray = "0.16" 56 56 thiserror = "2" 57 57 serde = { version = "1.0", features = ["derive"] } 58 + serde_json = "1.0" 58 59 tokio = { version = "1.0", features = ["full"] } 59 60 reqwest = { version = "0.12", features = ["stream"] } 60 61 futures-util = "0.3" ··· 75 76 76 77 [dev-dependencies] 77 78 tempfile = "3.8" 79 + 80 + [package.metadata.binstall] 81 + pkg-url = "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.gz" 82 + bin-dir = "{ bin }{ binary-ext }" 83 + pkg-fmt = "tgz" 78 84 79 85 [[example]] 80 86 name = "photo_coreml"
spatial-maker-v0.3.0-aarch64-apple-darwin.tar.gz

This is a binary file and will not be displayed.

+52 -49
src/video.rs
··· 45 45 pub type ProgressCallback = Box<dyn Fn(VideoProgress) + Send + Sync>; 46 46 47 47 pub async fn get_video_metadata(input_path: &Path) -> SpatialResult<VideoMetadata> { 48 + let input_str = input_path 49 + .to_str() 50 + .ok_or_else(|| SpatialError::Other("Invalid input path encoding".to_string()))?; 51 + 48 52 let output = Command::new("ffprobe") 49 53 .args([ 50 - "-v", 51 - "error", 52 - "-select_streams", 53 - "v:0", 54 - "-count_frames", 55 - "-show_entries", 56 - "stream=width,height,r_frame_rate,nb_read_frames,duration", 57 - "-show_entries", 58 - "format=duration", 59 - "-of", 60 - "csv=p=0", 61 - input_path.to_str().unwrap(), 54 + "-v", "error", 55 + "-select_streams", "v:0", 56 + "-show_entries", "stream=width,height,r_frame_rate,nb_frames,duration", 57 + "-show_entries", "format=duration", 58 + "-of", "json", 59 + input_str, 62 60 ]) 63 61 .output() 64 62 .await ··· 75 73 } 76 74 77 75 let stdout = String::from_utf8_lossy(&output.stdout); 78 - let parts: Vec<&str> = stdout.trim().split(',').collect(); 76 + let json: serde_json::Value = serde_json::from_str(&stdout) 77 + .map_err(|e| SpatialError::Other(format!("Failed to parse ffprobe JSON: {}", e)))?; 79 78 80 - if parts.len() < 4 { 81 - return Err(SpatialError::Other(format!( 82 - "Unexpected ffprobe output: {}", 83 - stdout 84 - ))); 85 - } 79 + let stream = json["streams"] 80 + .as_array() 81 + .and_then(|s| s.first()) 82 + .ok_or_else(|| SpatialError::Other("No video stream found".to_string()))?; 86 83 87 - let width = parts[0] 88 - .parse::<u32>() 89 - .map_err(|_| SpatialError::Other("Failed to parse width".to_string()))?; 90 - let height = parts[1] 91 - .parse::<u32>() 92 - .map_err(|_| SpatialError::Other("Failed to parse height".to_string()))?; 84 + let width = stream["width"] 85 + .as_u64() 86 + .ok_or_else(|| SpatialError::Other("Failed to parse width".to_string()))? as u32; 87 + let height = stream["height"] 88 + .as_u64() 89 + .ok_or_else(|| SpatialError::Other("Failed to parse height".to_string()))? as u32; 93 90 94 - let fps = if parts[2].contains('/') { 95 - let fps_parts: Vec<&str> = parts[2].split('/').collect(); 96 - let num: f64 = fps_parts[0].parse().unwrap_or(30.0); 97 - let den: f64 = fps_parts[1].parse().unwrap_or(1.0); 98 - num / den 99 - } else { 100 - parts[2].parse().unwrap_or(30.0) 101 - }; 91 + let fps = stream["r_frame_rate"] 92 + .as_str() 93 + .map(|s| { 94 + if let Some((num, den)) = s.split_once('/') { 95 + let n: f64 = num.parse().unwrap_or(30.0); 96 + let d: f64 = den.parse().unwrap_or(1.0); 97 + n / d 98 + } else { 99 + s.parse().unwrap_or(30.0) 100 + } 101 + }) 102 + .unwrap_or(30.0); 102 103 103 - let total_frames = parts[3] 104 - .parse::<u32>() 105 - .map_err(|_| SpatialError::Other("Failed to parse frame count".to_string()))?; 106 - 107 - let duration = parts 108 - .get(4) 104 + let duration = stream["duration"] 105 + .as_str() 109 106 .and_then(|s| s.parse::<f64>().ok()) 110 - .unwrap_or(total_frames as f64 / fps); 107 + .or_else(|| { 108 + json["format"]["duration"] 109 + .as_str() 110 + .and_then(|s| s.parse::<f64>().ok()) 111 + }) 112 + .unwrap_or(0.0); 113 + 114 + let total_frames = stream["nb_frames"] 115 + .as_str() 116 + .and_then(|s| s.parse::<u32>().ok()) 117 + .unwrap_or_else(|| (duration * fps).round() as u32); 111 118 112 119 let audio_output = Command::new("ffprobe") 113 120 .args([ 114 - "-v", 115 - "error", 116 - "-select_streams", 117 - "a:0", 118 - "-show_entries", 119 - "stream=codec_type", 120 - "-of", 121 - "csv=p=0", 122 - input_path.to_str().unwrap(), 121 + "-v", "error", 122 + "-select_streams", "a:0", 123 + "-show_entries", "stream=codec_type", 124 + "-of", "csv=p=0", 125 + input_str, 123 126 ]) 124 127 .output() 125 128 .await