feat(rag): 多媒体知识库接回——qwen3-vl在线(2560维)+CLIP保留给GPU;VDB地址读upapp;在线重排;配置界面4引擎

This commit is contained in:
ymq 2026-09-01 15:47:21 +08:00
parent 576f0b6368
commit 03dbf0f2b5
7 changed files with 334 additions and 45 deletions

View File

@ -41,10 +41,11 @@ async def kb_list_handler(request, params_kw, *args, **kwargs):
size_str = f"{total_size/1024:.0f}KB"
else:
size_str = f"{total_size}B"
engine = {'bge-m3': '文本 bge-m3', 'clip-vith14': '多媒体 CLIP', 'CLIP ViT-H-14': '多媒体 CLIP'}.get((r.embedding_engine or '').strip(), (r.embedding_engine or 'CLIP'))
engine = {'bge-m3': '文本·在线', 'qwen3-vl-embedding': '多媒体·在线', 'clip-vith14': '多媒体·GPU CLIP', 'CLIP ViT-H-14': '多媒体·GPU CLIP'}.get((r.embedding_engine or '').strip(), (r.embedding_engine or '未配置'))
dim = 2560 if (r.embedding_engine or '').strip() == 'qwen3-vl-embedding' else 1024
card = {"widgettype":"VBox","options":{"cwidth":16,"cheight":12,"bgcolor":"#f0f7ff","padding":"16px","css":"card clickable","border":"1px solid #d0e4f7"},"subwidgets":[
{"widgettype":"Text","options":{"text":"📚 " + str(r.name),"cfontsize":16,"fontWeight":"bold"}},
{"widgettype":"Text","options":{"text":str(engine)+" · 1024","cfontsize":12,"color":"#888"}},
{"widgettype":"Text","options":{"text":str(engine)+f" · {dim}","cfontsize":12,"color":"#888"}},
{"widgettype":"HBox","options":{"spacing":"12px"},"subwidgets":[
{"widgettype":"Text","options":{"text":"📄 "+str(doc_count)+"文档","cfontsize":12,"color":"#666"}},
{"widgettype":"Text","options":{"text":"💾 "+size_str,"cfontsize":12,"color":"#666"}}]}],
@ -992,7 +993,7 @@ async def engine_cfg_get_handler(request, params_kw, *args, **kwargs):
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT engine_type, model_name, endpoint_url, api_key, status FROM rag_engine_configs "
"WHERE engine_type IN ('embedding','rerank') ORDER BY engine_type", {})
"WHERE engine_type IN ('embedding','rerank','mm_embedding','mm_rerank') ORDER BY engine_type", {})
rows = []
for r in recs:
enc = getattr(r, "api_key", "") or ""
@ -1010,8 +1011,8 @@ async def engine_cfg_save_handler(request, params_kw, *args, **kwargs):
env = request._run_ns
try:
engine_type = (params_kw.get("engine_type") or "").strip()
if engine_type not in ("embedding", "rerank"):
return json.dumps({"error": "engine_type must be embedding/rerank"})
if engine_type not in ("embedding", "rerank", "mm_embedding", "mm_rerank"):
return json.dumps({"error": "engine_type must be embedding/rerank/mm_embedding/mm_rerank"})
model_id = (params_kw.get("model_id") or "").strip()
api_base = (params_kw.get("api_base") or "").strip()
api_key_plain = (params_kw.get("api_key") or "").strip()
@ -1071,6 +1072,33 @@ async def engine_cfg_test_handler(request, params_kw, *args, **kwargs):
import aiohttp
headers = {"Authorization": "Bearer " + api_key_plain, "Content-Type": "application/json"}
base = api_base.rstrip("/")
# 在线多模态DashScope 原生 API
if engine_type == "mm_embedding":
if "/api/v1" not in base:
base = base + "/api/v1"
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as s:
async with s.post(base + "/services/embeddings/multimodal-embedding/multimodal-embedding",
json={"model": model_id, "input": {"contents": [{"text": "连通性测试"}]}},
headers=headers) as resp:
data = await resp.json()
embs = (data.get("output") or {}).get("embeddings") or []
dim = len(embs[0]["embedding"]) if embs and embs[0].get("embedding") else 0
if not dim:
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
return json.dumps({"status": "SUCCEEDED", "message": f"mm_embedding OK维度 {dim}"}, ensure_ascii=False)
if engine_type == "mm_rerank":
if "/api/v1" not in base:
base = base + "/api/v1"
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as s:
async with s.post(base + "/services/rerank/text-rerank/text-rerank",
json={"model": model_id,
"input": {"query": "测试", "documents": ["", ""]},
"parameters": {"top_n": 2}},
headers=headers) as resp:
data = await resp.json()
if not (data.get("output") or {}).get("results"):
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
return json.dumps({"status": "SUCCEEDED", "message": "mm_rerank OK"}, ensure_ascii=False)
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as s:
if engine_type == "embedding":
async with s.post(base + "/embeddings", json={"model": model_id, "input": ["连通性测试"]},

153
rag/vl_online.py Normal file
View File

@ -0,0 +1,153 @@
# -*- coding:utf-8 -*-
"""在线多模态引擎qwen3-vl-embedding / qwen3-vl-rerank阿里 DashScope 原生 API
CLIPGPU 本地并存kb.embedding_engine=qwen3-vl-embedding 走在线
clip-vith14 仍走 GPU /mme 端点 GPU 的环境保留本地部署
配置来自 rag_engine_configsengine_type=mm_embedding / mm_rerank=能力屏蔽
"""
import json
from traceback import format_exc
from ahserver.serverenv import ServerEnv
from appPublic.log import exception
from sqlor.dbpools import get_sor_context
NATIVE_EMB_PATH = "/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding"
NATIVE_RR_PATH = "/api/v1/services/rerank/text-rerank/text-rerank"
DEFAULT_MM_DIM = 2560
TEXT_DIM = 1024
async def get_mm_cfg(engine_type="mm_embedding"):
"""读引擎配置,返回 {model_id, api_base, api_key 明文, dim};未配置/空 → None能力屏蔽"""
from appPublic.rc4 import unpassword
env = ServerEnv()
try:
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT model_name, endpoint_url, api_key, config_json FROM rag_engine_configs "
"WHERE engine_type=${t}$ AND status='active' ORDER BY is_default DESC, priority DESC LIMIT 1",
{"t": engine_type})
if not recs:
return None
r = recs[0]
model_id = (getattr(r, "model_name", "") or "").strip()
api_base = (getattr(r, "endpoint_url", "") or "").strip()
enc = (getattr(r, "api_key", "") or "").strip()
if not (model_id and api_base and enc):
return None
try:
from appPublic.jsonConfig import getConfig
key = getConfig().password_key or 'QRIVSRHrthhwyjy176556332'
api_key = unpassword(enc, key)
except Exception:
api_key = enc
dim = DEFAULT_MM_DIM
try:
cj = json.loads(getattr(r, "config_json", "") or "{}")
dim = int(cj.get("dim", DEFAULT_MM_DIM))
except Exception:
pass
return {"model_id": model_id, "api_base": api_base.rstrip("/"), "api_key": api_key, "dim": dim}
except Exception as e:
exception(f"mm cfg read failed ({engine_type}): {e}")
return None
async def _post_json(url, payload, headers, timeout=60):
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as s:
async with s.post(url, json=payload, headers=headers) as resp:
return await resp.json()
async def vl_embed_contents(contents, engine_type="mm_embedding"):
"""单条多模态向量化。contents 例:[{"text": "..."}] 或 [{"image": b64dataurl}] 或图文混合。"""
cfg = await get_mm_cfg(engine_type)
if not cfg:
return None
url = cfg["api_base"] + NATIVE_EMB_PATH
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
try:
data = await _post_json(url, {"model": cfg["model_id"], "input": {"contents": contents}}, headers)
embs = (data.get("output") or {}).get("embeddings") or []
return embs[0]["embedding"] if embs and embs[0].get("embedding") else None
except Exception as e:
exception(f"vl embed failed: {e}, {format_exc()}")
return None
async def vl_embed_texts(texts, engine_type="mm_embedding"):
"""批量纯文本向量化(走 vl 原生 APIcontents=[{"text":t}])。"""
cfg = await get_mm_cfg(engine_type)
if not cfg:
return []
url = cfg["api_base"] + NATIVE_EMB_PATH
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
out = []
for t in texts:
try:
data = await _post_json(url, {"model": cfg["model_id"], "input": {"contents": [{"text": t}]}}, headers)
embs = (data.get("output") or {}).get("embeddings") or []
out.append(embs[0]["embedding"] if embs and embs[0].get("embedding") else None)
except Exception as e:
exception(f"vl embed text failed: {e}")
out.append(None)
return out
async def vl_embed_image_bytes(image_bytes, engine_type="mm_embedding"):
"""图片 bytes → base64 data url → 在线向量化。"""
import base64
b64 = base64.b64encode(image_bytes).decode()
return await vl_embed_contents([{"image": "data:image/jpeg;base64," + b64}], engine_type)
async def vl_rerank(query, documents):
"""在线多模态重排。query: 文本 strdocuments: list[str] 或 [{\"image\": ...}]。
未配置/失败返回 None调用方回退召回分排序"""
cfg = await get_mm_cfg("mm_rerank")
if not cfg:
return None
base = cfg["api_base"]
if "compatible" in base:
base = base.replace("/compatible-mode", "").replace("/compatible-api", "")
if "/api/v1" not in base:
base = base + "/api/v1"
url = base + NATIVE_RR_PATH.replace("/api/v1", "")
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
payload = {"model": cfg["model_id"],
"input": {"query": query, "documents": documents},
"parameters": {"top_n": len(documents), "return_documents": False}}
try:
data = await _post_json(url, payload, headers, timeout=30)
results = (data.get("output") or {}).get("results") or []
if not results:
return None
scores = [0.0] * len(documents)
for it in results:
idx = it.get("index")
if isinstance(idx, int) and 0 <= idx < len(scores):
scores[idx] = it.get("relevance_score", 0)
return {"scores": scores}
except Exception as e:
exception(f"vl rerank failed: {e}")
return None
async def vdb_baseurl():
"""VDB 服务地址:读 upapp.rag-vdb生产已切内网 9186不再硬编码域名。"""
env = ServerEnv()
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
if not recs:
raise Exception("upapp rag-vdb not configured")
return (recs[0].baseurl or "").rstrip("/")
async def vdb_call(apiname, payload, timeout=20):
"""直调 VDB HTTP 接口createcollection/upsertURL 来自 upapp。"""
import aiohttp
base = await vdb_baseurl()
url = f"{base}/v1/{apiname}"
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as s:
async with s.post(url, json=payload, headers={"Content-Type": "application/json"}) as resp:
return await resp.json()

