Files

1 line
22 KiB
JSON

{"repo": "chengan666/zsn.agentbrook", "files": {"ZSN.AI.Core/Service/Providers/CompshareVideoProvider.cs": {"fbb9d25883921a02e10a7fa0804799c5bbaaf7cd": {"t": 1785596142.2958481, "n": 20225, "h": "cca743350e916457", "c": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing ZSN.AI.Core.Models.Video;\nusing ZSN.AI.Core.Models.Video.Parameters;\nusing ZSN.AI.Core.Utils;\nusing ZSN.AI.Entity;\n\nnamespace ZSN.AI.Core.Service.Providers\n{\n /// <summary>\n /// Compshare (优云智算) 视频生成提供商\n /// API文档: https://www.compshare.cn/docs/modelverse/models/video_api/\n /// </summary>\n public class CompshareVideoProvider : IVideoProvider\n {\n private const string SUBMIT_ENDPOINT = \"/tasks/submit\";\n private const string STATUS_ENDPOINT = \"/tasks/status\";\n\n /// <summary>\n /// 提交视频生成任务\n /// </summary>\n public async Task<VideoGenerationResponse> SubmitVideoTaskAsync(LargeModelInfo modelInfo, VideoGenerationRequest request)\n {\n try\n {\n var httpClient = OpenAIHttpClientHandlerUtil.GetHttpClient(modelInfo.EndPoint);\n \n // 构建API URL\n var endpoint = modelInfo.EndPoint?.TrimEnd('/') ?? throw new ArgumentException(\"模型端点不能为空\");\n var apiUrl = $\"{endpoint}{SUBMIT_ENDPOINT}\";\n\n // 构建请求体\n var requestBody = BuildSubmitRequestBody(modelInfo.ModelName, request);\n var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions \n { \n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull\n });\n var content = new StringContent(jsonContent, Encoding.UTF8, \"application/json\");\n\n // 设置Authorization header\n httpClient.DefaultRequestHeaders.Clear();\n httpClient.DefaultRequestHeaders.Add(\"Authorization\", modelInfo.ModelKey);\n\n Console.WriteLine($\"[Compshare视频] 提交任务到: {apiUrl}\");\n Console.WriteLine($\"[Compshare视频] 模型: {modelInfo.ModelName}\");\n //Console.WriteLine($\"[Compshare视频] 请求体: {jsonContent}\");\n\n // 调用API\n var response = await httpClient.PostAsync(apiUrl, content);\n\n if (!response.IsSuccessStatusCode)\n {\n var errorContent = await response.Content.ReadAsStringAsync();\n Console.WriteLine($\"[Compshare视频] API错误响应: {errorContent}\");\n throw new Exception($\"API调用失败: {response.StatusCode}, {errorContent}\");\n }\n\n var responseContent = await response.Content.ReadAsStringAsync();\n Console.WriteLine($\"[Compshare视频] API响应: {responseContent}\");\n\n // 解析响应\n return ParseSubmitResponse(responseContent);\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"[Compshare视频] 提交任务失败: {ex.Message}\");\n throw;\n }\n }\n\n /// <summary>\n /// 查询视频生成任务状态\n /// </summary>\n public async Task<VideoGenerationResponse> QueryVideoTaskStatusAsync(LargeModelInfo modelInfo, string taskId)\n {\n try\n {\n var httpClient = OpenAIHttpClientHandlerUtil.GetHttpClient(modelInfo.EndPoint);\n \n // 构建API URL\n var endpoint = modelInfo.EndPoint?.TrimEnd('/') ?? throw new ArgumentException(\"模型端点不能为空\");\n var apiUrl = $\"{endpoint}{STATUS_ENDPOINT}?task_id={taskId}\";\n\n // 设置Authorization header\n httpClient.DefaultRequestHeaders.Clear();\n httpClient.DefaultRequestHeaders.Add(\"Authorization\", modelInfo.ModelKey);\n\n Console.WriteLine($\"[Compshare视频] 查询任务状态: {apiUrl}\");\n\n // 调用API\n var response = await httpClient.GetAsync(apiUrl);\n\n if (!response.IsSuccessStatusCode)\n {\n var errorContent = await response.Content.ReadAsStringAsync();\n Console.WriteLine($\"[Compshare视频] API错误响应: {errorContent}\");\n throw new Exception($\"API调用失败: {response.StatusCode}, {errorContent}\");\n }\n\n var responseContent = await response.Content.ReadAsStringAsync();\n Console.WriteLine($\"[Compshare视频] API响应: {responseContent}\");\n\n // 解析响应\n return ParseStatusResponse(responseContent);\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"[Compshare视频] 查询任务状态失败: {ex.Message}\");\n throw;\n }\n }\n\n /// <summary>\n /// 构建提交任务的请求体\n /// </summary>\n private Dictionary<string, object> BuildSubmitRequestBody(string modelName, VideoGenerationRequest request)\n {\n var requestBody = new Dictionary<string, object>\n {\n [\"model\"] = modelName,\n [\"input\"] = BuildInputObject(modelName, request),\n [\"parameters\"] = BuildParametersObject(modelName, request)\n };\n\n return requestBody;\n }\n\n /// <summary>\n /// 构建input对象\n /// </summary>\n private Dictionary<string, object> BuildInputObject(string modelName, VideoGenerationRequest request)\n {\n var input = new Dictionary<string, object>\n {\n [\"prompt\"] = request.Prompt\n };\n\n // 负面提示词\n if (!string.IsNullOrEmpty(request.NegativePrompt))\n {\n input[\"negative_prompt\"] = request.NegativePrompt;\n }\n\n // 图生视频 - 首帧图片\n if (request.GenerationType == VideoGenerationType.ImageToVideo && !string.IsNullOrEmpty(request.ImageInput))\n {\n // Wan2.6-I2V使用img_url,其他模型使用first_frame_url\n var imageFieldName = modelName.Contains(\"Wan2.6-I2V\", StringComparison.OrdinalIgnoreCase) \n ? \"img_url\" \n : \"first_frame_url\";\n input[imageFieldName] = ConvertToDataUriIfNeeded(request.ImageInput);\n }\n\n // 参考图生成视频 - 参考图片列表\n if (request.GenerationType == VideoGenerationType.ReferenceToVideo && request.ReferenceImages != null && request.ReferenceImages.Count > 0)\n {\n // 转换所有参考图为Data URI格式\n var convertedImages = request.ReferenceImages.Select(img => ConvertToDataUriIfNeeded(img)).ToList();\n input[\"images\"] = convertedImages;\n }\n\n // 首尾帧生成视频\n if (request.GenerationType == VideoGenerationType.StartEndToVideo)\n {\n if (!string.IsNullOrEmpty(request.FirstFrameUrl))\n {\n input[\"first_frame_url\"] = ConvertToDataUriIfNeeded(request.FirstFrameUrl);\n }\n if (!string.IsNullOrEmpty(request.LastFrameUrl))\n {\n input[\"last_frame_url\"] = ConvertToDataUriIfNeeded(request.LastFrameUrl);\n }\n }\n\n return input;\n }\n\n /// <summary>\n /// 构建parameters对象\n /// </summary>\n private Dictionary<string, object> BuildParametersObject(string modelName, VideoGenerationRequest request)\n {\n var parameters = new Dictionary<string, object>();\n\n // 根据不同模型构建参数\n if (modelName.Contains(\"sora\", StringComparison.OrdinalIgnoreCase))\n {\n // Sora模型参数\n if (request is SoraVideoParameters soraParams)\n {\n if (!string.IsNullOrEmpty(soraParams.Size))\n parameters[\"size\"] = soraParams.Size;\n if (soraParams.Duration > 0)\n parameters[\"duration\"] = soraParams.Duration;\n }\n else\n {\n // 使用默认值\n if (!string.IsNullOrEmpty(request.Size))\n parameters[\"size\"] = request.Size;\n if (request.Duration.HasValue)\n parameters[\"duration\"] = request.Duration.Value;\n }\n }\n else if (modelName.Contains(\"vidu\", StringComparison.OrdinalIgnoreCase))\n {\n // Vidu模型参数\n if (request is ViduVideoParameters viduParams)\n {\n if (!string.IsNullOrEmpty(viduParams.ViduType))\n parameters[\"vidu_type\"] = viduParams.ViduType;\n if (viduParams.Duration > 0)\n parameters[\"duration\"] = viduParams.Duration;\n \n // text2video和reference2video使用aspect_ratio\n if (viduParams.ViduType == \"text2video\" || viduParams.ViduType == \"reference2video\")\n {\n if (!string.IsNullOrEmpty(viduParams.AspectRatio))\n parameters[\"aspect_ratio\"] = viduParams.AspectRatio;\n if (!string.IsNullOrEmpty(viduParams.Resolution))\n parameters[\"resolution\"] = viduParams.Resolution.ToLower(); // 720p格式\n }\n // img2video和start-end2video使用resolution\n else\n {\n if (!string.IsNullOrEmpty(viduParams.Resolution))\n parameters[\"resolution\"] = viduParams.Resolution;\n }\n \n if (!string.IsNullOrEmpty(viduParams.MovementAmplitude))\n parameters[\"movement_amplitude\"] = viduParams.MovementAmplitude;\n \n parameters[\"bgm\"] = viduParams.Bgm;\n }\n else\n {\n // 自动设置vidu_type\n var viduType = request.GenerationType switch\n {\n VideoGenerationType.TextToVideo => \"text2video\",\n VideoGenerationType.ImageToVideo => \"img2video\",\n VideoGenerationType.ReferenceToVideo => \"reference2video\",\n VideoGenerationType.StartEndToVideo => \"start-end2video\",\n _ => \"text2video\"\n };\n parameters[\"vidu_type\"] = viduType;\n \n if (request.Duration.HasValue)\n parameters[\"duration\"] = request.Duration.Value;\n if (!string.IsNullOrEmpty(request.AspectRatio))\n parameters[\"aspect_ratio\"] = request.AspectRatio;\n if (!string.IsNullOrEmpty(request.Resolution))\n parameters[\"resolution\"] = request.Resolution;\n }\n }\n else if (modelName.Contains(\"Wan-AI\", StringComparison.OrdinalIgnoreCase) || modelName.Contains(\"Wan\", StringComparison.OrdinalIgnoreCase))\n {\n // WanAI模型参数\n if (request is WanAIVideoParameters wanParams)\n {\n if (!string.IsNullOrEmpty(wanParams.Resolution))\n parameters[\"resolution\"] = wanParams.Resolution;\n if (wanParams.Seed.HasValue)\n parameters[\"seed\"] = wanParams.Seed.Value;\n \n // Wan2.6特殊参数\n if (modelName.Contains(\"Wan2.6\", StringComparison.OrdinalIgnoreCase))\n {\n if (wanParams.Duration.HasValue && wanParams.Duration.Value > 0)\n parameters[\"duration\"] = wanParams.Duration.Value;\n \n // 默认启用提示词扩展\n parameters[\"prompt_extend\"] = wanParams.PromptExtend ?? true;\n \n // 默认使用单镜头\n parameters[\"shot_type\"] = wanParams.ShotType ?? \"single\";\n }\n }\n else\n {\n if (!string.IsNullOrEmpty(request.Resolution))\n parameters[\"resolution\"] = request.Resolution;\n if (request.Seed.HasValue)\n parameters[\"seed\"] = request.Seed.Value;\n \n // Wan2.6特殊参数(使用默认值)\n if (modelName.Contains(\"Wan2.6\", StringComparison.OrdinalIgnoreCase))\n {\n if (request.Duration.HasValue && request.Duration.Value > 0)\n parameters[\"duration\"] = request.Duration.Value;\n else\n parameters[\"duration\"] = 5; // 默认5秒\n \n parameters[\"prompt_extend\"] = true;\n parameters[\"shot_type\"] = \"single\";\n }\n }\n }\n else if (modelName.Contains(\"Hailuo\", StringComparison.OrdinalIgnoreCase) || modelName.Contains(\"MiniMax\", StringComparison.OrdinalIgnoreCase))\n {\n // Hailuo模型参数\n if (request is HailuoVideoParameters hailuoParams)\n {\n if (hailuoParams.Duration > 0)\n parameters[\"duration\"] = hailuoParams.Duration;\n if (!string.IsNullOrEmpty(hailuoParams.Resolution))\n parameters[\"resolution\"] = hailuoParams.Resolution;\n parameters[\"prompt_optimizer\"] = hailuoParams.PromptOptimizer;\n parameters[\"fast_pretreatment\"] = hailuoParams.FastPretreatment;\n parameters[\"aigc_watermark\"] = hailuoParams.AigcWatermark;\n }\n else\n {\n if (request.Duration.HasValue)\n parameters[\"duration\"] = request.Duration.Value;\n if (!string.IsNullOrEmpty(request.Resolution))\n parameters[\"resolution\"] = request.Resolution;\n }\n }\n else if (modelName.Contains(\"doubao\", StringComparison.OrdinalIgnoreCase))\n {\n // Doubao模型参数\n if (request is DoubaoVideoParameters doubaoParams)\n {\n if (doubaoParams.Duration.HasValue)\n parameters[\"duration\"] = doubaoParams.Duration.Value;\n if (!string.IsNullOrEmpty(doubaoParams.Resolution))\n parameters[\"resolution\"] = doubaoParams.Resolution;\n }\n else\n {\n if (request.Duration.HasValue)\n parameters[\"duration\"] = request.Duration.Value;\n if (!string.IsNullOrEmpty(request.Resolution))\n parameters[\"resolution\"] = request.Resolution;\n }\n }\n\n // 添加扩展参数\n if (request.ExtendedParameters != null)\n {\n foreach (var kvp in request.ExtendedParameters)\n {\n parameters[kvp.Key] = kvp.Value;\n }\n }\n\n return parameters;\n }\n\n /// <summary>\n /// 解析提交任务的响应\n /// </summary>\n private VideoGenerationResponse ParseSubmitResponse(string responseContent)\n {\n var jsonResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);\n \n var response = new VideoGenerationResponse\n {\n TaskStatus = VideoTaskStatus.Pending\n };\n\n if (jsonResponse.TryGetProperty(\"output\", out var output))\n {\n if (output.TryGetProperty(\"task_id\", out var taskId))\n {\n response.TaskId = taskId.GetString();\n }\n }\n\n if (jsonResponse.TryGetProperty(\"request_id\", out var requestId))\n {\n response.RequestId = requestId.GetString();\n }\n\n return response;\n }\n\n /// <summary>\n /// 解析查询状态的响应\n /// </summary>\n private VideoGenerationResponse ParseStatusResponse(string responseContent)\n {\n var jsonResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);\n \n var response = new VideoGenerationResponse();\n\n if (jsonResponse.TryGetProperty(\"output\", out var output))\n {\n if (output.TryGetProperty(\"task_id\", out var taskId))\n {\n response.TaskId = taskId.GetString();\n }\n\n if (output.TryGetProperty(\"task_status\", out var taskStatus))\n {\n var statusStr = taskStatus.GetString();\n response.TaskStatus = statusStr switch\n {\n \"Pending\" => VideoTaskStatus.Pending,\n \"Running\" => VideoTaskStatus.Running,\n \"Success\" => VideoTaskStatus.Success,\n \"Failure\" => VideoTaskStatus.Failure,\n _ => VideoTaskStatus.Pending\n };\n }\n\n if (output.TryGetProperty(\"urls\", out var urls) && urls.ValueKind == JsonValueKind.Array)\n {\n response.VideoUrls = new List<string>();\n foreach (var url in urls.EnumerateArray())\n {\n response.VideoUrls.Add(url.GetString());\n }\n }\n\n if (output.TryGetProperty(\"submit_time\", out var submitTime))\n {\n response.SubmitTime = submitTime.GetInt64();\n }\n\n if (output.TryGetProperty(\"finish_time\", out var finishTime))\n {\n response.FinishTime = finishTime.GetInt64();\n }\n\n if (output.TryGetProperty(\"error_message\", out var errorMessage))\n {\n response.ErrorMessage = errorMessage.GetString();\n }\n \n // 如果任务失败但没有错误信息,尝试从整个响应中提取更多信息\n if (response.TaskStatus == VideoTaskStatus.Failure && string.IsNullOrEmpty(response.ErrorMessage))\n {\n // 记录完整的响应以便调试\n Console.WriteLine($\"[Compshare视频] 任务失败但未返回错误信息,完整响应: {responseContent}\");\n response.ErrorMessage = \"任务失败,但API未返回具体错误信息。请检查:1)图片URL是否可访问 2)图片格式是否支持 3)模型参数是否正确\";\n }\n }\n\n if (jsonResponse.TryGetProperty(\"usage\", out var usage))\n {\n response.Usage = new VideoUsageInfo();\n \n if (usage.TryGetProperty(\"duration\", out var duration))\n {\n response.Usage.Duration = duration.GetInt32();\n }\n }\n\n if (jsonResponse.TryGetProperty(\"request_id\", out var requestId))\n {\n response.RequestId = requestId.GetString();\n }\n\n return response;\n }\n\n /// <summary>\n /// 将Base64字符串转换为Data URI格式(如果需要)\n /// </summary>\n private string ConvertToDataUriIfNeeded(string input)\n {\n if (string.IsNullOrEmpty(input))\n return input;\n\n // 如果已经是HTTP URL,直接返回\n if (input.StartsWith(\"http://\", StringComparison.OrdinalIgnoreCase) ||\n input.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase))\n {\n return input;\n }\n\n // 如果已经是Data URI格式,直接返回\n if (input.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase))\n {\n return input;\n }\n\n // 否则认为是纯Base64字符串,转换为Data URI格式\n // 默认使用PNG格式,实际应该根据图片内容判断\n Console.WriteLine($\"[Compshare视频] 将Base64字符串转换为Data URI格式\");\n return $\"data:image/png;base64,{input}\";\n }\n }\n}\n"}}}}