diff --git a/docs/depth-media-api.md b/docs/depth-media-api.md index 4c2abbeac176..ae36f3f8021a 100644 --- a/docs/depth-media-api.md +++ b/docs/depth-media-api.md @@ -1,6 +1,6 @@ # DepthMedia API 调用说明 -DepthMedia 是统一的异步媒体处理接口。深度视频、图片去背景和图片高清放大都通过 +DepthMedia 是统一的异步媒体处理接口。深度视频、视频去字幕、图片去背景和图片高清放大都通过 `POST /v1/jobs` 提交。客户端会立即获得公开 `task_id`,随后可轮询任务状态,也可以 通过 Webhook 接收最终结果。 @@ -60,7 +60,8 @@ POST /v1/jobs | `image-upscale` | 高保真 4 倍 | `upscale` | `fidelity` | `4` | `$0.05` | | `image-upscale` | 锐化 4 倍 | `upscale` | `sharp` | `4` | `$0.05` | -模型广场只展示 `depth-video`、`background-remove`、`image-upscale` 三个模型。 +模型广场只展示 `depth-video`、`background-remove`、`image-upscale`、 +`subtitle-remove` 四个模型。 具体处理档位和价格由参数决定,并在模型详情抽屉中展示。图片格式支持上游允许的 `png` 和 `webp`。 @@ -80,6 +81,35 @@ curl https://api.opwan.ai/v1/jobs \ }' ``` +## 视频去字幕 + +模型:`subtitle-remove` + +按源视频实际时长计费,单价为 `$0.02/秒`。系统根据上游返回的帧数和 FPS +计算时长,不采用客户端申报值;不足一秒的部分向上取整。单个视频最长 600 秒, +提交时按 600 秒预扣,任务完成后按实际秒数结算并退回差额。 + +`subtitle_area` 支持: + +- `bottom`:只扫描画面底部字幕区域,默认值,速度更快。 +- `full`:扫描完整画面,适用于字幕位置不固定的视频。 + +```bash +curl https://api.opwan.ai/v1/jobs \ + -H "Authorization: Bearer $OPWAN_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "subtitle-remove", + "source_url": "https://cdn.example.com/captioned.mp4", + "operation": "remove_subtitles", + "quality": "quality", + "format": "mp4", + "subtitle_area": "bottom", + "webhook_url": "https://client.example.com/webhooks/depth-media", + "webhook_secret": "replace-with-your-secret" + }' +``` + ## 提交响应 提交成功返回 HTTP `202`: diff --git a/middleware/depth_media_adapter.go b/middleware/depth_media_adapter.go index 0f9760d38be3..129ff9d4324b 100644 --- a/middleware/depth_media_adapter.go +++ b/middleware/depth_media_adapter.go @@ -18,6 +18,7 @@ type depthMediaRequest struct { Quality string `json:"quality,omitempty"` Scale int `json:"scale,omitempty"` Format string `json:"format,omitempty"` + SubtitleArea string `json:"subtitle_area,omitempty"` WebhookURL string `json:"webhook_url,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"` } @@ -57,7 +58,8 @@ func DepthMediaRequestConvert() gin.HandlerFunc { modelName = taskdepthmedia.ModelDepthVideo } else if modelName == "" || modelName == taskdepthmedia.PublicModelBackgroundRemove || - modelName == taskdepthmedia.PublicModelImageUpscale { + modelName == taskdepthmedia.PublicModelImageUpscale || + modelName == taskdepthmedia.PublicModelSubtitleRemove { resolved, err := taskdepthmedia.ResolveModel(request.Operation, request.Quality, request.Scale) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -76,15 +78,22 @@ func DepthMediaRequestConvert() gin.HandlerFunc { c.Abort() return } + if modelName == taskdepthmedia.PublicModelSubtitleRemove && + !strings.EqualFold(strings.TrimSpace(request.Operation), "remove_subtitles") { + c.JSON(http.StatusBadRequest, gin.H{"error": "subtitle-remove requires operation remove_subtitles"}) + c.Abort() + return + } modelName = resolved } metadata := map[string]any{ - "source_url": request.SourceURL, - "operation": request.Operation, - "quality": request.Quality, - "scale": request.Scale, - "format": request.Format, + "source_url": request.SourceURL, + "operation": request.Operation, + "quality": request.Quality, + "scale": request.Scale, + "format": request.Format, + "subtitle_area": request.SubtitleArea, } unified := relaycommon.TaskSubmitReq{ Prompt: "process media", diff --git a/middleware/depth_media_adapter_test.go b/middleware/depth_media_adapter_test.go index eeb1e5ec3ae5..4421b52d8378 100644 --- a/middleware/depth_media_adapter_test.go +++ b/middleware/depth_media_adapter_test.go @@ -119,6 +119,11 @@ func TestDepthMediaRequestConvertAcceptsPublicCatalogAliases(t *testing.T) { body: `{"model":"image-upscale","source_url":"https://cdn.example.com/input.png","operation":"upscale","quality":"sharp","scale":4}`, wantModel: taskdepthmedia.ModelUpscaleSharp4X, }, + { + name: "subtitle removal", + body: `{"model":"subtitle-remove","source_url":"https://cdn.example.com/input.mp4","operation":"remove_subtitles","quality":"quality","format":"mp4","subtitle_area":"bottom"}`, + wantModel: taskdepthmedia.ModelSubtitleRemove, + }, } for _, tt := range tests { diff --git a/relay/channel/task/depthmedia/adaptor.go b/relay/channel/task/depthmedia/adaptor.go index bd6832d4c8ce..0ed5c71b446d 100644 --- a/relay/channel/task/depthmedia/adaptor.go +++ b/relay/channel/task/depthmedia/adaptor.go @@ -25,9 +25,10 @@ const ( ActionDepth = "depth" ActionMedia = "media" - maxDepthVideoBillingSeconds = 10 * 60 + maxVideoBillingSeconds = 10 * 60 ModelDepthVideo = "depth-anything-v2-small-video" + ModelSubtitleRemove = "subtitle-remove" ModelBackgroundFast = "background-remove-fast" ModelBackgroundQuality = "background-remove-quality" ModelBackgroundMatting = "background-remove-matting" @@ -37,12 +38,14 @@ const ( ModelUpscaleSharp4X = "image-upscale-sharp-4x" PublicModelDepthVideo = "depth-video" + PublicModelSubtitleRemove = ModelSubtitleRemove PublicModelBackgroundRemove = "background-remove" PublicModelImageUpscale = "image-upscale" ) var supportedModels = []string{ ModelDepthVideo, + ModelSubtitleRemove, ModelBackgroundFast, ModelBackgroundQuality, ModelBackgroundMatting, @@ -53,11 +56,12 @@ var supportedModels = []string{ } type requestPayload struct { - SourceURL string `json:"source_url"` - Operation string `json:"operation,omitempty"` - Quality string `json:"quality,omitempty"` - Scale int `json:"scale,omitempty"` - Format string `json:"format,omitempty"` + SourceURL string `json:"source_url"` + Operation string `json:"operation,omitempty"` + Quality string `json:"quality,omitempty"` + Scale int `json:"scale,omitempty"` + Format string `json:"format,omitempty"` + SubtitleArea string `json:"subtitle_area,omitempty"` } type responsePayload struct { @@ -105,6 +109,11 @@ func ResolveModel(operation, quality string, scale int) (string, error) { return ModelUpscaleSharp4X, nil } } + case "remove_subtitles": + normalizedQuality := strings.ToLower(strings.TrimSpace(quality)) + if scale == 0 && (normalizedQuality == "" || normalizedQuality == "quality") { + return ModelSubtitleRemove, nil + } } return "", fmt.Errorf("unsupported media profile: operation=%q quality=%q scale=%d", operation, quality, scale) } @@ -139,6 +148,21 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom if err := request.UnmarshalMetadata(&metadata); err != nil { return service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest) } + metadata.Operation = strings.ToLower(strings.TrimSpace(metadata.Operation)) + metadata.Quality = strings.ToLower(strings.TrimSpace(metadata.Quality)) + metadata.Format = strings.ToLower(strings.TrimSpace(metadata.Format)) + metadata.SubtitleArea = strings.ToLower(strings.TrimSpace(metadata.SubtitleArea)) + if metadata.Operation == "remove_subtitles" { + if metadata.Quality == "" { + metadata.Quality = "quality" + } + if metadata.Format == "" { + metadata.Format = "mp4" + } + if metadata.SubtitleArea == "" { + metadata.SubtitleArea = "bottom" + } + } resolved, err := ResolveModel(metadata.Operation, metadata.Quality, metadata.Scale) if err != nil || resolved != request.Model { if err == nil { @@ -146,6 +170,32 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom } return service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest) } + if resolved == ModelSubtitleRemove { + format := strings.ToLower(strings.TrimSpace(metadata.Format)) + if format != "" && format != "mp4" { + return service.TaskErrorWrapperLocal( + fmt.Errorf("subtitle removal format must be mp4"), + "invalid_request", + http.StatusBadRequest, + ) + } + subtitleArea := strings.ToLower(strings.TrimSpace(metadata.SubtitleArea)) + if subtitleArea != "" && subtitleArea != "bottom" && subtitleArea != "full" { + return service.TaskErrorWrapperLocal( + fmt.Errorf("subtitle_area must be bottom or full"), + "invalid_request", + http.StatusBadRequest, + ) + } + } + request.Metadata = map[string]interface{}{ + "source_url": metadata.SourceURL, + "operation": metadata.Operation, + "quality": metadata.Quality, + "scale": metadata.Scale, + "format": metadata.Format, + "subtitle_area": metadata.SubtitleArea, + } } info.Action = action c.Set("action", action) @@ -200,20 +250,23 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, bod } func (a *TaskAdaptor) EstimateBilling(_ *gin.Context, info *relaycommon.RelayInfo) map[string]float64 { - if info.OriginModelName != ModelDepthVideo { + if info.OriginModelName != ModelDepthVideo && info.OriginModelName != ModelSubtitleRemove { return nil } - return map[string]float64{"seconds": maxDepthVideoBillingSeconds} + return map[string]float64{"seconds": maxVideoBillingSeconds} } func (a *TaskAdaptor) AdjustBillingOnComplete(task *model.Task, taskResult *relaycommon.TaskInfo) int { - if task == nil || taskResult == nil || taskResult.Status != model.TaskStatusSuccess || - task.Action != ActionDepth { + if task == nil || taskResult == nil || taskResult.Status != model.TaskStatusSuccess { return 0 } billing := task.PrivateData.BillingContext - if billing == nil || billing.OriginModelName != ModelDepthVideo || - billing.ModelPrice <= 0 || billing.GroupRatio <= 0 { + if billing == nil || billing.ModelPrice <= 0 || billing.GroupRatio <= 0 { + return 0 + } + isDepthVideo := task.Action == ActionDepth && billing.OriginModelName == ModelDepthVideo + isSubtitleRemoval := task.Action == ActionMedia && billing.OriginModelName == ModelSubtitleRemove + if !isDepthVideo && !isSubtitleRemoval { return 0 } var response responsePayload @@ -222,7 +275,7 @@ func (a *TaskAdaptor) AdjustBillingOnComplete(task *model.Task, taskResult *rela return 0 } seconds := math.Ceil(float64(response.Frames) / response.FPS) - seconds = min(seconds, maxDepthVideoBillingSeconds) + seconds = min(seconds, maxVideoBillingSeconds) quota, clamp := common.QuotaFromFloatChecked( billing.ModelPrice * common.QuotaPerUnit * billing.GroupRatio * seconds, ) diff --git a/relay/channel/task/depthmedia/adaptor_test.go b/relay/channel/task/depthmedia/adaptor_test.go index 194a533ed1ea..b0e294abca96 100644 --- a/relay/channel/task/depthmedia/adaptor_test.go +++ b/relay/channel/task/depthmedia/adaptor_test.go @@ -44,6 +44,7 @@ func TestResolveModel(t *testing.T) { {name: "upscale fast 4x", operation: "upscale", quality: "fast", scale: 4, want: ModelUpscaleFast4X}, {name: "upscale fidelity", operation: "upscale", quality: "fidelity", scale: 4, want: ModelUpscaleFidelity4X}, {name: "upscale sharp", operation: "upscale", quality: "sharp", scale: 4, want: ModelUpscaleSharp4X}, + {name: "subtitle removal", operation: "remove_subtitles", quality: "quality", want: ModelSubtitleRemove}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -55,6 +56,8 @@ func TestResolveModel(t *testing.T) { _, err := ResolveModel("upscale", "fidelity", 2) require.Error(t, err) + _, err = ResolveModel("remove_subtitles", "quality", 2) + require.Error(t, err) } func TestTaskAdaptorBuildsMediaRequestWithoutGatewayWebhookFields(t *testing.T) { @@ -122,6 +125,43 @@ func TestTaskAdaptorBuildsDepthRequestWithUnifiedOperation(t *testing.T) { assert.Equal(t, "depth", payload["operation"]) } +func TestTaskAdaptorBuildsSubtitleRemovalRequest(t *testing.T) { + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest( + http.MethodPost, + "/v1/video/generations", + strings.NewReader(`{ + "model":"subtitle-remove", + "image":"https://cdn.example.com/captioned.mp4", + "metadata":{ + "operation":" Remove_Subtitles ", + "quality":" Quality ", + "format":"MP4", + "subtitle_area":"Bottom" + } + }`), + ) + c.Request.Header.Set("Content-Type", "application/json") + info := newTestRelayInfo("https://modal.example.com", "upstream-secret", "") + adaptor := &TaskAdaptor{} + adaptor.Init(info) + require.Nil(t, adaptor.ValidateRequestAndSetAction(c, info)) + + body, err := adaptor.BuildRequestBody(c, info) + require.NoError(t, err) + data, err := io.ReadAll(body) + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, common.Unmarshal(data, &payload)) + assert.Equal(t, "https://cdn.example.com/captioned.mp4", payload["source_url"]) + assert.Equal(t, "remove_subtitles", payload["operation"]) + assert.Equal(t, "quality", payload["quality"]) + assert.Equal(t, "mp4", payload["format"]) + assert.Equal(t, "bottom", payload["subtitle_area"]) +} + func TestTaskAdaptorValidatesDepthAndMediaRequests(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { @@ -140,6 +180,16 @@ func TestTaskAdaptorValidatesDepthAndMediaRequests(t *testing.T) { body: `{"model":"background-remove-fast","image":"https://cdn.example.com/input.png","metadata":{"operation":"remove_background","quality":"fast"}}`, wantAction: ActionMedia, }, + { + name: "subtitle removal", + body: `{"model":"subtitle-remove","image":"https://cdn.example.com/input.mp4","metadata":{"operation":"remove_subtitles","quality":"quality","format":"mp4","subtitle_area":"full"}}`, + wantAction: ActionMedia, + }, + { + name: "subtitle removal rejects scale", + body: `{"model":"subtitle-remove","image":"https://cdn.example.com/input.mp4","metadata":{"operation":"remove_subtitles","quality":"quality","format":"mp4","subtitle_area":"bottom","scale":2}}`, + wantError: true, + }, { name: "missing source", body: `{"model":"depth-anything-v2-small-video"}`, @@ -302,6 +352,10 @@ func TestTaskAdaptorEstimatesMaximumDepthVideoDuration(t *testing.T) { imageInfo := newTestRelayInfo("https://modal.example.com", "key", ActionMedia) imageInfo.OriginModelName = ModelUpscaleFast2X assert.Nil(t, adaptor.EstimateBilling(c, imageInfo)) + + subtitleInfo := newTestRelayInfo("https://modal.example.com", "key", ActionMedia) + subtitleInfo.OriginModelName = ModelSubtitleRemove + assert.Equal(t, map[string]float64{"seconds": 600}, adaptor.EstimateBilling(c, subtitleInfo)) } func TestTaskAdaptorReconcilesDepthVideoToActualDuration(t *testing.T) { @@ -368,3 +422,26 @@ func TestTaskAdaptorCapsReportedDurationAtMaximum(t *testing.T) { assert.Equal(t, 600000, quota) } + +func TestTaskAdaptorReconcilesSubtitleRemovalToActualDuration(t *testing.T) { + adaptor := &TaskAdaptor{} + task := &model.Task{ + Action: ActionMedia, + Data: []byte( + `{"id":"job_1","status":"completed","progress":100,"fps":24,"frames":73}`, + ), + PrivateData: model.TaskPrivateData{ + BillingContext: &model.TaskBillingContext{ + ModelPrice: 0.02, + GroupRatio: 1, + OriginModelName: ModelSubtitleRemove, + }, + }, + } + + quota := adaptor.AdjustBillingOnComplete(task, &relaycommon.TaskInfo{ + Status: model.TaskStatusSuccess, + }) + + assert.Equal(t, 40000, quota) +} diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 829e0794a157..f290c5cd5b58 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -301,6 +301,7 @@ var defaultModelPrice = map[string]float64{ "veo-3.0-fast-generate-001": 0.15, "veo-3.1-generate-preview": 0.4, "veo-3.1-fast-generate-preview": 0.15, + "subtitle-remove": 0.02, } var defaultAudioRatio = map[string]float64{ diff --git a/web/default/src/features/pricing/components/model-details-api.tsx b/web/default/src/features/pricing/components/model-details-api.tsx index f3b6c40221c3..d8fbbd848d0a 100644 --- a/web/default/src/features/pricing/components/model-details-api.tsx +++ b/web/default/src/features/pricing/components/model-details-api.tsx @@ -828,7 +828,9 @@ function profileParameterForDisplay( } const MEDIA_PARAMETER_DESCRIPTION_KEYS: Record = { + format: 'Output media format', quality: 'Media processing quality profile', + subtitle_area: 'Area to scan for hard-coded subtitles', webhook_url: 'URL receiving asynchronous task completion notifications', webhook_secret: 'Secret used to sign asynchronous task webhook deliveries', } diff --git a/web/default/src/features/pricing/lib/depth-media-catalog.test.ts b/web/default/src/features/pricing/lib/depth-media-catalog.test.ts index 8361c83b9002..dfa5a2b226da 100644 --- a/web/default/src/features/pricing/lib/depth-media-catalog.test.ts +++ b/web/default/src/features/pricing/lib/depth-media-catalog.test.ts @@ -48,6 +48,7 @@ const sourceModels: PricingModel[] = [ ['image-upscale-fast-4x', 0.02], ['image-upscale-fidelity-4x', 0.05], ['image-upscale-sharp-4x', 0.05], + ['subtitle-remove', 0.02], ].map(([modelName, price]) => ({ id: 74, model_name: String(modelName), @@ -62,12 +63,17 @@ const sourceModels: PricingModel[] = [ ] describe('DepthMedia model plaza catalog', () => { - test('collapses eight implementation profiles into three public models', () => { + test('collapses implementation profiles into four public models', () => { const models = consolidateDepthMediaModels(sourceModels) assert.deepEqual( models.map((model) => model.model_name), - ['depth-video', 'background-remove', 'image-upscale'] + [ + 'depth-video', + 'background-remove', + 'image-upscale', + 'subtitle-remove', + ] ) assert.ok( models.every( @@ -97,6 +103,20 @@ describe('DepthMedia model plaza catalog', () => { ) assert.equal(getFixedPriceUnit(models[0]), 'seconds') assert.equal(getFixedPriceUnit(models[1]), 'request') + assert.equal(getFixedPriceUnit(models[3]), 'seconds') + const subtitleParameters = + models[3]?.api_profile?.parameters?.filter((parameter) => + ['quality', 'format'].includes(parameter.name) + ) ?? [] + assert.ok( + subtitleParameters.every((parameter) => parameter.required !== true) + ) + assert.deepEqual( + models[3]?.api_profile?.pricing_variants?.map( + (variant) => variant.parameters.subtitle_area + ), + ['bottom', 'full'] + ) }) test('applies group and recharge multipliers to parameter prices', () => { @@ -152,6 +172,21 @@ describe('DepthMedia model plaza catalog', () => { assert.doesNotMatch(sample, /messages/) }) + test('generates the subtitle-removal job contract', () => { + const sample = buildDepthMediaJobSample('curl', { + baseUrl: 'https://api.opwan.ai', + apiKeyEnv: 'OPWAN_API_KEY', + modelName: 'subtitle-remove', + endpointPath: '/v1/jobs', + }) + + assert.match(sample, /"model": "subtitle-remove"/) + assert.match(sample, /"operation": "remove_subtitles"/) + assert.match(sample, /"quality": "quality"/) + assert.match(sample, /"format": "mp4"/) + assert.match(sample, /"subtitle_area": "bottom"/) + }) + test('generates a self-contained AI integration guide for one-click copy', () => { const models = consolidateDepthMediaModels(sourceModels) const guide = buildDepthMediaAiIntegrationGuide({ @@ -170,6 +205,16 @@ describe('DepthMedia model plaza catalog', () => { assert.match(guide, /0\.05 USD per request/) assert.match(guide, /background-remove/) assert.match(guide, /depth-video/) + assert.match(guide, /subtitle-remove/) + assert.match( + guide, + /operation=remove_subtitles, quality=quality, format=mp4, subtitle_area=bottom/ + ) + assert.match( + guide, + /operation=remove_subtitles, quality=quality, format=mp4, subtitle_area=full/ + ) + assert.match(guide, /0\.02 USD per second/) assert.match(guide, /Webhook/) assert.match(guide, /X-Webhook-Signature/) assert.match(guide, /v1=/) diff --git a/web/default/src/features/pricing/lib/depth-media-catalog.ts b/web/default/src/features/pricing/lib/depth-media-catalog.ts index cf3bea27ac60..1fe1b374b15a 100644 --- a/web/default/src/features/pricing/lib/depth-media-catalog.ts +++ b/web/default/src/features/pricing/lib/depth-media-catalog.ts @@ -44,6 +44,7 @@ export type DepthMediaAiIntegrationGuideContext = { } const DEPTH_MODEL = 'depth-anything-v2-small-video' +const SUBTITLE_MODEL = 'subtitle-remove' const BACKGROUND_PROFILES = [ { @@ -94,6 +95,7 @@ const SOURCE_MODEL_NAMES = new Set([ DEPTH_MODEL, ...BACKGROUND_PROFILES.map((profile) => profile.source), ...UPSCALE_PROFILES.map((profile) => profile.source), + SUBTITLE_MODEL, ]) function mediaProfile( @@ -178,6 +180,7 @@ export function consolidateDepthMediaModels( ): PricingModel[] { const indexed = new Map(models.map((model) => [model.model_name, model])) const depth = indexed.get(DEPTH_MODEL) + const subtitle = indexed.get(SUBTITLE_MODEL) const background = BACKGROUND_PROFILES.flatMap((profile) => { const model = indexed.get(profile.source) return model ? [{ model, profile }] : [] @@ -305,6 +308,68 @@ export function consolidateDepthMediaModels( ) ) } + if (subtitle) { + publicModels.push( + consolidatedModel( + [subtitle], + SUBTITLE_MODEL, + translate( + 'Remove hard-coded subtitles from videos and return a clean MP4.' + ), + translate('Video,Subtitle removal'), + mediaProfile( + 'remove_subtitles', + [ + { + name: 'quality', + type: 'enum', + default: 'quality', + enum_values: ['quality'], + description: 'Media processing quality profile', + }, + { + name: 'format', + type: 'enum', + default: 'mp4', + enum_values: ['mp4'], + description: 'Output media format', + }, + { + name: 'subtitle_area', + type: 'enum', + default: 'bottom', + enum_values: ['bottom', 'full'], + description: 'Area to scan for hard-coded subtitles', + }, + ], + [ + { + label: translate('Video subtitle removal'), + parameters: { + operation: 'remove_subtitles', + quality: 'quality', + format: 'mp4', + subtitle_area: 'bottom', + }, + price: subtitle.model_price ?? 0, + unit: 'second', + }, + { + label: translate('Video subtitle removal'), + parameters: { + operation: 'remove_subtitles', + quality: 'quality', + format: 'mp4', + subtitle_area: 'full', + }, + price: subtitle.model_price ?? 0, + unit: 'second', + }, + ] + ) + ) + ) + } if (publicModels.length === 0) return models @@ -338,6 +403,15 @@ function samplePayload(modelName: string): Record { quality: 'fast', format: 'webp', } + case SUBTITLE_MODEL: + return { + model: modelName, + source_url: 'https://cdn.example.com/captioned.mp4', + operation: 'remove_subtitles', + quality: 'quality', + format: 'mp4', + subtitle_area: 'bottom', + } default: return { model: modelName, diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index f05c65ce7d5b..f35a93d3aae7 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.", "Are you sure?": "Are you sure?", "Area Chart": "Area Chart", + "Area to scan for hard-coded subtitles": "Area to scan for hard-coded subtitles", "Args (space separated)": "Args (space separated)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.", "Asc": "Asc", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "Output compression level from 0 to 100", "Output Format": "Output Format", "Output image size": "Output image size", + "Output media format": "Output media format", "Output price": "Output price", "Output resolution supported by the selected model": "Output resolution supported by the selected model", "Output result": "Output result", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "Remove Duplicates", "Remove filter": "Remove filter", "Remove functionResponse.id field": "Remove functionResponse.id field", + "Remove hard-coded subtitles from videos and return a clean MP4.": "Remove hard-coded subtitles from videos and return a clean MP4.", "Remove image backgrounds with fast, high-quality, or precision matting.": "Remove image backgrounds with fast, high-quality, or precision matting.", "Remove mapped targets": "Remove mapped targets", "Remove Models": "Remove Models", @@ -5096,7 +5099,9 @@ "Video": "Video", "Video length in seconds": "Video length in seconds", "Video Remix": "Video Remix", + "Video subtitle removal": "Video subtitle removal", "Video,Depth map": "Video,Depth map", + "Video,Subtitle removal": "Video,Subtitle removal", "Vidu": "Vidu", "View": "View", "View all currently available models": "View all currently available models", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 152cb8d38f4b..d28f6a53e7d8 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Êtes-vous sûr de vouloir dissocier {{provider}} ? Vous ne pourrez plus vous connecter via cette méthode.", "Are you sure?": "Êtes-vous sûr ?", "Area Chart": "Graphique en aires", + "Area to scan for hard-coded subtitles": "Zone à analyser pour les sous-titres incrustés", "Args (space separated)": "Arguments (séparés par des espaces)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Tableau de préréglages de clients de chat. Chaque élément est un objet avec une paire clé-valeur : nom du client et son URL.", "Asc": "Asc", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "Niveau de compression de sortie de 0 à 100", "Output Format": "Format de sortie", "Output image size": "Taille de l'image de sortie", + "Output media format": "Format du média de sortie", "Output price": "Prix de sortie", "Output resolution supported by the selected model": "Résolution de sortie prise en charge par le modèle sélectionné", "Output result": "Résultat", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "Supprimer les doublons", "Remove filter": "Supprimer le filtre", "Remove functionResponse.id field": "Supprimer le champ functionResponse.id", + "Remove hard-coded subtitles from videos and return a clean MP4.": "Supprime les sous-titres incrustés des vidéos et renvoie un MP4 propre.", "Remove image backgrounds with fast, high-quality, or precision matting.": "Supprime l’arrière-plan avec des profils rapide, haute qualité ou détourage précis.", "Remove mapped targets": "Retirer les cibles mappées", "Remove Models": "Supprimer des modèles", @@ -5096,7 +5099,9 @@ "Video": "Vidéo", "Video length in seconds": "Durée de la vidéo en secondes", "Video Remix": "Remix vidéo", + "Video subtitle removal": "Suppression des sous-titres vidéo", "Video,Depth map": "Vidéo,Carte de profondeur", + "Video,Subtitle removal": "Vidéo,Suppression des sous-titres", "Vidu": "Vidu", "View": "Afficher", "View all currently available models": "Voir tous les modèles actuellement disponibles", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 143d11c23462..fdefdc1daab9 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "{{provider}}の連携を解除してもよろしいですか?この方法でログインできなくなります。", "Are you sure?": "よろしいですか?", "Area Chart": "面グラフ", + "Area to scan for hard-coded subtitles": "焼き付け字幕のスキャン領域", "Args (space separated)": "引数 (スペース区切り)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "チャットクライアントプリセットの配列。各項目は、クライアント名とそのURLという1つのキーと値のペアを持つオブジェクトです。", "Asc": "昇順", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "出力圧縮レベル(0~100)", "Output Format": "出力形式", "Output image size": "出力画像サイズ", + "Output media format": "出力メディア形式", "Output price": "出力価格", "Output resolution supported by the selected model": "選択したモデルが対応する出力解像度", "Output result": "出力結果", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "重複を削除", "Remove filter": "フィルターを削除", "Remove functionResponse.id field": "functionResponse.id フィールドを削除", + "Remove hard-coded subtitles from videos and return a clean MP4.": "動画の焼き付け字幕を除去し、クリーンな MP4 を返します。", "Remove image backgrounds with fast, high-quality, or precision matting.": "高速、高品質、精密マッティングで画像の背景を削除します。", "Remove mapped targets": "マッピング先を削除", "Remove Models": "モデルを削除", @@ -5096,7 +5099,9 @@ "Video": "動画", "Video length in seconds": "動画の長さ(秒)", "Video Remix": "動画 Remix", + "Video subtitle removal": "動画字幕の除去", "Video,Depth map": "動画,深度マップ", + "Video,Subtitle removal": "動画,字幕除去", "Vidu": "Vidu", "View": "表示", "View all currently available models": "現在利用可能なすべてのモデルを表示", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 9c1b5f8f8637..2770bbf494cd 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Вы уверены, что хотите отвязать {{provider}}? Вы больше не сможете входить через этот метод.", "Are you sure?": "Вы уверены?", "Area Chart": "Диаграмма с областями", + "Area to scan for hard-coded subtitles": "Область поиска вшитых субтитров", "Args (space separated)": "Аргументы (разделённые пробелами)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Массив предустановок чат-клиентов. Каждый элемент представляет собой объект с одной парой ключ-значение: имя клиента и его URL.", "Asc": "По возрастанию", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "Уровень сжатия на выходе от 0 до 100", "Output Format": "Формат вывода", "Output image size": "Размер выходного изображения", + "Output media format": "Формат выходного медиафайла", "Output price": "Цена выхода", "Output resolution supported by the selected model": "Поддерживаемое моделью разрешение", "Output result": "Результат", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "Удалить дубликаты", "Remove filter": "Удалить фильтр", "Remove functionResponse.id field": "Удалить поле functionResponse.id", + "Remove hard-coded subtitles from videos and return a clean MP4.": "Удаляет вшитые субтитры из видео и возвращает чистый MP4.", "Remove image backgrounds with fast, high-quality, or precision matting.": "Удаляет фон в быстром, качественном или точном режиме.", "Remove mapped targets": "Удалить сопоставленные цели", "Remove Models": "Удалить модели", @@ -5096,7 +5099,9 @@ "Video": "Видео", "Video length in seconds": "Длительность видео в секундах", "Video Remix": "Ремикс видео", + "Video subtitle removal": "Удаление субтитров из видео", "Video,Depth map": "Видео,Карта глубины", + "Video,Subtitle removal": "Видео,Удаление субтитров", "Vidu": "Vidu", "View": "Просмотр", "View all currently available models": "Просмотреть все доступные модели", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index e1d3c8105bd9..4fa3d48ed29d 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "Bạn có chắc chắn muốn hủy liên kết {{provider}}? Bạn sẽ không thể đăng nhập bằng phương thức này nữa.", "Are you sure?": "Bạn có chắc không?", "Area Chart": "Biểu đồ vùng", + "Area to scan for hard-coded subtitles": "Vùng quét phụ đề được nhúng", "Args (space separated)": "Đối số (cách nhau bằng khoảng trắng)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "Mảng các thiết lập sẵn của ứng dụng trò chuyện. Mỗi mục là một đối tượng với", "Asc": "Asc", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "Mức nén đầu ra từ 0 đến 100", "Output Format": "Định dạng đầu ra", "Output image size": "Kích thước ảnh đầu ra", + "Output media format": "Định dạng phương tiện đầu ra", "Output price": "Giá đầu ra", "Output resolution supported by the selected model": "Độ phân giải đầu ra được mô hình hỗ trợ", "Output result": "Kết quả đầu ra", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "Xóa trùng lặp", "Remove filter": "Xóa bộ lọc", "Remove functionResponse.id field": "Loại bỏ trường functionResponse.id", + "Remove hard-coded subtitles from videos and return a clean MP4.": "Xóa phụ đề được nhúng trong video và trả về tệp MP4 sạch.", "Remove image backgrounds with fast, high-quality, or precision matting.": "Xóa nền ảnh với chế độ nhanh, chất lượng cao hoặc tách nền chính xác.", "Remove mapped targets": "Xóa đích đã ánh xạ", "Remove Models": "Xóa mô hình", @@ -5096,7 +5099,9 @@ "Video": "Video", "Video length in seconds": "Độ dài video (giây)", "Video Remix": "Remix video", + "Video subtitle removal": "Xóa phụ đề video", "Video,Depth map": "Video,Bản đồ độ sâu", + "Video,Subtitle removal": "Video,Xóa phụ đề", "Vidu": "Vidu", "View": "Xem", "View all currently available models": "Xem tất cả mô hình hiện có", diff --git a/web/default/src/i18n/locales/zh-TW.json b/web/default/src/i18n/locales/zh-TW.json index 56bd0f43a057..c1444246d148 100644 --- a/web/default/src/i18n/locales/zh-TW.json +++ b/web/default/src/i18n/locales/zh-TW.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "確定要解綁 {{provider}} 嗎?解綁後將無法透過此方式登入。", "Are you sure?": "您確定嗎?", "Area Chart": "面積圖", + "Area to scan for hard-coded subtitles": "硬字幕掃描區域", "Args (space separated)": "參數 (空格分隔)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "聊天用戶端預設陣列。每個項目都是一個物件,包含一個鍵值對:用戶端名稱及其 URL。", "Asc": "升序", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "輸出壓縮等級(0 到 100)", "Output Format": "輸出格式", "Output image size": "輸出圖像尺寸", + "Output media format": "輸出媒體格式", "Output price": "輸出價格", "Output resolution supported by the selected model": "所選模型支援的輸出解析度", "Output result": "輸出結果", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "移除重複項", "Remove filter": "移除篩選", "Remove functionResponse.id field": "移除 functionResponse.id 欄位", + "Remove hard-coded subtitles from videos and return a clean MP4.": "移除影片中的硬字幕並傳回乾淨的 MP4。", "Remove image backgrounds with fast, high-quality, or precision matting.": "移除圖片背景,支援快速、高品質與精細去背。", "Remove mapped targets": "移除映射目標", "Remove Models": "刪除模型", @@ -5096,7 +5099,9 @@ "Video": "影片", "Video length in seconds": "影片時長(秒)", "Video Remix": "影片 Remix", + "Video subtitle removal": "影片去字幕", "Video,Depth map": "影片,深度圖", + "Video,Subtitle removal": "影片,去字幕", "Vidu": "Vidu", "View": "查看", "View all currently available models": "查看目前可用的所有模型", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 2ce007d86ced..5d4f65d9c9fc 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -448,6 +448,7 @@ "Are you sure you want to unbind {{provider}}? You will no longer be able to log in via this method.": "确定要解绑 {{provider}} 吗?解绑后将无法通过此方式登录。", "Are you sure?": "您确定吗?", "Area Chart": "面积图", + "Area to scan for hard-coded subtitles": "硬字幕扫描区域", "Args (space separated)": "参数 (空格分隔)", "Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.": "聊天客户端预设数组。每个项目都是一个对象,包含一个键值对:客户端名称及其 URL。", "Asc": "升序", @@ -3226,6 +3227,7 @@ "Output compression level from 0 to 100": "输出压缩级别(0 到 100)", "Output Format": "输出格式", "Output image size": "输出图像尺寸", + "Output media format": "输出媒体格式", "Output price": "输出价格", "Output resolution supported by the selected model": "当前模型支持的输出分辨率", "Output result": "输出结果", @@ -3780,6 +3782,7 @@ "Remove Duplicates": "移除重复项", "Remove filter": "移除筛选", "Remove functionResponse.id field": "移除 functionResponse.id 字段", + "Remove hard-coded subtitles from videos and return a clean MP4.": "去除视频中的硬字幕并返回干净的 MP4。", "Remove image backgrounds with fast, high-quality, or precision matting.": "去除图片背景,支持快速、高质量和精细抠图。", "Remove mapped targets": "移除映射目标", "Remove Models": "删除模型", @@ -5096,7 +5099,9 @@ "Video": "视频", "Video length in seconds": "视频时长(秒)", "Video Remix": "视频 Remix", + "Video subtitle removal": "视频去字幕", "Video,Depth map": "视频,深度图", + "Video,Subtitle removal": "视频,去字幕", "Vidu": "Vidu", "View": "查看", "View all currently available models": "查看当前可用的所有模型",