View File

@ -14,6 +14,8 @@ async def get_cfg(t):
emb = await get_cfg("embedding")
rr = await get_cfg("rerank")
mm_emb = await get_cfg("mm_embedding")
mm_rr = await get_cfg("mm_rerank")
def card(title, desc, prefix, cfg):
shielded = (not cfg["model_id"]) or cfg["status"] != "active"
@ -56,7 +58,9 @@ def card(title, desc, prefix, cfg):
}
result = {"widgettype": "VBox", "options": {"width": "100%", "spacing": "16px"}, "subwidgets": [
card("🔤 文本向量化Embedding", "文本知识库必需。模型留空或停用 = 屏蔽(文档仍入库可浏览,检索不可用)。", "embedding", emb),
card("🎯 重排序Rerank", "检索结果精排,可选。未配置时按召回分排序。", "rerank", rr)
card("🔤 文本向量化Embedding", "文本知识库(bge-m3引擎)必需。模型留空或停用 = 屏蔽(文档仍入库可浏览,检索不可用)。", "embedding", emb),
card("🎯 文本重排Rerank", "文本检索结果精排,可选。未配置时按召回分排序。", "rerank", rr),
card("🖼️ 多模态向量化qwen3-vl", "多媒体知识库必需(图文跨模态)。支持以文搜图/以图搜文。留空=屏蔽多媒体检索。", "mm_embedding", mm_emb),
card("🖼️ 多模态重排qwen3-vl-rerank", "多媒体/混合检索精排,可选。未配置时按召回分排序。", "mm_rerank", mm_rr)
]}
return json.dumps(result, ensure_ascii=False)

