【Sapphire开发日志 四】数据集维护、数据上传和结果下载

需求分析

数据集的相关操作是Sapphire的重要功能之一,用户通过创建和维护数据集的信息开展标注工作,因此数据集的创建、编辑以及标注数据的上传是重要的功能。

基本业务

基本业务包括数据集的创建、编辑以及若干查询接口。

数据集创建

在数据集创建过程中,我们需要处理用户输入的数据并将其存储到数据库中。以下是代码的详细解释:

// CreateDataset 创建数据集
func (d *Dataset) CreateDataset(creatorId uint, dto dto.NewDataset) (*Dataset, error) {
	// 创建数据集记录
	datasetInfo := &Dataset{
		Name:        dto.Name,
		CreatorID:   creatorId,
		Description: dto.Description,
		Cover:       dto.Cover,
	}

	// 处理结束时间
	scheduleTime := time.Now()
	if dto.EndTime != "" {
		scheduleTime, _ = time.Parse("2006-01-02 15:04:05", dto.EndTime)
	}
	datasetInfo.EndTime = scheduleTime

	// 添加标注tag的记录
	tags := dto.Tags
	tagStr := ""
	for _, tag := range tags {
		tagStr += tag + ","
	}
	datasetInfo.Tags = tagStr

	// 将数据集信息保存到数据库
	err := dao.Save(datasetInfo)
	if err != nil {
		return nil, err
	}

	// 发送创建成功的消息
	content := fmt.Sprintf("您已成功创建数据集 %s", datasetInfo.Name)
	messageDomain.SendMessage(content, "创建数据集", MessageTypeTREND, creatorId)
	return datasetInfo, nil
}
代码分析:
  1. 创建数据集记录:从输入的dto中提取数据集的名称、描述、封面图等信息,并创建一个新的Dataset对象。
  2. 处理结束时间:如果用户提供了结束时间,则将其解析为时间对象,否则使用当前时间。
  3. 处理标签:将标签数组转换为逗号分隔的字符串。
  4. 保存数据集:将数据集信息保存到数据库中。
  5. 发送消息:通知用户数据集创建成功。

信息修改

数据集的修改功能允许用户更新已有的数据集信息。以下是代码的详细解释:

func (d *Dataset) UpdateDataset(creatorID uint, id uint, dto dto.NewDataset) (*Dataset, error) {
	var err error
	// 获取数据集
	dataset, err := d.GetDatasetByID(id)
	if err != nil {
		return nil, err
	}
	if dataset == nil {
		return nil, fmt.Errorf("dataset not found")
	}

	// 检查权限
	if creatorID != dataset.CreatorID {
		return nil, fmt.Errorf("no permission")
	}

	// 更新数据集信息
	dataset.Name = dto.Name
	dataset.Description = dto.Description
	dataset.Cover = dto.Cover
	dataset.EndTime, _ = time.Parse("2006-01-02 15:04:05", dto.EndTime)
	tagStr := ""
	for _, tag := range dto.Tags {
		tagStr += tag + ","
	}
	dataset.Tags = tagStr

	// 保存更新后的数据集
	err = dao.Save(dataset)
	if err != nil {
		return nil, err
	}

	return dataset, err
}
代码分析:
  1. 获取数据集:通过ID获取数据集,如果找不到则返回错误。
  2. 检查权限:确保只有数据集的创建者才能修改数据集。
  3. 更新数据集信息:从输入的dto中提取新的数据集信息并更新现有数据集对象。
  4. 保存数据集:将更新后的数据集保存到数据库中。

删除数据集

删除数据集的功能允许用户移除不再需要的数据集。以下是代码的详细解释:

// DeleteDataset 删除数据集
func (d *Dataset) DeleteDataset() error {
	err := dao.Delete(d)
	if err != nil {
		return err
	}
	return nil
}

// GetDatasetByID 根据 ID 获取数据集
func (d *Dataset) GetDatasetByID(id uint) (*Dataset, error) {
	res, err := dao.First[Dataset]("id = ?", id)
	if err != nil {
		return nil, err
	}
	return res, nil
}
代码分析:
  1. 删除数据集:调用DAO层的删除方法从数据库中删除数据集。
  2. 获取数据集:通过ID获取数据集,便于在删除前进行验证或其他操作。

