需求分析
数据集的相关操作是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
}
代码分析:
- 创建数据集记录:从输入的dto中提取数据集的名称、描述、封面图等信息,并创建一个新的
Dataset
对象。 - 处理结束时间:如果用户提供了结束时间,则将其解析为时间对象,否则使用当前时间。
- 处理标签:将标签数组转换为逗号分隔的字符串。
- 保存数据集:将数据集信息保存到数据库中。
- 发送消息:通知用户数据集创建成功。
信息修改
数据集的修改功能允许用户更新已有的数据集信息。以下是代码的详细解释:
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
}
代码分析:
- 获取数据集:通过ID获取数据集,如果找不到则返回错误。
- 检查权限:确保只有数据集的创建者才能修改数据集。
- 更新数据集信息:从输入的dto中提取新的数据集信息并更新现有数据集对象。
- 保存数据集:将更新后的数据集保存到数据库中。
删除数据集
删除数据集的功能允许用户移除不再需要的数据集。以下是代码的详细解释:
// 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
}
代码分析:
- 删除数据集:调用DAO层的删除方法从数据库中删除数据集。
- 获取数据集:通过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
}
代码分析:
- 获取数据集列表:从数据库中获取所有数据集。
- 根据关键字列出数据集:根据用户提供的关键字查询数据集。
- 列出所有数据集:获取数据库中所有数据集的记录。
- 列出用户加入的数据集:查询用户加入的数据集,但排除用户自己创建的数据集。
- 列出用户创建的数据集:查询用户自己创建的数据集。
标注数据上传
标注数据的上传通过上传一个.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
}
代码分析:
- 读取dataset ID:从请求参数中获取数据集ID。
- 读取文件:从表单中读取上传的文件并保存到本地。
- 解压缩文件:将上传的
.zip
文件解压缩到指定目录。 - 遍历文件:遍历解压后的文件,将符合条件的文件上传到图床。
- 记录上传结果:将上传后的文件URL记录到数据库中。
- 清理临时文件:上传完成后删除本地临时文件。
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()
}
代码分析:
- 初始化定时任务:设置定时任务的时间间隔和等待时间。
- 定时任务逻辑:每隔10秒检查是否有未计算Embedding的图片,如果有则启动Embedding计算。
- 异步执行:通过
go routine
异步执行Embedding计算,避免阻塞主线程。 - 更新Embedding状态:计算完成后更新图片的Embedding状态。
总结
通过本篇博客,我们详细介绍了Sapphire系统中数据集管理和标注数据上传的实现细节。我们从需求分析入手,逐步讲解了数据集的创建、编辑、删除、查询以及标注数据上传的具体代码实现,并且介绍了Embedding计算的定时任务机制。希望这些内容能够帮助读者更好地理解Sapphire系统的设计与实现。