Fashion品牌用Shadow管道替代人工逐帧审核,通过面部相似度、品牌合规性、跨帧一致性三项指标自动评分,阈值0.87以下拒绝分发,附TypeScript架构实现。
每小时内向九个社交渠道投放角色驱动的时尚内容,这曾经意味着一件事:一位疲惫的艺术总监眯着眼睛盯着 400 帧画面,心里祈祷不要有什么尴尬的东西漏出去。我们用一套叫 Shadow 的流水线把这种眯眼变成了过去式。它在每条视频分发前对其运行多模态视觉分析,给出面部相似度、品牌合规性和视觉连贯性评分。以下是这套架构、数学原理,以及将它们粘合在一起的 TypeScript 代码。
时尚营销高度依赖 recurring characters:品牌大使、吉祥物角色、反复出镜的艺人。一条 12 秒的 TikTok 可能包含 280 个采样帧。人工验证模特在第 14 帧和第 207 帧之间颧骨轮廓是否发生了漂移,这不是人该干的活。这活应该交给一个被训练过"知道漂移长什么样"的视觉模型。
Shadow 对每次渲染运行三次顺序检查:
每次检查都会产生一个数值分数。分发服务拒绝综合分数低于 0.87 的任何内容。
这套流水线刻意做得很无聊。在生产环境凌晨 06:00 UTC 跑 cron 任务时,无聊是好事。
[Render Farm] -> [S3 Ingest Bucket]
|
v
[Shadow Worker Queue]
| | |
v v v
[FaceNet] [Palette] [Coherence]
| | |
v v v
[Postgres Audit Log] -> [Dispatch Gate]
Workers 是无状态的 Node 22 进程。它们拉取任务,运行三个视觉模块,然后将结果写入 Postgres。分发 gate 是一个 SQL 视图,任何客户端在推送到 Buffer、Hootsuite 或我们直接的 TikTok API 端点之前都会查询它。
我们选择 MiniMax-Text-01 是因为它的多模态 grounding 能力:它将我们的品牌简报作为自然语言读取,并在同一次前向传递中将该文本与像素数据交叉参考。当你的指南文档写着"暖米色,不是冷灰褐色",而一位初级调色师把灯光调高了 400K 时,这就很重要了。
以下是最核心的推理调用:
import { ShadowClient } from "@shadow/vision";
interface FrameJob {
assetId: string;
framePaths: string[];
brandBrief: string;
referenceEmbeddings: number[][];
}
interface AuditResult {
frameIndex: number;
faceSimilarity: number;
paletteDrift: number;
brandScore: number;
composite: number;
}
export async function auditFrames(job: FrameJob): Promise<AuditResult[]> {
const client = new ShadowClient({ region: "eu-west-2" });
const prompt = `
Evaluate this fashion campaign frame.
Brand brief: ${job.brandBrief}
Score 0.00 to 1.00 for:
- face similarity to reference identity
- adherence to stated palette
- prop placement correctness
- typography legibility
`;
const tasks = job.framePaths.map(async (path, idx) => {
const response = await client.analyse({
model: "MiniMax-Text-01",
image: path,
text: prompt,
responseFormat: "json",
});
return {
frameIndex: idx,
faceSimilarity: response.scores.face,
paletteDrift: response.scores.palette,
brandScore: response.scores.brand,
composite: 0,
};
});
const results = await Promise.all(tasks);
for (const r of results) {
r.composite = weightedScore(r);
}
return results;
}
function weightedScore(r: AuditResult): number {
return (
r.faceSimilarity * 0.45 +
(1 - r.paletteDrift) * 0.30 +
r.brandScore * 0.25
);
}
权重不是随意设定的。面部身份权重最高,因为观众会原谅稍微偏暖的帧,但绝不会原谅角色突然看起来像另一个人的情况。
视觉模型返回一个分数,但我们仍然并行运行基于 embedding 的检查。多模态模型可能被打光技巧欺骗。用参考集做 embedding 余弦相似度可以捕捉到语言条件模型漏掉的东西。
import { faceEmbedding, cosineSimilarity } from "@shadow/face";
export async function verifyFaceIdentity(
frame: Buffer,
refs: number[][]
): Promise<{ score: number; closestRef: number }> {
const embedding = await faceEmbedding(frame);
let best = -Infinity;
let closestIdx = -1;
refs.forEach((ref, i) => {
const sim = cosineSimilarity(embedding, ref);
if (sim > best) {
best = sim;
closestIdx = i;
}
});
return { score: best, closestRef: closestIdx };
}
我们对每个 recurring character 存储 12 个参考 embedding。这足以覆盖各种表情(笑脸、中性、侧脸),又不会撑大查询表。
合规是多模态的 MiniMax-Text-01 真正发挥价值的地方。我们把品牌简报 PDF(解析成 markdown)连同帧一起喂给模型。模型将"屏幕右侧不允许出现小于 40px 的 logo"这类文字锚定到视觉证据上。
以下是支撑合规 dashboard 的 SQL:
CREATE TABLE brand_compliance_log (
id BIGSERIAL PRIMARY KEY,
asset_id UUID NOT NULL,
frame_index INTEGER NOT NULL,
palette_hex TEXT[],
brand_score NUMERIC(4,3) NOT NULL,
flagged_reasons TEXT[],
audited_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_compliance_asset ON brand_compliance_log(asset_id);
CREATE INDEX idx_compliance_score ON brand_compliance_log(brand_score)
WHERE brand_score < 0.87;
CREATE MATERIALIZED VIEW mv_dispatch_eligibility AS
SELECT
asset_id,
MIN(brand_score) AS worst_brand,
AVG(brand_score) AS avg_brand,
MIN(face_similarity) AS worst_face,
COUNT(*) FILTER (WHERE brand_score < 0.87) AS fail_count
FROM brand_compliance_log
GROUP BY asset_id;
CREATE UNIQUE INDEX ON mv_dispatch_eligibility(asset_id);
brand_score < 0.87 的部分索引使失败查询保持高效,即使审核表增长到 4000 万行以上。我们使用 pg_cron 每 90 秒刷新一次物化视图。
在任何社交分发之前,publisher 服务会查询 eligibility 视图。如果 fail_count > 0 或 worst_face < 0.82,分发被阻止,该资产退回创意团队,并附带一个结构化的错误 payload。
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.SHADOW_DB });
export async function canDispatch(assetId: string): Promise<boolean> {
const { rows } = await pool.query(
`SELECT worst_brand, worst_face, fail_count
FROM mv_dispatch_eligibility
WHERE asset_id = $1`,
[assetId]
);
if (rows.length === 0) return false;
const r = rows[0];
return r.fail_count === 0 && r.worst_face >= 0.82;
}
0.82 这个面部相似度下限来自于今年三月我们发现的一次回归。我们的主要艺人在不同棚拍设置之间鼻梁轮廓发生了微妙变化。再宽松一点就要发出去了。
第三个检查是大多数人都会忘记的那个。一条视频可以在每一帧都拿到高分,但回放时仍然感觉不对。帧之间的漂移会打破连续性的幻觉。
我们将连贯性计算为整条视频中面部相似度的标准差。稳定的 cast 产生低标准差;变脸的 cast 产生高标准差。
export function coherenceGrade(results: AuditResult[]): number {
if (results.length < 2) return 1.0;
const sims = results.map((r) => r.faceSimilarity);
const mean = sims.reduce((a, b) => a + b, 0) / sims.length;
const variance =
sims.reduce((acc, v) => acc + (v - mean) ** 2, 0) / sims.length;
const stdev = Math.sqrt(variance);
// Lower stdev = more coherent. Map stdev 0.0..0.15 to score 1.0..0.0
return Math.max(0, 1 - stdev / 0.15);
}
连贯性分数低于 0.70 会触发自动重新渲染请求。我们发现 9% 通过逐帧检查的影片仍然在连贯性上 fail。多花的这点计算是值得的。
在 14,200 个已分发资产上运行三个月后:
1.1% 的漏报率是我们密切关注的那个。每周五人工抽查 40 条随机已批准资产。如果该比率攀升超过 2%,我们就收紧面部相似度下限。
上线时踩过的一些坑:
保持参考 embedding 版本化。当艺人换了发型,你希望显式地失效陈旧的参考,而不是意外覆盖。
用 pg_cron,而不是 cron 容器。数据库贴近的调度保持审核日志和 eligibility 视图同步。
持久化模型 prompt。当你在品牌简报 prompt 上做了调整,你需要知道哪些资产是在哪个 prompt 版本下打分的。
先在降采样集上跑连贯性评分。280 帧的完整逐帧连贯性比逐帧检查加起来还贵。除非资产短于 30 秒,否则每 10 帧采样一次。
Shadow 不做创意决策。它捕获的是人类审核员在喝第四杯咖啡后会漏掉的那些回归。多模态视觉分析、基于 embedding 的面部验证和物化合规视图的组合,把一个混乱的手动流程变成了我们可以真正推理的东西。数字在数据库里。阈值在代码里。分发 gate 是一个 SQL 查询。
这就是时尚工程团队能得到的近乎安心的状态了。
Written autonomously via Shadow