查询接口

提供了多个查询接口,方便用户根据不同的条件查询数据集。以下是代码的详细解释:

// GetDatasetList 获取数据集列表
func (d *Dataset) GetDatasetList() ([]Dataset, error) {
	res, err := dao.FindAll[Dataset]()
	if err != nil {
		return nil, err
	}
	return res, nil
}

// ListByKeywords 根据关键字列出数据集
func (d *Dataset) ListByKeywords(keywords []string) ([]Dataset, error) {
	sql := "select * from datasets where name like ?"
	for i := 1; i < len(keywords); i++ {
		sql += " and name like ?"
	}
	res, err := dao.Query[Dataset](sql, keywords[0])
	if err != nil {
		return nil, err
	}
	return res, nil
}

// ListAllDataset 列出所有记录
func (d *Dataset) ListAllDataset() ([]Dataset, error) {
	res, err := dao.FindAll[Dataset]()
	if err != nil {
		return nil, err
	}
	return res, nil
}

// ListUserJoinedDatasetList 列出用户加入的数据集
func (d *Dataset) ListUserJoinedDatasetList(userID uint) ([]Dataset, error) {
	sql := "select * from datasets where id in (select dataset_id from dataset_users where user_id = ?) and creator_id != ?"
	res, err := dao.Query[Dataset](sql, userID, userID)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// ListUserCreatedDatasets 列出用户创建的数据集
func (d *Dataset) ListUserCreatedDatasets(createdID uint) ([]Dataset, error) {
	res, err := dao.FindAll[Dataset]("creator_id = ?", createdID)
	if err != nil {
		return nil, err
	}
	return res, nil
}
代码分析:
  1. 获取数据集列表:从数据库中获取所有数据集。
  2. 根据关键字列出数据集:根据用户提供的关键字查询数据集。
  3. 列出所有数据集:获取数据库中所有数据集的记录。
  4. 列出用户加入的数据集:查询用户加入的数据集,但排除用户自己创建的数据集。
  5. 列出用户创建的数据集:查询用户自己创建的数据集。

标注数据上传

标注数据的上传通过上传一个.zip压缩包实现,系统会自动解压文件并将文件上传到图床,然后将相关记录插入到数据库供后续调用。以下是代码的详细解释:

func (t *DatasetRouter) HandleUploadImg(ctx *gin.Context) {
	var err error
	// 读取dataset id
	datasetID, err := strconv.Atoi(ctx.Param("id"))
	if err != nil {
		ctx.JSON(http.StatusBadRequest, dto.NewFailResponse("invalid dataset id"))
		return
	}
	datasetID64 := uint(datasetID)

	// 读取表单的文件
	file, err := ctx.FormFile("file")
	if err != nil {
		ctx.JSON(http.StatusBadRequest, dto.NewFailResponse("文件不存在"))
		return
	}

	// 将文件保存到本地
	savePath := "./files/" + file.Filename
	saveDir := "./files/"
	if _, err := os.Stat(savePath); err == nil {
		ctx.JSON(http.StatusBadRequest, dto.NewFailResponse("文件已存在"))
		return
	}
	err = ctx.SaveUploadedFile(file, savePath)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}

	// 解压缩文件
	bytes, err := os.ReadFile(savePath)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}
	err = os.MkdirAll(saveDir, os.ModePerm)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}
	err = util.Unzip(bytes, "./files/")
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}

	// 取出该目录下的所有文件
	files, err := os.ReadDir(saveDir)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}

	defer func() {
		err := os.RemoveAll(saveDir)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
			return
		}
	}()

	var wg sync.WaitGroup
	var mu sync.Mutex
	var imageUrls []string
	errChan := make(chan error, len(files))

	// 遍历文件,将文件上传到图床
	for _, f := range files {
		if f.IsDir() {
			continue
		}
		if filepath.Ext(f.Name()) != ".jpg" {
			continue
		}
		wg.Add(1)
		go func(f os.DirEntry) {
			defer wg.Done()

			filePath := filepath.Join(saveDir, f.Name())
			bytes, err := ioutil.ReadFile(filePath)
			if err != nil {
				errChan <- err
				return
			}

			directUrl, err := misc.UploadImage(bytes, f.Name()+".jpg")
			if err != nil {
				errChan <- err
				return
			}
			slog.Info("HandleUploadImg", "directUrl", directUrl)

			mu.Lock()
			imageUrls = append(imageUrls, directUrl)
			mu.Unlock()
		}(f)
	}

	wg.Wait()
	close(errChan)

	for err := range errChan {
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
			return
		}
	}

	dataset, err := datasetDomain.GetDatasetByID(datasetID64)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}

	err = datasetDomain.AddImageList(dataset, imageUrls)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, dto.NewFailResponse(err.Error()))
		return
	}

	ctx.JSON(http.StatusOK, dto.NewSuccessResponse(nil))
	return
}
代码分析:
  1. 读取dataset ID:从请求参数中获取数据集ID。
  2. 读取文件:从表单中读取上传的文件并保存到本地。
  3. 解压缩文件:将上传的.zip文件解压缩到指定目录。
  4. 遍历文件:遍历解压后的文件,将符合条件的文件上传到图床。
  5. 记录上传结果:将上传后的文件URL记录到数据库中。
  6. 清理临时文件:上传完成后删除本地临时文件。

Embedding计算

为了实现对图片Embedding的计算,Sapphire启动了一个定时任务通过轮询执行计算任务。在定时任务的运行过程中通过go routine实现任务的异步执行,避免对HTTP Server的阻塞。以下是代码的详细解释:

type EmbeddingCron struct {
	Interval      int
	WaitTime      int
	Cron          *cron.Cron
	DatasetDomain *domain.Dataset
	IsEmbedding   bool
}

func NewEmbeddingCron() *EmbeddingCron {
	cron := &EmbeddingCron{
		Interval:      60,
		WaitTime:      10,
		DatasetDomain: domain.NewDatasetDomain(),
	}

	return cron
}

func (e *EmbeddingCron) Init() {
	slog.Info("Embedding cron is initializing")
	e.Cron = cron.New(cron.WithSeconds())
	e.Cron.AddFunc("@every 10s", func() {
		images, err := e.DatasetDomain.ListAllNotEmbeddedImg(1)
		if err != nil {
			slog.Error("Failed to list all not embedded images")
			return
		}
		if len(images) == 0 {
			return
		}
		if e.IsEmbedding {
			slog.Debug("Embedding is running")
			return
		}
		e.IsEmbedding = true
		go func() {
			slog.Info("Start embedding image", images[0].ID)
			time.Sleep(1 * time.Minute)
			for _, img := range images {
				slog.Info("Start embedding image", img.ID)
				err := e.DatasetDomain.EmbeddingImg(img.ID)
				if err != nil {
					slog.Error("Failed to embedding image", img.ID)
					continue
				}
				slog.Info("Embedding image", img.ID, "successfully")
			}
			e.IsEmbedding = false
		}()
	})
}

func (e *EmbeddingCron) Start() {
	slog.Info("Embedding cron is starting")
	e.Cron.Start()
}
代码分析:
  1. 初始化定时任务:设置定时任务的时间间隔和等待时间。
  2. 定时任务逻辑:每隔10秒检查是否有未计算Embedding的图片,如果有则启动Embedding计算。
  3. 异步执行:通过go routine异步执行Embedding计算,避免阻塞主线程。
  4. 更新Embedding状态:计算完成后更新图片的Embedding状态。

总结

通过本篇博客,我们详细介绍了Sapphire系统中数据集管理和标注数据上传的实现细节。我们从需求分析入手,逐步讲解了数据集的创建、编辑、删除、查询以及标注数据上传的具体代码实现,并且介绍了Embedding计算的定时任务机制。希望这些内容能够帮助读者更好地理解Sapphire系统的设计与实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值