水印处理流程
“水印处理流程”章节沿用其他工具的「报价 → 提交 → 轮询 → 结果」流程。工具目录包含 watermark-find(查找候选水印)、watermark-mask(追踪并预览遮罩)以及适用于对应素材的去水印工具。提交前读取工具参数选项。候选标记需要复核,保存标记本身不会创建处理任务。
- 对选定输入版本运行
watermark-find,从结果读取watermarkFind。 - 复核候选区域,把确认后的标记保存到该版本。
- 携带确认的区域为
watermark-mask报价并提交,预览追踪结果。 - 检查
maskPreviewUrl和分析报告,再使用同一输入版本与确认参数提交对应去水印工具。
保存水印标记
“保存水印标记”章节此接口会整体替换指定版本上的标记。坐标是 0 到 1 的相对值,原点位于左上角,x + width、y + height 不得超过 1。timeMs 是视频时间点,单位毫秒;省略表示整段视频固定位置。相同 objectId 的标记表示同一处水印。
positive 表示有水印,negative 排除区域,absent 表示该时间段没有这个水印,suggested 是尚未确认的候选,不参与执行。negative/absent/suggested 必须传 timeMs,absent/suggested 还必须传 objectId。使用 endMs 时不得早于 timeMs。最多保存 64 条标记、8 个对象,实际处理最多使用 32 个正向区域。regions: [] 会清空标记,省略 objects 会清空水印名称。
示例保存第一帧上的一个确认矩形。它只持久化标注,不会自动启动或配置处理任务;追踪或去水印时,需要将确认区域传入任务的 params。
/api/v1/assets/{assetId}/versions/{versionId}/watermark-marks路径、查询与请求头
| 参数 | 位置 | 类型与约束 |
|---|---|---|
assetId必填 | path | string素材 ID |
versionId必填 | path | string版本 ID |
请求体字段 · application/json
objectsobject[]可选各个水印的名字(整体替换);不传 = 清空
maxItems: 8
展开字段 / 类型
namestring必填水印名称
minLength: 1 · maxLength: 24
objectIdinteger · int64必填minimum: 0 · maximum: 7
regionsobject[]必填该版本上的全部标记(整体替换);空数组 = 清空
maxItems: 64
展开字段 / 类型
colorstring可选纯色水印的颜色(#rrggbb,如白字 #ffffff);不填表示不按颜色收紧遮罩
pattern: "^#[0-9a-fA-F]{6}$"
endMsinteger · int64可选作用区间的结束时间(毫秒,含),须带 timeMs 且不早于它;不填表示只作用于 timeMs 这一帧。positive 带 endMs 表示确认这一段要修复
minimum: 0
heightnumber · double必填高度(占画面高度的比例)
minimum: 0 · maximum: 1
kind"positive" | "negative" | "absent" | "suggested"可选标记类型:positive(默认)这里有水印;negative 减选,这里不是水印;absent 这一段没有这个水印(须带 objectId);suggested 自动查找的建议,用户确认(改成 positive)前不参与识别
objectIdinteger · int64可选水印编号(前端同一种颜色):同一编号在不同时间点的标记是同一个水印,各自向前后跟踪到彼此相遇;不填表示每处标记各是一个水印
minimum: 0 · maximum: 7
strokesobject[]可选涂抹的画笔轨迹;为空表示框选(整个框都是水印)
maxItems: 64
展开字段 / 类型
pointsnumber · double[][]必填轨迹点 [x, y](相对画面)
minItems: 1 · maxItems: 4000
展开字段 / 类型
number · doubleradiusnumber · double必填画笔半径(占画面宽度的比例)
minimum: 0 · maximum: 0.25
timeMsinteger · int64可选标记时的视频时间点(毫秒);不填表示整段视频固定位置
minimum: 0
widthnumber · double必填宽度(占画面宽度的比例)
minimum: 0 · maximum: 1
xnumber · double必填左边缘(占画面宽度的比例)
minimum: 0 · maximum: 1
ynumber · double必填上边缘(占画面高度的比例)
minimum: 0 · maximum: 1
调用示例
curl --include --fail-with-body --silent --show-error --max-time 60 \ --request PUT 'https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks' \ --header "Authorization: Bearer $GLACIER_API_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}'// Node.js 22+; save as example.mjs, then: node example.mjsconst key = process.env.GLACIER_API_KEY;if (!key) throw new Error('Set GLACIER_API_KEY');const response = await fetch('https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks', { method: 'PUT', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}), signal: AbortSignal.timeout(60_000),});const text = await response.text();if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);console.log(response.status, text);# Python 3.10+; standard library; python example.pyimport osimport urllib.requestimport urllib.error
headers = {"Authorization": "Bearer " + os.environ["GLACIER_API_KEY"]}headers["Content-Type"] = "application/json"request = urllib.request.Request( "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks", method="PUT", headers=headers, data="""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""".encode("utf-8"),)try: with urllib.request.urlopen(request, timeout=60) as response: print(response.status, response.read().decode("utf-8"))except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from error// Go 1.22+; go run example.gopackage main
import ( "fmt" "io" "net/http" "os" "strings" "time")
func main() { key := os.Getenv("GLACIER_API_KEY") if key == "" { panic("Set GLACIER_API_KEY") } req, err := http.NewRequest("PUT", "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks", strings.NewReader(`{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}`)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer " + key) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 60 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { panic(err) } if res.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, data)) } fmt.Println(res.StatusCode, string(data))}// cargo add reqwest@0.13 --features blocking// Save in src/main.rs, then: cargo runuse std::{env, error::Error, time::Duration};
fn main() -> Result<(), Box<dyn Error>> { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(60)).build()?; let response = client.request(reqwest::Method::PUT, "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks") .bearer_auth(env::var("GLACIER_API_KEY")?) .header("Content-Type", "application/json") .body(r#"{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}"#) .send()?; let status = response.status(); let text = response.text()?; if !status.is_success() { return Err(format!("HTTP {status}: {text}").into()); } println!("{status} {text}"); Ok(())}// Java 17+; save as Example.java, then: java Example.javaimport java.net.URI;import java.net.http.*;import java.time.Duration;import java.util.Objects;
class Example { public static void main(String[] args) throws Exception { var key = Objects.requireNonNull(System.getenv("GLACIER_API_KEY")); var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var request = HttpRequest.newBuilder(URI.create("https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString("""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""")) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body()); System.out.println(response.statusCode() + " " + response.body()); }}// .NET 8+; dotnet new console; replace Program.cs; dotnet runusing System;using System.Net.Http;using System.Net.Http.Headers;using System.Text;
var key = Environment.GetEnvironmentVariable("GLACIER_API_KEY") ?? throw new InvalidOperationException("Set GLACIER_API_KEY");using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };using var request = new HttpRequestMessage(new HttpMethod("PUT"), "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks");request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);request.Content = new StringContent("""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""", Encoding.UTF8, "application/json");using var response = await client.SendAsync(request);var text = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {text}");Console.WriteLine($"{(int)response.StatusCode} {text}");响应字段 · 204
成功后无响应体。不要解析为空 JSON。
获取详细分析报告
“获取详细分析报告”章节使用识别任务 ID,或结果里的 watermarkAnalysisUrl 查询。它是需要 API Key 的 JSON 接口,不是签名文件下载地址。报告包含水印对象、时间区间和需要复核的区域,建议结合遮罩预览确认后再去水印。
/api/v1/tasks/{taskId}/watermark-analysis路径、查询与请求头
| 参数 | 位置 | 类型与约束 |
|---|---|---|
taskId必填 | path | string任务 ID |
调用示例
curl --include --fail-with-body --silent --show-error --max-time 60 \ --request GET 'https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis' \ --header "Authorization: Bearer $GLACIER_API_KEY"// Node.js 22+; save as example.mjs, then: node example.mjsconst key = process.env.GLACIER_API_KEY;if (!key) throw new Error('Set GLACIER_API_KEY');const response = await fetch('https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis', { method: 'GET', headers: { Authorization: `Bearer ${key}`, }, signal: AbortSignal.timeout(60_000),});const text = await response.text();if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);console.log(response.status, text);# Python 3.10+; standard library; python example.pyimport osimport urllib.requestimport urllib.error
headers = {"Authorization": "Bearer " + os.environ["GLACIER_API_KEY"]}request = urllib.request.Request( "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis", method="GET", headers=headers,)try: with urllib.request.urlopen(request, timeout=60) as response: print(response.status, response.read().decode("utf-8"))except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from error// Go 1.22+; go run example.gopackage main
import ( "fmt" "io" "net/http" "os" "time")
func main() { key := os.Getenv("GLACIER_API_KEY") if key == "" { panic("Set GLACIER_API_KEY") } req, err := http.NewRequest("GET", "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer " + key) client := &http.Client{Timeout: 60 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { panic(err) } if res.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, data)) } fmt.Println(res.StatusCode, string(data))}// cargo add reqwest@0.13 --features blocking// Save in src/main.rs, then: cargo runuse std::{env, error::Error, time::Duration};
fn main() -> Result<(), Box<dyn Error>> { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(60)).build()?; let response = client.request(reqwest::Method::GET, "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis") .bearer_auth(env::var("GLACIER_API_KEY")?) .send()?; let status = response.status(); let text = response.text()?; if !status.is_success() { return Err(format!("HTTP {status}: {text}").into()); } println!("{status} {text}"); Ok(())}// Java 17+; save as Example.java, then: java Example.javaimport java.net.URI;import java.net.http.*;import java.time.Duration;import java.util.Objects;
class Example { public static void main(String[] args) throws Exception { var key = Objects.requireNonNull(System.getenv("GLACIER_API_KEY")); var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var request = HttpRequest.newBuilder(URI.create("https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + key) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body()); System.out.println(response.statusCode() + " " + response.body()); }}// .NET 8+; dotnet new console; replace Program.cs; dotnet runusing System;using System.Net.Http;using System.Net.Http.Headers;using System.Text;
var key = Environment.GetEnvironmentVariable("GLACIER_API_KEY") ?? throw new InvalidOperationException("Set GLACIER_API_KEY");using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis");request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);using var response = await client.SendAsync(request);var text = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {text}");Console.WriteLine($"{(int)response.StatusCode} {text}");响应字段 · 200
detectionVersionstring必填识别算法版本(如 wm-mask@9)
framesinteger · int64必填heightinteger · int64必填marksHashstring必填标记的规范化哈希
objectsobject[]必填每个水印(同一水印同时出现在几处时各一项,objectId 相同)
展开字段 / 类型
colorobject必填自动估计的水印颜色;没有稳定的纯色证据为 null
展开字段 / 类型
alphanumber · double必填不透明度中位数(启发式)
rgbinteger · int64[]必填minItems: 3 · maxItems: 3
展开字段 / 类型
integer · int64samplesinteger · int64必填
intervalsobject[]必填有候选的时间段(没有候选的时间不在其中)
展开字段 / 类型
endFrameinteger · int64必填endMsinteger · int64必填reasonsstring[]必填原因码(前端按码显示说明),如 match、bg_clutter、score_between、template_ambiguous(分数够但模板在背景上也能到这么高)、arb_rejected(这一帧没有可靠位置)、pose_inferred(位姿插值 / 延伸的推断帧)、handoff_conflict、bridged、auto_color
展开字段 / 类型
stringsourcestring必填决定来源:observed / pose / bridge / manual / user-absent;空串 = 没有选中的候选
startFrameinteger · int64必填startMsinteger · int64必填state"present" | "bridge" | "manual" | "uncertain" | "absent"必填
namestring必填内部名称(#0、#0.1)
objectIdinteger | null · int64必填水印编号;旧标记没有编号为 null
poseobject[]必填位姿检测的每次尝试
展开字段 / 类型
detailstring必填okboolean必填reasonstring必填失败原因码:too_few_anchors / align_failed / no_separation / empty
sourcestring必填
shapeobject必填展开字段 / 类型
qualitystring必填refined / user / box / keyframe / fallback / sibling(碎片,用另一关键帧的形状)/ invalid(形状无效,不执行)/ stroke(涂抹按笔画收紧)
reasonstring必填自动收紧失败的原因
policystring必填不确定策略:skip / legacy / bridge
ratenumber · double必填帧率(帧号与毫秒的换算)
reviewobject[]必填需要复核的区间(按时间排序)
展开字段 / 类型
boxobject必填检测到的位置(相对画面):audit_event 有,其余为 null
展开字段 / 类型
heightnumber · double必填minimum: 0 · maximum: 1
widthnumber · double必填minimum: 0 · maximum: 1
xnumber · double必填minimum: 0 · maximum: 1
ynumber · double必填minimum: 0 · maximum: 1
endFrameinteger · int64必填endMsinteger · int64必填namestring必填objectIdinteger | null · int64必填reasonstring必填handoff_conflict(几个来源的位置冲突)/ bridged(短空档补全)/ long_uncertain(较长时间不确定)/ position_jump(位置跳变)/ appear / disappear / pose_failed / pose_unconfirmed(只有位姿检测、接不上可信证据,未处理)/ layout_conflict(与一起移动的另一个水印在其关键帧上的位置对不上,未处理)/ shape_invalid(关键帧上只认出水印的碎片,未处理)/ blurred_unseen(画面太糊、看不见水印,未处理)/ layout_unverified(按一起移动的另一个水印推算的位置没能确认,未处理)/ audit_event(首尾、跳变处在没处理的位置检测到短暂出现的水印,未处理,见 box)/ residue(去除后疑似残留)
scorenumber | null · double必填检测分数(启发式的 z 分数,不是置信度):audit_event 有,其余为 null
startFrameinteger · int64必填startMsinteger · int64必填
staticobject[]必填不带时间的标记(整段固定处理)的说明
展开字段 / 类型
namestring必填objectIdinteger | null · int64必填reasonstring必填shapestring必填
versioninteger · int64必填widthinteger · int64必填