Skip to main content

架构概览

核心组件

组件作用说明
Voyage AI图片 Embeddingvoyage-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 配置

参数说明
modelvoyage-multimodal-3.5多模态 embedding 模型
input_typedocument输入类型
output_dimension512输出向量维度
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 文本

相关文档