架构概览
核心组件
| 组件 | 作用 | 说明 |
|---|
| Voyage AI | 图片 Embedding | voyage-multimodal-3.5 模型,512 维向量 |
| LanceDB | 向量数据库 | 存储和搜索相似图片 |
| 内容审核 | 图片+文本审核 | 检测 NSFW/违规内容 |
发布流程
1. 生成 Embedding
使用 Voyage AI 的多模态模型生成图片向量:
export const getImageEmbedding = async (imageId: string) => {
const imageUrl = await getMessageFileUrl(imageId);
const response = await fetch("https://api.voyageai.com/v1/multimodalembeddings", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.VOYAGE_API_KEY}`,
},
body: JSON.stringify({
model: "voyage-multimodal-3.5",
input_type: "document",
inputs: [
{
content: [{ type: "image_url", image_url: imageUrl }],
},
],
output_dimension: 512,
}),
});
const result = await response.json();
return result.data?.[0]?.embedding;
};
2. 相似度检查
在 LanceDB 中检查是否有相似图片(只与当前用户的图片对比):
export async function hasSimilarInLanceDB(
userId: string,
embedding: number[],
threshold: number = 0.25, // cosine distance < 0.25 = 相似度 > 75%
): Promise<boolean> {
const table = await getFeedTable();
const results = await table
.search(embedding)
.where(`user_id = '${userId}'`)
.distanceType("cosine")
.limit(1)
.toArray();
return results.length > 0 && results[0]._distance < threshold;
}
相似度阈值 0.25 表示 cosine distance,对应相似度约 75%。只与同一用户的图片对比,不同用户之间不去重。
3. 内容审核
同时审核图片和文本(prompt):
const [imageModeration, textModeration] = await Promise.all([
moderateImageUrl(newImageId),
moderateText(image.prompt),
]);
const moderationPassed = imageModeration && textModeration;
4. 可见性判断(Flag 系统)
可见性使用 flag 数组决定。flag 为空时 Feed 可见,非空时不可见:
const flag: string[] = [];
if (userBanned) flag.push("ban"); // profile.flag includes "ban"
if (isSimilar) flag.push("duplicate"); // LanceDB vector similarity check
if (!moderationPassed) flag.push("nsfw"); // OpenAI moderation
// Feed is visible when flag array is empty
| Flag 值 | 说明 |
|---|
"ban" | 用户被封禁(profile.flag 包含 “ban”) |
"duplicate" | 与已有图片相似(LanceDB 向量检查) |
"nsfw" | 内容审核不通过(图片或文本) |
5. 存储 Embedding
发布成功后,将 embedding 存入 LanceDB:
if (!error && feedData && embedding) {
await saveFeedEmbedding({
id: feedData.id,
user_id: resourceId,
image_id: image.imageId,
vector: embedding,
created_at: new Date().toISOString(),
});
}
LanceDB 配置
const db = await lancedb.connect(process.env.LANCEDB_URI!, {
apiKey: process.env.LANCEDB_API_KEY,
});
表结构
type FeedEmbedding = {
id: string; // feed ID
user_id: string; // 用户 ID
image_id: string; // 图片 ID
vector: number[]; // 512 维向量
created_at: string;
};
环境变量
LANCEDB_URI=<lancedb-cloud-uri>
LANCEDB_API_KEY=<api-key>
VOYAGE_API_KEY=<voyage-api-key>
Voyage AI 配置
| 参数 | 值 | 说明 |
|---|
| model | voyage-multimodal-3.5 | 多模态 embedding 模型 |
| input_type | document | 输入类型 |
| output_dimension | 512 | 输出向量维度 |
Voyage AI 按 token 计费,每次调用会消耗一定费用。建议在自动发布场景中谨慎使用。
Analytics 事件
发布时记录 PostHog 事件:
posthog.capture({
distinctId: resourceId,
event: "feed_published",
properties: {
feed_id: feedData?.id,
media_type: "image", // 或 "video" / "audio"
image_id: image.imageId,
feed_image_id: newImageId,
flag,
},
});
flag 值
flag 属性为字符串数组,可能包含的值:"ban" | "duplicate" | "nsfw"。空数组表示 Feed 可见。
删除流程
删除 Feed 时同时删除 LanceDB 中的 embedding:
export async function deleteFeedEmbedding(feedId: string): Promise<void> {
const table = await getFeedTable();
await table.delete(`id = '${feedId}'`);
}
完整流程图
除了图片发布(POST /publish/),还有:
- 视频发布端点
POST /publish/video,流程类似但 media_type 为 "video"
- 音频发布端点
POST /publish/audio,流程类似但 media_type 为 "audio",审核包含封面图片 + 歌词/prompt 文本
相关文档