mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 10:28:06 +00:00
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、 再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个 致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上 只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。 本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆: 一、描述链彻底删除(无残留、无兼容分支) - media.Item 去掉 Description/DescribedBy 与对应列; - 删除 Store.Describe / Store.Search / Store.Pending; - 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项 core.memory.media.describe_on_ingest; - SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。 二、marker 机制删除,媒体归属改为结构化块边 - 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/ extractMediaDigests/sentenceWithMediaMarkers/docMediaContext; - memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样, 不再被 marker 污染; - 块以 sentence --contains--> block / document --contains--> block 结构边 挂到承载节点(新增 documents 表与 document 节点种类); - 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。 三、旧数据迁移(幂等) - 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest 还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行; - CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子 连同块边一起删掉。 四、向量融合:媒体按图本身被召回 - 新增 vector.FuseVectors(逐维求和 + L2 归一化); - Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合), 新增 Doc.DenseFP,指纹变化触发重算; - ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在 同一统一空间内比稠密余弦; - 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再 直接充当记忆检索结果。 五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建) - internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧 (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、 sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。 验证:go build ./... 、go vet ./...(含 -tags medialive)均通过; 在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/... 全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造 无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
158 lines
4.7 KiB
Go
158 lines
4.7 KiB
Go
//go:build onnxruntime
|
||
|
||
package qwen
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
_ "image/gif"
|
||
_ "image/jpeg"
|
||
_ "image/png"
|
||
"math"
|
||
)
|
||
|
||
const (
|
||
qwenImageSize = 768
|
||
qwenPatchSize = 16
|
||
qwenTemporalPatch = 2
|
||
qwenSpatialMerge = 2
|
||
qwenImagePatches = (qwenImageSize / qwenPatchSize) * (qwenImageSize / qwenPatchSize)
|
||
qwenVisualTokens = qwenImagePatches / (qwenSpatialMerge * qwenSpatialMerge)
|
||
qwenPatchVectorSize = 3 * qwenTemporalPatch * qwenPatchSize * qwenPatchSize
|
||
)
|
||
|
||
// preprocessImage 把任意图片转成固定 768×768 视觉塔输入。
|
||
//
|
||
// Vision.onnx 是经过 PyTorch 逐输出验证的固定 48×48 patch 图。为避免拉伸物体,
|
||
// 这里保持宽高比缩放并在中心补中性灰(归一化后约为 0);这与直接把长方形
|
||
// 强拉成正方形相比更能保留 Qwen 的视觉语义。已是 768×768 的输入不做插值,
|
||
// 便于用跨语言冻结向量精确回归 patch 排列。
|
||
func preprocessImage(raw []byte) ([]float32, error) {
|
||
src, _, err := image.Decode(bytes.NewReader(raw))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("qwen: decode image: %w", err)
|
||
}
|
||
b := src.Bounds()
|
||
if b.Dx() <= 0 || b.Dy() <= 0 {
|
||
return nil, fmt.Errorf("qwen: empty image")
|
||
}
|
||
|
||
scale := math.Min(float64(qwenImageSize)/float64(b.Dx()), float64(qwenImageSize)/float64(b.Dy()))
|
||
w := max(1, int(math.Round(float64(b.Dx())*scale)))
|
||
h := max(1, int(math.Round(float64(b.Dy())*scale)))
|
||
if w > qwenImageSize {
|
||
w = qwenImageSize
|
||
}
|
||
if h > qwenImageSize {
|
||
h = qwenImageSize
|
||
}
|
||
|
||
resized := resizeBicubic(src, w, h)
|
||
canvas := image.NewNRGBA(image.Rect(0, 0, qwenImageSize, qwenImageSize))
|
||
neutral := color.NRGBA{R: 128, G: 128, B: 128, A: 255}
|
||
for i := 0; i < len(canvas.Pix); i += 4 {
|
||
canvas.Pix[i], canvas.Pix[i+1], canvas.Pix[i+2], canvas.Pix[i+3] = neutral.R, neutral.G, neutral.B, neutral.A
|
||
}
|
||
ox, oy := (qwenImageSize-w)/2, (qwenImageSize-h)/2
|
||
for y := 0; y < h; y++ {
|
||
for x := 0; x < w; x++ {
|
||
canvas.SetNRGBA(ox+x, oy+y, resized.NRGBAAt(x, y))
|
||
}
|
||
}
|
||
|
||
// 与 transformers Qwen2VLImageProcessor 的排列严格一致:
|
||
// [grid_h/merge, grid_w/merge, merge_h, merge_w, channel,
|
||
// temporal_patch, patch_h, patch_w],然后 flatten。
|
||
out := make([]float32, 0, qwenImagePatches*qwenPatchVectorSize)
|
||
blocks := qwenImageSize / qwenPatchSize / qwenSpatialMerge
|
||
for bh := 0; bh < blocks; bh++ {
|
||
for bw := 0; bw < blocks; bw++ {
|
||
for mh := 0; mh < qwenSpatialMerge; mh++ {
|
||
for mw := 0; mw < qwenSpatialMerge; mw++ {
|
||
baseY := (bh*qwenSpatialMerge + mh) * qwenPatchSize
|
||
baseX := (bw*qwenSpatialMerge + mw) * qwenPatchSize
|
||
for c := 0; c < 3; c++ {
|
||
for temporal := 0; temporal < qwenTemporalPatch; temporal++ {
|
||
_ = temporal // 静态图复制同一图片形成 2 帧 temporal patch
|
||
for py := 0; py < qwenPatchSize; py++ {
|
||
for px := 0; px < qwenPatchSize; px++ {
|
||
p := canvas.NRGBAAt(baseX+px, baseY+py)
|
||
v := [3]uint8{p.R, p.G, p.B}[c]
|
||
out = append(out, float32(v)/127.5-1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// resizeBicubic 使用半像素中心的 Catmull-Rom 三次卷积。
|
||
func resizeBicubic(src image.Image, dstW, dstH int) *image.NRGBA {
|
||
b := src.Bounds()
|
||
if b.Dx() == dstW && b.Dy() == dstH {
|
||
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
|
||
for y := 0; y < dstH; y++ {
|
||
for x := 0; x < dstW; x++ {
|
||
dst.SetNRGBA(x, y, color.NRGBAModel.Convert(src.At(b.Min.X+x, b.Min.Y+y)).(color.NRGBA))
|
||
}
|
||
}
|
||
return dst
|
||
}
|
||
|
||
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
|
||
sx, sy := float64(b.Dx())/float64(dstW), float64(b.Dy())/float64(dstH)
|
||
for y := 0; y < dstH; y++ {
|
||
fy := (float64(y)+0.5)*sy - 0.5
|
||
y0 := int(math.Floor(fy))
|
||
for x := 0; x < dstW; x++ {
|
||
fx := (float64(x)+0.5)*sx - 0.5
|
||
x0 := int(math.Floor(fx))
|
||
var sum [4]float64
|
||
var weight float64
|
||
for j := -1; j <= 2; j++ {
|
||
wy := cubicWeight(fy - float64(y0+j))
|
||
yy := min(max(y0+j, 0), b.Dy()-1)
|
||
for i := -1; i <= 2; i++ {
|
||
w := wy * cubicWeight(fx-float64(x0+i))
|
||
xx := min(max(x0+i, 0), b.Dx()-1)
|
||
p := color.NRGBAModel.Convert(src.At(b.Min.X+xx, b.Min.Y+yy)).(color.NRGBA)
|
||
sum[0] += float64(p.R) * w
|
||
sum[1] += float64(p.G) * w
|
||
sum[2] += float64(p.B) * w
|
||
sum[3] += float64(p.A) * w
|
||
weight += w
|
||
}
|
||
}
|
||
if weight == 0 {
|
||
weight = 1
|
||
}
|
||
dst.SetNRGBA(x, y, color.NRGBA{
|
||
R: clampByte(sum[0] / weight), G: clampByte(sum[1] / weight),
|
||
B: clampByte(sum[2] / weight), A: clampByte(sum[3] / weight),
|
||
})
|
||
}
|
||
}
|
||
return dst
|
||
}
|
||
|
||
func cubicWeight(x float64) float64 {
|
||
x = math.Abs(x)
|
||
if x <= 1 {
|
||
return 1.5*x*x*x - 2.5*x*x + 1
|
||
}
|
||
if x < 2 {
|
||
return -0.5*x*x*x + 2.5*x*x - 4*x + 2
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func clampByte(v float64) uint8 {
|
||
return uint8(min(255, max(0, int(math.Round(v)))))
|
||
}
|