From 732a4327fae262c4f0d47249e737719db2c39b17 Mon Sep 17 00:00:00 2001 From: bjay kamwa watanabe Date: Sun, 15 Mar 2026 16:22:36 +0100 Subject: [PATCH 1/3] Add openapitools.json for OpenAPI generator version --- openapitools.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 openapitools.json diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 0000000..f052220 --- /dev/null +++ b/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.17.0" + } +} From b4378a9e9450881c86c13a240c1feed18a6ac5f9 Mon Sep 17 00:00:00 2001 From: bjay kamwa watanabe Date: Sun, 15 Mar 2026 16:23:10 +0100 Subject: [PATCH 2/3] change openapi --- src/tellers_api/openapi.tellers_public_api.yaml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tellers_api/openapi.tellers_public_api.yaml b/src/tellers_api/openapi.tellers_public_api.yaml index fff2117..5df2cd0 100644 --- a/src/tellers_api/openapi.tellers_public_api.yaml +++ b/src/tellers_api/openapi.tellers_public_api.yaml @@ -1558,9 +1558,6 @@ components: maintenance_mode: type: boolean title: Maintenance Mode - hd_enabled: - type: boolean - title: Hd Enabled available_agent_tools: items: additionalProperties: true @@ -1575,7 +1572,6 @@ components: type: object required: - maintenance_mode - - hd_enabled - available_agent_tools - available_llm_models title: AppSettings @@ -1949,6 +1945,19 @@ components: type: array - type: 'null' title: Override Entity Ids + generate_proxy: + items: + type: integer + enum: + - 360 + - 480 + - 720 + - 1080 + - 2160 + type: array + title: Generate Proxy + default: + - 720 type: object required: - assets From bc4c0a2ec98b8ae712b0f701eb140aa552d0a59b Mon Sep 17 00:00:00 2001 From: bjay kamwa watanabe Date: Sun, 15 Mar 2026 16:35:28 +0100 Subject: [PATCH 3/3] Remove interlaced in video --- src/commands/upload/main.rs | 30 ++++++++++++++++++++++++++ src/media/transcode.rs | 43 ++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/commands/upload/main.rs b/src/commands/upload/main.rs index 77c59c7..8dba06f 100644 --- a/src/commands/upload/main.rs +++ b/src/commands/upload/main.rs @@ -30,6 +30,7 @@ use tokio::sync::mpsc as tokio_mpsc; use tellers_api_client::apis::accepts_api_key_api as api; use tellers_api_client::apis::configuration::Configuration; +use tellers_api_client::models::process_assets_request::GenerateProxy; use tellers_api_client::models::{ AssetUploadRequest, AssetUploadResponse, CreateFolderRequest, ProcessAssetsRequest, SourceFileInfo, @@ -94,6 +95,10 @@ pub struct UploadCmdArgs { #[arg(long, default_value_t = false)] pub dry_run: bool, + /// Proxy heights to request from the server after upload (e.g. 720, 1080). Omit for server default; use empty (e.g. --generate-proxy) for none (e.g. when using --local-encoding). + #[arg(long, value_delimiter = ',', num_args = 0.., value_parser = parse_generate_proxy)] + pub generate_proxy: Option>, + #[arg(long, default_value_t = false)] pub disable_description_generation: bool, } @@ -111,6 +116,20 @@ enum DownscaleWork { Passthrough(PathBuf), } +fn parse_generate_proxy(s: &str) -> Result { + match s.trim() { + "360" => Ok(GenerateProxy::Variant360), + "480" => Ok(GenerateProxy::Variant480), + "720" => Ok(GenerateProxy::Variant720), + "1080" => Ok(GenerateProxy::Variant1080), + "2160" => Ok(GenerateProxy::Variant2160), + _ => Err(format!( + "generate_proxy must be one of 360, 480, 720, 1080, 2160, got '{}'", + s + )), + } +} + fn has_extension(file_path: &PathBuf, extensions: &[String]) -> bool { if extensions.is_empty() { return true; @@ -517,6 +536,7 @@ fn run_upload(args: UploadCmdArgs) -> Result<(), String> { bearer_header.as_deref(), &related_umids_per_file, args.disable_description_generation, + args.generate_proxy.as_ref(), ) .await; @@ -694,6 +714,8 @@ fn run_two_queue_pipeline( let user_id = user_id.to_string(); let upload_request_id = upload_request_id.to_string(); let disable_description_generation = args.disable_description_generation; + let local_encoding = args.local_encoding; + let generate_proxy = args.generate_proxy.clone(); let block_result = rt.block_on(async move { // Start render loop inside runtime so tokio::spawn has a current runtime @@ -802,6 +824,9 @@ fn run_two_queue_pipeline( ); preproc_req.generate_time_based_media_description = Some(!disable_description_generation); + // Use --generate-proxy if set; otherwise when local encoding use empty (no server proxies); else leave unset for server default. + preproc_req.generate_proxy = + generate_proxy.clone().or_else(|| if local_encoding { Some(vec![]) } else { None }); // related_umid_for_master_clip removed in current API; use override_entity_ids if needed if !file_related_umids.is_empty() { preproc_req.override_entity_ids = Some(Some(file_related_umids)); @@ -1020,6 +1045,7 @@ async fn upload_to_presigned_urls( bearer_opt: Option<&str>, related_umids_per_file: &[Vec], disable_description_generation: bool, + generate_proxy: Option<&Vec>, ) -> Result<(), String> { let http = Arc::new( reqwest::Client::builder() @@ -1055,6 +1081,7 @@ async fn upload_to_presigned_urls( let cfg_clone = cfg.clone(); let api_key_clone = api_key.clone(); let bearer_clone = bearer_opt.clone(); + let generate_proxy_clone = generate_proxy.cloned(); let file_name = file_path .file_name() @@ -1101,6 +1128,9 @@ async fn upload_to_presigned_urls( ); preproc_req.generate_time_based_media_description = Some(!disable_description_generation); + if let Some(proxy) = generate_proxy_clone.as_ref() { + preproc_req.generate_proxy = Some(proxy.clone()); + } if !file_related_umids.is_empty() { preproc_req.override_entity_ids = Some(Some(file_related_umids)); } diff --git a/src/media/transcode.rs b/src/media/transcode.rs index 024f0fd..a634577 100644 --- a/src/media/transcode.rs +++ b/src/media/transcode.rs @@ -3,8 +3,35 @@ use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel}; use std::path::PathBuf; +use crate::media::metadata::get_ffprobe_json; use crate::media::video_quality::VideoQuality; +/// Returns true if the first video stream is interlaced (field_order is tb, bt, tt, or bb). +fn is_interlaced(path: &PathBuf) -> Result { + let probe = match get_ffprobe_json(path)? { + Some(p) => p, + None => return Ok(false), + }; + let streams = probe + .get("streams") + .and_then(|s| s.as_array()) + .ok_or_else(|| "no streams in ffprobe output".to_string())?; + let video_stream = streams + .iter() + .find(|s| s.get("codec_type").and_then(|c| c.as_str()) == Some("video")) + .ok_or_else(|| "no video stream".to_string())?; + let field_order = video_stream + .get("field_order") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_lowercase(); + // Interlaced: tb, bt, tt, bb. Progressive or unknown => not interlaced. + Ok(matches!( + field_order.as_str(), + "tb" | "bt" | "tt" | "bb" + )) +} + /// Parse ffmpeg time string e.g. "00:01:23.45" into seconds. fn parse_ffmpeg_time(s: &str) -> Option { let s = s.trim(); @@ -123,6 +150,13 @@ pub fn create_rendition( } } + let interlaced = is_interlaced(input).unwrap_or(false); + if interlaced { + if let Some(f) = info_cb { + f("Input is interlaced; deinterlacing to progressive"); + } + } + let mut cmd = FfmpegCommand::new(); cmd.overwrite() .input(input.to_string_lossy()) @@ -131,7 +165,14 @@ pub fn create_rendition( if let Some(q) = definition.quality { let height = q.height(); - cmd.args(["-vf", &format!("scale=-2:{}", height)]); + let vf = if interlaced { + format!("bwdif=0:-1:0,scale=-2:{}", height) + } else { + format!("scale=-2:{}", height) + }; + cmd.args(["-vf", &vf]); + } else if interlaced { + cmd.args(["-vf", "bwdif=0:-1:0"]); } if let Some(p) = definition.preset { cmd.preset(p.as_str());