View File

@ -7,10 +7,10 @@ async with db.sqlorContext(dbname) as sor:
kb_id = uuid()
name = ns.get("name", "")
desc = ns.get("description", "")
# 向量引擎:clip-vith14=多媒体(CLIP)bge-m3=文本(bge-m3)。默认 clip-vith14 兼容旧数据
# 向量引擎:bge-m3=文本(在线) / qwen3-vl-embedding=多媒体(在线) / clip-vith14=多媒体(GPU本地CLIP)
emb_type = ns.get("embedding_type", "") or ns.get("embedding_engine", "")
if emb_type not in ("bge-m3", "clip-vith14"):
emb_type = "clip-vith14"
if emb_type not in ("bge-m3", "clip-vith14", "qwen3-vl-embedding"):
emb_type = "bge-m3"
await sor.sqlExe(
"INSERT INTO rag_knowledge_bases (id, name, description, org_id, embedding_engine, vdb_collection, doc_count, total_size, chunk_count, status, created_at) "
"VALUES (${id}$, ${name}$, ${desc}$, ${org_id}$, ${emb}$, 'rag_collection', 0, 0, 0, 'active', NOW())",

View File

@ -1,5 +1,6 @@
import json
return json.dumps([
{"value": "clip-vith14", "text": "多媒体(图文音视)"},
{"value": "bge-m3", "text": "文本(文档检索)"}
{"value": "bge-m3", "text": "文本(在线·文档检索)"},
{"value": "qwen3-vl-embedding", "text": "多媒体(在线·图文跨模态)"},
{"value": "clip-vith14", "text": "多媒体GPU本地CLIP·需GPU"}
], ensure_ascii=False)

View File

@ -75,7 +75,7 @@ hits = []
raw_rows = []
kw_rows = []
# 读知识库向量引擎,决定用文本(bge-m3 /txte)还是多媒体(CLIP /mme) embedding
# 读知识库向量引擎bge-m3(在线文本) / qwen3-vl-embedding(在线多模态) / clip-vith14(GPU本地)
emb_engine = 'clip-vith14'
try:
async with get_sor_context(env, 'rag') as sor:
@ -83,6 +83,7 @@ try:
if krecs:
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
except: pass
is_vl = emb_engine == 'qwen3-vl-embedding'
if emb_engine == 'bge-m3':
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
emb_model = 'bge-m3'
@ -90,25 +91,47 @@ else:
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
emb_model = 'CLIP-ViT-H-14'
# VDB 服务地址:读 upapp.rag-vdb生产已切内网不硬编码
try:
async with get_sor_context(env, 'rag') as sor:
_u = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
VDB_BASE = (_u[0].baseurl or '').rstrip('/') if _u else 'https://vectordb.opencomputing.net:10443'
except:
VDB_BASE = 'https://vectordb.opencomputing.net:10443'
if query:
# 1. Embed
# 1. Embed在线vl原生 / bge-m3兼容GPUCLIP /mme
vec = []
try:
client = StreamHttpClient()
resp = await client.request('POST', emb_url,
json={"texts": [query], "model": emb_model})
emb = json.loads(resp)
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
except:
pass
if is_vl:
try:
from rag.vl_online import vl_embed_contents
vec = await vl_embed_contents([{"text": query}]) or []
except:
vec = []
elif emb_engine == 'bge-m3':
try:
from rag.init import _online_embed
_vs = await _online_embed(env, [query])
vec = _vs[0] if _vs else []
except:
vec = []
else:
try:
client = StreamHttpClient()
resp = await client.request('POST', emb_url,
json={"texts": [query], "model": emb_model})
emb = json.loads(resp)
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
except:
pass
if not vec:
return json.dumps({"widgettype": "Text", "options": {"text": "向量化失败", "cfontsize": 14, "color": "#e74c3c"}}, ensure_ascii=False)
return json.dumps({"widgettype": "Text", "options": {"text": "向量化失败(引擎未配置或不可用,请到引擎配置页检查)", "cfontsize": 14, "color": "#e74c3c"}}, ensure_ascii=False)
# 2. VDB search
try:
recall_n = top_k * 3
client2 = StreamHttpClient()
resp2 = await client2.request('POST', 'https://vectordb.opencomputing.net:10443/v1/query',
resp2 = await client2.request('POST', VDB_BASE + '/v1/query',
json={"colname": kb_id, "vector": vec, "pagerows": recall_n, "output_fields": ["*"]})
vdb = json.loads(resp2)
raw_rows = vdb.get("data", {}).get("rows", [])
@ -172,7 +195,23 @@ if query:
hits.append(kr)
seen.add(kr["id"])
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
# 在线重排mm_rerank未配置/失败则保持召回分排序)
rr_applied = False
try:
from rag.vl_online import vl_rerank
cand = hits[:top_k * 3]
if cand:
rr = await vl_rerank(query, [h.get("text", "") for h in cand])
if rr and isinstance(rr.get("scores"), list) and len(rr["scores"]) == len(cand):
for i, s in enumerate(rr["scores"]):
cand[i]["score"] = s
cand.sort(key=lambda x: x.get("score", 0), reverse=True)
rr_applied = True
except Exception as e:
info('[search_result] rerank failed: %s' % e)
if not rr_applied:
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
hits = hits[:top_k]
else:

View File

@ -51,7 +51,10 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
# 后台任务无 request/env用注入的全局函数解析禁硬编码库名
_dbname = get_module_dbname('rag')
# 读知识库向量引擎bge-m3=文本(走 /txte)clip-vith14=多媒体(走 /mme)
# 读知识库向量引擎:
# bge-m3 → 在线文本(阿里, 1024维)
# qwen3-vl-embedding → 在线多模态(阿里, 2560维)
# clip-vith14 → GPU 本地 CLIP(1024维, 有GPU环境保留)
emb_engine = 'clip-vith14'
try:
async with db.sqlorContext(_dbname) as sor:
@ -60,6 +63,17 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
except: pass
is_text = emb_engine == 'bge-m3'
is_vl = emb_engine == 'qwen3-vl-embedding'
vdb_dim = 2560 if is_vl else 1024
# VDB 服务地址:读 upapp.rag-vdb生产已切内网不硬编码
try:
async with db.sqlorContext(_dbname) as sor:
_u = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
VDB_BASE = (_u[0].baseurl or '').rstrip('/') if _u else 'https://vectordb.opencomputing.net:10443'
except:
VDB_BASE = 'https://vectordb.opencomputing.net:10443'
if is_text:
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
emb_model = 'bge-m3'
@ -67,9 +81,49 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
emb_model = 'CLIP-ViT-H-14'
# 在线引擎:配置读取 + key 解密mm_embedding / mm_rerank
_vl_cfg = None
if is_vl or not is_text:
try:
from rag.vl_online import get_mm_cfg
_vl_cfg = await get_mm_cfg('mm_embedding')
except:
_vl_cfg = None
async def _online_text_embed(texts):
"""文本向量化vl 引擎走在线原生APIbge-m3 走在线兼容API经 engine_configs"""
if is_vl and _vl_cfg:
from rag.vl_online import vl_embed_texts
return await vl_embed_texts(texts)
try:
from rag.init import _online_embed
from ahserver.serverenv import ServerEnv
return await _online_embed(ServerEnv(), texts)
except:
return []
async def _media_embed_image(img_b64):
"""图片向量化vl 在线(base64) 或 GPU CLIP"""
if is_vl and _vl_cfg:
try:
from rag.vl_online import vl_embed_contents
v = await vl_embed_contents([{"image": "data:image/jpeg;base64," + img_b64}])
return v
except:
return None
try:
client = StreamHttpClient()
resp = await client.request('POST', emb_url,
json={"images": [img_b64], "model": emb_model})
emb_data = json.loads(resp)
es = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
return es[0] if es else None
except:
return None
async def ensure_vdb_collection(client, kb_id):
payload = {"colname": kb_id, "fields": [{"name": "id", "type": "str", "is_primary": True, "max_length": 64}, {"name": "vector", "type": "fvector", "dim": 1024}, {"name": "text", "type": "str", "max_length": 65535}], "description": "RAG kb", "metric": "COSINE"}
await client.request('POST', 'https://vectordb.opencomputing.net:10443/v1/createcollection', json=payload)
payload = {"colname": kb_id, "fields": [{"name": "id", "type": "str", "is_primary": True, "max_length": 64}, {"name": "vector", "type": "fvector", "dim": vdb_dim}, {"name": "text", "type": "str", "max_length": 65535}], "description": "RAG kb", "metric": "COSINE"}
await client.request('POST', VDB_BASE + '/v1/createcollection', json=payload)
text = ''
chunks_n = 0
@ -126,7 +180,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
except: pass
return
# --- IMAGE: face detection ---
# --- IMAGE: face detection + 图片向量化入库 ---
if ext_l in image_exts:
img_b64 = base64.b64encode(file_data).decode()
try:
@ -141,6 +195,24 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
meta_parts['face_bboxes'] = [f.get("bbox", {}) for f in faces[:10]]
meta_parts['face'] = face_count
except: pass
# 图片本身向量化vl在线 或 GPU CLIP存入 VDB 供以文搜图
try:
img_vec = await _media_embed_image(img_b64)
if img_vec:
client2 = StreamHttpClient()
await ensure_vdb_collection(client2, kb_id)
vdb_data = {"colname": kb_id, "data": [
{"id": doc_id + "_c0", "vector": img_vec, "text": file_name}]}
await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
async with db.sqlorContext(_dbname) as sor:
await sor.sqlExe(
"INSERT INTO rag_document_chunks (id, doc_id, kb_id, chunk_index, content, vector_id, created_at) "
"VALUES (${id}$, ${doc_id}$, ${kb_id}$, 0, ${content}$, ${vid}$, NOW())",
{"id": doc_id + "_c0", "doc_id": doc_id, "kb_id": kb_id,
"content": file_name, "vid": doc_id + "_c0"})
chunks_n = 1
meta_parts['image'] = 'embedded'
except: pass
# --- AUDIO: voiceprint ---
if ext_l in audio_exts:
@ -176,13 +248,10 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
face_count = len(faces)
frame_bboxes = [f.get("bbox", {}) for f in faces[:10]] if faces else []
except: pass
# --- CLIP image embedding for video frame ---
# --- 视频帧向量化vl在线 或 GPU CLIP ---
try:
client2 = StreamHttpClient()
resp2 = await client2.request('POST', emb_url,
json={"images": [img_b64], "model": emb_model})
emb_data = json.loads(resp2)
img_embeddings = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
frame_vec = await _media_embed_image(img_b64)
img_embeddings = [frame_vec] if frame_vec else []
except:
img_embeddings = []
if img_embeddings:
@ -192,7 +261,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
vdb_data = {"colname": kb_id, "data": [
{"id": doc_id + "_c0", "vector": img_embeddings[0], "text": file_name}
]}
await client3.request('POST', 'https://vectordb.opencomputing.net:10443/v1/upsert', json=vdb_data)
await client3.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
chunk_meta = {"start_time": 0}
if frame_bboxes:
chunk_meta["bboxes"] = frame_bboxes
@ -253,14 +322,9 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
if cur: chunks.append(cur)
if chunks:
try:
client = StreamHttpClient()
resp = await client.request('POST', emb_url,
json={"texts": chunks, "model": emb_model})
emb_data = json.loads(resp)
embeddings = emb_data.get("text_embeddings", emb_data.get("embeddings", []))
except:
embeddings = []
# 在线文本向量化vl引擎→qwen3-vl原生bge-m3→在线兼容GPU CLIP 时代的老路径已移除)
embeddings = await _online_text_embed(chunks)
embeddings = [e for e in embeddings if e] or []
vector_ids = []
if embeddings:
@ -270,7 +334,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
vdb_data = {"colname": kb_id, "data": [
{"id": doc_id + "_c" + str(i), "vector": emb, "text": chunks[i]}
for i, emb in enumerate(embeddings)]}
resp3 = await client2.request('POST', 'https://vectordb.opencomputing.net:10443/v1/upsert', json=vdb_data)
resp3 = await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
if json.loads(resp3).get('status') == 'SUCCEEDED':
vector_ids = [doc_id + "_c" + str(i) for i in range(len(embeddings))]
except: