本文介紹如何用阿里雲 Milvus 的四個 AI Function 串聯一條「產生 → 檢索」流水線:從分鏡參考圖產生主要畫面格、由主要畫面格產生運動鏡頭、為素材自動產生檢索描述,再把圖片與視頻寫入同一個多模態 Collection,實現以文搜圖、以文搜視頻與以圖搜視頻。
方案概述
AIGC 短劇、豎屏微短劇、品牌短視頻的生產節奏很快:一個團隊一周要產出幾十條甚至上百條成片,每條成片背後又有大量主要畫面格、分鏡與候選素材需要產生和篩選。典型生產鏈路可以拆成四段:
分鏡文案:先寫出一段段分鏡指令碼,例如「雨夜,女主撐傘站在便利店門口,暖色燈光打在臉上」。
主要畫面格生圖:把分鏡文案轉成主要畫面格圖片,作為鏡頭的視覺錨點,也可在已有素材上做背景替換或風格調整。
圖生視頻:把選定的主要畫面格作為首幀,產生一段運動鏡頭。
素材沉澱與檢索複用:為每天產生的圖片與視頻自動產生描述並轉成向量入庫,之後就能以文搜圖、以圖搜視頻、以文搜視頻,讓素材從一次性消耗品變成可複用資產。
如果不藉助統一的向量資料庫平台,自己從零對接各家產生模型再拼一套檢索系統,通常會遇到以下問題:生圖與生視頻模型分散在不同服務、鑒權與返回格式各異;視頻產生是長耗時非同步任務,需要自己管理輪詢、終態判定與逾時兜底;圖片、視頻、描述文本與向量分散在四處,檢索時要跨系統拼裝;跨模態檢索需要把文本、圖片、視頻映射到同一向量空間,缺少語義檢索時只能靠檔案名稱和記憶找素材。
阿里雲 Milvus 用四個 AI Function 把這條鏈路串起來,分為「產生」與「檢索」兩個階段、五個步驟:
Function | 作用 | 本文中的用途 |
| 映像產生與編輯,支援單圖編輯和多參考圖融合。 | 把分鏡參考圖按文案轉成主要畫面格圖片。 |
| 圖生視頻與文生視頻,非同步任務。 | 以主要畫面格為首幀產生運動鏡頭。 |
| 多模態內容理解,為圖片與視頻產生標題、描述、標籤。 | 為素材產生用於檢索的客觀描述。 |
| 寫入即向量化,把文本、圖片、視頻映射到同一向量空間。 | 素材入庫並支援跨模態檢索。 |
這條鏈路的關鍵在三點:產生即入庫,主要畫面格與運動鏡頭連同描述、標籤一起落庫,每個產生物都帶著結構化資訊和向量;同一向量空間,圖片和視頻共用同一個多模態模型與同一向量欄位,才能做以文搜圖、以圖搜視頻這類跨模態檢索;寫入即向量化、查詢即推理,應用側無需自己調用 embedding 模型。
前提條件
已建立 Milvus 2.6 版本執行個體。AI Function 依賴 2.6 版本核心,建立後無需單獨綁定模型服務。
如需從公網訪問執行個體,已在執行個體詳情頁的 安全配置 頁簽開啟 公網訪問 並將用戶端出口 IP 加入公網訪問白名單。
已安裝 pymilvus。本文樣本基於 pymilvus 3.0.0 驗證。
RESTful 介面與 gRPC 共用 19530 連接埠,調用時必須在地址中顯式帶上連接埠,例如 http://c-xxx.milvus.aliyuncs.com:19530;若省略連接埠將預設訪問 80 連接埠並導致連線逾時。
操作步驟
準備公用代碼
以下程式碼封裝含串連配置與 REST 調用工具,供步驟 1~3 共用。請將 MILVUS_ENDPOINT 與 MILVUS_TOKEN 替換為實際執行個體資訊。
from __future__ import annotations
import json
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from pymilvus import DataType, Function, FunctionType, MilvusClient
# ==== 全域配置(REST 與 gRPC 同在 19530 連接埠)====
MILVUS_ENDPOINT = "c-xxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = f"http://{MILVUS_ENDPOINT}"
MILVUS_URI = MILVUS_REST_BASE_URL
def post_json(path: str, body: dict[str, Any], timeout: int = 200) -> tuple[int, dict[str, Any]]:
"""步驟 1~3 共用的 REST 調用工具。"""
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
步驟一:主要畫面格生圖
拿一張分鏡參考圖,按分鏡文案做背景替換或風格調整,產出一張主要畫面格圖片,作為後續圖生視頻的首幀。
項 | 內容 |
Function / 模型 |
|
輸入 | 參考圖 URL + 分鏡文案 |
輸出 | 主要畫面格圖片 URL |
調用方式 | 同步返回 |
# ========== 步驟 1:主要畫面格生圖 AI_IMAGE_EDIT (wan2.7-image) ==========
IMAGE_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
PROMPT = (
"雨夜,一位女子撐著傘站在便利店門口,暖色的店內燈光打在她臉上,"
"電影級構圖,保留畫面主體。"
)
status, data = post_json(
"/v2/vectordb/ai/image_edit",
{
"model_name": "wan2.7-image",
"texts": [IMAGE_URL],
"params": {"prompt": PROMPT, "n": 1, "size": "1024*1024",
"watermark": False, "timeout_sec": 180},
},
)
assert status == 200 and data.get("code") == 0, data
keyframe_url = data["data"]["output"]["outputs"][0]
print(f"主要畫面格圖片 URL:{keyframe_url}")
單圖輸入用 texts 傳參考圖 URL,編輯指令寫在 params.prompt 中。n 表示產生數量,本例為 1。
步驟二:圖生視頻
把主要畫面格作為首幀產生一段運動鏡頭。視頻產生是長耗時非同步任務,需要先建立任務拿到 task_id,再輪詢查詢任務狀態。
項 | 內容 |
Function / 模型 |
|
輸入 | 首幀圖 + 運鏡文案 |
輸出 | 運動鏡頭 |
調用方式 | 非同步: |
# ========== 步驟 2:圖生視頻 AI_VIDEO_EDIT (happyhorse-1.1-i2v),非同步任務 ==========
VIDEO_MODEL = "happyhorse-1.1-i2v" # 建立與查詢必須使用同一模型名
# 用步驟 1 的主要畫面格作首幀(media.type=first_frame)
status, data = post_json(
"/v2/vectordb/ai/video_edit",
{
"model_name": VIDEO_MODEL,
"prompt": "鏡頭緩緩推進,雨絲飄落,暖光在傘面上輕輕閃動,保留首幀圖中的主體。",
"media": [{"type": "first_frame", "url": keyframe_url}],
"params": {"resolution": "720P", "audio_setting": "none",
"watermark": False, "timeout_sec": 180},
},
)
assert status == 200 and data.get("code") == 0, data
task_id = data["data"]["output"]["task_id"]
print(f"任務已建立 task_id = {task_id}")
# 輪詢查詢:tasks/describe 一次只查一個 task_id,帶 provider 與相同 model_name
video_url = None
for attempt in range(60): # 本地輪詢上限,達到上限只記錄 task_id,不判定服務端失敗
status, data = post_json(
"/v2/vectordb/ai/tasks/describe",
{"provider": "aliyun_milvus", "model_name": VIDEO_MODEL, "task_id": task_id},
)
task_status = data.get("data", {}).get("output", {}).get("task_status")
print(f" 輪詢 #{attempt:<2} -> {task_status}")
if task_status == "SUCCEEDED":
video_url = data["data"]["output"]["video_url"]
break
if task_status in ("FAILED", "CANCELED"):
raise RuntimeError(f"task ended in {task_status}: {json.dumps(data, ensure_ascii=False)}")
time.sleep(10)
assert video_url, f"輪詢達到本地上限,請稍後憑 task_id={task_id} 繼續查詢"
print(f"運動鏡頭 video_url:{video_url}")
輪詢返回的 task_status 取值說明:
task_status | 含義 | 處理方式 |
| 任務進行中,尚未完成。這是任務執行期間的正常傳回值。 | 繼續輪詢,不要判定為異常。 |
| 任務成功,可從 | 結束輪詢。 |
| 任務失敗。 | 終態,結束輪詢並排查。 |
| 任務被取消。 | 終態,結束輪詢。 |
實測一次 720P、5 秒的圖生視頻任務約需 2~3 分鐘,期間 task_status 會持續返回 UNKNOWN。tasks/describe 的返回體包含 task_id、task_status、video_url 三個欄位,且必須攜帶與建立時相同的 model_name 和 provider。
params.resolution 指定的是像素量檔位,不是固定的寬高。實際輸出尺寸由首幀圖的寬高比決定:本例首幀為 1024×1024 正方形,設定 720P 後實際輸出為 960×960(像素總量與 1280×720 相當),時間長度約 5 秒。如需 16:9 橫屏輸出,請使用對應寬高比的首幀圖。
步驟三:素材理解,產生描述與標籤
在素材入庫前,用多模態大模型給圖片和視頻各產生一句客觀的檢索描述,作為標題、簡介或標籤來源,隨素材一起在步驟四入庫。
項 | 內容 |
Function / 模型 |
|
輸入 | 圖片或視頻 URL |
輸出 | 一句中文描述,可作為標題、描述或標籤來源 |
# ========== 步驟 3:素材理解 AI_MULTI_MODAL_GENERATE (qwen3.7-plus) ==========
def describe_media(url: str, media_type: str, prompt: str) -> str:
status, data = post_json(
"/v2/vectordb/ai/multi_modal_generate",
{
"model_name": "qwen3.7-plus",
"texts": [url],
"params": {"media_type": media_type, "prompt": prompt, "temperature": 0},
},
)
assert status == 200 and data.get("code") == 0, data
return data["data"]["output"]["outputs"][0]
image_caption = describe_media(
keyframe_url, "image",
"用一句中文客觀描述這張主要畫面格圖片的主體、情境和色調,便於以文搜圖。",
)
video_caption = describe_media(
video_url, "video",
"用一句中文客觀描述這段視頻的主體、動作和畫面氛圍,便於按鏡頭檢索。",
)
print(f"[圖片描述] {image_caption}")
print(f"[視頻描述] {video_caption}")
圖片用 media_type=image,視頻用 media_type=video,prompt 為必填項。實測產生的描述樣本:
[圖片描述] 雨夜街頭,一位穿格子襯衫的女子撐著傘站在便利店門口,身旁坐著一隻金毛犬,
整體色調偏冷藍,燈光溫暖映襯濕漉地面,營造靜謐而略帶憂鬱的氛圍。
[視頻描述] 這段視頻呈現了雨夜街頭,一名身穿格子襯衫的女子撐著透明雨傘與一隻黃狗站在亮燈的
店鋪外,鏡頭由全景逐漸推近至女子面部的特寫,整體畫面色調偏冷且帶有濕潤的反光。描述文案的品質直接決定後續以文搜圖的召回效果,建議在 prompt 中明確要求「客觀描述主體、情境、色調、動作」等檢索時會用到的維度,避免產生帶主觀評價的文案。
步驟四:多模態向量入庫
建一個「寫入即向量化」的多模態素材 Collection,把圖片 URL、視頻 URL 連同描述、標籤寫入,Milvus 會自動為素材產生向量並落庫。
項 | 內容 |
Function / 模型 |
|
關鍵參數 |
|
輸入 | 步驟 1~3 產出的 URL + 描述 + 標籤 |
輸出 | 落庫的 2560 維向量 |
# ========== 步驟 4:建多模態 Collection 並向量入庫 AI_EMBEDDING (qwen3-vl-embedding) ==========
EMBED_MODEL = "qwen3-vl-embedding"
VECTOR_DIM = 2560 # qwen3-vl-embedding 多模態維度,向量欄位維度必須與 dim 一致
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
collection_name = "aigc_assets_multimodal"
if client.has_collection(collection_name):
client.drop_collection(collection_name)
schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("media_type", DataType.VARCHAR, max_length=16) # image / video
schema.add_field("media_ref", DataType.VARCHAR, max_length=4096) # 用於向量化的圖片/視頻 URL
schema.add_field("caption", DataType.VARCHAR, max_length=2048) # 步驟 3 產生的描述
schema.add_field("tags", DataType.VARCHAR, max_length=512)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
# 寫入 media_ref 時自動調用 qwen3-vl-embedding 產生 2560 維向量
schema.add_function(
Function(
name="embed_media",
function_type=FunctionType.TEXTEMBEDDING,
input_field_names=["media_ref"],
output_field_names=["embedding"],
params={
"provider": "aliyun_milvus",
"model_name": EMBED_MODEL,
"dim": VECTOR_DIM,
"is_multimodal": "true",
},
)
)
index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection(collection_name=collection_name, schema=schema,
index_params=index_params)
# 把步驟 1~3 的產出(url + caption + tags)向量化入庫
client.insert(
collection_name,
[
{"media_type": "image", "media_ref": keyframe_url,
"caption": image_caption, "tags": "主要畫面格,產生圖"},
{"media_type": "video", "media_ref": video_url,
"caption": video_caption, "tags": "運動鏡頭,產生視頻"},
],
)
client.flush(collection_name)
client.load_collection(collection_name)
print(f"圖片與視頻已向量化入庫,Collection:{collection_name}")
多模態 Function 必須帶 "is_multimodal": "true",否則在 create_collection 階段即報 multimodal=false ... 400 InvalidParameter url error。向量欄位的 dim 必須與 Function 參數中的 dim 一致(2560)。
寫入後必須調用 flush(),否則緊接著執行檢索可能返回空結果。圖片和視頻要落在同一個向量欄位、使用同一個模型,才能在同一向量空間裡做跨模態檢索。
產生物 URL 是臨時簽名地址,入庫前建議轉存。生圖與生視頻返回的 URL 均帶 OSS 簽名,實測有效期間約 24 小時。這會帶來兩類影響:
情境 | 影響 |
庫中 | 仍能正常召回(向量在寫入時已產生並落庫),但返回的 URL 已無法訪問,業務側展示素材會失敗。 |
用已失效的 URL 作為查詢做以圖搜圖 | 整個 |
因此生產環境中應在入庫前把產生物轉存到自己的 OSS,並把長期有效地址寫入 media_ref;同時保證查詢側傳入的媒體 URL 可訪問。
步驟五:內容搜尋
素材入庫後,用同一個多模態模型把查詢映射到同一向量空間,檢索最相似的素材實現複用。由於 Collection 已綁定 qwen3-vl-embedding 的 Function,search 的 data 裡直接傳原始文本或媒體 URL 即可,Milvus 會自動向量化查詢。
# ========== 步驟 5:內容搜尋 以文搜圖 / 以文搜視頻 / 以圖搜視頻 ==========
def search_assets(query: str, top_k: int = 5, media_type: str | None = None,
min_score: float = 0.0):
"""query 可以是一段中文/英文文案,也可以是一張查詢圖片的 URL。
min_score:相似性閾值,低於該值的結果視為不相關,直接丟棄。"""
filter_expr = f'media_type == "{media_type}"' if media_type else ""
results = client.search(
collection_name=collection_name,
data=[query], # 文本或圖片 URL,均由 qwen3-vl-embedding 向量化
anns_field="embedding",
limit=top_k,
filter=filter_expr,
output_fields=["media_type", "media_ref", "caption", "tags"],
)
for rank, hit in enumerate(results[0], 1):
if hit["distance"] < min_score:
continue
e = hit["entity"]
url = e["media_ref"].split("?")[0] # 去掉 OSS 簽名參數,展示更乾淨
print(f" {rank}. [相似性 {hit['distance']:.4f}] [{e['media_type']}] {e['caption'][:42]}")
print(f" {url}")
# 以文搜圖:限定只搜圖片
print("[5.1] 以文搜圖:")
search_assets("雨夜撐傘的女子站在便利店門口", media_type="image")
# 以文搜視頻:限定只搜視頻
print("[5.2] 以文搜視頻:")
search_assets("雨夜街頭鏡頭緩慢推進的運動鏡頭", media_type="video")
# 以圖搜視頻:用一張主要畫面格圖片 URL 去召迴風格相近的視頻素材
print("[5.3] 以圖搜視頻(用主要畫面格圖召回同源視頻):")
search_assets(keyframe_url, media_type="video")
# 跨模態混合檢索:去掉 filter,在圖片與視頻混合庫中檢索
print("[5.4] 跨模態混合檢索:")
search_assets("雨夜便利店暖光鏡頭")
實測三路檢索的召回結果與相似性:
檢索方式 | 查詢 | 命中 | 相似性 |
以文搜圖 | 雨夜撐傘的女子站在便利店門口 | image | 0.4834 |
以文搜視頻 | 雨夜街頭鏡頭緩慢推進的運動鏡頭 | video | 0.4967 |
以圖搜視頻 | 主要畫面格圖片 URL | video | 0.8715 |
跨模態混合(無 filter) | 雨夜便利店暖光鏡頭 | video / image | 0.4967 / 0.3816 |
其中以圖搜視頻達到 0.8715,是四種方式中最高的——該視頻正是由這張主要畫面格產生的,同源素材在同一向量空間下相似性極高。這也直觀體現了「圖片和視頻共用同一向量空間」的價值:把查詢文案換成一張圖片 URL,同一個 search 調用即可完成以圖搜圖與以圖搜視頻;去掉 filter 則在圖片與視頻混合庫中跨模態檢索。
查詢文案要與素材主題相關,並配合相似性閾值。多模態檢索按相似性排序返回 Top-K,即使查詢與素材完全無關也會返回結果。實測對比:
查詢文案 | 與素材的關係 | 相似性 |
雨夜撐傘的女子站在便利店門口 | 主題匹配 | 0.4834 |
室內暖光下的條紋毛衣造型 | 與素材無關 | 0.0715 |
角色近景,鏡頭緩慢推進的概念片 | 與素材無關 | 0.0694 |
相差近 7 倍。因此應用側應結合相似性閾值過濾(本例 search_assets 的 min_score 參數),把明顯不相關的長尾結果丟棄,避免把 0.07 這類結果當作有效召回展示給使用者。具體閾值需按業務素材分布調參。
方案價值
與自己從零對接各家產生模型、再拼一套檢索系統相比:
維度 | 傳統自建方案 | 阿里雲 Milvus |
產生模型接入 | 文生圖、圖生圖、文生視頻各家 SDK、鑒權與返回格式分散對接 | AI Function 統一封裝生圖、生視頻與內容理解 |
非同步任務管理 | 自建任務隊列、輪詢退避、終態判定與逾時兜底 |
|
素材理解 | 自建或外接標題、描述、標籤服務 |
|
向量化鏈路 | 應用側先調 embedding 再寫入 | 寫入即向量化,由 Collection Function 自動產生 |
檢索入口 | 圖片、視頻、文本、向量四份資料割裂,跨系統拼裝 | 單庫單次 |
這套方案的價值不只是產生快,更在於把每一次產生都沉澱為可檢索、可複用的素材資產,讓內容團隊從一次性消耗走向資產化經營。新劇本出現相似鏡頭需求時,先在歷史素材庫裡語義檢索一遍,能複用的直接複用,不能複用的再走產生,邊際成本持續下降。