From 92e99cfcb6649cbaa061449d1cc3b5bfddfddd9f Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 19 Jun 2026 10:44:22 -0400 Subject: [PATCH] video info handler (#1666) --- Cargo.lock | 1 + .../artcraft/artcraft_api_defs/src/lib.rs | 1 + .../artcraft_api_defs/src/video_info/mod.rs | 1 + .../src/video_info/read_only.rs | 167 +++++++++++++ crates/service/web/storyteller_web/Cargo.toml | 1 + .../app_startup/redis_rate_limiters.rs | 14 ++ .../web/storyteller_web/src/docs/api_doc.rs | 13 + .../src/http_server/endpoints/mod.rs | 1 + .../http_server/endpoints/video_info/mod.rs | 1 + .../video_info_read_info_handler.rs | 224 ++++++++++++++++++ .../add_application_routes.rs | 2 + .../routes/application_routes/mod.rs | 1 + .../application_routes/video_info_routes.rs | 26 ++ .../storyteller_web/src/state/server_state.rs | 3 + 14 files changed, 456 insertions(+) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/video_info/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/video_info/read_only.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/video_info/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/video_info/video_info_read_info_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/routes/application_routes/video_info_routes.rs diff --git a/Cargo.lock b/Cargo.lock index db731f8332..c650bf02c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11667,6 +11667,7 @@ dependencies = [ "utoipa", "utoipa-swagger-ui", "uuid_utils", + "video_info", "videos", "workspace-build-acceleration", "worldlabs_api_client", diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs index 156cbd21c5..2acf11f22f 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs @@ -16,4 +16,5 @@ pub mod subscriptions; pub mod user_referral_codes; pub mod users; pub mod utils; +pub mod video_info; pub mod web_referrals; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/mod.rs new file mode 100644 index 0000000000..2310fc26a7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/mod.rs @@ -0,0 +1 @@ +pub mod read_only; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/read_only.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/read_only.rs new file mode 100644 index 0000000000..6c6c9dffd5 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/video_info/read_only.rs @@ -0,0 +1,167 @@ +//! Response types for `POST /v1/video_info/read_only`. +//! +//! The endpoint accepts a `multipart/form-data` video upload, inspects its +//! embedded provenance (C2PA manifests, AIGC labels, in-stream watermarks), and +//! returns what it found. These are the public, wire-stable API types — they +//! deliberately mirror, but do not re-export, the internal `video_info` crate +//! types so the parsing library can evolve independently of the HTTP contract. + +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Response for `POST /v1/video_info/read_only`. +/// +/// [`kind`](Self::kind) names the detected provenance family; the matching +/// `maybe_*` field carries its details (all others are `null`). `unrecognized` +/// means no supported provenance was found — [`maybe_encoder`](Self::maybe_encoder) +/// still reports the container encoder tag when present (an `Lavf…` value means +/// the file was re-encoded through ffmpeg, which strips provenance). +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct VideoInfoReadOnlyResponse { + pub success: bool, + + /// Which provenance family was detected (if any). + pub kind: VideoProvenanceKind, + + /// The container `©too` encoder tag, e.g. `"Lavf60.16.100"` or `"Google"`. + pub maybe_encoder: Option, + + /// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Seedance`]. + pub maybe_seedance: Option, + + /// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Veo`]. + pub maybe_veo: Option, + + /// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Sora`]. + pub maybe_sora: Option, + + /// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Dreamina`]. + pub maybe_dreamina: Option, + + /// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Kling`]. + pub maybe_kling: Option, +} + +/// The recognized AI-video provenance family. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum VideoProvenanceKind { + /// ByteDance Seedance API (Volcengine / BytePlus C2PA). + Seedance, + /// Google Veo (Google Generative AI video C2PA). + Veo, + /// OpenAI Sora (OpenAI Generative AI video C2PA). + Sora, + /// Dreamina (ByteDance/CapCut consumer app `ilst` metadata). + Dreamina, + /// Kling (Kuaishou AIGC-label / in-stream watermark). + Kling, + /// No supported provenance was found (often a re-encoded / stripped file). + Unrecognized, +} + +/// Seedance (ByteDance) provenance from an embedded C2PA manifest. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct SeedanceVideoInfo { + /// `"volcengine"` or `"byteplus"`. + pub platform: String, + /// C2PA `softwareAgent`, e.g. `"Volcengine_Ark_CN"`. + pub software_agent: String, + pub maybe_software_agent_version: Option, + /// Full model identifier, e.g. `"doubao-seedance-2-0-fast"`. + pub model_name: String, + pub maybe_model_brand: Option, + /// Parsed version, e.g. `"2.0"` / `"1.0"`. + pub maybe_model_version: Option, + pub is_fast: bool, + pub is_lite: bool, + /// Raw generation timestamp string (RFC 3339). + pub generated_at: String, + /// Normalized RFC 3339 timestamp, when parseable. + pub maybe_generated_at_utc: Option, + pub maybe_log_id: Option, + pub maybe_log_id_decoded_hex: Option, + pub maybe_digital_source_type: Option, + pub maybe_claim_generator: Option, + pub maybe_claim_generator_version: Option, + pub maybe_manifest_id: Option, + pub maybe_instance_id: Option, + pub maybe_signer_email: Option, + pub maybe_signer_org_id: Option, + pub maybe_signer_country: Option, + pub maybe_cert_serial: Option, +} + +/// Google Veo provenance. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct VeoVideoInfo { + /// Always `"Google"`. + pub producer: String, + /// Whether a full Google C2PA manifest is present (`false` = recognized only + /// by the `encoder=Google` tag, C2PA likely stripped by a re-encode). + pub has_c2pa_manifest: bool, + pub maybe_encoder: Option, + pub maybe_created_description: Option, + pub has_synthid_watermark: bool, + pub maybe_synthid_description: Option, + pub maybe_digital_source_type: Option, + pub maybe_claim_generator: Option, + pub maybe_claim_generator_version: Option, + pub maybe_manifest_id: Option, + pub maybe_instance_id: Option, + pub maybe_cert_serial: Option, + /// Issuing intermediate-CA common name, e.g. `"Google C2PA Media Services 1P ICA G3"`. + pub maybe_signer_ca: Option, + /// Whether the manifest carries an RFC 3161 trusted timestamp. + pub is_timestamped: bool, + pub maybe_timestamp_authority: Option, +} + +/// OpenAI Sora provenance. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct SoraVideoInfo { + /// Always `"OpenAI"`. + pub producer: String, + pub maybe_model_name: Option, + pub maybe_created_description: Option, + pub maybe_digital_source_type: Option, + pub maybe_claim_generator: Option, + pub maybe_manifest_id: Option, + pub maybe_instance_id: Option, + pub maybe_cert_serial: Option, +} + +/// Dreamina (ByteDance/CapCut app) provenance from `ilst` metadata. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct DreaminaVideoInfo { + /// The product string, e.g. `"dreamina"`. + pub product: String, + pub maybe_export_type: Option, + pub maybe_os: Option, + pub maybe_source_info: Option, + pub maybe_aigc_label_type: Option, + pub maybe_video_id: Option, + pub has_c2pa: bool, + pub maybe_signer_org_id: Option, + pub maybe_signer_country: Option, + pub maybe_cert_serial: Option, +} + +/// Kling (Kuaishou) provenance from the AIGC label and/or in-stream watermark. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct KlingVideoInfo { + /// Model version when a clean export embeds it (e.g. `"2.5-pro"`); usually `null`. + pub maybe_model_version: Option, + /// Whether labeled AI-generated (`Label == "1"`, per China GB 45438). + pub is_ai_generated: bool, + pub maybe_label: Option, + pub maybe_content_producer: Option, + pub maybe_produce_id: Option, + pub maybe_content_propagator: Option, + pub maybe_propagate_id: Option, + pub maybe_reserved_code_1: Option, + pub maybe_reserved_code_2: Option, + /// Whether the in-stream `kling-ai` SEI watermark is present. + pub has_stream_watermark: bool, + pub maybe_watermark_uuid: Option, +} diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index 8863de2412..5610e879e4 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -38,6 +38,7 @@ actix_helpers.workspace = true artcraft_api_defs.workspace = true artcraft_api_keys.workspace = true artcraft_router.workspace = true +video_info.workspace = true bootstrap.workspace = true bucket_paths.workspace = true cloud_storage.workspace = true diff --git a/crates/service/web/storyteller_web/src/configs/app_startup/redis_rate_limiters.rs b/crates/service/web/storyteller_web/src/configs/app_startup/redis_rate_limiters.rs index 21db936680..58793827b6 100644 --- a/crates/service/web/storyteller_web/src/configs/app_startup/redis_rate_limiters.rs +++ b/crates/service/web/storyteller_web/src/configs/app_startup/redis_rate_limiters.rs @@ -116,6 +116,19 @@ pub fn configure_redis_rate_limiters() -> AnyhowResult { RedisRateLimiter::new(limiter, "file_upload_logged_in", limiter_enabled) }; + let video_info_read_only_rate_limiter = { + let limiter_enabled = easyenv::get_env_bool_or_default("LIMITER_VIDEO_INFO_READ_ONLY_ENABLED", true); + let limiter_max_requests = easyenv::get_env_num("LIMITER_VIDEO_INFO_READ_ONLY_MAX_REQUESTS", 1)?; + let limiter_window_seconds = easyenv::get_env_num("LIMITER_VIDEO_INFO_READ_ONLY_WINDOW_SECONDS", 5)?; + + let limiter = Limiter::build(&redis_connection_string) + .limit(limiter_max_requests) + .period(Duration::from_secs(limiter_window_seconds)) + .finish()?; + + RedisRateLimiter::new(limiter, "video_info_read_only", limiter_enabled) + }; + Ok(RedisRateLimiters { logged_out: logged_out_redis_rate_limiter, logged_in: logged_in_redis_rate_limiter, @@ -125,5 +138,6 @@ pub fn configure_redis_rate_limiters() -> AnyhowResult { model_upload: model_upload_rate_limiter, file_upload_logged_out: file_upload_logged_out_redis_rate_limiter, file_upload_logged_in: file_upload_logged_in_redis_rate_limiter, + video_info_read_only: video_info_read_only_rate_limiter, }) } diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 1eedb13c78..f1c82c35bf 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -364,6 +364,8 @@ use artcraft_api_defs::user_referral_codes::create_referral_code::*; use artcraft_api_defs::user_referral_codes::delete_referral_code::*; use artcraft_api_defs::user_referral_codes::list_referral_codes::*; use artcraft_api_defs::web_referrals::log_web_referral::*; +use artcraft_api_defs::video_info::read_only::*; +use crate::http_server::endpoints::video_info::video_info_read_info_handler::*; use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*; use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*; use crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::*; @@ -617,6 +619,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux // Subscriptions crate::http_server::endpoints::subscriptions::get_session_subscription_handler::get_session_subscription_handler, // Web Referrals + crate::http_server::endpoints::video_info::video_info_read_info_handler::video_info_read_info_handler, crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler, // User Referral Codes crate::http_server::endpoints::user_referral_codes::create_referral_code_handler::create_referral_code_handler, @@ -1249,6 +1252,16 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux Veo3p1FastMultiFunctionVideoGenRequest, Veo3p1FastMultiFunctionVideoGenResponse, + // Video info (provenance detection) + VideoInfoReadOnlyForm, + VideoInfoReadOnlyResponse, + VideoProvenanceKind, + SeedanceVideoInfo, + VeoVideoInfo, + SoraVideoInfo, + DreaminaVideoInfo, + KlingVideoInfo, + // Object Multi-Function types Hunyuan3dV3MultiFunctionObjectGenRequest, Hunyuan3dV3MultiFunctionObjectGenResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index bc28bdeafe..9108c35d06 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -32,6 +32,7 @@ pub mod user_ratings; pub mod user_referral_codes; pub mod user_wallets; pub mod users; +pub mod video_info; pub mod voice_clone_requests; pub mod voice_conversion; pub mod voice_designer; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/mod.rs new file mode 100644 index 0000000000..4e74234040 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/mod.rs @@ -0,0 +1 @@ +pub mod video_info_read_info_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/video_info_read_info_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/video_info_read_info_handler.rs new file mode 100644 index 0000000000..c6448b58b5 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/video_info/video_info_read_info_handler.rs @@ -0,0 +1,224 @@ +//! `POST /v1/video_info/read_only` — inspect an uploaded video's embedded +//! AI-generation provenance and return what was detected. +//! +//! Stateless and read-only: the bytes are parsed in memory by the `video_info` +//! crate and nothing is persisted. The internal `video_info` types are mapped +//! into the public [`artcraft_api_defs`] wire types so the parsing library can +//! evolve independently of the HTTP contract. + +use std::io::Read; +use std::sync::Arc; + +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::error; +use utoipa::ToSchema; + +use artcraft_api_defs::video_info::read_only::{ + DreaminaVideoInfo, KlingVideoInfo, SeedanceVideoInfo, SoraVideoInfo, VeoVideoInfo, + VideoInfoReadOnlyResponse, VideoProvenanceKind, +}; +use video_info::dreamina_info::DreaminaInfo; +use video_info::kling_info::KlingInfo; +use video_info::seedance_info::SeedanceInfo; +use video_info::sora_info::SoraInfo; +use video_info::veo_info::VeoInfo; +use video_info::VideoInfo; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::web_utils::redis_rate_limiter::RateLimiterError; +use crate::state::server_state::ServerState; + +/// Form-multipart request: a single video file under the `file` field. +#[derive(MultipartForm, ToSchema)] +#[multipart(duplicate_field = "deny")] +pub struct VideoInfoReadOnlyForm { + /// The video file to inspect. + #[multipart(limit = "256 MiB")] + #[schema(value_type = Vec, format = Binary)] + file: TempFile, +} + +/// Detect the AI-generation provenance of an uploaded video. +#[utoipa::path( + post, + tag = "Video Info", + path = "/v1/video_info/read_only", + responses( + (status = 200, description = "Detected provenance (or `unrecognized`)", body = VideoInfoReadOnlyResponse), + (status = 400, description = "Bad input", body = CommonWebError), + (status = 500, description = "Server error", body = CommonWebError), + ), + params( + ("request" = VideoInfoReadOnlyForm, description = "Multipart form with a single `file` video upload."), + ) +)] +pub async fn video_info_read_info_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(mut form): MultipartForm, +) -> Result, CommonWebError> { + // IP-based rate limit (1 req / 5s by default, configurable). Fails open: the + // limiter already treats Redis/parse errors as allowed, and we allow on any + // non-"limit exceeded" outcome here too. + match server_state.redis_rate_limiters.video_info_read_only.rate_limit_request(&http_request).await { + Ok(()) => {} + Err(RateLimiterError::RateLimitExceededError) => return Err(CommonWebError::TooManyRequests), + Err(RateLimiterError::ClientError) => {} // fail open + } + + let mut bytes = Vec::new(); + form.file.file.read_to_end(&mut bytes).map_err(|err| { + error!("video_info: problem reading uploaded file: {:?}", err); + CommonWebError::server_error_with_message("could not read uploaded file") + })?; + + if bytes.is_empty() { + return Err(CommonWebError::BadInputWithSimpleMessage("empty file".to_string())); + } + + let maybe_encoder = video_info::encoder_tag(&bytes); + + // Any parse error (unrecognized / malformed manifest) collapses to + // `Unrecognized` — we never surface internal error types to clients. + let response = match VideoInfo::from_bytes(&bytes) { + Ok(info) => to_response(info, maybe_encoder), + Err(_) => unrecognized_response(maybe_encoder), + }; + + Ok(Json(response)) +} + +// ── Mapping: internal `video_info` types → public API types ── + +fn to_response(info: VideoInfo, maybe_encoder: Option) -> VideoInfoReadOnlyResponse { + let mut response = unrecognized_response(maybe_encoder); + match info { + VideoInfo::Seedance(seedance) => { + response.kind = VideoProvenanceKind::Seedance; + response.maybe_seedance = Some(to_seedance(seedance)); + } + VideoInfo::Veo(veo) => { + response.kind = VideoProvenanceKind::Veo; + response.maybe_veo = Some(to_veo(veo)); + } + VideoInfo::Sora(sora) => { + response.kind = VideoProvenanceKind::Sora; + response.maybe_sora = Some(to_sora(sora)); + } + VideoInfo::Dreamina(dreamina) => { + response.kind = VideoProvenanceKind::Dreamina; + response.maybe_dreamina = Some(to_dreamina(dreamina)); + } + VideoInfo::Kling(kling) => { + response.kind = VideoProvenanceKind::Kling; + response.maybe_kling = Some(to_kling(kling)); + } + } + response +} + +fn unrecognized_response(maybe_encoder: Option) -> VideoInfoReadOnlyResponse { + VideoInfoReadOnlyResponse { + success: true, + kind: VideoProvenanceKind::Unrecognized, + maybe_encoder, + maybe_seedance: None, + maybe_veo: None, + maybe_sora: None, + maybe_dreamina: None, + maybe_kling: None, + } +} + +fn to_seedance(info: SeedanceInfo) -> SeedanceVideoInfo { + SeedanceVideoInfo { + platform: info.platform.as_str().to_string(), + software_agent: info.software_agent, + maybe_software_agent_version: info.software_agent_version, + model_name: info.model_name, + maybe_model_brand: info.model_brand, + maybe_model_version: info.model_version, + is_fast: info.is_fast, + is_lite: info.is_lite, + generated_at: info.generated_at, + maybe_generated_at_utc: info.generated_at_utc.map(|dt| dt.to_rfc3339()), + maybe_log_id: info.log_id, + maybe_log_id_decoded_hex: info.log_id_decoded_hex, + maybe_digital_source_type: info.digital_source_type, + maybe_claim_generator: info.claim_generator, + maybe_claim_generator_version: info.claim_generator_version, + maybe_manifest_id: info.manifest_id, + maybe_instance_id: info.instance_id, + maybe_signer_email: info.signer_email, + maybe_signer_org_id: info.signer_org_id, + maybe_signer_country: info.signer_country, + maybe_cert_serial: info.cert_serial, + } +} + +fn to_veo(info: VeoInfo) -> VeoVideoInfo { + VeoVideoInfo { + producer: info.producer, + has_c2pa_manifest: info.has_c2pa_manifest, + maybe_encoder: info.encoder, + maybe_created_description: info.created_description, + has_synthid_watermark: info.has_synthid_watermark, + maybe_synthid_description: info.synthid_description, + maybe_digital_source_type: info.digital_source_type, + maybe_claim_generator: info.claim_generator, + maybe_claim_generator_version: info.claim_generator_version, + maybe_manifest_id: info.manifest_id, + maybe_instance_id: info.instance_id, + maybe_cert_serial: info.cert_serial, + maybe_signer_ca: info.signer_ca, + is_timestamped: info.is_timestamped, + maybe_timestamp_authority: info.timestamp_authority, + } +} + +fn to_sora(info: SoraInfo) -> SoraVideoInfo { + SoraVideoInfo { + producer: info.producer, + maybe_model_name: info.model_name, + maybe_created_description: info.created_description, + maybe_digital_source_type: info.digital_source_type, + maybe_claim_generator: info.claim_generator, + maybe_manifest_id: info.manifest_id, + maybe_instance_id: info.instance_id, + maybe_cert_serial: info.cert_serial, + } +} + +fn to_dreamina(info: DreaminaInfo) -> DreaminaVideoInfo { + DreaminaVideoInfo { + product: info.product, + maybe_export_type: info.export_type, + maybe_os: info.os, + maybe_source_info: info.source_info, + maybe_aigc_label_type: info.aigc_label_type, + maybe_video_id: info.video_id, + has_c2pa: info.has_c2pa, + maybe_signer_org_id: info.signer_org_id, + maybe_signer_country: info.signer_country, + maybe_cert_serial: info.cert_serial, + } +} + +fn to_kling(info: KlingInfo) -> KlingVideoInfo { + KlingVideoInfo { + maybe_model_version: info.model_version, + is_ai_generated: info.is_ai_generated, + maybe_label: info.label, + maybe_content_producer: info.content_producer, + maybe_produce_id: info.produce_id, + maybe_content_propagator: info.content_propagator, + maybe_propagate_id: info.propagate_id, + maybe_reserved_code_1: info.reserved_code_1, + maybe_reserved_code_2: info.reserved_code_2, + has_stream_watermark: info.has_stream_watermark, + maybe_watermark_uuid: info.watermark_uuid, + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs index 3d0fddc7dc..1699b07ef3 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs @@ -21,6 +21,7 @@ use crate::http_server::routes::application_routes::user_bookmarks_routes::add_u use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes; use crate::http_server::routes::application_routes::user_referral_code_routes::add_user_referral_code_routes; use crate::http_server::routes::application_routes::user_routes::add_user_routes; +use crate::http_server::routes::application_routes::video_info_routes::add_video_info_routes; use crate::http_server::routes::application_routes::wallet_routes::add_wallet_routes; use crate::http_server::routes::application_routes::web_referrals_routes::add_web_referrals_routes; use crate::http_server::routes::application_routes::webhook_routes::add_webhook_routes; @@ -68,6 +69,7 @@ where app = add_job_routes(app); // Other useful tools + app = add_video_info_routes(app); // /v1/video_info/... (video provenance detection) app = add_analytics_routes(app); // /v1/analytics/... app = add_web_referrals_routes(app); // /v1/web_referrals/... app = add_dev_routes(app); // /v1/dev/... — dev-only error/response shape probes diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs index 80564ab5ae..d67c9c160c 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs @@ -21,6 +21,7 @@ mod user_bookmarks_routes; mod user_rating_routes; mod user_referral_code_routes; mod user_routes; +mod video_info_routes; mod wallet_routes; mod web_referrals_routes; mod webhook_routes; diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/video_info_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/video_info_routes.rs new file mode 100644 index 0000000000..f3f3472654 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/video_info_routes.rs @@ -0,0 +1,26 @@ +use actix_http::body::MessageBody; +use actix_service::ServiceFactory; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::error::Error; +use actix_web::{web, App, HttpResponse}; + +use crate::http_server::endpoints::video_info::video_info_read_info_handler::video_info_read_info_handler; + +pub fn add_video_info_routes (app: App) -> App +where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/v1/video_info") + .service(web::resource("/read_only") + .route(web::post().to(video_info_read_info_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} diff --git a/crates/service/web/storyteller_web/src/state/server_state.rs b/crates/service/web/storyteller_web/src/state/server_state.rs index 64bccf990a..b3ac2bf5a4 100644 --- a/crates/service/web/storyteller_web/src/state/server_state.rs +++ b/crates/service/web/storyteller_web/src/state/server_state.rs @@ -160,6 +160,9 @@ pub struct RedisRateLimiters { /// For uploading files for voice conversion, face animator, etc. pub file_upload_logged_in: RedisRateLimiter, + + /// IP-based limiter for the read-only video-info upload endpoint. Fails open. + pub video_info_read_only: RedisRateLimiter, } /// In-memory caches of several types.