use anyhow::{anyhow, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::{error, info, warn}; use uuid::Uuid; use chrono::{DateTime, Utc}; use hmac::{Hmac, Mac}; use sha2::{Digest, Sha256}; use hex; use url::Url; use crate::data::models::video_generation_record::{ VideoGenerationRecord, CreateVideoGenerationRequest, VideoGenerationQuery }; use crate::data::repositories::video_generation_record_repository::VideoGenerationRecordRepository; use crate::infrastructure::database::Database; use crate::business::services::cloud_upload_service::CloudUploadService; /// 火山云视频生成API响应(提交任务) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoGenerationResponse { pub code: i32, pub message: String, pub data: Option, pub request_id: String, pub status: i32, pub time_elapsed: String, } /// 提交任务返回数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoGenerationData { pub task_id: String, } /// 火山云查询任务API响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoQueryResponse { pub code: i32, pub message: String, pub data: Option, pub request_id: String, pub status: i32, pub time_elapsed: String, } /// 查询任务返回数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoQueryData { pub status: String, // in_queue, generating, done, not_found, expired pub video_url: Option, pub resp_data: Option, // JSON字符串,包含详细信息 } /// 驱动视频信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrivingVideoInfo { /// 驱动视频存储类型,传固定值0 pub store_type: i32, /// 视频url pub video_url: String, } /// 火山云视频生成请求参数(提交任务) /// 基于火山云实际API文档的参数 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoGenerationRequest { /// 服务标识,固定值 pub req_key: String, /// 输入图片URL(必需) pub image_url: String, /// 驱动视频信息 pub driving_video_info: DrivingVideoInfo, } /// 火山云查询任务请求参数 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoVideoQueryRequest { /// 服务标识,固定值 pub req_key: String, /// 任务ID pub task_id: String, } /// OmniHuman 主体识别请求参数 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RealmanAvatarPictureCreateRoleOmniRequest { /// 服务标识,固定值 pub req_key: String, /// 人像图片URL链接 pub image_url: String, } /// 火山云 API 响应元数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VolcanoResponseMetadata { #[serde(rename = "Action")] pub action: String, #[serde(rename = "Region")] pub region: String, #[serde(rename = "RequestId")] pub request_id: String, #[serde(rename = "Service")] pub service: String, #[serde(rename = "Version")] pub version: String, } /// OmniHuman 主体识别响应 (火山云格式) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RealmanAvatarPictureCreateRoleOmniResponse { #[serde(rename = "ResponseMetadata")] pub response_metadata: VolcanoResponseMetadata, #[serde(rename = "Result")] pub result: RealmanAvatarPictureCreateRoleOmniResult, } /// OmniHuman 主体识别结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RealmanAvatarPictureCreateRoleOmniResult { pub code: i32, pub message: String, pub data: Option, pub request_id: String, pub status: i32, pub time_elapsed: String, } /// OmniHuman 主体识别返回数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RealmanAvatarPictureCreateRoleOmniData { /// 输出处理过的图片url数组(单张图) pub image_urls: Option>, /// 返回图base64数组 pub binary_data_base64: Option>, /// 任务ID pub task_id: String, /// 算法返回数据 pub resp_data: Option, /// 任务状态 pub status: Option, /// 算法返回信息 pub algorithm_base_resp: Option, } /// 火山云视频生成服务 /// 遵循 Tauri 开发规范的服务层设计原则 pub struct VolcanoVideoService { database: Arc, repository: VideoGenerationRecordRepository, http_client: Client, cloud_upload_service: CloudUploadService, vol_access_key: String, vol_secret_key: String, } impl VolcanoVideoService { /// 创建新的火山云视频生成服务实例 pub fn new(database: Arc) -> Self { let repository = VideoGenerationRecordRepository::new(database.clone()); let http_client = Client::new(); // 从环境变量或配置中获取火山云密钥 let vol_access_key = std::env::var("VOL_ACCESS_KEY_ID") .unwrap_or_else(|_| "AKLTZmEwYzNlZDI4NDI2NDUyNDg1ZTQ5NzVlNmQ4OGNkMDY".to_string()); let vol_secret_key = std::env::var("VOL_ACCESS_SECRET_KEY") .unwrap_or_else(|_| "T1RRd1l6UTROV05pTUdRMU5EWTRNV0V4TldGak9ESmhZbVF4TnpCalpHTQ==".to_string()); Self { database, repository, http_client, cloud_upload_service: CloudUploadService::new(), vol_access_key, vol_secret_key, } } /// 创建视频生成任务 pub async fn create_video_generation(&self, request: CreateVideoGenerationRequest) -> Result { info!("创建视频生成任务: {}", request.name); // 验证输入参数 if request.image_url.is_none() { return Err(anyhow!("图片URL不能为空")); } // 创建记录 let id = Uuid::new_v4().to_string(); let mut record = VideoGenerationRecord::new( id, request.name, request.image_url, request.audio_url, request.prompt, ); // 设置可选参数 if let Some(desc) = request.description { record.description = Some(desc); } if let Some(negative_prompt) = request.negative_prompt { record.negative_prompt = Some(negative_prompt); } if let Some(duration) = request.duration { record.duration = duration; } if let Some(fps) = request.fps { record.fps = fps; } if let Some(resolution) = request.resolution { record.resolution = resolution; } if let Some(style) = request.style { record.style = Some(style); } if let Some(motion_strength) = request.motion_strength { record.motion_strength = motion_strength; } // 保存到数据库 self.repository.create(&record).await?; // 异步启动视频生成任务 let service_clone = self.clone(); let record_id = record.id.clone(); tokio::spawn(async move { if let Err(e) = service_clone.process_video_generation(record_id).await { error!("视频生成任务处理失败: {}", e); } }); Ok(record) } /// 处理视频生成任务 async fn process_video_generation(&self, record_id: String) -> Result<()> { info!("开始处理视频生成任务: {}", record_id); // 获取记录 let mut record = self.repository.get_by_id(&record_id).await? .ok_or_else(|| anyhow!("找不到视频生成记录: {}", record_id))?; // 标记为处理中 record.mark_as_processing(None); self.repository.update(&record).await?; // 调用火山云API match self.call_volcano_api(&record).await { Ok(response) => { if let Some(data) = response.data { // 更新任务ID record.vol_task_id = Some(data.task_id.clone()); record.vol_request_id = Some(response.request_id); self.repository.update(&record).await?; // 轮询任务状态 self.poll_task_status(record_id, data.task_id).await?; } else { record.mark_as_failed("火山云API返回数据为空".to_string(), Some("EMPTY_RESPONSE".to_string())); self.repository.update(&record).await?; } } Err(e) => { error!("调用火山云API失败: {}", e); record.mark_as_failed(e.to_string(), Some("API_ERROR".to_string())); self.repository.update(&record).await?; } } Ok(()) } /// 调用火山云视频生成API async fn call_volcano_api(&self, record: &VideoGenerationRecord) -> Result { // 获取本地文件路径 let image_path = record.image_url.as_ref() .ok_or_else(|| anyhow!("图片路径不能为空"))?; let driving_video_path = record.audio_url.as_ref() .ok_or_else(|| anyhow!("驱动视频路径不能为空,请上传驱动视频文件"))?; // 上传图片到云端 info!("正在上传图片到云端: {}", image_path); let image_upload_result = self.cloud_upload_service .upload_file(image_path, None, None) .await?; if !image_upload_result.success { return Err(anyhow!("图片上传失败: {}", image_upload_result.error_message.unwrap_or_default())); } let image_s3_url = image_upload_result.remote_url .ok_or_else(|| anyhow!("图片上传成功但未返回URL"))?; // 将S3 URL转换为HTTPS CDN URL let image_url = Self::convert_s3_to_cdn_url(&image_s3_url); info!("图片S3 URL: {} -> CDN URL: {}", image_s3_url, image_url); // 上传驱动视频到云端 info!("正在上传驱动视频到云端: {}", driving_video_path); let video_upload_result = self.cloud_upload_service .upload_file(driving_video_path, None, None) .await?; if !video_upload_result.success { return Err(anyhow!("驱动视频上传失败: {}", video_upload_result.error_message.unwrap_or_default())); } let driving_video_s3_url = video_upload_result.remote_url .ok_or_else(|| anyhow!("驱动视频上传成功但未返回URL"))?; // 将S3 URL转换为HTTPS CDN URL let driving_video_url = Self::convert_s3_to_cdn_url(&driving_video_s3_url); info!("驱动视频S3 URL: {} -> CDN URL: {}", driving_video_s3_url, driving_video_url); info!("文件上传完成 - 图片: {}, 驱动视频: {}", image_url, driving_video_url); let request_body = VolcanoVideoGenerationRequest { req_key: "realman_avatar_imitator_v2v_gen_video".to_string(), image_url, driving_video_info: DrivingVideoInfo { store_type: 0, // 固定值 video_url: driving_video_url, }, }; info!("调用火山云视频生成API: {:?}", request_body); // 实际的火山云API调用 - 提交任务 let api_url = "https://visual.volcengineapi.com?Action=CVSubmitTask&Version=2022-08-31"; // 构建认证头 let now = Utc::now(); let timestamp = now.format("%Y%m%dT%H%M%SZ").to_string(); let date = now.format("%Y%m%d").to_string(); let auth_header = self.build_auth_header("POST", api_url, ×tamp, &date, &request_body)?; let response = self.http_client .post(api_url) .header("Authorization", auth_header) .header("Content-Type", "application/json") .header("X-Date", timestamp) .json(&request_body) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); return Err(anyhow!("火山云API调用失败: {} - {}", status, error_text)); } let api_response: VolcanoVideoGenerationResponse = response.json().await?; if api_response.code != 10000 { return Err(anyhow!("火山云API返回错误: {} - {}", api_response.code, api_response.message)); } Ok(api_response) } /// 轮询任务状态 async fn poll_task_status(&self, record_id: String, task_id: String) -> Result<()> { info!("开始轮询任务状态: {} - {}", record_id, task_id); let mut attempts = 0; let max_attempts = 60; // 最多轮询60次(约10分钟) let mut consecutive_failures = 0; let max_consecutive_failures = 3; // 连续失败3次就标记为失败 while attempts < max_attempts { tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; attempts += 1; match self.check_task_status(&task_id).await { Ok(status_response) => { // 重置连续失败计数器 consecutive_failures = 0; if let Some(data) = status_response.data { let mut record = self.repository.get_by_id(&record_id).await? .ok_or_else(|| anyhow!("找不到视频生成记录: {}", record_id))?; match data.status.as_str() { "done" => { if let Some(video_url) = data.video_url { // 下载视频并上传到云端 match self.download_and_upload_video(&video_url, &record_id).await { Ok(cdn_url) => { record.mark_as_completed(cdn_url, None); // 使用CDN URL self.repository.update(&record).await?; info!("视频生成任务完成,已上传到CDN: {}", record_id); return Ok(()); } Err(e) => { warn!("视频上传到CDN失败,使用原始URL: {} - {}", record_id, e); record.mark_as_completed(video_url, None); // 使用原始URL作为fallback self.repository.update(&record).await?; info!("视频生成任务完成(使用原始URL): {}", record_id); return Ok(()); } } } else { record.mark_as_failed("视频URL为空".to_string(), Some("EMPTY_VIDEO_URL".to_string())); self.repository.update(&record).await?; return Err(anyhow!("视频生成完成但视频URL为空")); } } "not_found" => { record.mark_as_failed("任务未找到".to_string(), Some("TASK_NOT_FOUND".to_string())); self.repository.update(&record).await?; return Err(anyhow!("火山云任务未找到")); } "expired" => { record.mark_as_failed("任务已过期".to_string(), Some("TASK_EXPIRED".to_string())); self.repository.update(&record).await?; return Err(anyhow!("火山云任务已过期")); } "in_queue" | "generating" => { // 任务还在处理中,继续轮询 info!("任务状态: {} - {}", data.status, record_id); continue; } _ => { warn!("未知的任务状态: {}", data.status); continue; } } } } Err(e) => { warn!("检查任务状态失败: {}", e); consecutive_failures += 1; // 如果连续失败次数达到上限,标记任务为失败 if consecutive_failures >= max_consecutive_failures { error!("连续{}次检查任务状态失败,标记任务为失败: {}", max_consecutive_failures, e); let mut record = self.repository.get_by_id(&record_id).await? .ok_or_else(|| anyhow!("找不到视频生成记录: {}", record_id))?; record.mark_as_failed( format!("连续{}次API调用失败: {}", max_consecutive_failures, e), Some("API_CONSECUTIVE_FAILURES".to_string()) ); self.repository.update(&record).await?; return Err(anyhow!("连续{}次检查任务状态失败", max_consecutive_failures)); } continue; } } } // 超时处理 let mut record = self.repository.get_by_id(&record_id).await? .ok_or_else(|| anyhow!("找不到视频生成记录: {}", record_id))?; record.mark_as_failed("任务超时".to_string(), Some("TIMEOUT".to_string())); self.repository.update(&record).await?; Err(anyhow!("视频生成任务超时")) } /// 检查任务状态 async fn check_task_status(&self, task_id: &str) -> Result { info!("检查任务状态: {}", task_id); // 构建查询请求 let request_body = VolcanoVideoQueryRequest { req_key: "realman_avatar_imitator_v2v_gen_video".to_string(), task_id: task_id.to_string(), }; // 实际的火山云API调用 - 查询任务 let api_url = "https://visual.volcengineapi.com?Action=CVGetResult&Version=2022-08-31"; // 构建认证头 let now = Utc::now(); let timestamp = now.format("%Y%m%dT%H%M%SZ").to_string(); let date = now.format("%Y%m%d").to_string(); let auth_header = self.build_auth_header("POST", api_url, ×tamp, &date, &request_body)?; let response = self.http_client .post(api_url) .header("Authorization", auth_header) .header("Content-Type", "application/json") .header("X-Date", timestamp) .json(&request_body) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); return Err(anyhow!("火山云API调用失败: {} - {}", status, error_text)); } let api_response: VolcanoVideoQueryResponse = response.json().await?; if api_response.code != 10000 { return Err(anyhow!("火山云API返回错误: {} - {}", api_response.code, api_response.message)); } Ok(api_response) } /// 构建火山云API认证头 fn build_auth_header(&self, method: &str, url: &str, timestamp: &str, date: &str, body: &T) -> Result { // 1. 创建规范请求 (CanonicalRequest) let canonical_request = self.create_canonical_request(method, url, timestamp, body)?; // 2. 创建待签名字符串 (StringToSign) let credential_scope = format!("{}/cn-north-1/cv/request", date); let string_to_sign = self.create_string_to_sign(timestamp, &credential_scope, &canonical_request)?; // 3. 派生签名密钥 (kSigning) let signing_key = self.derive_signing_key(&self.vol_secret_key, date, "cn-north-1", "cv")?; // 4. 计算签名 (Signature) let signature = self.calculate_signature(&signing_key, &string_to_sign)?; // 5. 构建Authorization头 let auth_string = format!( "HMAC-SHA256 Credential={}/{}, SignedHeaders=host;x-date, Signature={}", self.vol_access_key, credential_scope, signature ); Ok(auth_string) } /// 获取视频生成记录列表 pub async fn get_video_generations(&self, query: VideoGenerationQuery) -> Result> { self.repository.get_list(query).await } /// 根据ID获取视频生成记录 pub async fn get_video_generation_by_id(&self, id: &str) -> Result> { self.repository.get_by_id(id).await } /// 删除视频生成记录 pub async fn delete_video_generation(&self, id: &str) -> Result<()> { self.repository.delete(id).await } /// OmniHuman 主体识别 - 提交任务 /// 用于识别图片中是否包含人、类人、拟人等主体 pub async fn realman_avatar_picture_create_role_omni_submit_task( &self, image_url: String ) -> Result { info!("开始 OmniHuman 主体识别任务: {}", image_url); // 构建请求参数 let request_body = RealmanAvatarPictureCreateRoleOmniRequest { req_key: "realman_avatar_picture_create_role_omni".to_string(), image_url, }; // 火山云API调用 - OmniHuman 主体识别 let api_url = "https://visual.volcengineapi.com?Action=RealmanAvatarPictureCreateRoleOmniSubmitTask&Version=2024-06-06"; // 构建认证头 let now = Utc::now(); let timestamp = now.format("%Y%m%dT%H%M%SZ").to_string(); let date = now.format("%Y%m%d").to_string(); let auth_header = self.build_auth_header("POST", api_url, ×tamp, &date, &request_body)?; info!("调用 OmniHuman 主体识别 API: {:?}", request_body); let response = self.http_client .post(api_url) .header("Authorization", auth_header) .header("Content-Type", "application/json") .header("X-Date", timestamp) .json(&request_body) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); return Err(anyhow!("OmniHuman 主体识别 API 调用失败: {} - {}", status, error_text)); } // 先获取响应文本进行调试 let response_text = response.text().await?; info!("OmniHuman API 原始响应: {}", response_text); // 尝试解析响应 let api_response: RealmanAvatarPictureCreateRoleOmniResponse = serde_json::from_str(&response_text) .map_err(|e| anyhow!("解析响应失败: {} - 响应内容: {}", e, response_text))?; if api_response.result.code != 10000 { return Err(anyhow!("OmniHuman 主体识别 API 返回错误: {} - {}", api_response.result.code, api_response.result.message)); } info!("OmniHuman 主体识别任务提交成功: {:?}", api_response); Ok(api_response) } /// 批量删除视频生成记录 pub async fn batch_delete_video_generations(&self, ids: Vec) -> Result<()> { for id in ids { self.repository.delete(&id).await?; } Ok(()) } /// 创建规范请求 fn create_canonical_request(&self, method: &str, url: &str, timestamp: &str, body: &T) -> Result { // 解析URL获取host和path let parsed_url = Url::parse(url)?; let host = parsed_url.host_str().ok_or_else(|| anyhow!("无效的URL"))?; let path = parsed_url.path(); let query = parsed_url.query().unwrap_or(""); // 规范化查询字符串 let canonical_query_string = self.canonicalize_query_string(query); // 规范化请求头 let canonical_headers = format!("host:{}\nx-date:{}\n", host, timestamp); // 签名的请求头 let signed_headers = "host;x-date"; // 计算请求体的哈希值 let body_json = serde_json::to_string(body)?; let body_hash = hex::encode(Sha256::digest(body_json.as_bytes())); // 构建规范请求 let canonical_request = format!( "{}\n{}\n{}\n{}\n{}\n{}", method, path, canonical_query_string, canonical_headers, signed_headers, body_hash ); Ok(canonical_request) } /// 规范化查询字符串 fn canonicalize_query_string(&self, query: &str) -> String { if query.is_empty() { return String::new(); } let mut params: Vec<(String, String)> = query .split('&') .filter_map(|param| { let mut parts = param.splitn(2, '='); let key = parts.next()?.to_string(); let value = parts.next().unwrap_or("").to_string(); Some((key, value)) }) .collect(); // 按参数名称的ASCII升序排序 params.sort_by(|a, b| a.0.cmp(&b.0)); // 构建规范化查询字符串 params .into_iter() .map(|(key, value)| format!("{}={}", key, value)) .collect::>() .join("&") } /// 创建待签名字符串 fn create_string_to_sign(&self, timestamp: &str, credential_scope: &str, canonical_request: &str) -> Result { let canonical_request_hash = hex::encode(Sha256::digest(canonical_request.as_bytes())); let string_to_sign = format!( "HMAC-SHA256\n{}\n{}\n{}", timestamp, credential_scope, canonical_request_hash ); Ok(string_to_sign) } /// 派生签名密钥 fn derive_signing_key(&self, secret_key: &str, date: &str, region: &str, service: &str) -> Result> { type HmacSha256 = Hmac; // kDate = HMAC(kSecret, Date) let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())?; mac.update(date.as_bytes()); let k_date = mac.finalize().into_bytes(); // kRegion = HMAC(kDate, Region) let mut mac = HmacSha256::new_from_slice(&k_date)?; mac.update(region.as_bytes()); let k_region = mac.finalize().into_bytes(); // kService = HMAC(kRegion, Service) let mut mac = HmacSha256::new_from_slice(&k_region)?; mac.update(service.as_bytes()); let k_service = mac.finalize().into_bytes(); // kSigning = HMAC(kService, "request") let mut mac = HmacSha256::new_from_slice(&k_service)?; mac.update(b"request"); let k_signing = mac.finalize().into_bytes(); Ok(k_signing.to_vec()) } /// 计算签名 fn calculate_signature(&self, signing_key: &[u8], string_to_sign: &str) -> Result { type HmacSha256 = Hmac; let mut mac = HmacSha256::new_from_slice(signing_key)?; mac.update(string_to_sign.as_bytes()); let signature = mac.finalize().into_bytes(); Ok(hex::encode(signature)) } /// 下载视频并上传到云端服务 async fn download_and_upload_video(&self, video_url: &str, record_id: &str) -> Result { info!("开始下载并上传视频: {} -> {}", video_url, record_id); // 创建临时文件路径 let temp_dir = std::env::temp_dir(); let temp_filename = format!("volcano_video_{}_{}.mp4", record_id, chrono::Utc::now().timestamp()); let temp_file_path = temp_dir.join(&temp_filename); let temp_file_str = temp_file_path.to_string_lossy().to_string(); // 下载视频到本地临时文件 info!("正在下载视频到临时文件: {}", temp_file_str); match self.download_video_to_file(video_url, &temp_file_str).await { Ok(_) => { info!("视频下载成功: {}", temp_file_str); } Err(e) => { error!("视频下载失败: {} - {}", video_url, e); return Err(anyhow!("视频下载失败: {}", e)); } } // 上传到云端服务 info!("正在上传视频到云端: {}", temp_file_str); let upload_result = match self.cloud_upload_service.upload_file(&temp_file_str, None, None).await { Ok(result) => result, Err(e) => { // 清理临时文件 let _ = tokio::fs::remove_file(&temp_file_path).await; error!("视频上传失败: {} - {}", temp_file_str, e); return Err(anyhow!("视频上传失败: {}", e)); } }; // 清理临时文件 if let Err(e) = tokio::fs::remove_file(&temp_file_path).await { warn!("清理临时文件失败: {} - {}", temp_file_str, e); } if !upload_result.success { let error_msg = upload_result.error_message.unwrap_or_default(); error!("视频上传失败: {}", error_msg); return Err(anyhow!("视频上传失败: {}", error_msg)); } let s3_url = upload_result.remote_url .ok_or_else(|| anyhow!("上传成功但未返回S3 URL"))?; // 转换S3 URL为CDN URL let cdn_url = Self::convert_s3_to_cdn_url(&s3_url); info!("视频上传成功: {} -> {} -> {}", video_url, s3_url, cdn_url); Ok(cdn_url) } /// 下载视频文件到本地 async fn download_video_to_file(&self, video_url: &str, file_path: &str) -> Result<()> { // 创建带有防盗链绕过的HTTP客户端 let client = reqwest::Client::builder() .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") .timeout(std::time::Duration::from_secs(300)) // 5分钟超时 .build() .map_err(|e| anyhow!("创建HTTP客户端失败: {}", e))?; // 构建请求,添加必要的头部信息绕过防盗链 let request = client .get(video_url) .header("Referer", "https://www.volcengine.com/") .header("Origin", "https://www.volcengine.com") .header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5") .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") .header("Cache-Control", "no-cache") .header("Pragma", "no-cache") .header("Sec-Fetch-Dest", "video") .header("Sec-Fetch-Mode", "no-cors") .header("Sec-Fetch-Site", "cross-site"); let response = request.send().await .map_err(|e| anyhow!("请求视频失败: {}", e))?; if !response.status().is_success() { return Err(anyhow!("下载视频失败,HTTP状态码: {}", response.status())); } let bytes = response.bytes().await .map_err(|e| anyhow!("读取视频数据失败: {}", e))?; tokio::fs::write(file_path, &bytes).await .map_err(|e| anyhow!("保存视频文件失败: {}", e))?; info!("视频文件保存成功: {} ({} bytes)", file_path, bytes.len()); Ok(()) } /// 将S3 URL转换为可访问的CDN地址 fn convert_s3_to_cdn_url(s3_url: &str) -> String { if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") { // 将 s3://ap-northeast-2/modal-media-cache/ 替换为 https://cdn.roasmax.cn/ s3_url.replace("s3://ap-northeast-2/modal-media-cache/", "https://cdn.roasmax.cn/") } else if s3_url.starts_with("s3://") { // 处理其他 s3:// 格式,转换为通用CDN格式 s3_url.replace("s3://", "https://cdn.roasmax.cn/") } else if s3_url.contains("amazonaws.com") { // 如果是完整的S3 HTTPS URL,提取key部分 if let Some(key_start) = s3_url.find(".com/") { let key = &s3_url[key_start + 5..]; format!("https://cdn.roasmax.cn/{}", key) } else { s3_url.to_string() } } else { // 如果不是预期的S3格式,返回原URL s3_url.to_string() } } } // 实现Clone trait以支持异步任务 impl Clone for VolcanoVideoService { fn clone(&self) -> Self { Self { database: self.database.clone(), repository: VideoGenerationRecordRepository::new(self.database.clone()), http_client: self.http_client.clone(), cloud_upload_service: CloudUploadService::new(), vol_access_key: self.vol_access_key.clone(), vol_secret_key: self.vol_secret_key.clone(), } } }